Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@
# gitleaks' generic-api-key rule fired only because the const identifier ends in
# `KEY` and the namespaced value has mild entropy. Scoped to its original commit.
86e23824a491bc8de59697b62169783f917e63b1:apps/desktop/src/renderer/lib/bannerDismiss.ts:generic-api-key:24

# Synthetic filename used to prove files under `.ade/secrets` cannot be
# imported as proof artifacts. No credential value is present; keep the
# exception scoped to the original PR commit and exact test finding.
631f4066397cd4f98b095d3f7d3a43d0cf758805:apps/desktop/src/main/services/computerUse/computerUseArtifactBrokerService.test.ts:generic-api-key:423
7 changes: 7 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,13 @@ ade code
ade code --embedded
ade tests run --lane lane-id --suite unit --wait
ade proof list --arg ownerKind=chat --arg ownerId=session-id
ade proof attach shots/result.png --caption "Checkout complete"
ade proof rm artifact-id
ade proof broken --text # list missing/unimported proof records
ade proof recover artifact-id # re-import when the original capture still exists
ade proof prune # preview broken records; does not delete
ade proof prune --broken # delete every broken proof record
ade proof actions --text # full computer_use_artifacts action inventory
ade ios-sim devices --text
ade --socket ios-sim apps --text
ade --socket ios-sim launch --target target-id --text
Expand Down
290 changes: 276 additions & 14 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,147 @@ describe("adeRpcServer", () => {
});
});

// `ade/actions/call` is the only way into `runTool`, and it dispatches off
// READ_ONLY_TOOLS / MUTATION_TOOLS. A tool registered in the inventory but
// absent from both sets is advertised and unreachable — which is how the
// whole proof delete surface (`ade proof rm|prune|recover`) shipped broken.
it("dispatches every registered computer-use mutation tool", async () => {
const { runtime } = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
await initialize(handler, { callerId: "chat-1", role: "agent" });

for (const name of [
"delete_computer_use_artifacts",
"prune_broken_computer_use_artifacts",
"recover_computer_use_artifact",
"list_broken_computer_use_artifacts",
]) {
const result = await callTool(handler, name, {});
const serialized = JSON.stringify(result ?? {});
expect(serialized).not.toContain(`Unsupported ADE action: ${name}`);
}
});

it("scopes proof lifecycle tools to the authenticated chat and lane owners", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
fixture.runtime.sessionService.get.mockReturnValue({ id: "chat-1", laneId: "lane-1" } as any);
const owned = {
id: "owned-proof",
laneId: "lane-1",
links: [],
};
const foreign = {
id: "foreign-proof",
links: [{ ownerKind: "chat_session", ownerId: "chat-2" }],
};
fixture.runtime.computerUseArtifactBrokerService.listArtifacts.mockImplementation((args: any) => {
if (args.artifactId === owned.id) return [owned];
if (args.artifactId === foreign.id) return [foreign];
if (args.ownerKind === "chat_session" && args.ownerId === "chat-1") return [owned];
return [];
});
fixture.runtime.computerUseArtifactBrokerService.deleteArtifacts = vi.fn(() => ({
deleted: [],
missing: [],
failed: [],
freedBytes: 0,
}));
fixture.runtime.computerUseArtifactBrokerService.listBrokenArtifacts = vi.fn(() => [
{ artifactId: owned.id },
{ artifactId: foreign.id },
]);
fixture.runtime.computerUseArtifactBrokerService.recoverArtifact = vi.fn();
await initialize(handler, {
callerId: "chat-1",
role: "agent",
chatSessionId: "chat-1",
});

const foreignOwnerIngest = await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "manual",
backendName: "ade-cli",
inputs: [{ kind: "screenshot", title: "Foreign owner", path: "proof.png" }],
owners: [{ kind: "chat_session", id: "chat-2" }],
});
expect(foreignOwnerIngest.isError).toBe(true);
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).not.toHaveBeenCalled();

await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "manual",
backendName: "ade-cli",
inputs: [{ kind: "screenshot", title: "Published proof", path: "proof.png" }],
owners: [{ kind: "github_pr", id: "https://github.com/arul28/ADE/pull/933" }],
});
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).toHaveBeenCalledTimes(1);

const listed = await callTool(handler, "list_computer_use_artifacts", {});
expect(listed.structuredContent.artifacts).toEqual([owned]);

const foreignList = await callTool(handler, "list_computer_use_artifacts", {
ownerKind: "chat_session",
ownerId: "chat-2",
});
expect(foreignList.isError).toBe(true);

const foreignDelete = await callTool(handler, "delete_computer_use_artifacts", {
artifactId: foreign.id,
});
expect(foreignDelete.isError).toBe(true);
expect(fixture.runtime.computerUseArtifactBrokerService.deleteArtifacts).not.toHaveBeenCalled();

await callTool(handler, "delete_computer_use_artifacts", { artifactId: owned.id });
expect(fixture.runtime.computerUseArtifactBrokerService.deleteArtifacts).toHaveBeenCalledWith({
artifactIds: [owned.id],
});

fixture.runtime.computerUseArtifactBrokerService.listArtifacts.mockClear();
const broken = await callTool(handler, "list_broken_computer_use_artifacts", { limit: 10 });
expect(broken.structuredContent.broken).toEqual([{ artifactId: owned.id }]);
expect(fixture.runtime.computerUseArtifactBrokerService.listArtifacts).toHaveBeenCalledTimes(2);

fixture.runtime.computerUseArtifactBrokerService.listArtifacts.mockClear();
fixture.runtime.computerUseArtifactBrokerService.deleteArtifacts.mockClear();
await callTool(handler, "prune_broken_computer_use_artifacts", {});
expect(fixture.runtime.computerUseArtifactBrokerService.listArtifacts).toHaveBeenCalledTimes(2);
expect(fixture.runtime.computerUseArtifactBrokerService.deleteArtifacts).toHaveBeenCalledWith({
artifactIds: [owned.id],
});

const foreignRecover = await callTool(handler, "recover_computer_use_artifact", {
artifactId: foreign.id,
});
expect(foreignRecover.isError).toBe(true);
expect(fixture.runtime.computerUseArtifactBrokerService.recoverArtifact).not.toHaveBeenCalled();

await callTool(handler, "recover_computer_use_artifact", { artifactId: owned.id });
expect(fixture.runtime.computerUseArtifactBrokerService.recoverArtifact).toHaveBeenCalledWith({
artifactId: owned.id,
});
});

it("sorts the scoped proof union before applying its limit", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
fixture.runtime.sessionService.get.mockReturnValue({ id: "chat-1", laneId: "lane-1" } as any);
const older = { id: "older", createdAt: "2026-07-29T01:00:00.000Z" };
const newer = { id: "newer", createdAt: "2026-07-29T02:00:00.000Z" };
fixture.runtime.computerUseArtifactBrokerService.listArtifacts.mockImplementation((args: any) => {
if (args.ownerKind === "chat_session") return [older];
if (args.ownerKind === "lane") return [newer];
return [];
});
await initialize(handler, {
callerId: "chat-1",
role: "agent",
chatSessionId: "chat-1",
});

const listed = await callTool(handler, "list_computer_use_artifacts", { limit: 1 });

expect(listed.structuredContent.artifacts).toEqual([newer]);
});

it("caps a session-bound CTO caller and scopes lifecycle actions to its own session", async () => {
await withEnv({ ADE_DEFAULT_ROLE: "cto", ADE_CHAT_SESSION_ID: undefined }, async () => {
const { runtime } = createRuntime();
Expand Down Expand Up @@ -1738,10 +1879,14 @@ describe("adeRpcServer", () => {
it("auto-links computer-use ingestion to standalone chat sessions", async () => {
const { runtime } = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" });
const laneRoot = runtime.laneService.getLaneWorktreePath("lane-1");
fs.mkdirSync(laneRoot, { recursive: true });
runtime.sessionService.get.mockReturnValue({ id: "chat-session-1", laneId: "lane-1" } as any);

await initialize(handler, {
callerId: "chat-session-1",
role: "agent",
chatSessionId: "chat-session-1",
});

await callTool(handler, "ingest_computer_use_artifacts", {
Expand All @@ -1751,7 +1896,7 @@ describe("adeRpcServer", () => {
{
kind: "screenshot",
title: "Chat proof",
path: "/tmp/chat-proof.png",
path: path.join(laneRoot, "chat-proof.png"),
},
],
});
Expand All @@ -1772,26 +1917,143 @@ describe("adeRpcServer", () => {
);
});

it("rejects computer-use manifests outside the project root", async () => {
it("forwards the caller's root so relative capture paths resolve in the agent's lane worktree", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
const outsideManifest = path.join(path.dirname(fixture.runtime.projectRoot), `ade-artifacts-${Date.now()}.json`);
fs.writeFileSync(outsideManifest, JSON.stringify([{ kind: "screenshot", path: "/tmp/shot.png" }]), "utf8");
const laneRoot = fixture.runtime.laneService.getLaneWorktreePath("lane-1");
fs.mkdirSync(laneRoot, { recursive: true });
fixture.runtime.sessionService.get.mockReturnValue({ id: "chat-session-1", laneId: "lane-1" } as any);

try {
await initialize(handler, { callerId: "chat-session-1", role: "agent" });
await initialize(handler, {
callerId: "chat-session-1",
role: "agent",
chatSessionId: "chat-session-1",
});
await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "manual",
backendName: "ade-cli",
callerRoot: laneRoot,
inputs: [{ kind: "screenshot", title: "Lane proof", path: "shots/proof.png" }],
});

expect(fixture.runtime.computerUseArtifactBrokerService.ingest).toHaveBeenCalledWith(
expect.objectContaining({ callerRoot: fs.realpathSync(laneRoot) }),
);
});

it("infers the lane scope for standalone ade proof attach calls", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
const laneRoot = fixture.runtime.laneService.getLaneWorktreePath("lane-1");
const callerRoot = path.join(laneRoot, "packages", "app");
fs.mkdirSync(callerRoot, { recursive: true });

await initialize(handler, {
callerId: "ade-cli:4242",
role: "agent",
});
const response = await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "manual",
backendName: "ade-cli",
toolName: "proof attach",
callerRoot,
inputs: [{ kind: "screenshot", title: "Standalone proof", path: path.join(callerRoot, "proof.png") }],
});

expect(response.isError).toBeUndefined();
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).toHaveBeenCalledWith(
expect.objectContaining({
callerRoot: fs.realpathSync(callerRoot),
owners: expect.arrayContaining([
expect.objectContaining({ kind: "lane", id: "lane-1" }),
]),
}),
);
});

it("rejects a relative caller root, which would resolve differently on each side", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
fs.mkdirSync(fixture.runtime.laneService.getLaneWorktreePath("lane-1"), { recursive: true });
fixture.runtime.sessionService.get.mockReturnValue({ id: "chat-session-1", laneId: "lane-1" } as any);

await initialize(handler, {
callerId: "chat-session-1",
role: "agent",
chatSessionId: "chat-session-1",
});
const response = await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "manual",
backendName: "ade-cli",
callerRoot: "../elsewhere",
inputs: [{ kind: "screenshot", title: "Proof", path: "shots/proof.png" }],
});

expect(response.isError).toBe(true);
expect(JSON.stringify(response.error ?? response.structuredContent ?? {})).toContain("absolute");
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).not.toHaveBeenCalled();
});

it("rejects caller roots and lane ids outside the server-authorized chat lane", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
fs.mkdirSync(fixture.runtime.laneService.getLaneWorktreePath("lane-1"), { recursive: true });
const lane2Root = fixture.runtime.laneService.getLaneWorktreePath("lane-2");
fs.mkdirSync(lane2Root, { recursive: true });
fixture.runtime.sessionService.get.mockReturnValue({ id: "chat-session-1", laneId: "lane-1" } as any);
await initialize(handler, {
callerId: "chat-session-1",
role: "agent",
chatSessionId: "chat-session-1",
});

for (const args of [
{
callerRoot: lane2Root,
inputs: [{ kind: "screenshot", title: "Wrong root", path: "shots/proof.png" }],
},
{
laneId: "lane-2",
inputs: [{ kind: "screenshot", title: "Wrong lane", path: "shots/proof.png" }],
},
{
inputs: [{ kind: "screenshot", title: "Wrong absolute path", path: path.join(lane2Root, "proof.png") }],
},
]) {
const response = await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "external_cli",
backendName: "agent-browser",
manifestPath: `../${path.basename(outsideManifest)}`,
backendStyle: "manual",
backendName: "ade-cli",
...args,
});

expect(response.isError).toBe(true);
expect(JSON.stringify(response.error ?? response.structuredContent ?? {})).toContain("project root");
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).not.toHaveBeenCalled();
} finally {
fs.rmSync(outsideManifest, { force: true });
}
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).not.toHaveBeenCalled();
});

it("allows absolute proof paths outside managed worktrees to reach the broker allow-list", async () => {
const fixture = createRuntime();
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });
fs.mkdirSync(fixture.runtime.laneService.getLaneWorktreePath("lane-1"), { recursive: true });
fixture.runtime.sessionService.get.mockReturnValue({ id: "chat-session-1", laneId: "lane-1" } as any);
await initialize(handler, {
callerId: "chat-session-1",
role: "agent",
chatSessionId: "chat-session-1",
});

const externalPath = path.join(os.tmpdir(), "ade-proof-external.png");
const response = await callTool(handler, "ingest_computer_use_artifacts", {
backendStyle: "external_cli",
backendName: "agent-browser",
inputs: [{ kind: "screenshot", title: "External proof", path: externalPath }],
});

expect(response.isError).toBeUndefined();
expect(fixture.runtime.computerUseArtifactBrokerService.ingest).toHaveBeenCalledWith(
expect.objectContaining({
inputs: [expect.objectContaining({ path: externalPath })],
}),
);
});


Expand Down
Loading