[PR #922] [MERGED] feat(apple): project macOS bundle anatomy in application graphs #2348

Closed
opened 2026-10-16 21:17:38 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/922
Author: @kaoru0822-kitauji
Created: 10/7/2026
Status: ✅ Merged
Merged: 10/7/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: feat/macos-bundle-anatomy


📝 Commits (6)

  • b72c079 fix(artifacts): accept APFS image disks ejected with their container
  • dd3e7ae feat(apple): project macOS bundle anatomy in application graphs
  • 62634e4 fix(artifacts): keep the post-snapshot detach check out of provenance
  • b956bd5 Merge branch 'fix/dmg-apfs-detach' into feat/macos-bundle-anatomy
  • 5524970 fix(apple): attribute only application-root components in macOS inventories
  • fd552a8 chore: merge main and regenerate combined catalog metadata

📊 Changes

24 files changed (+1712 additions, -76 deletions)

View changed files

📝 .github/workflows/ci.yml (+1 -0)
📝 README.md (+1 -1)
📝 docs/apple-application-analysis.md (+83 -4)
📝 docs/product-catalog.json (+3 -3)
📝 docs/testing.md (+8 -1)
📝 docs/verification/managed-conformance-manifest.json (+2 -2)
📝 package.json (+1 -0)
📝 scripts/lib/artifact-e2e.mjs (+7 -1)
➕ scripts/lib/macos-bundle-fixture.mjs (+269 -0)
➕ scripts/verify-macos-bundle.mjs (+167 -0)
📝 skills/reverse-engineer-anything/SKILL.md (+2 -2)
📝 skills/reverse-engineer-anything/references/native-and-artifacts.md (+8 -0)
📝 src/application/AppleApplicationService.ts (+2 -2)
📝 src/artifacts/NativeDmgArtifactReader.ts (+32 -1)
📝 src/cli/applicationCommands.ts (+1 -1)
📝 src/contracts/applicationToolContracts.ts (+7 -1)
📝 src/contracts/mobileApplicationGraphExamples.ts (+18 -1)
➕ src/domain/appleApplication.test.ts (+440 -0)
📝 src/domain/appleApplication.ts (+176 -50)
➕ src/domain/appleBundleAnatomy.ts (+295 -0)

...and 4 more files

📄 Description

Builds on #921, which this branch includes as its first commit. The DMG case of the new real lane needs that fix. Once #921 merges, this diff shrinks to the feature commit.

Summary

project_apple_application_graph accepted only IPA inventories. A macOS analyst's first question about a .app (what runs out of this bundle?) had no answer: XPC services, app extensions, login items, privileged helpers, system extensions and launchd jobs. The tool now projects macOS .app directories, and ZIP or DMG inventories that contain apps, into that anatomy. It stays execution-free and reads only paths and hashes.

Change and scope

This extends the existing tool rather than adding one, because the intent (project an Apple app inventory) and the result contract are unchanged. The schema change is additive. The domain logic is in the new src/domain/appleBundleAnatomy.ts.

Application roots

Inventory root Application root
IPA Payload/*.app (unchanged)
.app directory .
Directory, ditto ZIP, or DMG Each outermost *.app/Contents

New result fields

  • bundles: every application root and nested bundle, with parent_path, layout (macos-deep, versioned-framework or shallow), a path-convention role, info_plist_path, executable_candidates and signing_paths.
    • Roles: app-extension, xpc-service (including XPC services inside frameworks), framework, login-item, system-extension, driver-extension, plug-in, resource-bundle, bundle, helper-application and nested-application.
  • components.privileged_helpers: Contents/Library/LaunchServices/*.
  • components.launchd_plists: Contents/Library/Launch{Agents,Daemons}/*.plist, each with domain.
  • components.helpers: Contents/Helpers/*.
  • symlinks and platforms.

Behaviour

  • Symlink targets are not inventoried, so versioned frameworks are described through their real Versions/<version>/ directories. If the Info.plist that Versions/Current selects cannot be determined, it is null with a limitation.
  • AppleDouble sidecars (._*, __MACOSX/) are excluded from roles.
  • A DMG whose contents were not inventoried, such as one inventoried off macOS, is partial.
  • APK, ASAR and other formats remain input errors.

Not included: plist or CMS semantics. Roles come from path conventions, and the result points callers to inspect_plist for each info_plist_path. Dylib-load resolution is the planned follow-up.

Contract and boundary impact

  • Semantic owner: src/domain/appleBundleAnatomy.ts and src/domain/appleApplication.ts.
  • CLI and MCP contract: the output schema gains fields; the root_format enum widens from ipa to ipa | directory | zip | dmg. There is a new catalog example for a .app directory, and the descriptions are updated. Input is unchanged.
  • Evidence: the projection subject keeps the inventory subject's format instead of always ipa. projection_id now covers the new fields, so IPA projection IDs differ from earlier versions. No doc promises cross-version ID stability, so this is not marked breaking.
  • Generated metadata: product catalog, MCP tool catalog, skill (version 27) and conformance-manifest digests are regenerated.

Sanitized MCP result for the lane's fixture app, as a directory and trimmed:

{
  "root_format": "directory",
  "platforms": ["macos"],
  "application_roots": ["."],
  "bundles": [
    { "path": ".", "role": "application", "layout": "macos-deep", "parent_path": null,
      "info_plist_path": "Contents/Info.plist", "executable_candidates": ["Contents/MacOS/MacFixture"],
      "signing_paths": ["Contents/_CodeSignature/CodeResources"], "role_basis": "path-convention" },
    { "path": "Contents/Frameworks/Core.framework", "role": "framework", "layout": "versioned-framework",
      "parent_path": ".", "info_plist_path": "Contents/Frameworks/Core.framework/Versions/A/Resources/Info.plist" },
    { "path": "Contents/Frameworks/Core.framework/Versions/A/XPCServices/FwSvc.xpc", "role": "xpc-service",
      "parent_path": "Contents/Frameworks/Core.framework" },
    { "path": "Contents/Library/LoginItems/Login.app", "role": "login-item" },
    { "path": "Contents/PlugIns/Ext.appex", "role": "app-extension" },
    { "path": "Contents/XPCServices/Svc.xpc", "role": "xpc-service" }
  ],
  "components": {
    "privileged_helpers": [{ "path": "Contents/Library/LaunchServices/com.example.rea.helper" }],
    "launchd_plists": [
      { "path": "Contents/Library/LaunchAgents/com.example.rea.agent.plist", "domain": "agent" },
      { "path": "Contents/Library/LaunchDaemons/com.example.rea.helper.plist", "domain": "daemon" }
    ],
    "helpers": [{ "path": "Contents/Helpers/rea-tool" }]
  },
  "symlinks": ["Contents/Frameworks/Core.framework/Versions/Current"]
}

Evidence and regression coverage

  • src/domain/appleApplication.test.ts (new, 8 cases):

    • an .app directory as its own root, plus every role;
    • a non-.app bundle directory that is not an application root;
    • versioned frameworks without Versions/Current, and ambiguous versions;
    • AppleDouble sidecars;
    • a DMG without inventoried contents;
    • IPA shallow and iOS;
    • rejection of ASAR.
  • tests/composition/analysis-sessions/appleApplication.test.ts:

    • a real .app directory inventory through inventory_artifact;
    • a non-application ZIP now reports no roots instead of being rejected;
    • an APK is rejected.
  • Real lane npm run verify:macos-bundle, also added to the macOS CI job. It uses Command Line Tools only:

    • compiles a source-owned app with clang and signs it ad hoc;
    • packs it as a directory, a ditto ZIP, and an APFS DMG;
    • asserts identical anatomy across the three through the CLI, with stdio MCP parity;
    • asserts the DMG is detached afterwards.
  • Real apps in /Applications, on this branch's build:

    App Observed anatomy
    KeepingYouAwake login item; Sparkle Updater.app and two XPC services inside Sparkle.framework/Versions/B
    Raycast ExtensionKit .appex under Contents/Extensions; login item; 9 SwiftPM resource bundles
    Brave Browser SMJobBless UpdaterPrivilegedHelper; 9 helper apps under the framework's Versions/<ver>/Helpers
    OneDrive FinderSync and File Provider extensions; two login items
    Shadowrocket four .appex; a Catalyst bridge plug-in
    Discord Electron helpers as nested applications
  • Remaining proof gaps: roles are path conventions, not plist-verified. ZIP/IPA inventories without directory entries still project, through implied ancestors.

Validation performed

  • npm run check:changed: 289 files passed.
  • npm run test:focused -- tests/composition/analysis-sessions/appleApplication.test.ts tests/boundary/mcp/mobileApplicationGraphMcp.test.ts tests/boundary/cli/productCatalog.test.ts tests/boundary/mcp/toolSchemaValidity.test.ts tests/acceptance/contractPresentation.test.ts: passed.
  • npm run docs:generate && npm run docs:check: passed.
  • npm run verify:macos-bundle: {"ok":true,"mocked":false,"cli":true,"stdio_mcp":true,"containers":["directory","zip","dmg"],"bundles":6,"dmg_detached":true}.

Compatibility, safety, and release

  • Breaking changes: none to the input. The result is additive, but projection_id values change.
  • Real Hopper/Ghidra coverage: not applicable; no provider is touched.
  • Real OS coverage: macOS 26.6.2 arm64 locally, and the macOS 14 CI job.
  • Security and containment: execution-free, with no new process or filesystem effects. The lane's fixtures are built in a temporary directory and removed afterwards.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/922 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/7/2026 **Status:** ✅ Merged **Merged:** 10/7/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `feat/macos-bundle-anatomy` --- ### 📝 Commits (6) - [`b72c079`](https://github.com/morluto/rea/commit/b72c079a9a90c374bd6dd30ff8c4387306b8f59b) fix(artifacts): accept APFS image disks ejected with their container - [`dd3e7ae`](https://github.com/morluto/rea/commit/dd3e7aef0d52c478f6cdf475501c42ebe7f5c97e) feat(apple): project macOS bundle anatomy in application graphs - [`62634e4`](https://github.com/morluto/rea/commit/62634e4c0177bde8a08decc356da98fbe25d2000) fix(artifacts): keep the post-snapshot detach check out of provenance - [`b956bd5`](https://github.com/morluto/rea/commit/b956bd589f92488e4f703921b70232405ea2d929) Merge branch 'fix/dmg-apfs-detach' into feat/macos-bundle-anatomy - [`5524970`](https://github.com/morluto/rea/commit/55249704c5c83cbf8d20ac7869863cc196da00a6) fix(apple): attribute only application-root components in macOS inventories - [`fd552a8`](https://github.com/morluto/rea/commit/fd552a8c4bff9538981e48be6310a0db73af8202) chore: merge main and regenerate combined catalog metadata ### 📊 Changes **24 files changed** (+1712 additions, -76 deletions) <details> <summary>View changed files</summary> 📝 `.github/workflows/ci.yml` (+1 -0) 📝 `README.md` (+1 -1) 📝 `docs/apple-application-analysis.md` (+83 -4) 📝 `docs/product-catalog.json` (+3 -3) 📝 `docs/testing.md` (+8 -1) 📝 `docs/verification/managed-conformance-manifest.json` (+2 -2) 📝 `package.json` (+1 -0) 📝 `scripts/lib/artifact-e2e.mjs` (+7 -1) ➕ `scripts/lib/macos-bundle-fixture.mjs` (+269 -0) ➕ `scripts/verify-macos-bundle.mjs` (+167 -0) 📝 `skills/reverse-engineer-anything/SKILL.md` (+2 -2) 📝 `skills/reverse-engineer-anything/references/native-and-artifacts.md` (+8 -0) 📝 `src/application/AppleApplicationService.ts` (+2 -2) 📝 `src/artifacts/NativeDmgArtifactReader.ts` (+32 -1) 📝 `src/cli/applicationCommands.ts` (+1 -1) 📝 `src/contracts/applicationToolContracts.ts` (+7 -1) 📝 `src/contracts/mobileApplicationGraphExamples.ts` (+18 -1) ➕ `src/domain/appleApplication.test.ts` (+440 -0) 📝 `src/domain/appleApplication.ts` (+176 -50) ➕ `src/domain/appleBundleAnatomy.ts` (+295 -0) _...and 4 more files_ </details> ### 📄 Description Builds on #921, which this branch includes as its first commit. The DMG case of the new real lane needs that fix. Once #921 merges, this diff shrinks to the feature commit. ## Summary `project_apple_application_graph` accepted only IPA inventories. A macOS analyst's first question about a `.app` (what runs out of this bundle?) had no answer: XPC services, app extensions, login items, privileged helpers, system extensions and launchd jobs. The tool now projects macOS `.app` directories, and ZIP or DMG inventories that contain apps, into that anatomy. It stays execution-free and reads only paths and hashes. ## Change and scope This extends the existing tool rather than adding one, because the intent (project an Apple app inventory) and the result contract are unchanged. The schema change is additive. The domain logic is in the new `src/domain/appleBundleAnatomy.ts`. **Application roots** | Inventory root | Application root | | --- | --- | | IPA | `Payload/*.app` (unchanged) | | `.app` directory | `.` | | Directory, ditto ZIP, or DMG | Each outermost `*.app/Contents` | **New result fields** - `bundles`: every application root and nested bundle, with `parent_path`, `layout` (`macos-deep`, `versioned-framework` or `shallow`), a path-convention `role`, `info_plist_path`, `executable_candidates` and `signing_paths`. - Roles: `app-extension`, `xpc-service` (including XPC services inside frameworks), `framework`, `login-item`, `system-extension`, `driver-extension`, `plug-in`, `resource-bundle`, `bundle`, `helper-application` and `nested-application`. - `components.privileged_helpers`: `Contents/Library/LaunchServices/*`. - `components.launchd_plists`: `Contents/Library/Launch{Agents,Daemons}/*.plist`, each with `domain`. - `components.helpers`: `Contents/Helpers/*`. - `symlinks` and `platforms`. **Behaviour** - Symlink targets are not inventoried, so versioned frameworks are described through their real `Versions/<version>/` directories. If the `Info.plist` that `Versions/Current` selects cannot be determined, it is `null` with a limitation. - AppleDouble sidecars (`._*`, `__MACOSX/`) are excluded from roles. - A DMG whose contents were not inventoried, such as one inventoried off macOS, is `partial`. - APK, ASAR and other formats remain input errors. Not included: plist or CMS semantics. Roles come from path conventions, and the result points callers to `inspect_plist` for each `info_plist_path`. Dylib-load resolution is the planned follow-up. ## Contract and boundary impact - Semantic owner: `src/domain/appleBundleAnatomy.ts` and `src/domain/appleApplication.ts`. - CLI and MCP contract: the output schema gains fields; the `root_format` enum widens from `ipa` to `ipa | directory | zip | dmg`. There is a new catalog example for a `.app` directory, and the descriptions are updated. Input is unchanged. - Evidence: the projection subject keeps the inventory subject's format instead of always `ipa`. `projection_id` now covers the new fields, so **IPA projection IDs differ from earlier versions**. No doc promises cross-version ID stability, so this is not marked breaking. - Generated metadata: product catalog, MCP tool catalog, skill (version 27) and conformance-manifest digests are regenerated. Sanitized MCP result for the lane's fixture app, as a directory and trimmed: ```json { "root_format": "directory", "platforms": ["macos"], "application_roots": ["."], "bundles": [ { "path": ".", "role": "application", "layout": "macos-deep", "parent_path": null, "info_plist_path": "Contents/Info.plist", "executable_candidates": ["Contents/MacOS/MacFixture"], "signing_paths": ["Contents/_CodeSignature/CodeResources"], "role_basis": "path-convention" }, { "path": "Contents/Frameworks/Core.framework", "role": "framework", "layout": "versioned-framework", "parent_path": ".", "info_plist_path": "Contents/Frameworks/Core.framework/Versions/A/Resources/Info.plist" }, { "path": "Contents/Frameworks/Core.framework/Versions/A/XPCServices/FwSvc.xpc", "role": "xpc-service", "parent_path": "Contents/Frameworks/Core.framework" }, { "path": "Contents/Library/LoginItems/Login.app", "role": "login-item" }, { "path": "Contents/PlugIns/Ext.appex", "role": "app-extension" }, { "path": "Contents/XPCServices/Svc.xpc", "role": "xpc-service" } ], "components": { "privileged_helpers": [{ "path": "Contents/Library/LaunchServices/com.example.rea.helper" }], "launchd_plists": [ { "path": "Contents/Library/LaunchAgents/com.example.rea.agent.plist", "domain": "agent" }, { "path": "Contents/Library/LaunchDaemons/com.example.rea.helper.plist", "domain": "daemon" } ], "helpers": [{ "path": "Contents/Helpers/rea-tool" }] }, "symlinks": ["Contents/Frameworks/Core.framework/Versions/Current"] } ``` ## Evidence and regression coverage - `src/domain/appleApplication.test.ts` (new, 8 cases): - an `.app` directory as its own root, plus every role; - a non-`.app` bundle directory that is not an application root; - versioned frameworks without `Versions/Current`, and ambiguous versions; - AppleDouble sidecars; - a DMG without inventoried contents; - IPA shallow and iOS; - rejection of ASAR. - `tests/composition/analysis-sessions/appleApplication.test.ts`: - a real `.app` directory inventory through `inventory_artifact`; - a non-application ZIP now reports no roots instead of being rejected; - an APK is rejected. - Real lane `npm run verify:macos-bundle`, also added to the macOS CI job. It uses Command Line Tools only: - compiles a source-owned app with `clang` and signs it ad hoc; - packs it as a directory, a `ditto` ZIP, and an APFS DMG; - asserts identical anatomy across the three through the CLI, with stdio MCP parity; - asserts the DMG is detached afterwards. - Real apps in `/Applications`, on this branch's build: | App | Observed anatomy | | --- | --- | | KeepingYouAwake | login item; Sparkle `Updater.app` and two XPC services inside `Sparkle.framework/Versions/B` | | Raycast | ExtensionKit `.appex` under `Contents/Extensions`; login item; 9 SwiftPM resource bundles | | Brave Browser | SMJobBless `UpdaterPrivilegedHelper`; 9 helper apps under the framework's `Versions/<ver>/Helpers` | | OneDrive | FinderSync and File Provider extensions; two login items | | Shadowrocket | four `.appex`; a Catalyst bridge plug-in | | Discord | Electron helpers as nested applications | - Remaining proof gaps: roles are path conventions, not plist-verified. ZIP/IPA inventories without directory entries still project, through implied ancestors. ## Validation performed - `npm run check:changed`: 289 files passed. - `npm run test:focused -- tests/composition/analysis-sessions/appleApplication.test.ts tests/boundary/mcp/mobileApplicationGraphMcp.test.ts tests/boundary/cli/productCatalog.test.ts tests/boundary/mcp/toolSchemaValidity.test.ts tests/acceptance/contractPresentation.test.ts`: passed. - `npm run docs:generate && npm run docs:check`: passed. - `npm run verify:macos-bundle`: `{"ok":true,"mocked":false,"cli":true,"stdio_mcp":true,"containers":["directory","zip","dmg"],"bundles":6,"dmg_detached":true}`. ## Compatibility, safety, and release - Breaking changes: none to the input. The result is additive, but `projection_id` values change. - Real Hopper/Ghidra coverage: not applicable; no provider is touched. - Real OS coverage: macOS 26.6.2 arm64 locally, and the macOS 14 CI job. - Security and containment: execution-free, with no new process or filesystem effects. The lane's fixtures are built in a temporary directory and removed afterwards. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-16 21:17:38 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2348
No description provided.