@koda-sl/baker-bridge 0.123.0-dev.f9f855bf1 → 0.124.0-dev.7e3937022

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 (177) hide show
  1. package/README.md +55 -17
  2. package/dist/broker/baker-api.d.ts +23 -0
  3. package/dist/broker/baker-api.d.ts.map +1 -0
  4. package/dist/broker/baker-api.js +55 -0
  5. package/dist/broker/baker-api.js.map +1 -0
  6. package/dist/broker/broker-client.d.ts +10 -0
  7. package/dist/broker/broker-client.d.ts.map +1 -0
  8. package/dist/broker/broker-client.js +60 -0
  9. package/dist/broker/broker-client.js.map +1 -0
  10. package/dist/broker/ca.d.ts +38 -0
  11. package/dist/broker/ca.d.ts.map +1 -0
  12. package/dist/broker/ca.js +80 -0
  13. package/dist/broker/ca.js.map +1 -0
  14. package/dist/broker/ca.test.d.ts +2 -0
  15. package/dist/broker/ca.test.d.ts.map +1 -0
  16. package/dist/broker/ca.test.js +58 -0
  17. package/dist/broker/ca.test.js.map +1 -0
  18. package/dist/broker/credential-cache.d.ts +24 -0
  19. package/dist/broker/credential-cache.d.ts.map +1 -0
  20. package/dist/broker/credential-cache.js +69 -0
  21. package/dist/broker/credential-cache.js.map +1 -0
  22. package/dist/broker/credential-cache.test.d.ts +2 -0
  23. package/dist/broker/credential-cache.test.d.ts.map +1 -0
  24. package/dist/broker/credential-cache.test.js +120 -0
  25. package/dist/broker/credential-cache.test.js.map +1 -0
  26. package/dist/broker/credential-text.d.ts +45 -0
  27. package/dist/broker/credential-text.d.ts.map +1 -0
  28. package/dist/broker/credential-text.js +103 -0
  29. package/dist/broker/credential-text.js.map +1 -0
  30. package/dist/broker/credential-text.test.d.ts +2 -0
  31. package/dist/broker/credential-text.test.d.ts.map +1 -0
  32. package/dist/broker/credential-text.test.js +54 -0
  33. package/dist/broker/credential-text.test.js.map +1 -0
  34. package/dist/broker/env.d.ts +19 -0
  35. package/dist/broker/env.d.ts.map +1 -0
  36. package/dist/broker/env.js +16 -0
  37. package/dist/broker/env.js.map +1 -0
  38. package/dist/broker/main.d.ts +3 -0
  39. package/dist/broker/main.d.ts.map +1 -0
  40. package/dist/broker/main.js +51 -0
  41. package/dist/broker/main.js.map +1 -0
  42. package/dist/broker/proxy.d.ts +24 -0
  43. package/dist/broker/proxy.d.ts.map +1 -0
  44. package/dist/broker/proxy.integration.test.d.ts +2 -0
  45. package/dist/broker/proxy.integration.test.d.ts.map +1 -0
  46. package/dist/broker/proxy.integration.test.js +201 -0
  47. package/dist/broker/proxy.integration.test.js.map +1 -0
  48. package/dist/broker/proxy.js +147 -0
  49. package/dist/broker/proxy.js.map +1 -0
  50. package/dist/broker/rewrite.d.ts +44 -0
  51. package/dist/broker/rewrite.d.ts.map +1 -0
  52. package/dist/broker/rewrite.js +291 -0
  53. package/dist/broker/rewrite.js.map +1 -0
  54. package/dist/broker/rewrite.test.d.ts +2 -0
  55. package/dist/broker/rewrite.test.d.ts.map +1 -0
  56. package/dist/broker/rewrite.test.js +171 -0
  57. package/dist/broker/rewrite.test.js.map +1 -0
  58. package/dist/broker/socket-service.d.ts +18 -0
  59. package/dist/broker/socket-service.d.ts.map +1 -0
  60. package/dist/broker/socket-service.js +103 -0
  61. package/dist/broker/socket-service.js.map +1 -0
  62. package/dist/broker/socket-service.test.d.ts +2 -0
  63. package/dist/broker/socket-service.test.d.ts.map +1 -0
  64. package/dist/broker/socket-service.test.js +73 -0
  65. package/dist/broker/socket-service.test.js.map +1 -0
  66. package/dist/broker/token-bucket.d.ts +9 -0
  67. package/dist/broker/token-bucket.d.ts.map +1 -0
  68. package/dist/broker/token-bucket.js +18 -0
  69. package/dist/broker/token-bucket.js.map +1 -0
  70. package/dist/broker/usage-reporter.d.ts +18 -0
  71. package/dist/broker/usage-reporter.d.ts.map +1 -0
  72. package/dist/broker/usage-reporter.js +31 -0
  73. package/dist/broker/usage-reporter.js.map +1 -0
  74. package/dist/broker/usage-reporter.test.d.ts +2 -0
  75. package/dist/broker/usage-reporter.test.d.ts.map +1 -0
  76. package/dist/broker/usage-reporter.test.js +20 -0
  77. package/dist/broker/usage-reporter.test.js.map +1 -0
  78. package/dist/hono/agent-session.d.ts +3 -0
  79. package/dist/hono/agent-session.d.ts.map +1 -1
  80. package/dist/hono/agent-session.js +15 -0
  81. package/dist/hono/agent-session.js.map +1 -1
  82. package/dist/hono/agent.d.ts +2 -0
  83. package/dist/hono/agent.d.ts.map +1 -1
  84. package/dist/hono/agent.js +10 -2
  85. package/dist/hono/agent.js.map +1 -1
  86. package/dist/hono/avatar-accent.d.ts +17 -5
  87. package/dist/hono/avatar-accent.d.ts.map +1 -1
  88. package/dist/hono/avatar-accent.js +31 -64
  89. package/dist/hono/avatar-accent.js.map +1 -1
  90. package/dist/hono/avatar-accent.test.js +46 -35
  91. package/dist/hono/avatar-accent.test.js.map +1 -1
  92. package/dist/hono/avatar-casting.d.ts +1 -0
  93. package/dist/hono/avatar-casting.d.ts.map +1 -1
  94. package/dist/hono/avatar-casting.js +15 -2
  95. package/dist/hono/avatar-casting.js.map +1 -1
  96. package/dist/hono/avatar-casting.test.js +5 -0
  97. package/dist/hono/avatar-casting.test.js.map +1 -1
  98. package/dist/hono/avatar-face-question.d.ts +23 -6
  99. package/dist/hono/avatar-face-question.d.ts.map +1 -1
  100. package/dist/hono/avatar-face-question.js +48 -59
  101. package/dist/hono/avatar-face-question.js.map +1 -1
  102. package/dist/hono/avatar-face-question.test.js +46 -101
  103. package/dist/hono/avatar-face-question.test.js.map +1 -1
  104. package/dist/hono/baker-ui-tool-schemas.test.js +6 -0
  105. package/dist/hono/baker-ui-tool-schemas.test.js.map +1 -1
  106. package/dist/hono/bridge-job.d.ts +19 -1
  107. package/dist/hono/bridge-job.d.ts.map +1 -1
  108. package/dist/hono/bridge-job.js +94 -15
  109. package/dist/hono/bridge-job.js.map +1 -1
  110. package/dist/hono/bridge-job.test.js +152 -2
  111. package/dist/hono/bridge-job.test.js.map +1 -1
  112. package/dist/hono/connection-input.d.ts +3 -0
  113. package/dist/hono/connection-input.d.ts.map +1 -1
  114. package/dist/hono/connection-input.js +28 -4
  115. package/dist/hono/connection-input.js.map +1 -1
  116. package/dist/hono/connection-input.test.js +11 -0
  117. package/dist/hono/connection-input.test.js.map +1 -1
  118. package/dist/hono/convex.d.ts +8 -0
  119. package/dist/hono/convex.d.ts.map +1 -1
  120. package/dist/hono/convex.js +8 -0
  121. package/dist/hono/convex.js.map +1 -1
  122. package/dist/hono/credential-input.d.ts +39 -0
  123. package/dist/hono/credential-input.d.ts.map +1 -0
  124. package/dist/hono/credential-input.js +78 -0
  125. package/dist/hono/credential-input.js.map +1 -0
  126. package/dist/hono/credential-scrub.d.ts +41 -0
  127. package/dist/hono/credential-scrub.d.ts.map +1 -0
  128. package/dist/hono/credential-scrub.js +152 -0
  129. package/dist/hono/credential-scrub.js.map +1 -0
  130. package/dist/hono/credential-scrub.test.d.ts +2 -0
  131. package/dist/hono/credential-scrub.test.d.ts.map +1 -0
  132. package/dist/hono/credential-scrub.test.js +105 -0
  133. package/dist/hono/credential-scrub.test.js.map +1 -0
  134. package/dist/hono/jev.d.ts +16 -0
  135. package/dist/hono/jev.d.ts.map +1 -0
  136. package/dist/hono/jev.js +60 -0
  137. package/dist/hono/jev.js.map +1 -0
  138. package/dist/hono/jev.test.d.ts +2 -0
  139. package/dist/hono/jev.test.d.ts.map +1 -0
  140. package/dist/hono/jev.test.js +15 -0
  141. package/dist/hono/jev.test.js.map +1 -0
  142. package/dist/hono/latitude-telemetry.d.ts.map +1 -1
  143. package/dist/hono/latitude-telemetry.js +8 -2
  144. package/dist/hono/latitude-telemetry.js.map +1 -1
  145. package/dist/hono/openrouter-session.d.ts.map +1 -1
  146. package/dist/hono/openrouter-session.js +12 -1
  147. package/dist/hono/openrouter-session.js.map +1 -1
  148. package/dist/hono/openrouter-session.test.js +9 -3
  149. package/dist/hono/openrouter-session.test.js.map +1 -1
  150. package/dist/hono/prompt-context.d.ts +8 -1
  151. package/dist/hono/prompt-context.d.ts.map +1 -1
  152. package/dist/hono/prompt-context.js +45 -7
  153. package/dist/hono/prompt-context.js.map +1 -1
  154. package/dist/hono/prompt-context.test.js +35 -1
  155. package/dist/hono/prompt-context.test.js.map +1 -1
  156. package/dist/hono/relay.d.ts +10 -1
  157. package/dist/hono/relay.d.ts.map +1 -1
  158. package/dist/hono/relay.js +6 -0
  159. package/dist/hono/relay.js.map +1 -1
  160. package/dist/hono/server.d.ts.map +1 -1
  161. package/dist/hono/server.js +22 -0
  162. package/dist/hono/server.js.map +1 -1
  163. package/dist/hono/tag-input.d.ts +106 -3
  164. package/dist/hono/tag-input.d.ts.map +1 -1
  165. package/dist/hono/tag-input.js +20 -9
  166. package/dist/hono/tag-input.js.map +1 -1
  167. package/dist/hono/tag-input.test.js +19 -1
  168. package/dist/hono/tag-input.test.js.map +1 -1
  169. package/dist/hono/types.d.ts +8 -0
  170. package/dist/hono/types.d.ts.map +1 -1
  171. package/dist/review/digest.d.ts +8 -1
  172. package/dist/review/digest.d.ts.map +1 -1
  173. package/dist/review/digest.js +10 -43
  174. package/dist/review/digest.js.map +1 -1
  175. package/dist/review/digest.test.js +11 -6
  176. package/dist/review/digest.test.js.map +1 -1
  177. package/package.json +5 -2
@@ -13,13 +13,16 @@ import { AVATAR_CASTING_TOOL_NAME, normalizeAvatarCastingToolInput, } from "./av
13
13
  import { asksForAvatarFace, FACE_QUESTION_MESSAGE } from "./avatar-face-question.js";
14
14
  import { CONNECTION_INPUT_TOOL_NAME, CONNECTION_PLATFORMS, connectedButNotLoaded, normalizeConnectionInputToolInput, toolsConnected, } from "./connection-input.js";
15
15
  import { messageContextTokens, pickContextWindow, streamEventContextUsage, turnContextUsage, } from "./context-usage.js";
16
+ import { CREDENTIAL_INPUT_TOOL_NAME, normalizeCredentialInputToolInput, } from "./credential-input.js";
17
+ import { credentialScrubHooks } from "./credential-scrub.js";
16
18
  import { FLOW_INPUT_TOOL_NAME, nodeTargetRefusals, normalizeFlowInputToolInput, readRawFlowTree, sanitizeFlowRequests, } from "./flow-input.js";
17
19
  import { writesInventedLogo } from "./invented-logo.js";
20
+ import { jevJudge } from "./jev.js";
18
21
  import { installedLatitudeHook, latitudeTelemetryFor } from "./latitude-telemetry.js";
19
22
  import { composioScopesToInvalidate, failedCustomServers, reportMcpInvalid, serversConnectedWithNoTools, } from "./mcp-servers.js";
20
23
  import { agentModelOverride, autoCompactWindowForModel, betasForModel, contextWindowForModel, } from "./model-capabilities.js";
21
24
  import { BEDROCK_SERVABLE_CAPABILITIES, buildAgentExtraBody, EXTRA_BODY_ENV, MODEL_CAPABILITIES_ENV, } from "./openrouter-session.js";
22
- import { currentReferenceReminder } from "./prompt-context.js";
25
+ import { credentialReminder, currentReferenceReminder } from "./prompt-context.js";
23
26
  import { bridgeRuntimeFiles } from "./runtime-files.js";
24
27
  import { applyCurrentSenderAttribution, buildCleanBoundary, buildRehydratedPrompt, compactionRetryAttempt, emptyInProgressSession, isContextOverflowError, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
25
28
  import { readSessionSkills } from "./session-skills.js";
@@ -220,6 +223,16 @@ export function reviewQueryOptions(job) {
220
223
  ? { outputFormat: { type: "json_schema", schema: job.review.outputSchema }, hooks: reviewOutputHooks() }
221
224
  : {};
222
225
  }
226
+ /**
227
+ * Every job's tool results pass through the credential scrub, appended after
228
+ * any other PostToolUse hook so its rewrite is the one the SDK keeps.
229
+ */
230
+ function withCredentialScrub(options) {
231
+ const scrub = credentialScrubHooks();
232
+ const hooks = { ...(options.hooks ?? {}) };
233
+ hooks.PostToolUse = [...(hooks.PostToolUse ?? []), ...(scrub.PostToolUse ?? [])];
234
+ return { ...options, hooks };
235
+ }
223
236
  /**
224
237
  * The review runs on Baker's own OpenRouter credential and its own read-only
225
238
  * key, both replacing whatever the Runtime was started with — the client never
@@ -246,6 +259,12 @@ export const REVIEW_DEBUG_LOG = "/tmp/baker-review-cli.jsonl";
246
259
  // briefly; model activity cancels it (the CLI clearly isn't idle). Turns that
247
260
  // never ran a task skip the delay entirely.
248
261
  const DEFAULT_TURN_END_SETTLE_MS = 1_000;
262
+ // When the last task settles after the clean result, the CLI wakes the model on
263
+ // it and streams nothing until the woken turn's first token — seconds, and 30+
264
+ // on a slow route. Ending input inside that silence strands the woken turn on a
265
+ // closed channel, and the CLI's exit at its result kills anything it backgrounds
266
+ // (dev, Oct 2026: a render). A settle that wakes nobody waits this long once.
267
+ const DEFAULT_TURN_END_WAKE_MS = 45_000;
249
268
  // After input ends, a healthy idle CLI exits within seconds. If the stream is
250
269
  // still open past this window with no messages arriving, the CLI is wedged —
251
270
  // close the query so the turn can finish instead of hanging to the watchdog.
@@ -273,10 +292,10 @@ const UNATTENDED_PROMPT_MESSAGE = "This run is unattended — it was started by
273
292
  * asked for in a text prompt renders a question nobody can answer. In each case the user
274
293
  * is left waiting for something that was never going to come.
275
294
  */
276
- function refuseMalformedCall(toolName, input, userText, priorUserText) {
295
+ async function refuseMalformedCall(toolName, input, judge, userText, priorUserText) {
277
296
  if (toolName === "Bash") {
278
297
  const command = typeof input?.command === "string" ? input.command : undefined;
279
- return toolRunAsCommand(command) ?? accentContradictsUser(command, userText, priorUserText);
298
+ return toolRunAsCommand(command) ?? (await accentContradictsUser(command, userText, priorUserText, judge));
280
299
  }
281
300
  if (toolName === "Agent" || toolName === "Task") {
282
301
  return blockingToolDelegated(typeof input?.prompt === "string" ? input.prompt : undefined);
@@ -284,11 +303,18 @@ function refuseMalformedCall(toolName, input, userText, priorUserText) {
284
303
  if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
285
304
  return writesInventedLogo(typeof input?.file_path === "string" ? input.file_path : undefined);
286
305
  }
287
- if (toolName === "AskUserQuestion" && asksForAvatarFace(input)) {
306
+ if (toolName === "AskUserQuestion" && (await asksForAvatarFace(input, judge))) {
288
307
  return FACE_QUESTION_MESSAGE;
289
308
  }
290
309
  return null;
291
310
  }
311
+ /**
312
+ * Whether this Runtime has Credentials: the backend sets `BAKER_CREDENTIALS=1`
313
+ * alongside the credential broker's proxy env. Read per turn, not at import.
314
+ */
315
+ function credentialsEnabled() {
316
+ return process.env.BAKER_CREDENTIALS === "1";
317
+ }
292
318
  /** Blocking prompts: every tool that would leave a card the backend parks on. */
293
319
  function deniedUnattendedPrompt(toolName) {
294
320
  return (toolName === "AskUserQuestion" ||
@@ -296,6 +322,7 @@ function deniedUnattendedPrompt(toolName) {
296
322
  toolName === TAG_INPUT_TOOL_NAME ||
297
323
  toolName === FLOW_INPUT_TOOL_NAME ||
298
324
  toolName === CONNECTION_INPUT_TOOL_NAME ||
325
+ toolName === CREDENTIAL_INPUT_TOOL_NAME ||
299
326
  toolName === AVATAR_CASTING_TOOL_NAME);
300
327
  }
301
328
  export class StreamStallError extends Error {
@@ -817,7 +844,7 @@ beforeEndInput) {
817
844
  }, timings.exitGuardMs);
818
845
  exitGuard.unref?.();
819
846
  };
820
- const scheduleEnd = () => {
847
+ const scheduleEnd = (delayMs = timings.settleMs) => {
821
848
  if (settleTimer) {
822
849
  return;
823
850
  }
@@ -829,7 +856,7 @@ beforeEndInput) {
829
856
  endInput();
830
857
  return;
831
858
  }
832
- settleTimer = setTimeout(endInput, timings.settleMs);
859
+ settleTimer = setTimeout(endInput, delayMs);
833
860
  settleTimer.unref?.();
834
861
  };
835
862
  return {
@@ -895,7 +922,7 @@ beforeEndInput) {
895
922
  // second result) would strand the turn until the watchdog. An emptied
896
923
  // background_tasks_changed level counts as a settle the same way.
897
924
  if (lastWasTaskLifecycle && cleanResultSeen && !hasInFlightWork()) {
898
- scheduleEnd();
925
+ scheduleEnd(timings.wakeMs);
899
926
  }
900
927
  },
901
928
  hasInFlightWork,
@@ -988,12 +1015,14 @@ export class BridgeJobExecutor {
988
1015
  stallCheckIntervalMs;
989
1016
  recordTurnEvent;
990
1017
  turnEndSettleMs;
1018
+ turnEndWakeMs;
991
1019
  turnEndExitGuardMs;
992
1020
  now;
993
1021
  readUsageBaseline;
994
1022
  stalledCliExitMs;
995
1023
  readFlowTree;
996
1024
  settleWorkspace;
1025
+ judge;
997
1026
  constructor(dependencies) {
998
1027
  this.sdk = dependencies.sdk;
999
1028
  this.relay = dependencies.relay;
@@ -1006,6 +1035,7 @@ export class BridgeJobExecutor {
1006
1035
  this.stallCheckIntervalMs = dependencies.stallCheckIntervalMs ?? DEFAULT_STALL_CHECK_INTERVAL_MS;
1007
1036
  this.recordTurnEvent = dependencies.recordTurnEvent ?? recordTurnEvent;
1008
1037
  this.turnEndSettleMs = dependencies.turnEndSettleMs ?? DEFAULT_TURN_END_SETTLE_MS;
1038
+ this.turnEndWakeMs = dependencies.turnEndWakeMs ?? DEFAULT_TURN_END_WAKE_MS;
1009
1039
  this.turnEndExitGuardMs = dependencies.turnEndExitGuardMs ?? DEFAULT_TURN_END_EXIT_GUARD_MS;
1010
1040
  this.now = dependencies.now ?? Date.now;
1011
1041
  this.readUsageBaseline =
@@ -1015,6 +1045,7 @@ export class BridgeJobExecutor {
1015
1045
  this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
1016
1046
  this.readFlowTree =
1017
1047
  dependencies.readFlowTree ?? ((flowSlug) => readRawFlowTree(bridgeRuntimeFiles.flowsDir(), flowSlug));
1048
+ this.judge = dependencies.judge ?? jevJudge(process.env.OPENROUTER_API_KEY);
1018
1049
  }
1019
1050
  async execute(job, controls) {
1020
1051
  const state = createBridgeJobState();
@@ -1087,7 +1118,11 @@ export class BridgeJobExecutor {
1087
1118
  const verbatim = verbatimSlashCommand(job.prompt) ??
1088
1119
  leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
1089
1120
  let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
1090
- const reminder = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
1121
+ const reminders = [
1122
+ verbatim === null ? currentReferenceReminder(job.currentReference) : undefined,
1123
+ credentialReminder(job.credentials ?? [], credentialsEnabled()),
1124
+ ].filter((text) => text !== undefined);
1125
+ const reminder = reminders.length > 0 ? reminders.join("\n\n") : undefined;
1091
1126
  if (job.attachments && job.attachments.length > 0) {
1092
1127
  requestPrompt += await this.prepareAttachments(job.attachments, "message");
1093
1128
  }
@@ -1364,7 +1399,7 @@ export class BridgeJobExecutor {
1364
1399
  if (!servers) {
1365
1400
  return notReady;
1366
1401
  }
1367
- const result = await settleWithin(query.setMcpServers({ ...servers, baker_ui: createBakerUiServer() }), MCP_RELOAD_TIMEOUT_MS);
1402
+ const result = await settleWithin(query.setMcpServers({ ...servers, baker_ui: createBakerUiServer({ credentials: credentialsEnabled() }) }), MCP_RELOAD_TIMEOUT_MS);
1368
1403
  const failed = Object.keys(result?.errors ?? {});
1369
1404
  if (failed.length > 0) {
1370
1405
  console.error(`[mcp] reload after a connection could not start ${failed.join(", ")}`);
@@ -1396,7 +1431,7 @@ export class BridgeJobExecutor {
1396
1431
  * handlers — except on an unattended turn, where there is nobody to answer and
1397
1432
  * every one of them is refused up front (see `UNATTENDED_PROMPT_MESSAGE`).
1398
1433
  */
1399
- routeToolUse(job, controls, toolName, input, opts) {
1434
+ async routeToolUse(job, controls, toolName, input, opts) {
1400
1435
  const refusedShell = reviewShellRefusal(job, toolName, input);
1401
1436
  if (refusedShell) {
1402
1437
  return Promise.resolve({ behavior: "deny", message: refusedShell });
@@ -1412,9 +1447,9 @@ export class BridgeJobExecutor {
1412
1447
  });
1413
1448
  return Promise.resolve({ behavior: "deny", message: UNATTENDED_PROMPT_MESSAGE });
1414
1449
  }
1415
- const malformed = refuseMalformedCall(toolName, input, job.prompt, userTurnsBefore(job));
1450
+ const malformed = await refuseMalformedCall(toolName, input, this.judge, job.prompt, userTurnsBefore(job));
1416
1451
  if (malformed)
1417
- return Promise.resolve({ behavior: "deny", message: malformed });
1452
+ return { behavior: "deny", message: malformed };
1418
1453
  // Cast what is owed FIRST, whatever comes next — including another create. Checked
1419
1454
  // the other way round, a second create (the agent retrying after a rejected flag,
1420
1455
  // which is what happens in practice) would overwrite the handle and the first
@@ -1467,6 +1502,15 @@ export class BridgeJobExecutor {
1467
1502
  if (toolName === CONNECTION_INPUT_TOOL_NAME) {
1468
1503
  return this.handleConnectionInputRequest(job, controls, input, opts.toolUseID, opts.signal);
1469
1504
  }
1505
+ if (toolName === CREDENTIAL_INPUT_TOOL_NAME) {
1506
+ if (!credentialsEnabled()) {
1507
+ return Promise.resolve({
1508
+ behavior: "deny",
1509
+ message: "Saved credentials are not available in this workspace. Ask the user to share what you need another way, and never ask them to paste a password or key into the chat.",
1510
+ });
1511
+ }
1512
+ return this.handleCredentialInputRequest(job, controls, input, opts.toolUseID, opts.signal);
1513
+ }
1470
1514
  if (toolName === AVATAR_CASTING_TOOL_NAME) {
1471
1515
  return this.handleAvatarCastingRequest(job, controls, input, opts.toolUseID, opts.signal);
1472
1516
  }
@@ -1537,7 +1581,7 @@ export class BridgeJobExecutor {
1537
1581
  settingSources: ["project"],
1538
1582
  // `baker_ui` hosts in-process interactive dashboard tools (secure tag
1539
1583
  // form). Always present; HTTP (Composio) servers merge alongside it.
1540
- mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer() },
1584
+ mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer({ credentials: credentialsEnabled() }) },
1541
1585
  // Only send the key when there is something in it: the SDK forwards
1542
1586
  // `settings` to the CLI whenever it is present, so an empty object still
1543
1587
  // becomes a `--settings {}` flag tier for the CLI to parse.
@@ -1550,7 +1594,7 @@ export class BridgeJobExecutor {
1550
1594
  // the workspace could not be read; omit rather than allowlist nothing.
1551
1595
  ...(skills ? { skills } : {}),
1552
1596
  disallowedTools: disallowedToolsFor(job),
1553
- ...reviewQueryOptions(job),
1597
+ ...withCredentialScrub(reviewQueryOptions(job)),
1554
1598
  ...(job.planMode ? { permissionMode: "plan" } : {}),
1555
1599
  canUseTool: (toolName, input, opts) => this.routeToolUse(job, controls, toolName, input, opts),
1556
1600
  // No `fallbackModel`, and no server-side refusal `fallbacks` in the extra
@@ -1575,6 +1619,10 @@ export class BridgeJobExecutor {
1575
1619
  // system-reminder repeatedly pushed the agent toward that ephemeral
1576
1620
  // backlog instead of the product's own Tasks (`baker actions`).
1577
1621
  CLAUDE_CODE_ENABLE_TASKS: "0",
1622
+ // The CLI clamps any Bash timeout to 10 minutes unless told otherwise;
1623
+ // the workspace's `inject-timeouts` hook gives a render up to 30, and at
1624
+ // the clamp the render was moved to the background mid-repair.
1625
+ BASH_MAX_TIMEOUT_MS: String(30 * 60_000),
1578
1626
  // `citty` writes ANSI escapes into `baker --help` unconditionally —
1579
1627
  // they are never TTY-gated, so every help read the agent does inside
1580
1628
  // the sandbox pays ~5-10% of its tokens on colour codes it can't see.
@@ -1649,7 +1697,7 @@ export class BridgeJobExecutor {
1649
1697
  // A reading that answers after the query drained (past its timeout) belongs
1650
1698
  // to a query already folded into the turn — and a retry reuses `state`.
1651
1699
  let drained = false;
1652
- const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent, async () => {
1700
+ const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, wakeMs: this.turnEndWakeMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent, async () => {
1653
1701
  const reading = await query.usage?.();
1654
1702
  if (reading && !drained) {
1655
1703
  settledCostUsd = recordCumulativeUsage(state, reading);
@@ -1757,6 +1805,7 @@ export class BridgeJobExecutor {
1757
1805
  controls.pendingTagInputs.size > 0 ||
1758
1806
  controls.pendingFlowInputs.size > 0 ||
1759
1807
  controls.pendingConnectionInputs.size > 0 ||
1808
+ controls.pendingCredentialInputs.size > 0 ||
1760
1809
  controls.pendingAvatarCastings.size > 0,
1761
1810
  onStall: () => {
1762
1811
  console.error(`[BridgeJob] no stream data for ${this.stallTimeoutMs}ms while awaiting the model — closing query for retry (thread ${job.threadId})`);
@@ -2265,6 +2314,36 @@ export class BridgeJobExecutor {
2265
2314
  : decision.value;
2266
2315
  return { behavior: "allow", updatedInput: { connections, resolution } };
2267
2316
  }
2317
+ async handleCredentialInputRequest(job, controls, input, toolUseId, signal) {
2318
+ const target = { threadId: job.threadId, turnId: job.turnId };
2319
+ const request = normalizeCredentialInputToolInput(input);
2320
+ if (!request) {
2321
+ return {
2322
+ behavior: "deny",
2323
+ message: "request_credential requires { label, reason, allowedHosts: [...] } (and optionally a lowercase `name`). Retry with that shape.",
2324
+ };
2325
+ }
2326
+ const posted = await this.relay.credentialInputRequest(target, { toolUseId, ...request });
2327
+ if (!posted) {
2328
+ console.error(`[request_credential] failed to post credential-input-request for ${toolUseId} — denying`);
2329
+ return {
2330
+ behavior: "deny",
2331
+ message: "The credential request could not be delivered to the dashboard — retry request_credential in a moment.",
2332
+ };
2333
+ }
2334
+ const decision = await this.awaitPromptDecision({
2335
+ label: "request_credential",
2336
+ signal,
2337
+ controls,
2338
+ pending: controls.pendingCredentialInputs,
2339
+ toolUseId,
2340
+ });
2341
+ if (decision.kind === "unanswered") {
2342
+ return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
2343
+ }
2344
+ void this.relay.credentialInputResolved(target);
2345
+ return { behavior: "allow", updatedInput: { ...request, resolution: decision.value } };
2346
+ }
2268
2347
  async handlePlanReview(job, controls, input, toolUseId, signal) {
2269
2348
  const target = { threadId: job.threadId, turnId: job.turnId };
2270
2349
  controls.markEvent?.();