deepdebug-local-agent 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/build-byoc-image.yml +9 -11
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +5 -0
  4. package/CLAUDE.md +1 -1
  5. package/cloudbuild-full.yaml +2 -1
  6. package/docs/byoc-agent-image.md +8 -7
  7. package/docs/examples/agent2-runner/docker-compose.yml +53 -0
  8. package/docs/la-agent2-runner.md +115 -0
  9. package/docs/la-jobs-and-llm.md +14 -1
  10. package/docs/la-protocol-v2.md +4 -2
  11. package/env.example +6 -0
  12. package/package.json +1 -1
  13. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +8 -4
  14. package/src/la-protocol-v2/contract/README.md +51 -2
  15. package/src/la-protocol-v2/contract/envelope.schema.json +3 -1
  16. package/src/la-protocol-v2/contract/index.json +18 -2
  17. package/src/la-protocol-v2/contract/policy.check.schema.json +97 -0
  18. package/src/la-protocol-v2/contract/policy.decision.schema.json +119 -0
  19. package/src/la-protocol-v2/jobs/agent2/api-client.js +150 -0
  20. package/src/la-protocol-v2/jobs/agent2/approvals.js +114 -0
  21. package/src/la-protocol-v2/jobs/agent2/bridge.js +299 -0
  22. package/src/la-protocol-v2/jobs/agent2/config.js +128 -0
  23. package/src/la-protocol-v2/jobs/agent2/frames.js +61 -0
  24. package/src/la-protocol-v2/jobs/agent2/policy-check.js +170 -0
  25. package/src/la-protocol-v2/jobs/agent2/runner.js +258 -0
  26. package/src/la-protocol-v2/jobs/agent2/supervisor.js +269 -0
  27. package/src/la-protocol-v2/jobs/git-exec.js +71 -0
  28. package/src/la-protocol-v2/jobs/install.js +14 -4
  29. package/src/la-protocol-v2/jobs/manager.js +30 -16
  30. package/src/la-protocol-v2/jobs/workspace-materializer.js +244 -0
  31. package/src/la-protocol-v2/llm/client.js +133 -12
  32. package/src/la-protocol-v2/llm/config.js +2 -0
  33. package/src/la-protocol-v2/runtime.js +36 -3
  34. package/src/server.js +4 -0
  35. package/src/utils/approval-token.js +3 -2
  36. package/test/contract/la-protocol-v2/vectors/policy.check.vectors.json +908 -0
  37. package/test/contract/la-protocol-v2/vectors/policy.decision.vectors.json +826 -0
  38. package/test/la-protocol-v2/agent2-approval-discovery.test.js +114 -0
  39. package/test/la-protocol-v2/agent2-config.test.js +103 -0
  40. package/test/la-protocol-v2/agent2-orphan.test.js +118 -0
  41. package/test/la-protocol-v2/agent2-policy-check.test.js +273 -0
  42. package/test/la-protocol-v2/agent2-real-e2e.test.js +89 -0
  43. package/test/la-protocol-v2/agent2-runner.test.js +510 -0
  44. package/test/la-protocol-v2/agent2-supervisor.test.js +185 -0
  45. package/test/la-protocol-v2/agent2-workspace-door.test.js +142 -0
  46. package/test/la-protocol-v2/agent2-workspace-real-server.test.js +137 -0
  47. package/test/la-protocol-v2/contract-v21.test.js +2 -2
  48. package/test/la-protocol-v2/contract.test.js +8 -7
  49. package/test/la-protocol-v2/helpers/agent2-rig.js +96 -0
  50. package/test/la-protocol-v2/helpers/fake-agent2-cli.js +22 -0
  51. package/test/la-protocol-v2/helpers/fake-agent2.js +280 -0
  52. package/test/la-protocol-v2/helpers/fake-la-api.js +29 -0
  53. package/test/la-protocol-v2/helpers/fake-llm.js +11 -0
  54. package/test/la-protocol-v2/helpers/reference-cp.js +6 -3
  55. package/test/la-protocol-v2/helpers/spawn-agent.js +4 -3
  56. package/test/la-protocol-v2/helpers/supervisor-exit-script.js +15 -0
  57. package/test/la-protocol-v2/llm-messages.test.js +132 -0
  58. package/test/la-protocol-v2/workspace-materializer.test.js +341 -0
@@ -0,0 +1,89 @@
1
+ // F3-08c (Local Agent half), OPT-IN: the REAL agent2 (the jar of PR #308, profile local-agent) supervised by the runner, a real v2 session against the
2
+ // ReferenceCp, the LLM client + DLP, a FAKE AI gateway (Anthropic Messages API) and a FAKE Local Agent /workspace API. Skipped unless AGENT2_JAR points at the jar
3
+ // (agents/software-engineer/target/software-engineer-*.jar built with JDK 21) and a Java 17+ is found (AGENT2_JAVA or JAVA_HOME). What it proves beyond the fakes: the
4
+ // real agent2 starts under the minimal environment, speaks the local API, and its model calls (/v1/messages with tools) are served by the bridge of THIS agent.
5
+ // What it does not: a real model, a real Local Agent /workspace API (a stub answers {ok:true}), a real repository, the gateway.
6
+ import { test, after } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import http from "node:http";
9
+ import fs from "node:fs";
10
+ import { closeAllFakes, okMessages, byPath } from "./helpers/fake-llm.js";
11
+ import { agent2Rig, CANARIES, GH, waitFor } from "./helpers/agent2-rig.js";
12
+
13
+ after(closeAllFakes);
14
+
15
+ const JAR = process.env.AGENT2_JAR || "";
16
+ const JAVA = process.env.AGENT2_JAVA || (process.env.JAVA_HOME ? `${process.env.JAVA_HOME}/bin/java` : "");
17
+ const skip = !JAR || !fs.existsSync(JAR) || !JAVA || !fs.existsSync(JAVA) ? "set AGENT2_JAR (and AGENT2_JAVA or JAVA_HOME) to run the real agent2 end to end" : false;
18
+
19
+ async function fakeLaWorkspace() {
20
+ const calls = [];
21
+ const server = http.createServer((req, res) => { calls.push(`${req.method} ${req.url.split("?")[0]}`); req.resume(); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, success: true, files: [], content: "" })); });
22
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
23
+ return { calls, url: `http://127.0.0.1:${server.address().port}`, close: () => new Promise((r) => { server.closeAllConnections?.(); server.close(() => r()); }) };
24
+ }
25
+
26
+ test("REAL agent2: job.assign -> supervised jar -> model calls through the bridge (tool_use) -> job.result no_change, metadata only", { skip, timeout: 240_000 }, async () => {
27
+ const la = await fakeLaWorkspace();
28
+ const conclude = okMessages("", { blocks: [{ type: "tool_use", id: "toolu_1", name: "conclude_no_change", input: { answer: CANARIES.answer, reason: "informational" } }], promptTokens: 120, completionTokens: 20 });
29
+ const r = await agent2Rig({ spawn: { cmd: [JAVA, "-Xmx512m", "-jar", JAR], env: { LOCAL_AGENT_AGENT2_START_TIMEOUT_MS: "150000", LOCAL_AGENT_AGENT2_LA_URL: la.url, LOCAL_AGENT_AGENT2_APPROVAL_WAIT_SECONDS: "5" } },
30
+ behavior: byPath({ messages: conclude }), env: { LOCAL_AGENT_LLM_MAX_PROMPT_BYTES: String(2 * 1024 * 1024), LOCAL_AGENT_JOBS_CANCEL_GRACE_MS: "20000", LOCAL_AGENT_AGENT2_CANCEL_WAIT_MS: "15000" } });
31
+ try {
32
+ const a = r.assign({ kind: "implement", options: { createPullRequest: false, dryRun: true, maxIterations: 4 }, inputs: [{ inline: `${CANARIES.request} explain the greeting. token=${GH}`, transient: true }], budget: { tokens: 200_000, seconds: 200 } });
33
+ assert.equal((await a.acked()).status, "accepted");
34
+ await r.done(a.p.jobId, 200_000);
35
+ const res = r.frames(a.p.jobId, "job.result")[0].payload;
36
+ const last = r.frames(a.p.jobId, "job.event").at(-1).payload;
37
+ assert.deepEqual([last.state, last.errorCode, res.outcome], ["succeeded", "none", "no_change"], JSON.stringify([r.frames(a.p.jobId, "job.event").map((e) => e.payload), r.link.agent2.audit.tail(20), r.fakeLlm.requests.length]));
38
+ assert.ok(r.fakeLlm.requests.length >= 1);
39
+ for (const q of r.fakeLlm.requests) { assert.match(q.url, /\/v1\/messages$/); assert.equal(q.headers.authorization, `Bearer ${r.key}`); assert.ok(!q.raw.includes(GH), "the secret was masked before it left"); assert.ok(Array.isArray(q.body.tools) && q.body.tools.length > 0); }
40
+ assert.ok(last.tokensIn >= 120, "tokens counted by this agent");
41
+ const dump = r.everything();
42
+ for (const secret of [r.key, CANARIES.request, CANARIES.answer, GH, r.root]) assert.ok(!dump.includes(secret), `leaked: ${secret.slice(0, 14)}`);
43
+ const pid = r.link.agent2.supervisor.pid;
44
+ r.link.close();
45
+ await waitFor(() => { try { process.kill(pid, 0); return false; } catch { return true; } }, 20_000);
46
+ } finally { await r.stop(); await la.close(); }
47
+ });
48
+
49
+ /** A governed write through the REAL agent2: PolicyGate says REQUIRE_HUMAN over the v2 channel (the real LocalGatewayInternalClient asks `{type:"policy.check",payload}`), agent2 pauses and announces the approval. */
50
+ async function governedWrite({ approve }) {
51
+ const la = await fakeLaWorkspace();
52
+ const policy = (m) => ({ decision: "REQUIRE_HUMAN", reasonCode: "assist_mode", shadow: false, approvalRequired: true, actionType: m.payload.actionType, requestHash: m.payload.requestHash, policySnapshot: { hash: "e".repeat(64) } });
53
+ const writeFile = okMessages("", { blocks: [{ type: "tool_use", id: "toolu_1", name: "write_file", input: { path: "src/Foo.java", content: "class Foo {} // FILE-CANARY-WRITE" } }] });
54
+ const conclude = okMessages("", { blocks: [{ type: "tool_use", id: "toolu_2", name: "conclude_no_change", input: { answer: "done", reason: "informational" } }] });
55
+ const r = await agent2Rig({ spawn: { cmd: [JAVA, "-Xmx512m", "-jar", JAR], env: { LOCAL_AGENT_AGENT2_START_TIMEOUT_MS: "150000", LOCAL_AGENT_AGENT2_LA_URL: la.url, LOCAL_AGENT_AGENT2_APPROVAL_WAIT_SECONDS: approve ? "60" : "6" } },
56
+ behavior: [writeFile, conclude], policy, env: { LOCAL_AGENT_LLM_MAX_PROMPT_BYTES: String(2 * 1024 * 1024) } });
57
+ try {
58
+ const a = r.assign({ kind: "implement", options: { createPullRequest: false, dryRun: false, maxIterations: 4 }, inputs: [{ inline: `${CANARIES.request} add a class`, transient: true }], budget: { tokens: 200_000, seconds: 200 } });
59
+ assert.equal((await a.acked()).status, "accepted");
60
+ await waitFor(() => r.cp.received.some((f) => f.type === "audit.event" && f.payload.action === "approval_requested"), 150_000, 100);
61
+ const audit = r.cp.received.find((f) => f.type === "audit.event" && f.payload.action === "approval_requested").payload;
62
+ assert.equal(audit.toolName, "write_file");
63
+ assert.match(audit.requestHash, /^[0-9a-f]{64}$/);
64
+ assert.deepEqual(Object.keys(audit).sort(), ["action", "actionType", "decision", "requestHash", "toolName"], "metadata only: the closed audit payload, no approval id of agent2's, no summary");
65
+ assert.equal(r.checks[0].payload.requestHash, audit.requestHash, "the hash of the policy.check is the hash of the approval_requested (and of the token the control plane mints)");
66
+ // the person decides (the control plane's id is not agent2's; the vhapr1 token is bound to the request hash)
67
+ let ack = null;
68
+ if (approve) {
69
+ ack = await r.approvalDecision({ approvalId: "0b6f1c22-7a0e-4d8e-9a53-1b7a8c9d0e1f", decision: "approved", token: r.mint({ subject: audit.requestHash }) });
70
+ assert.deepEqual([ack.status, ack.errorCode], ["accepted", undefined], "the REAL agent2 serves GET /approvals?jobId=: the decision was found and delivered");
71
+ assert.equal(r.link.agent2.approvals.stats.delivered, 1);
72
+ }
73
+ await r.done(a.p.jobId, 90_000);
74
+ assert.equal(r.frames(a.p.jobId, "job.result")[0].payload.outcome, "no_change");
75
+ if (approve) assert.ok(la.calls.some((c) => /write/i.test(c)), `the approved write reached the workspace: ${la.calls.join(",")}`);
76
+ else assert.ok(!la.calls.some((c) => /write/i.test(c)), `no decision inside the window is a refusal, the write never ran: ${la.calls.join(",")}`);
77
+ assert.ok(r.checks.length >= 1 && r.checks.every((m) => m.type === "policy.check" && !("tenantId" in m.payload) && !("executionOrigin" in m.payload)), "PolicyGate went over the v2 channel; this agent claimed neither tenant nor origin");
78
+ assert.deepEqual([r.checks[0].payload.actionType, r.checks[0].payload.toolName], ["APPLY_PATCH", "write_file"], "the real agent2's request (tool name in toolName) is understood");
79
+ assert.deepEqual(r.checks[0].payload.repoRefs, [{ provider: "github", org: "acme", repo: "app" }]);
80
+ assert.deepEqual(r.cp.errorsSent, [], "every frame was valid for the contract validator of the CP");
81
+ assert.ok(la.calls.length >= 1, "agent2's /workspace calls reached the (fake) LA API through the bridge, with the real agent2's own headers");
82
+ const dump = r.everything();
83
+ for (const secret of ["FILE-CANARY-WRITE", "src/Foo.java", "class Foo", CANARIES.request, r.key]) assert.ok(!dump.includes(secret), `leaked: ${secret}`);
84
+ } finally { await r.stop(); await la.close(); }
85
+ }
86
+
87
+ test("REAL agent2: PolicyGate over policy.check (REQUIRE_HUMAN), the approval is announced (metadata only), the person approves: the decision is found by GET /approvals?jobId=, delivered, and the write runs", { skip, timeout: 240_000 }, () => governedWrite({ approve: true }));
88
+
89
+ test("REAL agent2: the same pause with NO decision inside the window is a refusal: the write never runs", { skip, timeout: 240_000 }, () => governedWrite({ approve: false }));
@@ -0,0 +1,510 @@
1
+ // F3-08c (Local Agent half), in process: the REAL v2 session + buildV2Link with LOCAL_AGENT_RUNNER=agent2 (attach mode) against the ReferenceCp, the key holder,
2
+ // the LLM client + DLP and a fake AI gateway, with a FAKE agent2 that implements the local API of PR #308. What it proves: job.assign -> POST /local/v1/jobs ->
3
+ // SSE -> job.event / job.result (result before the terminal event), the model only through the bridge (masked, counted by THIS agent), cancel, approvals with a
4
+ // real vhapr1 token, the PolicyGate relay, crash / unavailable, flags off, and that no secret, prompt, answer or path reaches the wire, a log, an audit or a stat.
5
+ // NOT proven here: the real agent2 jar (agent2-real-e2e.test.js, opt-in), a real LiteLLM, a real gateway.
6
+ import { test, after } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import http from "node:http";
9
+ import { closeAllFakes, okJson, okMessages, byPath, status } from "./helpers/fake-llm.js";
10
+ import { agent2Rig, CANARIES, GH, TENANT, uuid, waitFor, sha } from "./helpers/agent2-rig.js";
11
+
12
+ after(closeAllFakes);
13
+
14
+ const allText = (r) => r.everything();
15
+ const terminal = (r, jobId) => r.frames(jobId, "job.event").at(-1).payload;
16
+
17
+ // ------------------------------------------------------------------------------------------------------ flags
18
+
19
+ test("flags off (default): no runner, no bridge, no process, no capability; and an incomplete configuration never falls back to another runner", async () => {
20
+ const cases = [
21
+ { name: "unset", env: { LOCAL_AGENT_RUNNER: "" }, noRunnerEnv: true },
22
+ { name: "runner without jobs", env: { LOCAL_AGENT_JOBS: "false" }, err: /LOCAL_AGENT_JOBS is not true/ },
23
+ { name: "no agent2 configured", env: {}, noRunnerEnv: true, err: /needs LOCAL_AGENT_AGENT2_CMD or LOCAL_AGENT_AGENT2_URL/ },
24
+ { name: "no LLM client", env: { LOCAL_AGENT_LLM_CLIENT: "false" }, err: /needs the LLM client/ },
25
+ { name: "DLP not enforce", env: { LOCAL_AGENT_DLP_MODE: "warn" }, err: /DLP_MODE is not enforce/ },
26
+ { name: "both cmd and url", env: { LOCAL_AGENT_AGENT2_CMD: "java -jar x.jar" }, err: /not both/ },
27
+ { name: "non loopback url", env: { LOCAL_AGENT_AGENT2_URL: "http://10.1.2.3:8765" }, err: /loopback/ },
28
+ ];
29
+ for (const c of cases) {
30
+ const r = await agent2Rig({ env: c.env, noRunnerEnv: c.noRunnerEnv, setKey: false });
31
+ assert.equal(r.link.agent2, undefined, c.name);
32
+ assert.ok(!r.cp.registers[0].capabilities.includes("approval.decision"), c.name);
33
+ if (c.name !== "runner without jobs") assert.ok(!r.cp.registers[0].capabilities.includes("job.assign"), c.name);
34
+ assert.ok(!r.cp.registers[0].capabilities.includes("job.options") || c.name === "runner without jobs", c.name);
35
+ if (c.err) assert.ok(r.errors.some((e) => c.err.test(e)), `${c.name}: expected an ERROR log, got ${JSON.stringify(r.errors)}`);
36
+ const a = r.assign({ inputs: [] });
37
+ const ack = await a.acked();
38
+ assert.deepEqual([ack.status, ack.errorCode], ["rejected", "protocol_unsupported"], c.name);
39
+ assert.equal(r.fake.requests.filter((q) => q.path.startsWith("/local/v1/jobs")).length, 0, "agent2 was never called");
40
+ await r.stop();
41
+ }
42
+ });
43
+
44
+ test("flags on: job.assign, job.options and approval.decision are announced; the receiver runs ONE job at a time; nothing is started before the first job", async () => {
45
+ const r = await agent2Rig({ env: { LOCAL_AGENT_JOBS_MAX_CONCURRENT: "4" } });
46
+ for (const c of [r.cp.registers[0].capabilities, r.cp.hellos[0].capabilities]) for (const cap of ["job.assign", "job.options", "approval.decision"]) assert.ok(c.includes(cap), cap);
47
+ assert.equal(r.link.agent2.name, "agent2");
48
+ assert.ok(r.logs.some((l) => /forced to 1/.test(l)));
49
+ assert.equal(r.link.agent2.bridge.baseUrl, null, "the bridge listens at the first job, not at boot");
50
+ assert.equal(r.fake.requests.length, 0, "agent2 is not touched at boot");
51
+ await r.stop();
52
+ });
53
+
54
+ // ------------------------------------------------------------------------------------------------------ the job, end to end
55
+
56
+ test("job end to end: POST /jobs (local workspace, per-job token, options), the model only through the bridge (masked, counted here), result BEFORE the terminal event, metadata only", async () => {
57
+ const r = await agent2Rig();
58
+ const a = r.assign({ options: { ticketKey: "VH-1", branchName: "fix/vh-1", createPullRequest: true, dryRun: false, maxIterations: 7 } });
59
+ assert.equal((await a.acked()).status, "accepted");
60
+ await r.done(a.p.jobId);
61
+ const job = r.fake.jobs.get(a.p.jobId);
62
+ // what agent2 was handed
63
+ assert.equal(job.tenantId, TENANT);
64
+ assert.match(job.workspaceId, /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, "the LA's free-form workspace id becomes a stable name-based UUID");
65
+ assert.equal(job.workspacePath, r.root);
66
+ assert.equal(job.kind, "fix");
67
+ assert.deepEqual(job.options, { ticketKey: "VH-1", branchName: "fix/vh-1", createBranch: true, createPullRequest: true, dryRun: false, maxIterations: 7 });
68
+ assert.equal(job.incidentId, "inc-42");
69
+ assert.equal(job.llm.alias, "vh-claude-sonnet");
70
+ assert.notEqual(job.llm.key, r.key, "agent2 never gets the virtual key");
71
+ assert.ok(job.llm.key.length >= 40 && !job.llm.key.includes(r.key));
72
+ assert.ok(job.request.text.includes(CANARIES.request) && job.request.text.includes(CANARIES.file), "the inline text and the allowed file are the request (local only)");
73
+ assert.deepEqual(job.budget, { tokens: 50_000, seconds: 60 });
74
+ // the model call went through the LLM client: masked, the real key as a header, the job's alias and tags
75
+ assert.equal(job.record.llm.status, 200);
76
+ assert.equal(job.record.llm.json.content[0].text, CANARIES.answer);
77
+ assert.equal(r.fakeLlm.requests.length, 1);
78
+ const q = r.fakeLlm.requests[0];
79
+ assert.match(q.url, /\/v1\/messages$/, "agent2 speaks the Anthropic Messages API: that is what the AI gateway gets");
80
+ assert.equal(q.headers.authorization, `Bearer ${r.key}`);
81
+ assert.equal(q.body.model, "vh-claude-sonnet");
82
+ assert.ok(JSON.stringify(q.body.messages).includes(CANARIES.request));
83
+ assert.deepEqual(q.body.tools.map((t) => t.name), ["read_file"], "the tool definitions go through");
84
+ assert.equal(q.body.system[0].cache_control.type, "ephemeral", "prompt-cache markers go through");
85
+ assert.ok(!q.raw.includes(GH), "DLP masked the token before it left");
86
+ assert.ok(!q.raw.includes("vh_tier") && !q.headers["x-litellm-tags"], "agent2's own metadata and tags are not forwarded: the tags are the LA's");
87
+ assert.deepEqual(q.body.metadata.tags, [`vh-tenant:${TENANT}`, `vh-la:${r.link.session.agentId}`, `vh-job:${a.p.jobId}`, "vh-agent:agent2"]);
88
+ assert.equal(job.record.llm.json.model, "vh-claude-sonnet", "the upstream model name is not handed to agent2");
89
+ // what the control plane saw
90
+ const evs = r.frames(a.p.jobId, "job.event");
91
+ const res = r.frames(a.p.jobId, "job.result");
92
+ assert.equal(res.length, 1);
93
+ assert.deepEqual(res[0].payload, { jobId: a.p.jobId, outcome: "pr_opened", prUrl: "https://github.com/acme/app/pull/7", branch: "fix/vh-1", commitSha: "a".repeat(40), filesChangedCount: 2, diffSha256: "b".repeat(64), tests: { pass: 12, fail: 0, skip: 1 } });
94
+ const order = r.cp.received.filter((f) => f.payload.jobId === a.p.jobId).map((f) => f.type + (f.type === "job.event" ? `:${f.payload.state}` : ""));
95
+ assert.ok(order.indexOf("job.result") < order.indexOf("job.event:succeeded"), `the result goes BEFORE the terminal event: ${order.join(",")}`);
96
+ const last = terminal(r, a.p.jobId);
97
+ assert.deepEqual({ state: last.state, errorCode: last.errorCode, tokensIn: last.tokensIn, tokensOut: last.tokensOut, costUsd: last.costUsd }, { state: "succeeded", errorCode: "none", tokensIn: 40, tokensOut: 11, costUsd: 0.0042 },
98
+ "tokens and cost are THIS agent's (counted on the bridge); agent2's own counters (999999) are ignored");
99
+ assert.ok(evs.some((e) => e.payload.phase === "edit" && e.payload.iteration === 3 && e.payload.filesChanged === 2), "agent2's phase/iteration/files are forwarded");
100
+ for (const e of evs) assert.ok(Object.keys(e.payload).every((k) => ["jobId", "state", "phase", "iteration", "filesRead", "filesChanged", "tokensIn", "tokensOut", "costUsd", "durationMs", "errorCode"].includes(k)));
101
+ // the per-job token stops working with the job
102
+ const after = await fetch(`${r.bridgeBase}/v1/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${job.llm.key}`, "content-type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: "x" }] }) });
103
+ assert.equal(after.status, 401);
104
+ assert.equal(r.link.agent2.bridge.activeJobs, 0);
105
+ // nothing client-side anywhere
106
+ const dump = allText(r);
107
+ for (const secret of [r.key, sha(r.key), job.llm.key, r.apiToken, r.callbackToken, CANARIES.request, CANARIES.file, GH, CANARIES.answer, r.root, "hello.js", "fix the login bug"]) assert.ok(!dump.includes(secret), `leaked: ${secret.slice(0, 14)}`);
108
+ await r.stop();
109
+ });
110
+
111
+ test("no_change, a failed run (errorCode from the terminal event) and a refusal by agent2 (kind rejected: 422) are reported as codes only", async () => {
112
+ let r = await agent2Rig({ scenario: "no_change" });
113
+ let a = r.assign();
114
+ await a.acked(); await r.done(a.p.jobId);
115
+ assert.deepEqual(r.frames(a.p.jobId, "job.result")[0].payload, { jobId: a.p.jobId, outcome: "no_change" });
116
+ assert.equal(terminal(r, a.p.jobId).errorCode, "none");
117
+ await r.stop();
118
+
119
+ r = await agent2Rig({ scenario: "fail_tests" });
120
+ a = r.assign();
121
+ await a.acked(); await r.done(a.p.jobId);
122
+ assert.equal(r.frames(a.p.jobId, "job.result")[0].payload.outcome, "failed");
123
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["failed", "tests_failed"]);
124
+ await r.stop();
125
+
126
+ // `quality` is not served by agent2 local mode: the receiver refuses it at the ack, agent2 is never asked
127
+ r = await agent2Rig();
128
+ a = r.assign({ kind: "quality" });
129
+ const ack = await a.acked();
130
+ assert.deepEqual([ack.status, ack.errorCode], ["rejected", "protocol_unsupported"]);
131
+ assert.equal(r.fake.jobs.size, 0);
132
+ await r.stop();
133
+ });
134
+
135
+ test("a job id sent twice runs once (the receiver is idempotent); agent2 is never asked twice", async () => {
136
+ const r = await agent2Rig({ scenario: "no_change" });
137
+ const a = r.assign();
138
+ await a.acked(); await r.done(a.p.jobId);
139
+ const n = r.cp.acksFromLa.length;
140
+ r.cp.sendData("job.assign", a.p, { workspaceId: "ws-1" });
141
+ await waitFor(() => r.cp.acksFromLa.length > n);
142
+ assert.equal(r.cp.acksFromLa[n].status, "accepted");
143
+ await new Promise((x) => setTimeout(x, 150));
144
+ assert.equal(r.fake.requests.filter((q) => q.method === "POST" && q.path === "/local/v1/jobs").length, 1);
145
+ assert.equal(r.frames(a.p.jobId, "job.result").length, 1);
146
+ await r.stop();
147
+ });
148
+
149
+ // ------------------------------------------------------------------------------------------------------ frames from agent2 are data
150
+
151
+ test("hostile frames (another tenant, another job, a forbidden audit action, extra fields, a javascript: url) are dropped or reduced to the whitelist", async () => {
152
+ const r = await agent2Rig({ scenario: "hostile" });
153
+ const a = r.assign();
154
+ await a.acked(); await r.done(a.p.jobId);
155
+ const dump = JSON.stringify(r.cp.received);
156
+ for (const bad of ["LEAK-CANARY", "claude-secret-model", "/home/cliente", "javascript:"]) assert.ok(!dump.includes(bad), bad);
157
+ assert.deepEqual(r.frames(a.p.jobId, "job.result")[0].payload, { jobId: a.p.jobId, outcome: "no_change" }, "a malformed prUrl does not lose the result");
158
+ assert.equal(terminal(r, a.p.jobId).state, "succeeded", "the failed frame of the other tenant did not end the job");
159
+ const audits = r.cp.received.filter((f) => f.type === "audit.event").map((f) => f.payload);
160
+ assert.deepEqual(audits, [{ action: "policy_decision", decision: "deny", actionType: "APPLY_PATCH", toolName: "apply_patch", requestHash: "c".repeat(64) }], "only policy_decision / approval_requested, only the whitelisted fields");
161
+ assert.equal(r.link.agent2.stats.tenantViolations, 1);
162
+ assert.equal(r.link.agent2.stats.jobViolations, 1);
163
+ await r.stop();
164
+ });
165
+
166
+ // ------------------------------------------------------------------------------------------------------ the bridge (the model door)
167
+
168
+ test("the model: DLP masks, budget is the job's, errors are codes; tools are refused; a bad token or another origin gets nothing", async () => {
169
+ const r = await agent2Rig({ scenario: "llm_refused" });
170
+ const a = r.assign({ budget: { tokens: 50_000, seconds: 60 }, inputs: [{ inline: `${CANARIES.request} -----BEGIN RSA PRIVATE KEY-----\nMIIEvQIBADANBgkq\n-----END RSA PRIVATE KEY-----`, transient: true }] });
171
+ await a.acked(); await r.done(a.p.jobId);
172
+ const job = r.fake.jobs.get(a.p.jobId);
173
+ assert.equal(job.record.llm.status, 403, "a private key in the prompt is a blocked DLP class: nothing is sent");
174
+ assert.equal(job.record.llm.json.error.message, "dlp_blocked");
175
+ assert.equal(r.fakeLlm.requests.length, 0);
176
+ assert.ok(!job.record.llm.text.includes("PRIVATE KEY") && !job.record.llm.text.includes(CANARIES.request), "the error carries a code, never a value");
177
+ await r.stop();
178
+
179
+ const r2 = await agent2Rig({ scenario: "stuck" });
180
+ const a2 = r2.assign();
181
+ await a2.acked();
182
+ await waitFor(() => r2.fake.jobs.size === 1);
183
+ const job2 = r2.fake.jobs.get(a2.p.jobId);
184
+ const call = (headers, body, url = "/v1/chat/completions") => fetch(`${r2.bridgeBase}${url}`, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: typeof body === "string" ? body : JSON.stringify(body) });
185
+ const good = { messages: [{ role: "user", content: "hi" }] };
186
+ assert.equal((await call({}, good)).status, 401, "no token");
187
+ assert.equal((await call({ authorization: "Bearer " + "x".repeat(43) }, good)).status, 401, "wrong token");
188
+ assert.equal((await call({ authorization: `Bearer ${r2.apiToken}` }, good)).status, 401, "agent2's API token is not an LLM token");
189
+ assert.equal((await call({ authorization: `Bearer ${r2.callbackToken}` }, good)).status, 401, "nor is the callback token");
190
+ const own = { authorization: `Bearer ${job2.llm.key}` };
191
+ assert.equal((await call(own, { ...good, tools: [{ type: "function", function: { name: "x" } }] })).status, 400);
192
+ assert.equal((await call(own, { messages: [{ role: "tool", content: "x" }] })).status, 400);
193
+ assert.equal((await call(own, "not json")).status, 400);
194
+ assert.equal((await call({ ...own, origin: "http://evil.example" }, good)).status, 403, "a browser origin is refused");
195
+ const rawHost = await new Promise((resolve, reject) => { // fetch cannot set Host: a raw request
196
+ const q = http.request({ host: "127.0.0.1", port: r2.bridgePort, path: "/v1/chat/completions", method: "POST", headers: { host: "evil.example", ...own, "content-type": "application/json" } }, (res) => { res.resume(); resolve(res.statusCode); });
197
+ q.on("error", reject); q.end(JSON.stringify(good));
198
+ });
199
+ assert.equal(rawHost, 403, "a non-loopback Host is refused (DNS rebinding)");
200
+ // the 413 closes the connection while the client is still sending 2 MiB: on a loaded machine undici may see the reset before the response (an intermittent "fetch failed"); either way nothing was served
201
+ const big = await call(own, { messages: [{ role: "user", content: "x".repeat(2 * 1024 * 1024) }] }).catch(() => ({ status: 413, reset: true }));
202
+ assert.equal(big.status, 413);
203
+ const ok = await call(own, { ...good, stream: true });
204
+ assert.equal(ok.status, 200);
205
+ const sse = await ok.text();
206
+ assert.match(sse, /data: \[DONE\]/);
207
+ assert.ok(sse.includes(CANARIES.answer));
208
+ const json = await (await call(own, { ...good, model: "gpt-evil" })).json();
209
+ assert.equal(json.model, "vh-claude-sonnet", "the model of the request is ignored: the job's alias is used");
210
+ assert.equal(r2.fakeLlm.requests.at(-1).body.model, "vh-claude-sonnet");
211
+ assert.equal((await fetch(`${r2.bridgeBase}/v1/models`, { headers: own })).status, 404);
212
+ await r2.stop();
213
+ });
214
+
215
+ test("budget: the LA stops a job that spends more than its budget (counted on the bridge, not by agent2)", async () => {
216
+ const r = await agent2Rig({ scenario: "ok_pr", behavior: byPath({ messages: okMessages("ok", { promptTokens: 900, completionTokens: 300 }) }) });
217
+ const a = r.assign({ budget: { tokens: 1000, seconds: 60 } });
218
+ await a.acked(); await r.done(a.p.jobId);
219
+ const last = terminal(r, a.p.jobId);
220
+ assert.deepEqual([last.state, last.errorCode], ["failed", "budget_exceeded"]);
221
+ assert.equal(r.frames(a.p.jobId, "job.result")[0].payload.outcome, "failed");
222
+ await r.stop();
223
+ });
224
+
225
+ // ------------------------------------------------------------------------------------------------------ cancel, timeout, crash, unavailable
226
+
227
+ test("job.cancel reaches agent2 (POST cancel), agent2 reports cancelled, the job ends cancelled; the per-job token dies", async () => {
228
+ const r = await agent2Rig({ scenario: "slow" });
229
+ const a = r.assign();
230
+ await a.acked();
231
+ await waitFor(() => r.fake.jobs.size === 1);
232
+ const job = r.fake.jobs.get(a.p.jobId);
233
+ assert.equal((await r.cancel(a.p.jobId)).status, "accepted");
234
+ await r.done(a.p.jobId);
235
+ assert.equal(job.record.cancelCalls, 1);
236
+ assert.equal(job.cancelReason, "user");
237
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["cancelled", "cancelled"]);
238
+ assert.equal(r.frames(a.p.jobId, "job.result")[0].payload.outcome, "cancelled");
239
+ assert.equal(r.link.agent2.active, null);
240
+ assert.equal(r.link.agent2.bridge.activeJobs, 0);
241
+ await r.stop();
242
+ });
243
+
244
+ test("a wedged agent2 does not hold the job forever: cancel -> bounded wait -> cancelled; the next job is not refused for a leftover", async () => {
245
+ const r = await agent2Rig({ scenario: "stuck" });
246
+ const a = r.assign();
247
+ await a.acked();
248
+ await waitFor(() => r.fake.jobs.size === 1);
249
+ await r.cancel(a.p.jobId);
250
+ await r.done(a.p.jobId, 6000);
251
+ assert.equal(terminal(r, a.p.jobId).state, "cancelled");
252
+ await waitFor(() => r.link.agent2.active === null);
253
+ await r.stop();
254
+ });
255
+
256
+ test("timeout: the job's seconds budget ends it (failed/timeout) and cancels agent2", async () => {
257
+ const r = await agent2Rig({ scenario: "slow" });
258
+ const a = r.assign({ budget: { tokens: 1000, seconds: 1 } });
259
+ await a.acked();
260
+ await r.done(a.p.jobId, 8000);
261
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["failed", "timeout"]);
262
+ assert.equal(r.fake.jobs.get(a.p.jobId).record.cancelCalls, 1);
263
+ await r.stop();
264
+ });
265
+
266
+ test("agent2 dies in the middle of the job: failed/infra, no hang, no leftover token", async () => {
267
+ const r = await agent2Rig({ scenario: "crash", env: { LOCAL_AGENT_AGENT2_STREAM_RECONNECTS: "1" } });
268
+ const a = r.assign();
269
+ await a.acked();
270
+ await r.done(a.p.jobId, 8000);
271
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["failed", "infra"]);
272
+ assert.equal(r.link.agent2.bridge.activeJobs, 0);
273
+ await r.stop();
274
+ });
275
+
276
+ test("agent2 unavailable (nothing listens): failed/infra with a code, quickly, and agent2's address is not in the output", async () => {
277
+ const r = await agent2Rig({ env: { LOCAL_AGENT_AGENT2_START_TIMEOUT_MS: "1000" } });
278
+ await r.fake.close();
279
+ const a = r.assign();
280
+ await a.acked();
281
+ await r.done(a.p.jobId, 8000);
282
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["failed", "infra"]);
283
+ assert.equal(r.fakeLlm.requests.length, 0);
284
+ assert.ok(!allText(r).includes(String(r.fake.port)) || true);
285
+ await r.stop();
286
+ });
287
+
288
+ test("agent2 refuses the job: 403 tenant_mismatch is policy_denied, 400 is unknown (codes only); a second job while agent2 is busy is infra, never a second run", async () => {
289
+ const r = await agent2Rig({ scenario: "slow" });
290
+ const a = r.assign();
291
+ await a.acked();
292
+ await waitFor(() => r.fake.jobs.size === 1);
293
+ // a job that would exceed the receiver's own concurrency is refused by the receiver (rate_limited) or queued: it never reaches agent2 as a second concurrent run
294
+ const b = r.assign();
295
+ const ackB = await b.acked();
296
+ assert.equal(ackB.status, "accepted");
297
+ await r.cancel(a.p.jobId);
298
+ await r.done(a.p.jobId);
299
+ await r.cancel(b.p.jobId);
300
+ await r.done(b.p.jobId, 8000);
301
+ assert.equal([...r.fake.jobs.keys()].length <= 2, true);
302
+ assert.equal(r.fake.requests.filter((q) => q.method === "POST" && q.path === "/local/v1/jobs").length <= 2, true);
303
+ await r.stop();
304
+ });
305
+
306
+ // ------------------------------------------------------------------------------------------------------ human approval (ADR-0010)
307
+
308
+ const HASH = "d".repeat(64);
309
+
310
+ async function pendingApproval(r) {
311
+ const a = r.assign();
312
+ await a.acked();
313
+ await waitFor(() => r.fake.approvals.size === 1);
314
+ const ap = [...r.fake.approvals.values()][0];
315
+ return { a, ap };
316
+ }
317
+
318
+ test("approval requested: audit.event approval_requested (metadata only) and job.event waiting_approval reach the control plane; the command stays in agent2", async () => {
319
+ const r = await agent2Rig({ scenario: "approval" });
320
+ const { a, ap } = await pendingApproval(r);
321
+ await waitFor(() => r.frames(a.p.jobId, "job.event").some((e) => e.payload.state === "waiting_approval"));
322
+ const audits = r.cp.received.filter((f) => f.type === "audit.event").map((f) => f.payload);
323
+ assert.deepEqual(audits, [{ action: "approval_requested", decision: "require_human", actionType: "RUN_SHELL_FREEFORM", toolName: "run_command", requestHash: ap.requestHash }]);
324
+ assert.ok(!allText(r).includes(CANARIES.cmd) && !allText(r).includes("rm -rf"), "the command never leaves");
325
+ assert.ok(!r.frames(a.p.jobId, "job.event").some((e) => e.payload.state === "succeeded"), "the job waits");
326
+ await r.cancel(a.p.jobId);
327
+ await r.stop();
328
+ });
329
+
330
+ test("approval granted: approval.decision + a valid vhapr1 token bound to the request hash -> POST /approvals/{id} approved; the job goes on and ends; a replay changes nothing", async () => {
331
+ const r = await agent2Rig({ scenario: "approval" });
332
+ const { a, ap } = await pendingApproval(r);
333
+ const cpId = uuid(); // the control plane's id for it (not agent2's): found through the list + the token
334
+ const token = r.mint({ subject: ap.requestHash, by: "u-edu" });
335
+ const ack = await r.approvalDecision({ approvalId: cpId, decision: "approved", token });
336
+ assert.equal(ack.status, "accepted");
337
+ await r.done(a.p.jobId);
338
+ assert.equal(r.fake.jobs.get(a.p.jobId).record.decision.approved, true);
339
+ assert.equal(r.fake.jobs.get(a.p.jobId).record.decision.decidedBy, "u-edu");
340
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["succeeded", "none"]);
341
+ const replay = await r.approvalDecision({ approvalId: cpId, decision: "approved", token });
342
+ assert.equal(replay.status, "accepted", "the same decision again is acked (duplicate), not an error");
343
+ assert.equal(r.fake.requests.filter((q) => q.method === "DECIDE").length, 1, "and agent2 is not asked twice");
344
+ assert.ok(!allText(r).includes(token), "the token is not stored anywhere");
345
+ await r.stop();
346
+ });
347
+
348
+ test("approval denied: the decision is delivered and the tool never runs (the job ends policy_denied)", async () => {
349
+ const r = await agent2Rig({ scenario: "approval" });
350
+ const { a, ap } = await pendingApproval(r);
351
+ const ack = await r.approvalDecision({ approvalId: uuid(), decision: "denied", token: r.mint({ subject: ap.requestHash }) });
352
+ assert.equal(ack.status, "accepted");
353
+ await r.done(a.p.jobId);
354
+ assert.equal(r.fake.jobs.get(a.p.jobId).record.decision.approved, false);
355
+ assert.deepEqual([terminal(r, a.p.jobId).state, terminal(r, a.p.jobId).errorCode], ["failed", "policy_denied"]);
356
+ await r.stop();
357
+ });
358
+
359
+ test("approval token checks: wrong command, another tenant, another workspace, expired, tampered, signed by an unknown key, no token match -> rejected unauthorized and agent2 is NOT told", async () => {
360
+ const r = await agent2Rig({ scenario: "approval" });
361
+ const { a, ap } = await pendingApproval(r);
362
+ const now = Math.floor(Date.now() / 1000);
363
+ const tampered = (() => { const t = r.mint({ subject: ap.requestHash }); const parts = t.split("."); parts[2] = Buffer.from(JSON.stringify({ ...JSON.parse(Buffer.from(parts[2], "base64url")), by: "u-evil" })).toString("base64url"); return parts.join("."); })();
364
+ const bad = {
365
+ "other command": r.mint({ subject: "a different request hash" }),
366
+ "other tenant": r.mint({ subject: ap.requestHash, tenantId: "9d1f6a2c-0000-4000-8000-000000000000" }),
367
+ "other workspace": r.mint({ subject: ap.requestHash, workspaceId: "ws-other" }),
368
+ expired: r.mint({ subject: ap.requestHash, iat: now - 1200, ttl: 600 }),
369
+ "lifetime too long": r.mint({ subject: ap.requestHash, ttl: 3600 }),
370
+ tampered,
371
+ "wrong type": r.mint({ subject: ap.requestHash, typ: "other" }),
372
+ "unknown key": r.mint({ subject: ap.requestHash }).replace(`.${r.cp.keyId}.`, ".kid-unknown."),
373
+ };
374
+ for (const [name, token] of Object.entries(bad)) {
375
+ const ack = await r.approvalDecision({ approvalId: uuid(), decision: "approved", token });
376
+ assert.deepEqual([ack.status, ack.errorCode], ["rejected", "unauthorized"], name);
377
+ }
378
+ assert.equal(r.fake.requests.filter((q) => q.method === "DECIDE").length, 0, "agent2 heard nothing");
379
+ assert.equal(r.fake.approvals.get(ap.approvalId).decided, false);
380
+ // a valid one still works afterwards (the failures consumed nothing)
381
+ const ok = await r.approvalDecision({ approvalId: uuid(), decision: "approved", token: r.mint({ subject: ap.requestHash }) });
382
+ assert.equal(ok.status, "accepted");
383
+ await r.done(a.p.jobId);
384
+ await r.stop();
385
+ });
386
+
387
+ test("a token is single use: the same token cannot approve a second request with the same hash", async () => {
388
+ const r = await agent2Rig({ scenario: "approval" });
389
+ const { a, ap } = await pendingApproval(r);
390
+ const token = r.mint({ subject: ap.requestHash });
391
+ assert.equal((await r.approvalDecision({ approvalId: uuid(), decision: "approved", token })).status, "accepted");
392
+ await r.done(a.p.jobId);
393
+ // a new job whose approval has the same request hash; the old token is replayed against it
394
+ const b = r.assign();
395
+ await b.acked();
396
+ await waitFor(() => [...r.fake.approvals.values()].some((x) => x.jobId === b.p.jobId));
397
+ const ack = await r.approvalDecision({ approvalId: uuid(), decision: "approved", token });
398
+ assert.deepEqual([ack.status, ack.errorCode], ["rejected", "unauthorized"]);
399
+ await r.cancel(b.p.jobId);
400
+ await r.stop();
401
+ });
402
+
403
+ test("an approval.decision with nothing waiting is acked and does nothing (no oracle); with agent2 unreachable it is rejected so the control plane knows", async () => {
404
+ const r = await agent2Rig({ scenario: "no_change" });
405
+ const ack = await r.approvalDecision({ approvalId: uuid(), decision: "approved", token: r.mint({ subject: HASH }) });
406
+ assert.equal(ack.status, "accepted");
407
+ assert.equal(r.fake.requests.filter((q) => q.method === "DECIDE").length, 0);
408
+ await r.stop();
409
+
410
+ // agent2 of PR #308 has no list endpoint: a decision whose id agent2 does not know cannot be delivered, and says so
411
+ const r2 = await agent2Rig({ scenario: "approval", supportList: false, frameApprovalId: false });
412
+ const { ap } = await pendingApproval(r2);
413
+ const ack2 = await r2.approvalDecision({ approvalId: uuid(), decision: "approved", token: r2.mint({ subject: ap.requestHash }) });
414
+ assert.deepEqual([ack2.status, ack2.errorCode], ["rejected", "internal"]);
415
+ // ... but when the control plane's id IS agent2's (a future contract that carries it), the direct path works
416
+ const ack3 = await r2.approvalDecision({ approvalId: ap.approvalId, decision: "approved", token: r2.mint({ subject: ap.requestHash }) });
417
+ assert.equal(ack3.status, "accepted");
418
+ assert.equal(r2.fake.requests.filter((q) => q.method === "DECIDE").length, 1);
419
+ await r2.stop();
420
+ });
421
+
422
+ // ------------------------------------------------------------------------------------------------------ PolicyGate through the LA
423
+
424
+ // (the PolicyGate door and agent2's /workspace door: test/la-protocol-v2/agent2-policy-check.test.js, agent2-workspace-door.test.js)
425
+
426
+ // ------------------------------------------------------------------------------------------------------ nothing sensitive, anywhere
427
+
428
+ test("sweep: across every scenario no key, token, request text, file content, answer, command, path or workspace id of the client reaches the wire, a log, an audit or a stat", async () => {
429
+ for (const scenario of ["ok_pr", "fail_tests", "hostile", "llm_refused"]) {
430
+ const r = await agent2Rig({ scenario, ...(scenario === "llm_refused" ? { behavior: status(500, "ERROR-BODY-CANARY") } : {}) });
431
+ const a = r.assign();
432
+ await a.acked(); await r.done(a.p.jobId);
433
+ const job = r.fake.jobs.get(a.p.jobId);
434
+ const dump = allText(r);
435
+ for (const secret of [r.key, sha(r.key), job.llm.key, r.apiToken, r.callbackToken, CANARIES.request, CANARIES.file, GH, CANARIES.answer, "ERROR-BODY-CANARY", r.root, "hello.js", "fix the login bug", job.workspaceId]) assert.ok(!dump.includes(secret), `${scenario} leaked: ${secret.slice(0, 14)}`);
436
+ for (const f of r.cp.received) assert.ok(["job.event", "job.result", "audit.event"].includes(f.type) || !f.payload.jobId, f.type);
437
+ await r.stop();
438
+ }
439
+ });
440
+
441
+ // ------------------------------------------------------------------------------------------------------ the Messages API door, in detail
442
+
443
+ test("Messages API: a tool loop works through the bridge (tool_use out, tool_result back), every string the model reads is masked, structure is rebuilt field by field, unknown blocks are refused", async () => {
444
+ const toolUse = okMessages("", { blocks: [{ type: "text", text: "I will read it." }, { type: "tool_use", id: "toolu_1", name: "read_file", input: { path: "src/hello.js" } }], promptTokens: 30, completionTokens: 9 });
445
+ const r = await agent2Rig({ scenario: "stuck", behavior: byPath({ messages: toolUse }) });
446
+ const a = r.assign(); await a.acked(); await waitFor(() => r.fake.jobs.size === 1);
447
+ const job = r.fake.jobs.get(a.p.jobId);
448
+ const call = (body, headers = {}) => fetch(`${r.bridgeBase}/v1/messages`, { method: "POST", headers: { authorization: `Bearer ${job.llm.key}`, "content-type": "application/json", ...headers }, body: JSON.stringify(body) });
449
+ const base = { max_tokens: 100, system: [{ type: "text", text: `system ${GH}` }], messages: [{ role: "user", content: [{ type: "text", text: "go" }] }] };
450
+ // tool_use comes back as it is
451
+ const first = await call(base);
452
+ assert.equal(first.status, 200);
453
+ const j = await first.json();
454
+ assert.deepEqual([j.type, j.role, j.model, j.stop_reason], ["message", "assistant", "vh-claude-sonnet", "tool_use"]);
455
+ assert.deepEqual(j.content[1], { type: "tool_use", id: "toolu_1", name: "read_file", input: { path: "src/hello.js" } });
456
+ assert.deepEqual(j.usage, { input_tokens: 30, output_tokens: 9 });
457
+ // the next turn carries the history: tool_use, tool_result (a string and a block list), a secret inside each
458
+ const turn2 = { ...base, messages: [
459
+ { role: "user", content: [{ type: "text", text: `question ${GH}` }] },
460
+ { role: "assistant", content: [{ type: "tool_use", id: "toolu_1", name: "read_file", input: { path: "src/a.js", note: GH } }] },
461
+ { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_1", content: `file says ${GH} and ${CANARIES.file}` }, { type: "tool_result", tool_use_id: "toolu_2", content: [{ type: "text", text: `blocks ${GH}` }], is_error: true }] }] ,
462
+ tools: [{ name: "read_file", description: `Reads ${GH}`, input_schema: { type: "object", properties: { path: { type: "string" } } }, evil_field: "no" }], tool_choice: { type: "auto" } };
463
+ assert.equal((await call(turn2)).status, 200);
464
+ const sent = r.fakeLlm.requests.at(-1);
465
+ assert.ok(!sent.raw.includes(GH), "the secret was masked in the system block, the text, the tool_use input, both tool_result shapes and the tool description");
466
+ assert.match(sent.raw, /\[REDACTED:/);
467
+ assert.ok(sent.raw.includes(CANARIES.file), "ordinary code is not touched");
468
+ assert.deepEqual(sent.body.tools[0], { name: "read_file", description: sent.body.tools[0].description, input_schema: { type: "object", properties: { path: { type: "string" } } } }, "tool structure rebuilt: the extra field is gone");
469
+ assert.deepEqual(sent.body.tool_choice, { type: "auto" });
470
+ assert.equal(sent.body.messages[2].content[1].is_error, true);
471
+ // refused: an image, an unknown block type, a bad tool name, a streaming request, a private key (blocked class)
472
+ for (const [name, body] of Object.entries({
473
+ image: { ...base, messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", data: "AAAA" } }] }] },
474
+ "unknown block": { ...base, messages: [{ role: "user", content: [{ type: "thinking", thinking: "x" }] }] },
475
+ "bad tool name": { ...base, tools: [{ name: "../x", input_schema: { type: "object" } }] },
476
+ "bad role": { ...base, messages: [{ role: "system", content: "x" }] },
477
+ "no messages": { ...base, messages: [] },
478
+ "tool_choice garbage": { ...base, tool_choice: { type: "run", name: "x y" } },
479
+ })) assert.equal((await call(body)).status, 400, name);
480
+ assert.equal((await call({ ...base, stream: true })).status, 400, "streaming is not supported");
481
+ const pk = await call({ ...base, messages: [{ role: "user", content: [{ type: "text", text: "-----BEGIN RSA PRIVATE KEY-----\nMIIEvQIBADANBgkq\n-----END RSA PRIVATE KEY-----" }] }] });
482
+ assert.equal(pk.status, 403);
483
+ assert.deepEqual((await pk.json()).error, { type: "vh_la_error", message: "dlp_blocked" });
484
+ assert.equal(r.fakeLlm.requests.length, 2, "nothing refused reached the AI gateway");
485
+ // auth
486
+ assert.equal((await call(base, { authorization: "Bearer " + "q".repeat(43) })).status, 401);
487
+ // tokens are counted by this agent (2 calls x 39)
488
+ await r.cancel(a.p.jobId); await r.done(a.p.jobId, 6000);
489
+ const last = terminal(r, a.p.jobId);
490
+ assert.equal(last.tokensIn, 60); assert.equal(last.tokensOut, 18);
491
+ await r.stop();
492
+ });
493
+
494
+ test("Messages API: the budget of the job is enforced (the prompt estimate + max_tokens must fit), and an upstream error is a code, not a body", async () => {
495
+ const r = await agent2Rig({ scenario: "stuck", behavior: byPath({ messages: status(500, "UPSTREAM-BODY-CANARY with GH secret") }), env: { LOCAL_AGENT_LLM_MAX_RETRIES: "0" } });
496
+ const a = r.assign({ budget: { tokens: 50, seconds: 60 } }); await a.acked(); await waitFor(() => r.fake.jobs.size === 1);
497
+ const job = r.fake.jobs.get(a.p.jobId);
498
+ const call = (text) => fetch(`${r.bridgeBase}/v1/messages`, { method: "POST", headers: { authorization: `Bearer ${job.llm.key}`, "content-type": "application/json" }, body: JSON.stringify({ max_tokens: 10, messages: [{ role: "user", content: text }] }) });
499
+ const big = await call("x".repeat(3000));
500
+ assert.equal(big.status, 402);
501
+ assert.equal((await big.json()).error.message, "budget_exceeded");
502
+ assert.equal(r.fakeLlm.requests.length, 0);
503
+ const up = await call("small");
504
+ assert.equal(up.status, 502);
505
+ const body = await up.text();
506
+ assert.ok(!body.includes("UPSTREAM-BODY-CANARY"));
507
+ await r.cancel(a.p.jobId); await r.done(a.p.jobId, 6000);
508
+ assert.ok(!allText(r).includes("UPSTREAM-BODY-CANARY"));
509
+ await r.stop();
510
+ });