Resolve conflicts against main:
- Inventory moved to src/artifacts/inventory (#1032); point the xar and
cpio reader imports at their new relative paths.
- README.md: take main's simplified English README, which no longer
lists individual package formats. PKG coverage stays documented in
the skill reference and docs/testing.md.
- Skill: the references gain the PKG paragraph, so the version moves to
29 (main already uses 28).
- Regenerate the product catalog, conformance manifest, skill digest and
package metadata. The MCP tool catalog is now built rather than
committed (#997), so it is removed here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts against main:
- README.md: take main's simplified English README, which no longer has
the tool-family table.
- README_ja.md: take main's reworded table and count
trace_dylib_resolution in the artifact graph row (5 -> 6).
- Skill: the references gain the trace_dylib_resolution paragraph, so
the version moves to 29 (main already uses 28).
- Regenerate the product catalog, conformance manifest, skill digest and
package metadata (135 tools). The MCP tool catalog is now built rather
than committed (#997), so it is removed here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(artifacts): stream ASAR members without full buffering
* fix(artifacts): close unopened ASAR member streams
* fix(artifacts): preserve unpacked ASAR mismatch digests
* fix(artifacts): preserve ASAR read failures
* fix(artifacts): retain unpacked ASAR stat failures
* fix(artifacts): validate ASAR entry producer identity
Follow #952's layout: the installer-package lane moves to
scripts/verify/apple/installer-package.mjs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A file with a recognized Mach-O or FAT magic but fewer than 8 bytes is a
malformed image, so it stays visible in coverage and root diagnostics.
- Canonicalize the analyzed root and target inside the translated region,
so a permission revoked after open_binary keeps its reason and path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow #952's Apple ownership: the Mach-O load-command reader, dylib
resolution adapter and fixture move to src/artifacts/apple/, the resolver,
dyld path semantics and findings to src/domain/apple/, and the dylib lane
helper to scripts/verify/apple/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edges below a conditional fallback now carry loader_conditional, reuse of
a conditional image by install name is conditional, and their findings say
they apply only if that image loads.
- Hash each traced image only if it is still the file whose headers were
parsed (device, inode, size and change times), before and after hashing.
- Resolve explicit roots segment by segment, so a root below a symlinked
directory that points outside the analyzed root is rejected.
- Require a complete 16-byte LC_CODE_SIGNATURE command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Inventory scans now verify xar extracted checksums themselves, under the
caller's integrity policy. Under "fail" a mismatch still fails, now as
artifact_integrity_mismatch with the logical path. Under
"record-and-continue" the member stays in the graph as mismatched, with
the declared and observed values, and is not expanded. A declared SHA-256
becomes an integrity contradiction. Extraction keeps verifying in the reader.
- Read cpio symlink targets only up to 4096 bytes; longer declared targets
are streamed past and recorded with a limitation instead of buffered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Treat the target as a bundle only when it was opened from an app bundle
directory (it carries the bundle's Info.plist); a regular Mach-O file whose
name ends in .app is traced from its parent directory.
- Classify a FAT header with zero architecture records as malformed, so it
stays visible in unclassified roots and coverage.
- State that dylib compatibility versions are not compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review findings in PKG inventory:
- Read the xar TOC checksum only when its declared size equals the
algorithm's digest length. A crafted size can no longer trigger a huge
allocation.
- Associate cpio hard links whose bytes are stored on another member
(newc stores them on the last link). Linked bytes are kept, within 16
MiB per member and 64 MiB in total, and served to every path. A link
whose bytes never appear is recorded with content unavailable and a
limitation, not hashed as an empty file. ArtifactEntry gains
`contentUnavailable` for this.
- Verify `070702` member CRCs whether or not a member is opened.
- Report malformed zlib members as `format`, and keep cancellation as
`cancelled` while loading, opening, or streaming members.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review findings in trace_dylib_resolution:
- Resolve LC_LAZY_LOAD_DYLIB dependencies without traversing them, since
dyld loads them on first use. An unresolved one becomes
`lazy-load-unresolved`, not a launch failure.
- Accept FAT tables up to 128 records, as target opening does. A table
that names no Mach-O CPU type, such as a Java class file, is not
Mach-O.
- Require 8-byte load-command alignment in 64-bit images. 1,508 real
slices still match otool.
- Report EACCES/EPERM while probing, reading, or hashing as an
`unavailable` failure naming the code and path, rather than generic
io.
Findings and limitations move to dylibResolutionFindings.ts to keep the
resolver under the module size limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .pkg inventory recorded only the root hash. Flat installer packages
are xar archives, and their install scripts and payloads are what an
analyst needs to review.
A pure TypeScript xar reader, which also runs off macOS:
- reads the TOC (zlib XML) and verifies the TOC checksum;
- walks nested product-package members, such as Component.pkg/;
- decodes raw and zlib ("x-gzip") members, verifying each extracted
SHA-1/MD5 checksum as the bytes stream;
- rejects unsafe member names.
Gzip-cpio Scripts and Payload members are expanded as nested containers
by a streaming odc/newc cpio reader. It reads members once, in archive
order, and holds at most one chunk. pbzx (xz) payloads are recorded
with a limitation that names pkgutil --expand-full. Unsupported member
encodings, traversal, and truncation fail with typed reasons.
@xmldom/xmldom (already installed through plist) becomes a direct,
pinned dependency for TOC parsing.
verify:installer-package builds component, product, and pbzx packages
with pkgbuild and productbuild. It checks the expanded Payload against
`pkgutil --payload-files`, script and payload content digests, product
nesting, and CLI/MCP parity, and runs in the macOS CI job.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review findings in trace_dylib_resolution:
- Choose child slices by dyld's graded architectures, so an x86_64h
process loads a generic x86_64 slice instead of reporting
architecture-missing.
- Read FAT containers with the byte-swapped FAT_CIGAM and FAT_CIGAM_64
magics, which target opening already accepts.
- Report string-bearing load commands too short to hold their string
offset as malformed instead of failing with a RangeError.
- Keep recognized but unsupported images as an `unsupported` candidate
outcome. It is undeterminable like an outside-root path, not
malformed.
- List Mach-O files that do not parse during root enumeration as
unparsed images, so coverage is partial rather than silently complete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add trace_dylib_resolution (CLI trace-dylib-resolution) to the artifact
family. For the active Mach-O, or every executable in the active .app
(the main app, XPC services, extensions, login items, helpers), it
reports which file each dylib load reaches and how.
A pure TypeScript reader decodes FAT and Mach-O load commands:
LC_LOAD/WEAK/REEXPORT/LAZY/UPWARD_DYLIB, including the newer
dylib_use_command encoding with weak, re-export, upward and delayed-init
flags, plus LC_ID_DYLIB, LC_RPATH, LC_DYLD_ENVIRONMENT and code-signature
presence. Malformed tables are reported with the failing command instead
of being guessed.
Resolution follows dyld:
- @executable_path comes from each process root;
- @loader_path comes from the loading image, or from the owner of the
LC_RPATH entry being expanded;
- @rpath searches the loader's rpaths first, then up the load chain;
- dependencies load dependents-first in load-command order, once per
process, and a matching install name reuses an already loaded image;
- symlinks are followed inside the analyzed root, and escapes stop.
Every candidate path is reported with its outcome. Each edge resolves to
resolved, conditional (an outside-root candidate such as /usr/lib/swift
precedes), unresolved, or undetermined. Absolute and shared-cache system
paths are never checked against the host. Derived findings cover
unresolved required or weak loads, absent earlier @rpath candidates (with
library validation left unknown), and LC_DYLD_ENVIRONMENT.
verify:macos-bundle now also:
- checks every traced image against otool -l;
- asserts each resolution status on a clang-built fixture that adds a
missing Overrides rpath, an absolute rpath, and a delay-init library;
- compares the predicted load order of two process roots with what dyld
loads under DYLD_PRINT_LIBRARIES.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inventory snapshot captures reader provenance before close() runs,
so a `hdiutil info -plist` entry recorded during cleanup never reached
CLI or MCP results. Detach entries already share that timing. Record
neither instead of implying the query is observable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An APFS disk image attaches as an image disk plus a synthesized APFS
container, and hdiutil reports both as whole disks. Detaching the
container also ejects the image disk, so the reverse-order detach then
failed with "No such file or directory" and every APFS DMG inventory
ended in "DMG detach or mount-root cleanup failed". APFS is the default
format of `hdiutil create` on current macOS.
When a detach fails, ask `hdiutil info -plist` whether the device is
still attached. A device that is gone counts as detached; one that is
still listed, or whose state cannot be read, remains a cleanup failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(artifacts): project compiled AppKit actions from the control to its target
NSNibControlConnector archives the sending control as NSSource and the
action target as NSDestination; a nil target is the first responder. The
NIBArchive projection swapped them for control connectors, so every
compiled AppKit action ran from its target to the control. The action
trace, which follows control -> action, could not find them, and the
selector node was keyed by the control instead of the target class. When
the target was nil, the swapped source was null and the action was
dropped: Chess lost 29 menu actions, KeepingYouAwake 7.
Connectors now keep NSSource -> NSDestination. The NIBArchive test
encoder moves to a fixture module so a bundle test can build compiled
NIBs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(artifacts): report observed action routes when a compiled route is missing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(artifacts): report compiled connector records when a storyboard route is missing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(artifacts): write the storyboard fixture's action in Cocoa syntax
MacFixture.storyboard declared its button action with the iOS attribute,
<action selector="buildTapped:" destination="vc1">. Cocoa documents
name an action's receiver with target=. ibtool compiled the iOS spelling
into an NSNibControlConnector whose NSSource is the view controller
placeholder and whose NSDestination is the button. That is the reverse
of what Xcode-authored AppKit NIBs contain (Music, Chess,
KeepingYouAwake, and Transmission all archive NSSource as the control),
and it is what the old inversion had matched. With target= the compiled
fixture archives the button as NSSource. The temporary connector dump is
removed; the observed routes remain in the assertion message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(browser): trace captured website module imports
Compose verified exported scripts, inert native import syntax and a narrow
Chromium resolution port. Preserve query/fragment identity, import-map context,
all reported capture candidates and explicit unknown execution. Share named
CLI/MCP contracts and retain native reports, diagnostics and cleanup failures.
Add focused boundary regressions and a real module-loading oracle to the
existing conditional Chrome lane. No captured application code is executed
by module tracing, and no selected website asset is refetched.
Refs #851, #774
Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
* fix(browser): preserve module contexts and source bindings
Keep capture completeness inline, validate actual source text against its
digest, and separate unsupported URL contexts from malformed input.
Use the importer origin for native module resolution and the selected map
base independently, including mixed HTTP/HTTPS and escaped HTML attributes.
Refs #851, #774
Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
The `plist` XML decoder throws on the legal dictionary key `__proto__`
(its CVE-2022-22912 guard). An app whose XML Info.plist held such an
entry could not be opened at all ("app has no readable
CFBundleExecutable"), and entitlements, XML keyed archives, and
Interface Builder plists failed the same way. On the plutil JSON route,
validation silently dropped the key from `inspect_plist` results.
Decode XML plists through a shared reader that substitutes such keys
before the decoder runs, then omits and counts those entries.
`inspect_plist` and `inspect_keyed_archive` report the omission as a
limitation instead of failing or dropping it silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Foundation encodes `encode(_:forKey: "UID")` as a `$top` dictionary
whose single key is `UID`. The keyed archive projection treated every
one-key `UID`/`CF$UID` dictionary as a reference marker, so that root
became a malformed reference and the real link to its object was lost.
Treat a one-key dictionary as a UID only when its value is a number,
matching the binary decoder's UID shape and Foundation's XML `CF$UID`
rule; otherwise descend into it as an ordinary dictionary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Artifact path failures told callers to "inspect the reported path" but
reported none: `ArtifactReaderFailure` messages such as
`Artifact path collision: ...` were dropped when converted to
`ArtifactOperationError`, leaving only `{ operation, reason: "path" }`.
Carry the failure message into `details.detail` for artifact tools and
JavaScript application analysis. Name both spellings in case-fold
collisions and include the JSON-escaped input in unsafe and
non-normalized path messages.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`inspect_plist` converted every plist with `plutil -convert json`, which
rejects plists containing `<data>`, `<date>`, or non-finite `<real>`
values. Such valid plists (including Stocks.app's Info.plist) failed with
a cause-free `execution_failure`.
When the JSON conversion exits nonzero, decode `plutil -convert xml1`
instead. Data and dates use the `$plist_type` objects the keyed-archive
reader already emits; non-finite reals become an explicit unknown because
the XML decoder reports NaN and infinity alike. Provenance records both
conversions and limitations explain the representation. Plists that JSON
can express keep the unchanged single-conversion path.
The projection moves to `src/domain/plistValue.ts` and is shared with the
keyed-archive reader, which keeps rejecting non-finite reals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SafeOutputTree.create() called fchmod(0o700) on a directory handle and
commit() fsynced a directory handle. Both are POSIX-only: Windows rejects
them with EPERM ("operation not permitted, fchmod"/"fsync"), so every
extract_artifact call failed with a generic io error and produced no output.
Skip the redundant directory chmod and directory fsync on Windows. mkdir
already applies the requested mode, and file contents are synced in write().
Add a Windows regression test and run it, plus the archive-safety test, in
the curated Windows CI lane so the behavior is covered.
Move each error family out of domain/errors.ts into its own module
(base, core, provider selection/adapter, browser observation,
artifact, evidence, unknown registry, hopper, configuration) and
relocate AnalysisErrorProjection to analysisErrorProjection.
Migrate all 162 importers to the owning modules and delete
errors.ts. No re-export shims; behavior is unchanged.
* refactor(domain): consolidate canonical ordering and digest helpers (#624)
* refactor(domain): consolidate canonical ordering and digest helpers
Single owner for code-point ordering (domain/canonicalOrdering.ts)
and canonical JSON digests (domain/comparisonSemantics.ts). Delete
all local compareCodePoints/compareText/digestCanonical duplicates
and migrate every importer. No re-export shims.
* fix(ci): allow slower real browser startup
* chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks
Delete eslint.config.js (oxlint is the linter), drop lint:dead alias,
pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run
cacheable, align oxlint ignores.
* fix(config): hash all test inputs for cached runs
* docs: fix architecture diagram and pretty-print tool catalog
Drop dangling REPLAY edges, add dotnet/reference/error/evaluation
nodes. Emit 2-space JSON from the catalog generator and regenerate.
* test: extract shared fixtures, drop knip carve-outs
Dedupe android/apple session tests, bundler adapter headers, and
reconstruction helpers into tests/support fixtures. Delete unused
type aliases and the knip ignoreIssues block. Lower jscpd threshold
2.7 to 2.5.
* refactor(app): split Setup and ProcessOwnership modules
Extract SetupTypes/SetupHost from Setup.ts and move observation
helpers into ProcessOwnershipObservation/ProcessTree. Migrate all
importers including CLI commands. No re-export shims.
* refactor(runtime): centralize safe JSON parsing and preserve error causes
Add domain/safeJson.safeParseJson and migrate all raw JSON.parse
call sites. Name catch causes and preserve failure reasons instead
of swallowing; annotate genuine best-effort cleanup.
* fix(runtime): retain JSON parse error causes
Cancel ZIP extraction promptly when an unread consumer applies backpressure, releasing the producer instead of leaving it parked on a drain that can never arrive.
## Changes
Related to #545 and the permission-system removal in #555.
- Upgrade the shipped skill to v24 and remove obsolete filesystem grants, per-call approval flags, Electron file-root configuration, and DMG approval flags. Calls follow the user-requested investigation directly. Keep setup plan confirmation, actual OS requirements, and process/archive lifecycle boundaries.
- Correct the passive browser guide: endpoint/scope come from requests; ordinary local URL queries are retained, while credentials remain redacted.
- Audit canonical tool effects: declare possible DMG process/temp-directory effects, mark fresh extraction non-idempotent, and mark comparisons of supplied captures/PNGs as closed-world. Align artifact-provider capability effects with temporary mount-directory writes.
- Retain non-read-only hints for calls that add Evidence to the session; document that annotations are hints and that clients control their confirmation UI. Do not mislabel session mutations as read-only to suppress prompts.
- Regenerate package, skill, MCP, and product metadata from source.
## Verification
- `npm run test:focused -- <11 affected test files>`: **40 tests passed**, including skill transaction/backup/idempotence, SDK tools/list annotations, actual local capture/PNG comparisons, artifact extraction/containment/cancellation, DMG launcher/cleanup seam, setup confirmation, browser MCP and scenario MCP, CLI catalog parity.
- `npm run check`: typecheck, lint, format, knip, package metadata.
- `npm run docs:check`: generated catalogs, skill metadata, documentation facts, completion ledger, error schema.
No local real Hopper/Ghidra run: their adapters and analysis operations are unchanged. Native macOS DMG behavior is covered locally through the launcher seam; no local macOS/hdiutil claim is made.
Real browser verification passed using existing Headless Chrome143.0.7499.4: actual CLI/stdio MCP,9,331,407-byte PNG, passive capture/source maps, and owned/external scenario cleanup. An initial full Chrome143 run failed the existing screenshot-height assertion (1397 versus1536); the dedicated headless-shell run passed the complete verifier without code changes. The verifier run-lineage field remained unavailable; no lineage-coverage claim is made. Browser download into `.codex` was blocked by CDN403, so the successful run used an existing browser without changing host configuration.