mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(apple): derive launchd domains from owning directories
Use the matched Contents/Library/LaunchAgents or LaunchDaemons suffix to select a plist's domain. Remove the independent scan of every ancestor. Keep regular-file, application-root, sidecar and suffix admission rules. Adapt @bnkaiteam's proposal and ZIP boundary regression from PR #3504, adding public CLI/stdio MCP controls for nested bundles, case variants, misleading ancestors and excluded paths. Validation: 53 focused checks, build, check:fast and docs:check pass. Two new controls fail before the fix. 12 completed actual built CLI cost controls preserve the ordinary reply and projection ID byte for byte. No heavy profiling, target or real deep-provider execution; npm release and whole-agent costs remain unverified. Fixes #3353
This commit is contained in:
@@ -241,6 +241,14 @@ const appleRootFormat = (format: string): RootFormat => {
|
||||
const isNativeLibrary = (path: string): boolean =>
|
||||
/\.(?:dylib|so)$/iu.test(path);
|
||||
|
||||
const launchdDomain = (path: string): "agent" | "daemon" | undefined => {
|
||||
if (anatomyTailMatches(path, ["Contents", "Library", "LaunchAgents", "*"]))
|
||||
return "agent";
|
||||
if (anatomyTailMatches(path, ["Contents", "Library", "LaunchDaemons", "*"]))
|
||||
return "daemon";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const classifyComponents = (
|
||||
entries: readonly ProjectedInventoryEntry[],
|
||||
all: readonly Component[],
|
||||
@@ -280,29 +288,12 @@ const classifyComponents = (
|
||||
privileged_helpers: matching((path) =>
|
||||
anatomyTailMatches(path, ["Contents", "Library", "LaunchServices", "*"]),
|
||||
),
|
||||
launchd_plists: matching(
|
||||
(path) =>
|
||||
path.endsWith(".plist") &&
|
||||
(anatomyTailMatches(path, [
|
||||
"Contents",
|
||||
"Library",
|
||||
"LaunchAgents",
|
||||
"*",
|
||||
]) ||
|
||||
anatomyTailMatches(path, [
|
||||
"Contents",
|
||||
"Library",
|
||||
"LaunchDaemons",
|
||||
"*",
|
||||
])),
|
||||
).map((component) => ({
|
||||
...component,
|
||||
domain: component.path
|
||||
.split("/")
|
||||
.some((segment) => anatomyNameIs(segment, "LaunchDaemons"))
|
||||
? ("daemon" as const)
|
||||
: ("agent" as const),
|
||||
})),
|
||||
launchd_plists: matching((path) => path.endsWith(".plist")).flatMap(
|
||||
(component) => {
|
||||
const domain = launchdDomain(component.path);
|
||||
return domain === undefined ? [] : [{ ...component, domain }];
|
||||
},
|
||||
),
|
||||
helpers: matching((path) =>
|
||||
anatomyTailMatches(path, ["Contents", "Helpers", "*"]),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { appleApplicationProjectionResultSchema } from "../../../src/domain/apple/appleApplication.js";
|
||||
import { compareUnicodeCodePoints } from "../../../src/domain/unicodeCodePointOrder.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"Apple launchd domains follow their owning directory through CLI and MCP",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-apple-launchd-cli-");
|
||||
const environment = { HOME: root, USERPROFILE: root };
|
||||
const expected = [
|
||||
[
|
||||
"LaunchDaemons/Sample.app/Contents/Library/LaunchAgents/agent.plist",
|
||||
"agent",
|
||||
],
|
||||
[
|
||||
"LaunchDaemons/Sample.app/Contents/Resources/LaunchDaemons/Nested.app/Contents/Library/LaunchAgents/nested.plist",
|
||||
"agent",
|
||||
],
|
||||
[
|
||||
"LaunchAgents/Other.app/Contents/Library/LaunchDaemons/daemon.plist",
|
||||
"daemon",
|
||||
],
|
||||
[
|
||||
"launchdaemons/Case.app/contents/library/launchagents/mixed.plist",
|
||||
"agent",
|
||||
],
|
||||
] as const;
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
for (const [path] of expected)
|
||||
await writer.add(path, new TextReader("<plist/>"));
|
||||
for (const path of [
|
||||
"LaunchDaemons/Sample.app/Contents/Library/LaunchAgents/nested/ignored.plist",
|
||||
"LaunchDaemons/Sample.app/Contents/Library/LaunchAgents/not-a-plist",
|
||||
"Contents/Library/LaunchAgents/unrooted.plist",
|
||||
"__MACOSX/Sample.app/Contents/Library/LaunchAgents/sidecar.plist",
|
||||
])
|
||||
await writer.add(path, new TextReader("<plist/>"));
|
||||
await writer.add(
|
||||
"LaunchDaemons/Sample.app/Contents/Library/LaunchAgents/directory.plist/",
|
||||
undefined,
|
||||
{ directory: true },
|
||||
);
|
||||
const path = join(root, "Launchd.zip");
|
||||
await writeFile(path, await writer.close());
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-artifact", path, "--json"],
|
||||
environment,
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const source = parseEvidence(inspected.json);
|
||||
const input = { inventory_evidence: [source] };
|
||||
const projected = await cli.run({
|
||||
arguments: [
|
||||
"project-apple-application-graph",
|
||||
JSON.stringify(input),
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
});
|
||||
expect(projected.exitCode, projected.stderr).toBe(0);
|
||||
const evidence = parseEvidence(projected.json);
|
||||
const result = appleApplicationProjectionResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(
|
||||
result.components.launchd_plists.map(({ path, domain }) => [
|
||||
path,
|
||||
domain,
|
||||
]),
|
||||
).toEqual(
|
||||
[...expected].sort(([left], [right]) =>
|
||||
compareUnicodeCodePoints(left, right),
|
||||
),
|
||||
);
|
||||
expect(result.coverage).toEqual({
|
||||
status: "complete-within-inventory",
|
||||
inventory_complete: true,
|
||||
});
|
||||
const { call } = await connectLocalToolsMcp(environment);
|
||||
const mcp = await call("project_apple_application_graph", input);
|
||||
expect(mcp.isError).not.toBe(true);
|
||||
expect(parseEvidence(mcp.structuredContent)).toEqual(evidence);
|
||||
},
|
||||
);
|
||||
@@ -318,6 +318,57 @@ it("applies application-root and sidecar rules to unread entries", async () => {
|
||||
});
|
||||
|
||||
describe("macOS application projection", () => {
|
||||
it("classifies launchd plists by their owning directory instead of unrelated ancestors", async () => {
|
||||
const root = await createTestTempDirectory("rea-apple-launchd-domain-");
|
||||
const path = join(root, "Launchd.zip");
|
||||
const expected = [
|
||||
[
|
||||
"LaunchDaemons/Sample.app/Contents/Library/LaunchAgents/agent.plist",
|
||||
"agent",
|
||||
],
|
||||
[
|
||||
"LaunchDaemons/Sample.app/Contents/Resources/LaunchDaemons/Nested.app/Contents/Library/LaunchAgents/nested.plist",
|
||||
"agent",
|
||||
],
|
||||
[
|
||||
"LaunchAgents/Other.app/Contents/Library/LaunchDaemons/daemon.plist",
|
||||
"daemon",
|
||||
],
|
||||
[
|
||||
"launchdaemons/Case.app/contents/library/launchagents/mixed.plist",
|
||||
"agent",
|
||||
],
|
||||
] as const;
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
for (const [entry] of expected)
|
||||
await writer.add(entry, new TextReader("<plist><dict/></plist>"));
|
||||
await writer.add(
|
||||
"LaunchDaemons/Sample.app/Contents/Library/LaunchAgents/nested/ignored.plist",
|
||||
new TextReader("<plist><dict/></plist>"),
|
||||
);
|
||||
await writeFile(path, await writer.close());
|
||||
|
||||
const inventory = parseEvidence(
|
||||
await runProviderAnalysis(path, "inventory_artifact", {}),
|
||||
);
|
||||
const projection = appleApplicationProjectionResultSchema.parse(
|
||||
requireSuccessfulProjection(
|
||||
projectAppleApplicationEvidence({ inventory_evidence: [inventory] }),
|
||||
).normalized_result,
|
||||
);
|
||||
expect(
|
||||
projection.components.launchd_plists.map(({ path, domain }) => [
|
||||
path,
|
||||
domain,
|
||||
]),
|
||||
).toEqual(expect.arrayContaining([...expected]));
|
||||
expect(projection.components.launchd_plists).toHaveLength(expected.length);
|
||||
expect(projection.coverage).toEqual({
|
||||
status: "complete-within-inventory",
|
||||
inventory_complete: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("projects a macOS .app directory inventory with nested bundle roles", async () => {
|
||||
const root = await createTestTempDirectory("rea-apple-macos-");
|
||||
const app = join(root, "Fixture.app");
|
||||
|
||||
Reference in New Issue
Block a user