@koda-sl/baker-bridge 0.109.0-dev.f28c07bd2 → 0.109.0-dev.f7ef4c7e1
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.
- package/README.md +107 -51
- package/dist/broker/baker-api.d.ts +23 -0
- package/dist/broker/baker-api.d.ts.map +1 -0
- package/dist/broker/baker-api.js +55 -0
- package/dist/broker/baker-api.js.map +1 -0
- package/dist/broker/broker-client.d.ts +10 -0
- package/dist/broker/broker-client.d.ts.map +1 -0
- package/dist/broker/broker-client.js +60 -0
- package/dist/broker/broker-client.js.map +1 -0
- package/dist/broker/broker-process.d.ts +10 -0
- package/dist/broker/broker-process.d.ts.map +1 -0
- package/dist/broker/broker-process.js +20 -0
- package/dist/broker/broker-process.js.map +1 -0
- package/dist/broker/broker-process.test.d.ts +2 -0
- package/dist/broker/broker-process.test.d.ts.map +1 -0
- package/dist/broker/broker-process.test.js +13 -0
- package/dist/broker/broker-process.test.js.map +1 -0
- package/dist/broker/ca.d.ts +38 -0
- package/dist/broker/ca.d.ts.map +1 -0
- package/dist/broker/ca.js +80 -0
- package/dist/broker/ca.js.map +1 -0
- package/dist/broker/ca.test.d.ts +2 -0
- package/dist/broker/ca.test.d.ts.map +1 -0
- package/dist/broker/ca.test.js +58 -0
- package/dist/broker/ca.test.js.map +1 -0
- package/dist/broker/credential-cache.d.ts +24 -0
- package/dist/broker/credential-cache.d.ts.map +1 -0
- package/dist/broker/credential-cache.js +76 -0
- package/dist/broker/credential-cache.js.map +1 -0
- package/dist/broker/credential-cache.test.d.ts +2 -0
- package/dist/broker/credential-cache.test.d.ts.map +1 -0
- package/dist/broker/credential-cache.test.js +136 -0
- package/dist/broker/credential-cache.test.js.map +1 -0
- package/dist/broker/credential-text.d.ts +45 -0
- package/dist/broker/credential-text.d.ts.map +1 -0
- package/dist/broker/credential-text.js +103 -0
- package/dist/broker/credential-text.js.map +1 -0
- package/dist/broker/credential-text.test.d.ts +2 -0
- package/dist/broker/credential-text.test.d.ts.map +1 -0
- package/dist/broker/credential-text.test.js +54 -0
- package/dist/broker/credential-text.test.js.map +1 -0
- package/dist/broker/env.d.ts +21 -0
- package/dist/broker/env.d.ts.map +1 -0
- package/dist/broker/env.js +20 -0
- package/dist/broker/env.js.map +1 -0
- package/dist/broker/main.d.ts +3 -0
- package/dist/broker/main.d.ts.map +1 -0
- package/dist/broker/main.js +57 -0
- package/dist/broker/main.js.map +1 -0
- package/dist/broker/proxy.d.ts +24 -0
- package/dist/broker/proxy.d.ts.map +1 -0
- package/dist/broker/proxy.integration.test.d.ts +2 -0
- package/dist/broker/proxy.integration.test.d.ts.map +1 -0
- package/dist/broker/proxy.integration.test.js +197 -0
- package/dist/broker/proxy.integration.test.js.map +1 -0
- package/dist/broker/proxy.js +147 -0
- package/dist/broker/proxy.js.map +1 -0
- package/dist/broker/rewrite.d.ts +44 -0
- package/dist/broker/rewrite.d.ts.map +1 -0
- package/dist/broker/rewrite.js +291 -0
- package/dist/broker/rewrite.js.map +1 -0
- package/dist/broker/rewrite.test.d.ts +2 -0
- package/dist/broker/rewrite.test.d.ts.map +1 -0
- package/dist/broker/rewrite.test.js +171 -0
- package/dist/broker/rewrite.test.js.map +1 -0
- package/dist/broker/socket-service.d.ts +18 -0
- package/dist/broker/socket-service.d.ts.map +1 -0
- package/dist/broker/socket-service.js +113 -0
- package/dist/broker/socket-service.js.map +1 -0
- package/dist/broker/socket-service.test.d.ts +2 -0
- package/dist/broker/socket-service.test.d.ts.map +1 -0
- package/dist/broker/socket-service.test.js +80 -0
- package/dist/broker/socket-service.test.js.map +1 -0
- package/dist/broker/supervisor-main.d.ts +3 -0
- package/dist/broker/supervisor-main.d.ts.map +1 -0
- package/dist/broker/supervisor-main.js +70 -0
- package/dist/broker/supervisor-main.js.map +1 -0
- package/dist/broker/supervisor.d.ts +25 -0
- package/dist/broker/supervisor.d.ts.map +1 -0
- package/dist/broker/supervisor.js +88 -0
- package/dist/broker/supervisor.js.map +1 -0
- package/dist/broker/supervisor.test.d.ts +2 -0
- package/dist/broker/supervisor.test.d.ts.map +1 -0
- package/dist/broker/supervisor.test.js +161 -0
- package/dist/broker/supervisor.test.js.map +1 -0
- package/dist/broker/token-bucket.d.ts +9 -0
- package/dist/broker/token-bucket.d.ts.map +1 -0
- package/dist/broker/token-bucket.js +18 -0
- package/dist/broker/token-bucket.js.map +1 -0
- package/dist/broker/usage-reporter.d.ts +18 -0
- package/dist/broker/usage-reporter.d.ts.map +1 -0
- package/dist/broker/usage-reporter.js +31 -0
- package/dist/broker/usage-reporter.js.map +1 -0
- package/dist/broker/usage-reporter.test.d.ts +2 -0
- package/dist/broker/usage-reporter.test.d.ts.map +1 -0
- package/dist/broker/usage-reporter.test.js +20 -0
- package/dist/broker/usage-reporter.test.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/hono/agent-session.d.ts +4 -0
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +28 -2
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +37 -0
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent-tasks.d.ts.map +1 -1
- package/dist/hono/agent-tasks.js +11 -7
- package/dist/hono/agent-tasks.js.map +1 -1
- package/dist/hono/agent-tasks.test.js +15 -0
- package/dist/hono/agent-tasks.test.js.map +1 -1
- package/dist/hono/agent.d.ts +2 -0
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +19 -2
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/agent.test.js +8 -2
- package/dist/hono/agent.test.js.map +1 -1
- package/dist/hono/avatar-accent.d.ts +23 -5
- package/dist/hono/avatar-accent.d.ts.map +1 -1
- package/dist/hono/avatar-accent.js +54 -63
- package/dist/hono/avatar-accent.js.map +1 -1
- package/dist/hono/avatar-accent.test.js +64 -35
- package/dist/hono/avatar-accent.test.js.map +1 -1
- package/dist/hono/avatar-casting.d.ts +1 -0
- package/dist/hono/avatar-casting.d.ts.map +1 -1
- package/dist/hono/avatar-casting.js +18 -4
- package/dist/hono/avatar-casting.js.map +1 -1
- package/dist/hono/avatar-casting.test.js +5 -0
- package/dist/hono/avatar-casting.test.js.map +1 -1
- package/dist/hono/avatar-face-question.d.ts +23 -6
- package/dist/hono/avatar-face-question.d.ts.map +1 -1
- package/dist/hono/avatar-face-question.js +48 -59
- package/dist/hono/avatar-face-question.js.map +1 -1
- package/dist/hono/avatar-face-question.test.js +46 -101
- package/dist/hono/avatar-face-question.test.js.map +1 -1
- package/dist/hono/baker-ui-tool-schemas.test.js +30 -4
- package/dist/hono/baker-ui-tool-schemas.test.js.map +1 -1
- package/dist/hono/blocking-card.d.ts +12 -0
- package/dist/hono/blocking-card.d.ts.map +1 -0
- package/dist/hono/blocking-card.js +12 -0
- package/dist/hono/blocking-card.js.map +1 -0
- package/dist/hono/bridge-job.d.ts +68 -2
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +301 -64
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +489 -38
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/check-content.d.ts +49 -0
- package/dist/hono/check-content.d.ts.map +1 -0
- package/dist/hono/check-content.js +182 -0
- package/dist/hono/check-content.js.map +1 -0
- package/dist/hono/check-content.test.d.ts +2 -0
- package/dist/hono/check-content.test.d.ts.map +1 -0
- package/dist/hono/check-content.test.js +87 -0
- package/dist/hono/check-content.test.js.map +1 -0
- package/dist/hono/connection-input.d.ts +170 -12
- package/dist/hono/connection-input.d.ts.map +1 -1
- package/dist/hono/connection-input.js +166 -14
- package/dist/hono/connection-input.js.map +1 -1
- package/dist/hono/connection-input.test.js +112 -9
- package/dist/hono/connection-input.test.js.map +1 -1
- package/dist/hono/convex.d.ts +8 -0
- package/dist/hono/convex.d.ts.map +1 -1
- package/dist/hono/convex.js +8 -0
- package/dist/hono/convex.js.map +1 -1
- package/dist/hono/convex.test.js +4 -1
- package/dist/hono/convex.test.js.map +1 -1
- package/dist/hono/credential-input.d.ts +39 -0
- package/dist/hono/credential-input.d.ts.map +1 -0
- package/dist/hono/credential-input.js +78 -0
- package/dist/hono/credential-input.js.map +1 -0
- package/dist/hono/credential-scrub.d.ts +42 -0
- package/dist/hono/credential-scrub.d.ts.map +1 -0
- package/dist/hono/credential-scrub.js +173 -0
- package/dist/hono/credential-scrub.js.map +1 -0
- package/dist/hono/credential-scrub.test.d.ts +2 -0
- package/dist/hono/credential-scrub.test.d.ts.map +1 -0
- package/dist/hono/credential-scrub.test.js +130 -0
- package/dist/hono/credential-scrub.test.js.map +1 -0
- package/dist/hono/env.d.ts +1 -0
- package/dist/hono/env.d.ts.map +1 -1
- package/dist/hono/env.js +2 -0
- package/dist/hono/env.js.map +1 -1
- package/dist/hono/flow-crypto.d.ts +2 -1
- package/dist/hono/flow-crypto.d.ts.map +1 -1
- package/dist/hono/flow-crypto.js +50 -26
- package/dist/hono/flow-crypto.js.map +1 -1
- package/dist/hono/flow-crypto.test.js +32 -0
- package/dist/hono/flow-crypto.test.js.map +1 -1
- package/dist/hono/flow-input-card-fields.test.d.ts +10 -0
- package/dist/hono/flow-input-card-fields.test.d.ts.map +1 -0
- package/dist/hono/flow-input-card-fields.test.js +26 -0
- package/dist/hono/flow-input-card-fields.test.js.map +1 -0
- package/dist/hono/flow-input-fields.test.js +1 -1
- package/dist/hono/flow-input-fields.test.js.map +1 -1
- package/dist/hono/flow-input.d.ts +23 -2
- package/dist/hono/flow-input.d.ts.map +1 -1
- package/dist/hono/flow-input.js +63 -14
- package/dist/hono/flow-input.js.map +1 -1
- package/dist/hono/flow-input.test.js +67 -11
- package/dist/hono/flow-input.test.js.map +1 -1
- package/dist/hono/jev.d.ts +16 -0
- package/dist/hono/jev.d.ts.map +1 -0
- package/dist/hono/jev.js +60 -0
- package/dist/hono/jev.js.map +1 -0
- package/dist/hono/jev.test.d.ts +2 -0
- package/dist/hono/jev.test.d.ts.map +1 -0
- package/dist/hono/jev.test.js +15 -0
- package/dist/hono/jev.test.js.map +1 -0
- package/dist/hono/knowledge-content.d.ts.map +1 -1
- package/dist/hono/knowledge-content.js +4 -2
- package/dist/hono/knowledge-content.js.map +1 -1
- package/dist/hono/knowledge-content.test.js +6 -5
- package/dist/hono/knowledge-content.test.js.map +1 -1
- package/dist/hono/mcp-servers.d.ts +8 -1
- package/dist/hono/mcp-servers.d.ts.map +1 -1
- package/dist/hono/mcp-servers.js +17 -6
- package/dist/hono/mcp-servers.js.map +1 -1
- package/dist/hono/mcp-servers.test.js +25 -0
- package/dist/hono/mcp-servers.test.js.map +1 -1
- package/dist/hono/model-capabilities.d.ts.map +1 -1
- package/dist/hono/model-capabilities.js +20 -9
- package/dist/hono/model-capabilities.js.map +1 -1
- package/dist/hono/model-capabilities.test.js +16 -8
- package/dist/hono/model-capabilities.test.js.map +1 -1
- package/dist/hono/oom-priority.agent.test.d.ts +2 -0
- package/dist/hono/oom-priority.agent.test.d.ts.map +1 -0
- package/dist/hono/oom-priority.agent.test.js +138 -0
- package/dist/hono/oom-priority.agent.test.js.map +1 -0
- package/dist/hono/oom-priority.d.ts +39 -0
- package/dist/hono/oom-priority.d.ts.map +1 -0
- package/dist/hono/oom-priority.js +138 -0
- package/dist/hono/oom-priority.js.map +1 -0
- package/dist/hono/oom-priority.test.d.ts +2 -0
- package/dist/hono/oom-priority.test.d.ts.map +1 -0
- package/dist/hono/oom-priority.test.js +52 -0
- package/dist/hono/oom-priority.test.js.map +1 -0
- package/dist/hono/openrouter-session.d.ts.map +1 -1
- package/dist/hono/openrouter-session.js +12 -1
- package/dist/hono/openrouter-session.js.map +1 -1
- package/dist/hono/openrouter-session.test.js +9 -3
- package/dist/hono/openrouter-session.test.js.map +1 -1
- package/dist/hono/prompt-context.d.ts +8 -1
- package/dist/hono/prompt-context.d.ts.map +1 -1
- package/dist/hono/prompt-context.js +45 -7
- package/dist/hono/prompt-context.js.map +1 -1
- package/dist/hono/prompt-context.test.js +32 -1
- package/dist/hono/prompt-context.test.js.map +1 -1
- package/dist/hono/publish-recovery.d.ts +1 -1
- package/dist/hono/publish-recovery.d.ts.map +1 -1
- package/dist/hono/publish-recovery.js +9 -6
- package/dist/hono/publish-recovery.js.map +1 -1
- package/dist/hono/publish-recovery.test.js +11 -2
- package/dist/hono/publish-recovery.test.js.map +1 -1
- package/dist/hono/relay.d.ts +10 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js +6 -0
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/sandbox-content.d.ts +2 -0
- package/dist/hono/sandbox-content.d.ts.map +1 -1
- package/dist/hono/sandbox-content.js +8 -4
- package/dist/hono/sandbox-content.js.map +1 -1
- package/dist/hono/sandbox-content.test.js +20 -1
- package/dist/hono/sandbox-content.test.js.map +1 -1
- package/dist/hono/server.d.ts.map +1 -1
- package/dist/hono/server.js +32 -0
- package/dist/hono/server.js.map +1 -1
- package/dist/hono/server.test.js +21 -1
- package/dist/hono/server.test.js.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +10 -1
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/tag-fields.d.ts.map +1 -1
- package/dist/hono/tag-fields.js +2 -0
- package/dist/hono/tag-fields.js.map +1 -1
- package/dist/hono/tag-input.d.ts +124 -3
- package/dist/hono/tag-input.d.ts.map +1 -1
- package/dist/hono/tag-input.js +27 -21
- package/dist/hono/tag-input.js.map +1 -1
- package/dist/hono/tag-input.test.js +19 -1
- package/dist/hono/tag-input.test.js.map +1 -1
- package/dist/hono/tool-mentions.d.ts.map +1 -1
- package/dist/hono/tool-mentions.js +1 -0
- package/dist/hono/tool-mentions.js.map +1 -1
- package/dist/hono/tool-mentions.test.js +5 -0
- package/dist/hono/tool-mentions.test.js.map +1 -1
- package/dist/hono/types.d.ts +8 -0
- package/dist/hono/types.d.ts.map +1 -1
- package/dist/hono/workspace-script.d.ts +10 -0
- package/dist/hono/workspace-script.d.ts.map +1 -0
- package/dist/hono/workspace-script.js +19 -0
- package/dist/hono/workspace-script.js.map +1 -0
- package/dist/hono/workspace-settle.d.ts +20 -0
- package/dist/hono/workspace-settle.d.ts.map +1 -0
- package/dist/hono/workspace-settle.js +50 -0
- package/dist/hono/workspace-settle.js.map +1 -0
- package/dist/review/digest.d.ts +8 -1
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +10 -43
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +11 -6
- package/dist/review/digest.test.js.map +1 -1
- package/dist/review/evidence-block.js +1 -1
- package/dist/review/evidence-block.js.map +1 -1
- package/dist/review/evidence-block.test.js +0 -10
- package/dist/review/evidence-block.test.js.map +1 -1
- package/dist/review/shell-policy.d.ts +6 -0
- package/dist/review/shell-policy.d.ts.map +1 -1
- package/dist/review/shell-policy.js +240 -72
- package/dist/review/shell-policy.js.map +1 -1
- package/dist/review/shell-policy.test.js +114 -0
- package/dist/review/shell-policy.test.js.map +1 -1
- package/package.json +9 -5
- package/dist/hono/baker-ui-server.test.d.ts +0 -2
- package/dist/hono/baker-ui-server.test.d.ts.map +0 -1
- package/dist/hono/baker-ui-server.test.js +0 -46
- package/dist/hono/baker-ui-server.test.js.map +0 -1
- package/dist/hono/latitude-telemetry.d.ts +0 -17
- package/dist/hono/latitude-telemetry.d.ts.map +0 -1
- package/dist/hono/latitude-telemetry.js +0 -52
- package/dist/hono/latitude-telemetry.js.map +0 -1
- package/dist/hono/latitude-telemetry.test.d.ts +0 -2
- package/dist/hono/latitude-telemetry.test.d.ts.map +0 -1
- package/dist/hono/latitude-telemetry.test.js +0 -18
- package/dist/hono/latitude-telemetry.test.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -16,8 +17,10 @@ vi.mock("./runtime-files.ts", async (importOriginal) => {
|
|
|
16
17
|
};
|
|
17
18
|
});
|
|
18
19
|
import { AVATAR_CASTING_TOOL_NAME } from "./avatar-casting.js";
|
|
20
|
+
import { FACE_PICK_QUESTION } from "./avatar-face-question.js";
|
|
19
21
|
import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
|
|
20
22
|
import { CONNECTION_INPUT_TOOL_NAME } from "./connection-input.js";
|
|
23
|
+
import { CREDENTIAL_INPUT_TOOL_NAME } from "./credential-input.js";
|
|
21
24
|
import { FLOW_INPUT_TOOL_NAME } from "./flow-input.js";
|
|
22
25
|
import { bridgeRuntimeFiles } from "./runtime-files.js";
|
|
23
26
|
import { TAG_INPUT_TOOL_NAME } from "./tag-input.js";
|
|
@@ -49,6 +52,7 @@ function activeControls() {
|
|
|
49
52
|
pendingTagInputs: new Map(),
|
|
50
53
|
pendingFlowInputs: new Map(),
|
|
51
54
|
pendingConnectionInputs: new Map(),
|
|
55
|
+
pendingCredentialInputs: new Map(),
|
|
52
56
|
pendingVoiceCasts: new Map(),
|
|
53
57
|
pendingAvatarCastings: new Map(),
|
|
54
58
|
isActive: () => active,
|
|
@@ -82,6 +86,8 @@ function recordingRelay() {
|
|
|
82
86
|
flowInputResolved: () => Promise.resolve(),
|
|
83
87
|
connectionInputRequest: () => Promise.resolve(true),
|
|
84
88
|
connectionInputResolved: () => Promise.resolve(),
|
|
89
|
+
credentialInputRequest: () => Promise.resolve(true),
|
|
90
|
+
credentialInputResolved: () => Promise.resolve(),
|
|
85
91
|
avatarCastingRequest: () => Promise.resolve(true),
|
|
86
92
|
avatarCastingResolved: () => Promise.resolve(),
|
|
87
93
|
assistantEvent: (target, event, model) => {
|
|
@@ -222,7 +228,10 @@ function createExecutor(options) {
|
|
|
222
228
|
stallTimeoutMs: options.stallTimeoutMs,
|
|
223
229
|
stallCheckIntervalMs: options.stallCheckIntervalMs,
|
|
224
230
|
turnEndSettleMs: options.turnEndSettleMs,
|
|
231
|
+
// A test that ends on a silent settle waits no longer than its settle window.
|
|
232
|
+
turnEndWakeMs: options.turnEndWakeMs ?? options.turnEndSettleMs,
|
|
225
233
|
turnEndExitGuardMs: options.turnEndExitGuardMs,
|
|
234
|
+
settleWorkspace: options.settleWorkspace ?? (() => Promise.resolve()),
|
|
226
235
|
...(options.turnEvents
|
|
227
236
|
? { recordTurnEvent: (event) => options.turnEvents?.push(event) }
|
|
228
237
|
: {}),
|
|
@@ -315,12 +324,12 @@ describe("isControlChannelDeny", () => {
|
|
|
315
324
|
})).toBe(false);
|
|
316
325
|
});
|
|
317
326
|
it("does not match a PreToolUse hook block that quotes the envelope in its explanation", () => {
|
|
318
|
-
// Live-run finding: the scaffold's guard-background-commands hook
|
|
319
|
-
//
|
|
327
|
+
// Live-run finding: the scaffold's guard-background-commands hook, when it
|
|
328
|
+
// still blocked backgrounded subagents, explained why by quoting the failure.
|
|
320
329
|
// Its block arrives as an is_error tool_result, so an envelope-substring
|
|
321
330
|
// match counted it as a deny — three blocked spawns in one turn would have
|
|
322
331
|
// tripped the breaker and errored a perfectly healthy turn.
|
|
323
|
-
expect(isControlChannelDeny(controlChannelDenyMessage("PreToolUse:Agent hook error: [node __tooling__/
|
|
332
|
+
expect(isControlChannelDeny(controlChannelDenyMessage("PreToolUse:Agent hook error: [node __tooling__/checks/base/guard-background-commands/guard-background-commands.mjs]: " +
|
|
324
333
|
"✗ guard-background-commands — blocked: subagent spawned in the background.\n" +
|
|
325
334
|
" A subagent that outlives the turn loses its permission channel and orphans\n" +
|
|
326
335
|
" any work it started (a render inside it fails 'Tool permission request failed')."))).toBe(false);
|
|
@@ -1096,7 +1105,10 @@ describe("BridgeJobExecutor", () => {
|
|
|
1096
1105
|
// web-plugin opt-out at their integration point: all must actually reach the
|
|
1097
1106
|
// subprocess body via CLAUDE_CODE_EXTRA_BODY.
|
|
1098
1107
|
expect(JSON.parse(env?.CLAUDE_CODE_EXTRA_BODY ?? "{}")).toEqual({
|
|
1099
|
-
provider: {
|
|
1108
|
+
provider: {
|
|
1109
|
+
order: ["amazon-bedrock", "amazon-bedrock/eu-west-1", "amazon-bedrock/us-east-1"],
|
|
1110
|
+
allow_fallbacks: true,
|
|
1111
|
+
},
|
|
1100
1112
|
session_id: "thread_env",
|
|
1101
1113
|
plugins: [{ id: "web", enabled: false }],
|
|
1102
1114
|
});
|
|
@@ -1112,18 +1124,6 @@ describe("BridgeJobExecutor", () => {
|
|
|
1112
1124
|
await executor.execute({ threadId: "thread_bedrock", turnId: "turn_bedrock", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1113
1125
|
expect(queryInputs[0]?.options.env).toMatchObject({ CLAUDE_CODE_MODEL_CAPABILITIES: "-per_turn_effort" });
|
|
1114
1126
|
});
|
|
1115
|
-
// Behind the OpenRouter base URL the CLI loads every MCP tool schema up front:
|
|
1116
|
-
// a connected Make toolkit put ~180 schemas (~115k tokens) into every request of
|
|
1117
|
-
// a Session that called two of them. Tool search defers them to their names.
|
|
1118
|
-
it("defers MCP tool schemas behind tool search instead of loading them into every request", async () => {
|
|
1119
|
-
const queryInputs = [];
|
|
1120
|
-
const executor = createExecutor({
|
|
1121
|
-
queryInputs,
|
|
1122
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_tool_search"))],
|
|
1123
|
-
});
|
|
1124
|
-
await executor.execute({ threadId: "thread_tool_search", turnId: "turn_tool_search", prompt: "Check Make", history: history([]) }, activeControls());
|
|
1125
|
-
expect(queryInputs[0]?.options.env).toMatchObject({ ENABLE_TOOL_SEARCH: "true" });
|
|
1126
|
-
});
|
|
1127
1127
|
it("silences the CLI task reminders and ANSI colour in the agent subprocess", async () => {
|
|
1128
1128
|
const queryInputs = [];
|
|
1129
1129
|
const executor = createExecutor({
|
|
@@ -1136,7 +1136,31 @@ describe("BridgeJobExecutor", () => {
|
|
|
1136
1136
|
NO_COLOR: "1",
|
|
1137
1137
|
});
|
|
1138
1138
|
});
|
|
1139
|
-
//
|
|
1139
|
+
// Without the wrapper, a burst of parallel commands can fill the sandbox's
|
|
1140
|
+
// memory and the OOM killer takes the agent or the credential broker instead.
|
|
1141
|
+
it("runs every agent command through the kill-first wrapper, whatever started the bridge", async () => {
|
|
1142
|
+
const queryInputs = [];
|
|
1143
|
+
const executor = createExecutor({
|
|
1144
|
+
queryInputs,
|
|
1145
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_wrapped"))],
|
|
1146
|
+
});
|
|
1147
|
+
await executor.execute({ threadId: "thread_wrapped", turnId: "turn_wrapped", prompt: "Research competitors", history: history([]) }, activeControls());
|
|
1148
|
+
const prefix = queryInputs[0]?.options.env?.CLAUDE_CODE_SHELL_PREFIX ?? "";
|
|
1149
|
+
expect(readFileSync(prefix, "utf8")).toContain("oom_score_adj");
|
|
1150
|
+
});
|
|
1151
|
+
// The CLI clamps a Bash call's timeout to 10 minutes by default, so the
|
|
1152
|
+
// workspace hook's 30 minutes for `baker canvas run` became 600 s and a
|
|
1153
|
+
// render with a repair round was moved to the background (dev, Oct 2026).
|
|
1154
|
+
it("lets a Bash call run the 30 minutes the workspace's timeout hook gives a render", async () => {
|
|
1155
|
+
const queryInputs = [];
|
|
1156
|
+
const executor = createExecutor({
|
|
1157
|
+
queryInputs,
|
|
1158
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_long"))],
|
|
1159
|
+
});
|
|
1160
|
+
await executor.execute({ threadId: "thread_long", turnId: "turn_long", prompt: "Render the ad", history: history([]) }, activeControls());
|
|
1161
|
+
expect(Number(queryInputs[0]?.options.env?.BASH_MAX_TIMEOUT_MS)).toBeGreaterThanOrEqual(30 * 60_000);
|
|
1162
|
+
});
|
|
1163
|
+
// The four shared subagents pin themselves to Sonnet 5.5 in their own
|
|
1140
1164
|
// frontmatter. Since CLI 2.1.251 CLAUDE_CODE_SUBAGENT_MODEL is only the default
|
|
1141
1165
|
// under that pin, so the override needs the FORCE flag to outrank it.
|
|
1142
1166
|
it("sends the Runtime's model override down to the fan-out workers", async () => {
|
|
@@ -1153,7 +1177,7 @@ describe("BridgeJobExecutor", () => {
|
|
|
1153
1177
|
});
|
|
1154
1178
|
});
|
|
1155
1179
|
// Production pins nothing, and the variable must then be absent rather than
|
|
1156
|
-
// empty: sending it would strip every worker's own Sonnet 5 pin.
|
|
1180
|
+
// empty: sending it would strip every worker's own Sonnet 5.5 pin.
|
|
1157
1181
|
it("leaves the workers on their own pinned model when nothing is overridden", async () => {
|
|
1158
1182
|
vi.stubEnv("BAKER_AGENT_MODEL", "");
|
|
1159
1183
|
const queryInputs = [];
|
|
@@ -1182,8 +1206,7 @@ describe("BridgeJobExecutor", () => {
|
|
|
1182
1206
|
// Read off a session's own `init` tool list, the CLI offers these to every
|
|
1183
1207
|
// Session. Each one reaches past what a marketer's chat is for: the worktree
|
|
1184
1208
|
// pair rearranges the branches Publish owns, `Workflow` fans out a fleet on
|
|
1185
|
-
// the organization's OpenRouter key
|
|
1186
|
-
// single-session Runtime does not have.
|
|
1209
|
+
// the organization's OpenRouter key.
|
|
1187
1210
|
it("removes the tools that reach outside what a Session is", async () => {
|
|
1188
1211
|
const queryInputs = [];
|
|
1189
1212
|
const executor = createExecutor({
|
|
@@ -1192,10 +1215,12 @@ describe("BridgeJobExecutor", () => {
|
|
|
1192
1215
|
});
|
|
1193
1216
|
await executor.execute({ threadId: "thread_reach", turnId: "turn_reach", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1194
1217
|
const disallowed = queryInputs[0]?.options.disallowedTools ?? [];
|
|
1195
|
-
expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow"
|
|
1218
|
+
expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow"]));
|
|
1196
1219
|
// `Task` is the subagent spawn itself — the scaffold's own fan-out workers
|
|
1197
|
-
// ride it, so blocking multi-agent orchestration must not catch it.
|
|
1220
|
+
// ride it, so blocking multi-agent orchestration must not catch it. And
|
|
1221
|
+
// `SendMessage` is how the agent follows up with a subagent it already ran.
|
|
1198
1222
|
expect(disallowed).not.toContain("Task");
|
|
1223
|
+
expect(disallowed).not.toContain("SendMessage");
|
|
1199
1224
|
});
|
|
1200
1225
|
it("allowlists the workspace's own skills so the CLI's built-in developer skills stay out of the session", async () => {
|
|
1201
1226
|
const workspace = await mkdtemp(join(tmpdir(), "baker-job-skills-"));
|
|
@@ -1383,6 +1408,24 @@ describe("BridgeJobExecutor", () => {
|
|
|
1383
1408
|
await executor.execute({ threadId: "thread_uw", turnId: "turn_uw", prompt: "Hi", history: history([]) }, activeControls());
|
|
1384
1409
|
expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("autoCompactWindow");
|
|
1385
1410
|
});
|
|
1411
|
+
it("asks for manual approvals outside plan mode, so canUseTool decides every tool call instead of auto mode", async () => {
|
|
1412
|
+
const queryInputs = [];
|
|
1413
|
+
const executor = createExecutor({
|
|
1414
|
+
queryInputs,
|
|
1415
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_permission_mode"))],
|
|
1416
|
+
});
|
|
1417
|
+
await executor.execute({ threadId: "thread_pm", turnId: "turn_pm", prompt: "Hi", history: history([]) }, activeControls());
|
|
1418
|
+
expect(queryInputs[0]?.options.permissionMode).toBe("default");
|
|
1419
|
+
});
|
|
1420
|
+
it("keeps subagent prompt caches for an hour, so a subagent outliving a long render still reads its cache", async () => {
|
|
1421
|
+
const queryInputs = [];
|
|
1422
|
+
const executor = createExecutor({
|
|
1423
|
+
queryInputs,
|
|
1424
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_subagent_ttl"))],
|
|
1425
|
+
});
|
|
1426
|
+
await executor.execute({ threadId: "thread_ttl", turnId: "turn_ttl", prompt: "Hi", history: history([]) }, activeControls());
|
|
1427
|
+
expect(queryInputs[0]?.options.settings).toMatchObject({ subagentPromptCacheTtl: "1h" });
|
|
1428
|
+
});
|
|
1386
1429
|
// No output style is sent. `Concise` was pinned against Opus 5's preamble; on
|
|
1387
1430
|
// Opus 5.5 it suppressed the one-line progress notes a marketer reads while a
|
|
1388
1431
|
// long turn works. The voice rules live in the scaffold CLAUDE.md instead.
|
|
@@ -1395,20 +1438,6 @@ describe("BridgeJobExecutor", () => {
|
|
|
1395
1438
|
await executor.execute({ threadId: "thread_style", turnId: "turn_style_1", prompt: "Hi", history: history([]) }, activeControls());
|
|
1396
1439
|
expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("outputStyle");
|
|
1397
1440
|
});
|
|
1398
|
-
// Nothing to put in the settings tier (unknown model window, no chat id for the
|
|
1399
|
-
// memory directory) means no `settings` key at all: the SDK forwards even `{}`
|
|
1400
|
-
// to the CLI as a `--settings` flag tier.
|
|
1401
|
-
it("omits the settings tier when nothing in it applies", async () => {
|
|
1402
|
-
vi.stubEnv("BAKER_CHAT_ID", "");
|
|
1403
|
-
const queryInputs = [];
|
|
1404
|
-
const executor = createExecutor({
|
|
1405
|
-
queryInputs,
|
|
1406
|
-
model: "claude-unknown",
|
|
1407
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
|
|
1408
|
-
});
|
|
1409
|
-
await executor.execute({ threadId: "thread_ns", turnId: "turn_ns", prompt: "Hi", history: history([]) }, activeControls());
|
|
1410
|
-
expect(queryInputs[0]?.options).not.toHaveProperty("settings");
|
|
1411
|
-
});
|
|
1412
1441
|
it("runs exact publish commands with the prompt passed through verbatim", async () => {
|
|
1413
1442
|
const queryInputs = [];
|
|
1414
1443
|
const executor = createExecutor({
|
|
@@ -1453,6 +1482,24 @@ describe("BridgeJobExecutor", () => {
|
|
|
1453
1482
|
expect(queryInputs[0]?.prompt).toBe("make the headline bigger");
|
|
1454
1483
|
expect(queryInputs[0]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1455
1484
|
});
|
|
1485
|
+
it("explains a credential the message names beside it, together with the viewing reference", async () => {
|
|
1486
|
+
const queryInputs = [];
|
|
1487
|
+
const executor = createExecutor({
|
|
1488
|
+
queryInputs,
|
|
1489
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_credential"))],
|
|
1490
|
+
});
|
|
1491
|
+
await executor.execute({
|
|
1492
|
+
threadId: "thread_credential",
|
|
1493
|
+
turnId: "turn_credential",
|
|
1494
|
+
prompt: "log in with user admin and password {{credential:password}}",
|
|
1495
|
+
history: history([]),
|
|
1496
|
+
currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
|
|
1497
|
+
credentials: [{ name: "password", label: "Login password", allowedHosts: [] }],
|
|
1498
|
+
}, activeControls());
|
|
1499
|
+
expect(queryInputs[0]?.prompt).toBe("log in with user admin and password {{credential:password}}");
|
|
1500
|
+
expect(queryInputs[0]?.reminder).toContain('landing "Homepage"');
|
|
1501
|
+
expect(queryInputs[0]?.reminder).toContain("`{{credential:password}}` is a credential the user saved");
|
|
1502
|
+
});
|
|
1456
1503
|
it("strips newlines from the viewing reference so a crafted title can't inject instructions", async () => {
|
|
1457
1504
|
const queryInputs = [];
|
|
1458
1505
|
const executor = createExecutor({
|
|
@@ -2124,6 +2171,224 @@ describe("BridgeJobExecutor", () => {
|
|
|
2124
2171
|
expect(connectionRequests).toHaveLength(1);
|
|
2125
2172
|
expect(decision).toMatchObject({ behavior: "allow" });
|
|
2126
2173
|
});
|
|
2174
|
+
it("loads a server the user connected from the card into this same turn, and tells the agent it is ready", async () => {
|
|
2175
|
+
const relay = recordingRelay();
|
|
2176
|
+
relay.connectionInputRequest = () => Promise.resolve(true);
|
|
2177
|
+
const controls = activeControls();
|
|
2178
|
+
const acme = { type: "http", url: "https://acme.dev/mcp" };
|
|
2179
|
+
const asks = [];
|
|
2180
|
+
const loaded = [];
|
|
2181
|
+
let decision;
|
|
2182
|
+
const executor = new BridgeJobExecutor({
|
|
2183
|
+
sdk: {
|
|
2184
|
+
query: (input) => ({
|
|
2185
|
+
async *[Symbol.asyncIterator]() {
|
|
2186
|
+
const asked = input.options.canUseTool?.(CONNECTION_INPUT_TOOL_NAME, { connections: [{ server: { name: "acme", url: "https://acme.dev/mcp", auth: "none" }, reason: "r" }] }, { toolUseID: "t1", requestId: "req_t1", signal: new AbortController().signal });
|
|
2187
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2188
|
+
for (const [, pending] of controls.pendingConnectionInputs) {
|
|
2189
|
+
pending.resolve({ results: [{ status: "connected", server: "acme", scope: "company" }] });
|
|
2190
|
+
}
|
|
2191
|
+
decision = await asked;
|
|
2192
|
+
yield resultMessage(0, "session_after_connect");
|
|
2193
|
+
},
|
|
2194
|
+
setMcpServers: (servers) => {
|
|
2195
|
+
loaded.push(servers);
|
|
2196
|
+
return Promise.resolve({ added: ["acme"], removed: [], errors: {} });
|
|
2197
|
+
},
|
|
2198
|
+
}),
|
|
2199
|
+
},
|
|
2200
|
+
model: "claude-test",
|
|
2201
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2202
|
+
relay,
|
|
2203
|
+
mcpServersProvider: (_threadId, _actor, options) => {
|
|
2204
|
+
asks.push(options ?? {});
|
|
2205
|
+
const servers = options?.fresh ? { acme } : {};
|
|
2206
|
+
return Promise.resolve(servers);
|
|
2207
|
+
},
|
|
2208
|
+
});
|
|
2209
|
+
await executor.execute({ threadId: "thread_mcp", turnId: "turn_mcp", prompt: "Connect Acme" }, controls);
|
|
2210
|
+
expect(asks).toEqual([{}, { fresh: true }]);
|
|
2211
|
+
expect(Object.keys(loaded[0] ?? {}).sort()).toEqual(["acme", "baker_ui"]);
|
|
2212
|
+
expect(decision).toMatchObject({ behavior: "allow", updatedInput: { resolution: { toolsReady: true } } });
|
|
2213
|
+
});
|
|
2214
|
+
it("names a server that would not start, so the agent does not promise it for the next message", async () => {
|
|
2215
|
+
const relay = recordingRelay();
|
|
2216
|
+
relay.connectionInputRequest = () => Promise.resolve(true);
|
|
2217
|
+
const controls = activeControls();
|
|
2218
|
+
let decision;
|
|
2219
|
+
const executor = new BridgeJobExecutor({
|
|
2220
|
+
sdk: {
|
|
2221
|
+
query: (input) => ({
|
|
2222
|
+
async *[Symbol.asyncIterator]() {
|
|
2223
|
+
const asked = input.options.canUseTool?.(CONNECTION_INPUT_TOOL_NAME, { connections: [{ server: { name: "acme", url: "https://acme.dev/mcp" }, reason: "r" }] }, { toolUseID: "t1", requestId: "req_t1", signal: new AbortController().signal });
|
|
2224
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2225
|
+
for (const [, pending] of controls.pendingConnectionInputs) {
|
|
2226
|
+
pending.resolve({ results: [{ status: "connected", server: "acme", scope: "company" }] });
|
|
2227
|
+
}
|
|
2228
|
+
decision = await asked;
|
|
2229
|
+
yield resultMessage(0, "session_after_failed_load");
|
|
2230
|
+
},
|
|
2231
|
+
setMcpServers: () => Promise.resolve({ added: [], removed: [], errors: { acme: "401 Unauthorized" } }),
|
|
2232
|
+
}),
|
|
2233
|
+
},
|
|
2234
|
+
model: "claude-test",
|
|
2235
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2236
|
+
relay,
|
|
2237
|
+
mcpServersProvider: () => Promise.resolve({ acme: { type: "http", url: "https://acme.dev/mcp" } }),
|
|
2238
|
+
});
|
|
2239
|
+
await executor.execute({ threadId: "thread_bad", turnId: "turn_bad", prompt: "Connect Acme" }, controls);
|
|
2240
|
+
expect(decision).toMatchObject({
|
|
2241
|
+
updatedInput: { resolution: { toolsReady: false, failedToLoad: ["acme"] } },
|
|
2242
|
+
});
|
|
2243
|
+
});
|
|
2244
|
+
it("does not call an app ready when the reloaded tools do not include it", async () => {
|
|
2245
|
+
const relay = recordingRelay();
|
|
2246
|
+
relay.connectionInputRequest = () => Promise.resolve(true);
|
|
2247
|
+
const controls = activeControls();
|
|
2248
|
+
let decision;
|
|
2249
|
+
const executor = new BridgeJobExecutor({
|
|
2250
|
+
sdk: {
|
|
2251
|
+
query: (input) => ({
|
|
2252
|
+
async *[Symbol.asyncIterator]() {
|
|
2253
|
+
const asked = input.options.canUseTool?.(CONNECTION_INPUT_TOOL_NAME, { connections: [{ app: "notion", reason: "r" }] }, { toolUseID: "t1", requestId: "req_t1", signal: new AbortController().signal });
|
|
2254
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2255
|
+
for (const [, pending] of controls.pendingConnectionInputs) {
|
|
2256
|
+
pending.resolve({ results: [{ status: "connected", app: "notion", scope: "company" }] });
|
|
2257
|
+
}
|
|
2258
|
+
decision = await asked;
|
|
2259
|
+
yield resultMessage(0, "session_after_missing_app");
|
|
2260
|
+
},
|
|
2261
|
+
setMcpServers: () => Promise.resolve({ added: [], removed: [], errors: {} }),
|
|
2262
|
+
}),
|
|
2263
|
+
},
|
|
2264
|
+
model: "claude-test",
|
|
2265
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2266
|
+
relay,
|
|
2267
|
+
mcpServersProvider: () => Promise.resolve({}),
|
|
2268
|
+
});
|
|
2269
|
+
await executor.execute({ threadId: "thread_app", turnId: "turn_app", prompt: "Connect Notion" }, controls);
|
|
2270
|
+
expect(decision).toMatchObject({
|
|
2271
|
+
updatedInput: { resolution: { toolsReady: false, failedToLoad: ["notion"] } },
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
2274
|
+
it("leaves the turn's tools alone when only a platform was connected", async () => {
|
|
2275
|
+
const relay = recordingRelay();
|
|
2276
|
+
relay.connectionInputRequest = () => Promise.resolve(true);
|
|
2277
|
+
const controls = activeControls();
|
|
2278
|
+
let reloads = 0;
|
|
2279
|
+
const executor = new BridgeJobExecutor({
|
|
2280
|
+
sdk: {
|
|
2281
|
+
query: (input) => ({
|
|
2282
|
+
async *[Symbol.asyncIterator]() {
|
|
2283
|
+
const asked = input.options.canUseTool?.(CONNECTION_INPUT_TOOL_NAME, { connections: [{ platform: "hubspot", reason: "r" }] }, { toolUseID: "t1", requestId: "req_t1", signal: new AbortController().signal });
|
|
2284
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2285
|
+
for (const [, pending] of controls.pendingConnectionInputs) {
|
|
2286
|
+
pending.resolve({ results: [{ status: "connected", platform: "hubspot", selected: [] }] });
|
|
2287
|
+
}
|
|
2288
|
+
await asked;
|
|
2289
|
+
yield resultMessage(0, "session_after_platform");
|
|
2290
|
+
},
|
|
2291
|
+
setMcpServers: () => {
|
|
2292
|
+
reloads += 1;
|
|
2293
|
+
return Promise.resolve({ added: [], removed: [], errors: {} });
|
|
2294
|
+
},
|
|
2295
|
+
}),
|
|
2296
|
+
},
|
|
2297
|
+
model: "claude-test",
|
|
2298
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2299
|
+
relay,
|
|
2300
|
+
});
|
|
2301
|
+
await executor.execute({ threadId: "thread_pl", turnId: "turn_pl", prompt: "Connect HubSpot" }, controls);
|
|
2302
|
+
expect(reloads).toBe(0);
|
|
2303
|
+
});
|
|
2304
|
+
it("turns request_credential away in a Runtime without Credentials", async () => {
|
|
2305
|
+
const relay = recordingRelay();
|
|
2306
|
+
const requests = [];
|
|
2307
|
+
relay.credentialInputRequest = (_target, request) => {
|
|
2308
|
+
requests.push(request);
|
|
2309
|
+
return Promise.resolve(true);
|
|
2310
|
+
};
|
|
2311
|
+
let decision;
|
|
2312
|
+
const executor = new BridgeJobExecutor({
|
|
2313
|
+
sdk: {
|
|
2314
|
+
query: (input) => ({
|
|
2315
|
+
async *[Symbol.asyncIterator]() {
|
|
2316
|
+
decision = await input.options.canUseTool?.(CREDENTIAL_INPUT_TOOL_NAME, {
|
|
2317
|
+
label: "Shopify admin token",
|
|
2318
|
+
reason: "so I can update the theme",
|
|
2319
|
+
allowedHosts: ["acme.myshopify.com"],
|
|
2320
|
+
}, {
|
|
2321
|
+
toolUseID: "t1",
|
|
2322
|
+
requestId: "req_t1",
|
|
2323
|
+
signal: input.options.abortController?.signal ?? new AbortController().signal,
|
|
2324
|
+
});
|
|
2325
|
+
yield resultMessage(0, "session_credential_off");
|
|
2326
|
+
},
|
|
2327
|
+
}),
|
|
2328
|
+
},
|
|
2329
|
+
model: "claude-test",
|
|
2330
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2331
|
+
relay,
|
|
2332
|
+
});
|
|
2333
|
+
await executor.execute({ threadId: "thread_co", turnId: "turn_co", prompt: "Fix my theme" }, activeControls());
|
|
2334
|
+
expect(requests).toEqual([]);
|
|
2335
|
+
expect(decision).toMatchObject({ behavior: "deny", message: expect.stringContaining("not available") });
|
|
2336
|
+
});
|
|
2337
|
+
it("parks a credential request and hands the agent only the placeholder the user's answer stored", async () => {
|
|
2338
|
+
vi.stubEnv("BAKER_CREDENTIALS", "1");
|
|
2339
|
+
const relay = recordingRelay();
|
|
2340
|
+
const requests = [];
|
|
2341
|
+
relay.credentialInputRequest = (_target, request) => {
|
|
2342
|
+
requests.push(request);
|
|
2343
|
+
return Promise.resolve(true);
|
|
2344
|
+
};
|
|
2345
|
+
const controls = activeControls();
|
|
2346
|
+
let decision;
|
|
2347
|
+
const executor = new BridgeJobExecutor({
|
|
2348
|
+
sdk: {
|
|
2349
|
+
query: (input) => ({
|
|
2350
|
+
async *[Symbol.asyncIterator]() {
|
|
2351
|
+
const asked = input.options.canUseTool?.(CREDENTIAL_INPUT_TOOL_NAME, {
|
|
2352
|
+
label: "Shopify admin token",
|
|
2353
|
+
reason: "so I can update the theme",
|
|
2354
|
+
allowedHosts: ["acme.myshopify.com"],
|
|
2355
|
+
}, {
|
|
2356
|
+
toolUseID: "t1",
|
|
2357
|
+
requestId: "req_t1",
|
|
2358
|
+
signal: input.options.abortController?.signal ?? new AbortController().signal,
|
|
2359
|
+
});
|
|
2360
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
2361
|
+
for (const [, pending] of controls.pendingCredentialInputs) {
|
|
2362
|
+
pending.resolve({
|
|
2363
|
+
status: "stored",
|
|
2364
|
+
name: "shopify-admin-token",
|
|
2365
|
+
placeholder: "{{credential:shopify-admin-token}}",
|
|
2366
|
+
allowedHosts: ["acme.myshopify.com"],
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
decision = await asked;
|
|
2370
|
+
yield resultMessage(0, "session_credential");
|
|
2371
|
+
},
|
|
2372
|
+
}),
|
|
2373
|
+
},
|
|
2374
|
+
model: "claude-test",
|
|
2375
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2376
|
+
relay,
|
|
2377
|
+
});
|
|
2378
|
+
await executor.execute({ threadId: "thread_cr", turnId: "turn_cr", prompt: "Fix my theme" }, controls);
|
|
2379
|
+
expect(requests).toEqual([
|
|
2380
|
+
{
|
|
2381
|
+
toolUseId: "t1",
|
|
2382
|
+
label: "Shopify admin token",
|
|
2383
|
+
reason: "so I can update the theme",
|
|
2384
|
+
allowedHosts: ["acme.myshopify.com"],
|
|
2385
|
+
},
|
|
2386
|
+
]);
|
|
2387
|
+
expect(decision).toMatchObject({
|
|
2388
|
+
behavior: "allow",
|
|
2389
|
+
updatedInput: { resolution: { status: "stored", placeholder: "{{credential:shopify-admin-token}}" } },
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2127
2392
|
it("refuses to ask for an avatar's face in a tool that renders only text", async () => {
|
|
2128
2393
|
// Shipped three times, in both modes, after the agent had just read the doc saying
|
|
2129
2394
|
// not to: "Elige el rostro para Clara, tu presentadora de placas solares" with
|
|
@@ -2164,6 +2429,8 @@ describe("BridgeJobExecutor", () => {
|
|
|
2164
2429
|
model: "claude-test",
|
|
2165
2430
|
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2166
2431
|
relay,
|
|
2432
|
+
// The decision model reads it as a face pick, and not as a question about a page.
|
|
2433
|
+
judge: async (questions) => questions.map(({ instructions }) => (instructions.question === FACE_PICK_QUESTION ? 0.95 : 0.02)),
|
|
2167
2434
|
});
|
|
2168
2435
|
await executor.execute({ threadId: "thread_face", turnId: "turn_face", prompt: "Make a TikTok ad" }, controls);
|
|
2169
2436
|
expect(decision).toMatchObject({ behavior: "deny" });
|
|
@@ -2980,6 +3247,30 @@ describe("createStallDetector", () => {
|
|
|
2980
3247
|
h.detector.dispose();
|
|
2981
3248
|
}
|
|
2982
3249
|
});
|
|
3250
|
+
// Claude Code 2.1.287+ delivers a background task's heartbeats while the model's
|
|
3251
|
+
// stream is stalled; they say nothing about the model, so they must not disarm it.
|
|
3252
|
+
it("still stalls when a background task's heartbeat arrives during the model's silence", async () => {
|
|
3253
|
+
vi.useFakeTimers();
|
|
3254
|
+
const h = harness();
|
|
3255
|
+
try {
|
|
3256
|
+
await h.advance(20);
|
|
3257
|
+
h.detector.observeMessage({
|
|
3258
|
+
type: "tool_progress",
|
|
3259
|
+
tool_use_id: "toolu_bg",
|
|
3260
|
+
tool_name: "Bash",
|
|
3261
|
+
parent_tool_use_id: null,
|
|
3262
|
+
elapsed_time_seconds: 20,
|
|
3263
|
+
heartbeat: true,
|
|
3264
|
+
uuid: "uuid_hb",
|
|
3265
|
+
session_id: "session_a",
|
|
3266
|
+
});
|
|
3267
|
+
await h.advance(30);
|
|
3268
|
+
expect(h.stalls).toHaveLength(1);
|
|
3269
|
+
}
|
|
3270
|
+
finally {
|
|
3271
|
+
h.detector.dispose();
|
|
3272
|
+
}
|
|
3273
|
+
});
|
|
2983
3274
|
it("does not stall while the silence is still inside the window", async () => {
|
|
2984
3275
|
vi.useFakeTimers();
|
|
2985
3276
|
const h = harness();
|
|
@@ -3164,6 +3455,29 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3164
3455
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.2 });
|
|
3165
3456
|
expect(endInputAt.count).toBe(4);
|
|
3166
3457
|
});
|
|
3458
|
+
it("tells the session watchdog while a tool call or background task is running", async () => {
|
|
3459
|
+
const endInputAt = { count: -1 };
|
|
3460
|
+
const executor = createExecutor({
|
|
3461
|
+
turnEndSettleMs: 20,
|
|
3462
|
+
queryResults: [
|
|
3463
|
+
cliLikeQuery(endInputAt, toolUseAssistantMessage("Bash", { command: "baker canvas run x.canvas.json" }), {
|
|
3464
|
+
type: "user",
|
|
3465
|
+
session_id: "session_bg",
|
|
3466
|
+
message: { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_stream", content: "ok" }] },
|
|
3467
|
+
}, taskStarted("task_render"), resultMessage(0.1, "session_bg"), taskSettled("task_render"), resultMessage(0.2, "session_bg")),
|
|
3468
|
+
],
|
|
3469
|
+
});
|
|
3470
|
+
const reported = [];
|
|
3471
|
+
const controls = {
|
|
3472
|
+
...activeControls(),
|
|
3473
|
+
setWorkInFlight: (inFlight) => {
|
|
3474
|
+
if (reported.at(-1) !== inFlight)
|
|
3475
|
+
reported.push(inFlight);
|
|
3476
|
+
},
|
|
3477
|
+
};
|
|
3478
|
+
await executor.execute({ threadId: "thread_bg", turnId: "turn_bg", prompt: "Render", history: history([]) }, controls);
|
|
3479
|
+
expect(reported).toEqual([true, false, true, false]);
|
|
3480
|
+
});
|
|
3167
3481
|
it("ends the turn when the last task settles without waking the model into a second result", async () => {
|
|
3168
3482
|
// A settle is not guaranteed to produce another result. If only a result
|
|
3169
3483
|
// could arm the end, this shape would strand in `for await` until the
|
|
@@ -3179,6 +3493,120 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3179
3493
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3180
3494
|
expect(endInputAt.count).toBe(3);
|
|
3181
3495
|
});
|
|
3496
|
+
it("bills what a task spent after the last result when it settles without waking the model", async () => {
|
|
3497
|
+
// The last result reported $0.10; the task kept working after it. With no
|
|
3498
|
+
// second result, only the CLI's own running total can say what it cost.
|
|
3499
|
+
const endInputAt = { count: -1 };
|
|
3500
|
+
const query = cliLikeQuery(endInputAt, taskStarted("task_silent"), resultMessage(0.1, "session_bg"), taskSettled("task_silent"));
|
|
3501
|
+
const executor = createExecutor({
|
|
3502
|
+
turnEndSettleMs: 20,
|
|
3503
|
+
queryResults: [
|
|
3504
|
+
{ ...query, usage: () => Promise.resolve({ totalCostUsd: 0.35, modelUsage: {} }) },
|
|
3505
|
+
],
|
|
3506
|
+
});
|
|
3507
|
+
const outcome = await executor.execute({ threadId: "thread_silent", turnId: "turn_silent", prompt: "Fan out one producer", history: history([]) }, activeControls());
|
|
3508
|
+
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.35 });
|
|
3509
|
+
});
|
|
3510
|
+
it("settles the workspace itself when a turn that ran tasks ends, since its last Stop may never come", async () => {
|
|
3511
|
+
// A task settling without waking the model ends the turn with no further
|
|
3512
|
+
// Stop, so the end-of-turn hooks that deferred to a later Stop never run.
|
|
3513
|
+
let settled = 0;
|
|
3514
|
+
const executor = createExecutor({
|
|
3515
|
+
turnEndSettleMs: 5,
|
|
3516
|
+
settleWorkspace: () => {
|
|
3517
|
+
settled += 1;
|
|
3518
|
+
return Promise.resolve();
|
|
3519
|
+
},
|
|
3520
|
+
queryResults: [
|
|
3521
|
+
cliLikeQuery({ count: -1 }, taskStarted("task_silent"), resultMessage(0.1, "session_bg"), taskSettled("task_silent")),
|
|
3522
|
+
cliLikeQuery({ count: -1 }, resultMessage(0.1, "session_plain")),
|
|
3523
|
+
],
|
|
3524
|
+
});
|
|
3525
|
+
await executor.execute({ threadId: "thread_silent", turnId: "turn_silent", prompt: "Fan out", history: history([]) }, activeControls());
|
|
3526
|
+
expect(settled).toBe(1);
|
|
3527
|
+
await executor.execute({ threadId: "thread_plain", turnId: "turn_plain", prompt: "Say hi", history: history([]) }, activeControls());
|
|
3528
|
+
expect(settled).toBe(1);
|
|
3529
|
+
});
|
|
3530
|
+
it("leaves the workspace untouched after a Session review that ran tasks", async () => {
|
|
3531
|
+
// A review runs with every hook off and may not change anything; the
|
|
3532
|
+
// settle steps format files and reconcile a publish.
|
|
3533
|
+
let settled = 0;
|
|
3534
|
+
const executor = createExecutor({
|
|
3535
|
+
turnEndSettleMs: 5,
|
|
3536
|
+
settleWorkspace: () => {
|
|
3537
|
+
settled += 1;
|
|
3538
|
+
return Promise.resolve();
|
|
3539
|
+
},
|
|
3540
|
+
queryResults: [
|
|
3541
|
+
cliLikeQuery({ count: -1 }, taskStarted("task_read"), taskSettled("task_read"), {
|
|
3542
|
+
...resultMessage(0.1, "session_review"),
|
|
3543
|
+
structured_output: { summary: "fine", findings: [] },
|
|
3544
|
+
}),
|
|
3545
|
+
],
|
|
3546
|
+
});
|
|
3547
|
+
const outcome = await executor.execute({
|
|
3548
|
+
threadId: "thread_review",
|
|
3549
|
+
turnId: "turn_review",
|
|
3550
|
+
prompt: "You are reviewing…",
|
|
3551
|
+
review: {
|
|
3552
|
+
reviewId: "rev_1",
|
|
3553
|
+
apiKey: "bk_review_key",
|
|
3554
|
+
modelAuthToken: "sk-or-platform",
|
|
3555
|
+
model: "claude-review-model",
|
|
3556
|
+
outputSchema: { type: "object" },
|
|
3557
|
+
threadIds: ["thread_visible"],
|
|
3558
|
+
},
|
|
3559
|
+
}, activeControls());
|
|
3560
|
+
expect(outcome).toMatchObject({ kind: "completed" });
|
|
3561
|
+
expect(settled).toBe(0);
|
|
3562
|
+
});
|
|
3563
|
+
it("keeps input open for a wake that lands while the usage reading is in flight", async () => {
|
|
3564
|
+
// The usage reading sits between deciding to end and ending. A wake queued
|
|
3565
|
+
// in the CLI can land inside it; closing stdin then would deny the woken
|
|
3566
|
+
// turn's first permission-gated tool call.
|
|
3567
|
+
const endInputAt = { count: -1 };
|
|
3568
|
+
const query = cliLikeQuery(endInputAt, taskStarted("task_bg"), resultMessage(0.1, "session_bg"), taskSettled("task_bg"), { pauseMs: 40 }, wakeMessage(),
|
|
3569
|
+
// The woken turn is still on time-to-first-token when the reading returns.
|
|
3570
|
+
{ pauseMs: 150 }, resultMessage(0.2, "session_bg"));
|
|
3571
|
+
const executor = createExecutor({
|
|
3572
|
+
turnEndSettleMs: 5,
|
|
3573
|
+
queryResults: [
|
|
3574
|
+
{
|
|
3575
|
+
...query,
|
|
3576
|
+
usage: () => new Promise((resolveUsage) => setTimeout(() => resolveUsage({ totalCostUsd: 0.2, modelUsage: {} }), 100)),
|
|
3577
|
+
},
|
|
3578
|
+
],
|
|
3579
|
+
});
|
|
3580
|
+
await executor.execute({ threadId: "thread_wake", turnId: "turn_wake", prompt: "Fan out", history: history([]) }, activeControls());
|
|
3581
|
+
expect(endInputAt.count).toBe(5);
|
|
3582
|
+
});
|
|
3583
|
+
it("leaves a query that ended during the usage reading alone when the reading returns", async () => {
|
|
3584
|
+
// A Stop can close the CLI while the reading is in flight. The run is over
|
|
3585
|
+
// by the time it answers; ending the turn then would log a turn end that
|
|
3586
|
+
// never happened and arm an exit guard against a query nobody is draining.
|
|
3587
|
+
const query = cliLikeQuery({ count: -1 }, taskStarted("task_bg"), resultMessage(0.1, "session_bg"), taskSettled("task_bg"));
|
|
3588
|
+
let answerUsage;
|
|
3589
|
+
const turnEvents = [];
|
|
3590
|
+
const executor = createExecutor({
|
|
3591
|
+
turnEndSettleMs: 5,
|
|
3592
|
+
turnEvents,
|
|
3593
|
+
queryResults: [
|
|
3594
|
+
{
|
|
3595
|
+
...query,
|
|
3596
|
+
usage: () => {
|
|
3597
|
+
query.close?.();
|
|
3598
|
+
return new Promise((resolveUsage) => {
|
|
3599
|
+
answerUsage = () => resolveUsage({ totalCostUsd: 0.2, modelUsage: {} });
|
|
3600
|
+
});
|
|
3601
|
+
},
|
|
3602
|
+
},
|
|
3603
|
+
],
|
|
3604
|
+
});
|
|
3605
|
+
await executor.execute({ threadId: "thread_stop", turnId: "turn_stop", prompt: "Fan out", history: history([]) }, activeControls());
|
|
3606
|
+
answerUsage();
|
|
3607
|
+
await new Promise((resolveTick) => setImmediate(resolveTick));
|
|
3608
|
+
expect(turnEvents.map((event) => event.event)).not.toContain("turn_end");
|
|
3609
|
+
});
|
|
3182
3610
|
it("re-arms the settle window when a passive system event trails the result", async () => {
|
|
3183
3611
|
// Passive trailing events (files persisted, session state) must not cancel
|
|
3184
3612
|
// a scheduled end — only genuine model activity does. A cancel with no
|
|
@@ -3248,6 +3676,24 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3248
3676
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3249
3677
|
expect(endInputAt.count).toBe(3);
|
|
3250
3678
|
});
|
|
3679
|
+
// Dev chat, Oct 2026: an auto-backgrounded render settled, input ended one
|
|
3680
|
+
// settle window later, and the model woke ~3 s after the settle on a closed
|
|
3681
|
+
// channel; it started a second render in the background, and the CLI's exit
|
|
3682
|
+
// at its result killed it. The CLI streams nothing between a settle and the
|
|
3683
|
+
// woken turn's first token, so only a longer wait after a settle covers it.
|
|
3684
|
+
it("keeps input open for the model a settled background command wakes, though nothing streams for a while", async () => {
|
|
3685
|
+
const endInputAt = { count: -1 };
|
|
3686
|
+
const executor = createExecutor({
|
|
3687
|
+
turnEndSettleMs: 20,
|
|
3688
|
+
turnEndWakeMs: 5_000,
|
|
3689
|
+
queryResults: [
|
|
3690
|
+
cliLikeQuery(endInputAt, backgroundTasksChanged(["bash_render"]), resultMessage(0.1, "session_bg"), backgroundTasksChanged([]), { pauseMs: 60 }, assistantMessage("msg_wake", "The third render is ready", "session_bg"), resultMessage(0.3, "session_bg")),
|
|
3691
|
+
],
|
|
3692
|
+
});
|
|
3693
|
+
const outcome = await executor.execute({ threadId: "thread_wake_late", turnId: "turn_wake_late", prompt: "Render three", history: history([]) }, activeControls());
|
|
3694
|
+
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.3 });
|
|
3695
|
+
expect(endInputAt.count).toBe(5);
|
|
3696
|
+
});
|
|
3251
3697
|
// The production shape: the model refused, the CLI answered with an error
|
|
3252
3698
|
// result and then waited on stdin like after any other result. The error was
|
|
3253
3699
|
// no longer thrown, and only a clean result ended input, so the turn sat idle
|
|
@@ -3362,7 +3808,10 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
3362
3808
|
const queryInputs = [];
|
|
3363
3809
|
const executor = createExecutor({ queryInputs });
|
|
3364
3810
|
await executor.execute(reviewJob(), activeControls());
|
|
3365
|
-
|
|
3811
|
+
const [reviewQuery] = queryInputs;
|
|
3812
|
+
if (!reviewQuery)
|
|
3813
|
+
throw new Error("expected the review to start one query");
|
|
3814
|
+
expect(reviewQuery.options.outputFormat).toEqual({
|
|
3366
3815
|
type: "json_schema",
|
|
3367
3816
|
schema: review.outputSchema,
|
|
3368
3817
|
});
|
|
@@ -3399,7 +3848,9 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
3399
3848
|
const options = queryInputs[0]?.options;
|
|
3400
3849
|
expect(options.model).toBe("claude-session-model");
|
|
3401
3850
|
expect(options.outputFormat).toBeUndefined();
|
|
3402
|
-
|
|
3851
|
+
// Only the credential scrub, which every turn runs; none of the review's hooks.
|
|
3852
|
+
expect(Object.keys(options.hooks ?? {})).toEqual(["PostToolUse"]);
|
|
3853
|
+
expect(options.hooks?.PostToolUse).toHaveLength(1);
|
|
3403
3854
|
expect(options.env?.MAX_STRUCTURED_OUTPUT_RETRIES).toBeUndefined();
|
|
3404
3855
|
expect(options.disallowedTools).not.toContain("Write");
|
|
3405
3856
|
expect(options.env?.BAKER_DEBUG_LOG).toBeUndefined();
|