@koda-sl/baker-bridge 0.108.0 → 0.109.0-dev.557795bef

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 (320) hide show
  1. package/README.md +107 -51
  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/broker-process.d.ts +10 -0
  11. package/dist/broker/broker-process.d.ts.map +1 -0
  12. package/dist/broker/broker-process.js +20 -0
  13. package/dist/broker/broker-process.js.map +1 -0
  14. package/dist/broker/broker-process.test.d.ts +2 -0
  15. package/dist/broker/broker-process.test.d.ts.map +1 -0
  16. package/dist/broker/broker-process.test.js +13 -0
  17. package/dist/broker/broker-process.test.js.map +1 -0
  18. package/dist/broker/ca.d.ts +38 -0
  19. package/dist/broker/ca.d.ts.map +1 -0
  20. package/dist/broker/ca.js +80 -0
  21. package/dist/broker/ca.js.map +1 -0
  22. package/dist/broker/ca.test.d.ts +2 -0
  23. package/dist/broker/ca.test.d.ts.map +1 -0
  24. package/dist/broker/ca.test.js +58 -0
  25. package/dist/broker/ca.test.js.map +1 -0
  26. package/dist/broker/credential-cache.d.ts +24 -0
  27. package/dist/broker/credential-cache.d.ts.map +1 -0
  28. package/dist/broker/credential-cache.js +76 -0
  29. package/dist/broker/credential-cache.js.map +1 -0
  30. package/dist/broker/credential-cache.test.d.ts +2 -0
  31. package/dist/broker/credential-cache.test.d.ts.map +1 -0
  32. package/dist/broker/credential-cache.test.js +136 -0
  33. package/dist/broker/credential-cache.test.js.map +1 -0
  34. package/dist/broker/credential-text.d.ts +45 -0
  35. package/dist/broker/credential-text.d.ts.map +1 -0
  36. package/dist/broker/credential-text.js +103 -0
  37. package/dist/broker/credential-text.js.map +1 -0
  38. package/dist/broker/credential-text.test.d.ts +2 -0
  39. package/dist/broker/credential-text.test.d.ts.map +1 -0
  40. package/dist/broker/credential-text.test.js +54 -0
  41. package/dist/broker/credential-text.test.js.map +1 -0
  42. package/dist/broker/env.d.ts +21 -0
  43. package/dist/broker/env.d.ts.map +1 -0
  44. package/dist/broker/env.js +20 -0
  45. package/dist/broker/env.js.map +1 -0
  46. package/dist/broker/main.d.ts +3 -0
  47. package/dist/broker/main.d.ts.map +1 -0
  48. package/dist/broker/main.js +57 -0
  49. package/dist/broker/main.js.map +1 -0
  50. package/dist/broker/proxy.d.ts +24 -0
  51. package/dist/broker/proxy.d.ts.map +1 -0
  52. package/dist/broker/proxy.integration.test.d.ts +2 -0
  53. package/dist/broker/proxy.integration.test.d.ts.map +1 -0
  54. package/dist/broker/proxy.integration.test.js +197 -0
  55. package/dist/broker/proxy.integration.test.js.map +1 -0
  56. package/dist/broker/proxy.js +147 -0
  57. package/dist/broker/proxy.js.map +1 -0
  58. package/dist/broker/rewrite.d.ts +44 -0
  59. package/dist/broker/rewrite.d.ts.map +1 -0
  60. package/dist/broker/rewrite.js +291 -0
  61. package/dist/broker/rewrite.js.map +1 -0
  62. package/dist/broker/rewrite.test.d.ts +2 -0
  63. package/dist/broker/rewrite.test.d.ts.map +1 -0
  64. package/dist/broker/rewrite.test.js +171 -0
  65. package/dist/broker/rewrite.test.js.map +1 -0
  66. package/dist/broker/socket-service.d.ts +18 -0
  67. package/dist/broker/socket-service.d.ts.map +1 -0
  68. package/dist/broker/socket-service.js +113 -0
  69. package/dist/broker/socket-service.js.map +1 -0
  70. package/dist/broker/socket-service.test.d.ts +2 -0
  71. package/dist/broker/socket-service.test.d.ts.map +1 -0
  72. package/dist/broker/socket-service.test.js +80 -0
  73. package/dist/broker/socket-service.test.js.map +1 -0
  74. package/dist/broker/supervisor-main.d.ts +3 -0
  75. package/dist/broker/supervisor-main.d.ts.map +1 -0
  76. package/dist/broker/supervisor-main.js +70 -0
  77. package/dist/broker/supervisor-main.js.map +1 -0
  78. package/dist/broker/supervisor.d.ts +25 -0
  79. package/dist/broker/supervisor.d.ts.map +1 -0
  80. package/dist/broker/supervisor.js +88 -0
  81. package/dist/broker/supervisor.js.map +1 -0
  82. package/dist/broker/supervisor.test.d.ts +2 -0
  83. package/dist/broker/supervisor.test.d.ts.map +1 -0
  84. package/dist/broker/supervisor.test.js +161 -0
  85. package/dist/broker/supervisor.test.js.map +1 -0
  86. package/dist/broker/token-bucket.d.ts +9 -0
  87. package/dist/broker/token-bucket.d.ts.map +1 -0
  88. package/dist/broker/token-bucket.js +18 -0
  89. package/dist/broker/token-bucket.js.map +1 -0
  90. package/dist/broker/usage-reporter.d.ts +18 -0
  91. package/dist/broker/usage-reporter.d.ts.map +1 -0
  92. package/dist/broker/usage-reporter.js +31 -0
  93. package/dist/broker/usage-reporter.js.map +1 -0
  94. package/dist/broker/usage-reporter.test.d.ts +2 -0
  95. package/dist/broker/usage-reporter.test.d.ts.map +1 -0
  96. package/dist/broker/usage-reporter.test.js +20 -0
  97. package/dist/broker/usage-reporter.test.js.map +1 -0
  98. package/dist/cli.d.ts.map +1 -1
  99. package/dist/cli.js +2 -0
  100. package/dist/cli.js.map +1 -1
  101. package/dist/hono/agent-session.d.ts +4 -0
  102. package/dist/hono/agent-session.d.ts.map +1 -1
  103. package/dist/hono/agent-session.js +28 -2
  104. package/dist/hono/agent-session.js.map +1 -1
  105. package/dist/hono/agent-session.test.js +37 -0
  106. package/dist/hono/agent-session.test.js.map +1 -1
  107. package/dist/hono/agent-tasks.d.ts.map +1 -1
  108. package/dist/hono/agent-tasks.js +11 -7
  109. package/dist/hono/agent-tasks.js.map +1 -1
  110. package/dist/hono/agent-tasks.test.js +15 -0
  111. package/dist/hono/agent-tasks.test.js.map +1 -1
  112. package/dist/hono/agent.d.ts +2 -0
  113. package/dist/hono/agent.d.ts.map +1 -1
  114. package/dist/hono/agent.js +19 -2
  115. package/dist/hono/agent.js.map +1 -1
  116. package/dist/hono/agent.test.js +8 -2
  117. package/dist/hono/agent.test.js.map +1 -1
  118. package/dist/hono/avatar-accent.d.ts +23 -5
  119. package/dist/hono/avatar-accent.d.ts.map +1 -1
  120. package/dist/hono/avatar-accent.js +54 -63
  121. package/dist/hono/avatar-accent.js.map +1 -1
  122. package/dist/hono/avatar-accent.test.js +64 -35
  123. package/dist/hono/avatar-accent.test.js.map +1 -1
  124. package/dist/hono/avatar-casting.d.ts +1 -0
  125. package/dist/hono/avatar-casting.d.ts.map +1 -1
  126. package/dist/hono/avatar-casting.js +18 -4
  127. package/dist/hono/avatar-casting.js.map +1 -1
  128. package/dist/hono/avatar-casting.test.js +5 -0
  129. package/dist/hono/avatar-casting.test.js.map +1 -1
  130. package/dist/hono/avatar-face-question.d.ts +23 -6
  131. package/dist/hono/avatar-face-question.d.ts.map +1 -1
  132. package/dist/hono/avatar-face-question.js +48 -59
  133. package/dist/hono/avatar-face-question.js.map +1 -1
  134. package/dist/hono/avatar-face-question.test.js +46 -101
  135. package/dist/hono/avatar-face-question.test.js.map +1 -1
  136. package/dist/hono/baker-ui-tool-schemas.test.js +30 -4
  137. package/dist/hono/baker-ui-tool-schemas.test.js.map +1 -1
  138. package/dist/hono/blocking-card.d.ts +12 -0
  139. package/dist/hono/blocking-card.d.ts.map +1 -0
  140. package/dist/hono/blocking-card.js +12 -0
  141. package/dist/hono/blocking-card.js.map +1 -0
  142. package/dist/hono/bridge-job.d.ts +68 -2
  143. package/dist/hono/bridge-job.d.ts.map +1 -1
  144. package/dist/hono/bridge-job.js +301 -56
  145. package/dist/hono/bridge-job.js.map +1 -1
  146. package/dist/hono/bridge-job.test.js +489 -26
  147. package/dist/hono/bridge-job.test.js.map +1 -1
  148. package/dist/hono/check-content.d.ts +49 -0
  149. package/dist/hono/check-content.d.ts.map +1 -0
  150. package/dist/hono/check-content.js +182 -0
  151. package/dist/hono/check-content.js.map +1 -0
  152. package/dist/hono/check-content.test.d.ts +2 -0
  153. package/dist/hono/check-content.test.d.ts.map +1 -0
  154. package/dist/hono/check-content.test.js +87 -0
  155. package/dist/hono/check-content.test.js.map +1 -0
  156. package/dist/hono/connection-input.d.ts +170 -12
  157. package/dist/hono/connection-input.d.ts.map +1 -1
  158. package/dist/hono/connection-input.js +166 -14
  159. package/dist/hono/connection-input.js.map +1 -1
  160. package/dist/hono/connection-input.test.js +112 -9
  161. package/dist/hono/connection-input.test.js.map +1 -1
  162. package/dist/hono/convex.d.ts +8 -0
  163. package/dist/hono/convex.d.ts.map +1 -1
  164. package/dist/hono/convex.js +8 -0
  165. package/dist/hono/convex.js.map +1 -1
  166. package/dist/hono/convex.test.js +4 -1
  167. package/dist/hono/convex.test.js.map +1 -1
  168. package/dist/hono/credential-input.d.ts +39 -0
  169. package/dist/hono/credential-input.d.ts.map +1 -0
  170. package/dist/hono/credential-input.js +78 -0
  171. package/dist/hono/credential-input.js.map +1 -0
  172. package/dist/hono/credential-scrub.d.ts +42 -0
  173. package/dist/hono/credential-scrub.d.ts.map +1 -0
  174. package/dist/hono/credential-scrub.js +173 -0
  175. package/dist/hono/credential-scrub.js.map +1 -0
  176. package/dist/hono/credential-scrub.test.d.ts +2 -0
  177. package/dist/hono/credential-scrub.test.d.ts.map +1 -0
  178. package/dist/hono/credential-scrub.test.js +130 -0
  179. package/dist/hono/credential-scrub.test.js.map +1 -0
  180. package/dist/hono/env.d.ts +1 -0
  181. package/dist/hono/env.d.ts.map +1 -1
  182. package/dist/hono/env.js +2 -0
  183. package/dist/hono/env.js.map +1 -1
  184. package/dist/hono/flow-crypto.d.ts +2 -1
  185. package/dist/hono/flow-crypto.d.ts.map +1 -1
  186. package/dist/hono/flow-crypto.js +50 -26
  187. package/dist/hono/flow-crypto.js.map +1 -1
  188. package/dist/hono/flow-crypto.test.js +32 -0
  189. package/dist/hono/flow-crypto.test.js.map +1 -1
  190. package/dist/hono/flow-input-card-fields.test.d.ts +10 -0
  191. package/dist/hono/flow-input-card-fields.test.d.ts.map +1 -0
  192. package/dist/hono/flow-input-card-fields.test.js +26 -0
  193. package/dist/hono/flow-input-card-fields.test.js.map +1 -0
  194. package/dist/hono/flow-input-fields.test.js +1 -1
  195. package/dist/hono/flow-input-fields.test.js.map +1 -1
  196. package/dist/hono/flow-input.d.ts +23 -2
  197. package/dist/hono/flow-input.d.ts.map +1 -1
  198. package/dist/hono/flow-input.js +63 -14
  199. package/dist/hono/flow-input.js.map +1 -1
  200. package/dist/hono/flow-input.test.js +67 -11
  201. package/dist/hono/flow-input.test.js.map +1 -1
  202. package/dist/hono/jev.d.ts +16 -0
  203. package/dist/hono/jev.d.ts.map +1 -0
  204. package/dist/hono/jev.js +60 -0
  205. package/dist/hono/jev.js.map +1 -0
  206. package/dist/hono/jev.test.d.ts +2 -0
  207. package/dist/hono/jev.test.d.ts.map +1 -0
  208. package/dist/hono/jev.test.js +15 -0
  209. package/dist/hono/jev.test.js.map +1 -0
  210. package/dist/hono/knowledge-content.d.ts.map +1 -1
  211. package/dist/hono/knowledge-content.js +4 -2
  212. package/dist/hono/knowledge-content.js.map +1 -1
  213. package/dist/hono/knowledge-content.test.js +6 -5
  214. package/dist/hono/knowledge-content.test.js.map +1 -1
  215. package/dist/hono/mcp-servers.d.ts +8 -1
  216. package/dist/hono/mcp-servers.d.ts.map +1 -1
  217. package/dist/hono/mcp-servers.js +17 -6
  218. package/dist/hono/mcp-servers.js.map +1 -1
  219. package/dist/hono/mcp-servers.test.js +25 -0
  220. package/dist/hono/mcp-servers.test.js.map +1 -1
  221. package/dist/hono/model-capabilities.d.ts.map +1 -1
  222. package/dist/hono/model-capabilities.js +20 -9
  223. package/dist/hono/model-capabilities.js.map +1 -1
  224. package/dist/hono/model-capabilities.test.js +16 -8
  225. package/dist/hono/model-capabilities.test.js.map +1 -1
  226. package/dist/hono/oom-priority.agent.test.d.ts +2 -0
  227. package/dist/hono/oom-priority.agent.test.d.ts.map +1 -0
  228. package/dist/hono/oom-priority.agent.test.js +138 -0
  229. package/dist/hono/oom-priority.agent.test.js.map +1 -0
  230. package/dist/hono/oom-priority.d.ts +39 -0
  231. package/dist/hono/oom-priority.d.ts.map +1 -0
  232. package/dist/hono/oom-priority.js +138 -0
  233. package/dist/hono/oom-priority.js.map +1 -0
  234. package/dist/hono/oom-priority.test.d.ts +2 -0
  235. package/dist/hono/oom-priority.test.d.ts.map +1 -0
  236. package/dist/hono/oom-priority.test.js +52 -0
  237. package/dist/hono/oom-priority.test.js.map +1 -0
  238. package/dist/hono/openrouter-session.d.ts.map +1 -1
  239. package/dist/hono/openrouter-session.js +12 -1
  240. package/dist/hono/openrouter-session.js.map +1 -1
  241. package/dist/hono/openrouter-session.test.js +9 -3
  242. package/dist/hono/openrouter-session.test.js.map +1 -1
  243. package/dist/hono/prompt-context.d.ts +8 -1
  244. package/dist/hono/prompt-context.d.ts.map +1 -1
  245. package/dist/hono/prompt-context.js +45 -7
  246. package/dist/hono/prompt-context.js.map +1 -1
  247. package/dist/hono/prompt-context.test.js +32 -1
  248. package/dist/hono/prompt-context.test.js.map +1 -1
  249. package/dist/hono/publish-recovery.d.ts +1 -1
  250. package/dist/hono/publish-recovery.d.ts.map +1 -1
  251. package/dist/hono/publish-recovery.js +9 -6
  252. package/dist/hono/publish-recovery.js.map +1 -1
  253. package/dist/hono/publish-recovery.test.js +11 -2
  254. package/dist/hono/publish-recovery.test.js.map +1 -1
  255. package/dist/hono/relay.d.ts +10 -1
  256. package/dist/hono/relay.d.ts.map +1 -1
  257. package/dist/hono/relay.js +6 -0
  258. package/dist/hono/relay.js.map +1 -1
  259. package/dist/hono/sandbox-content.d.ts +2 -0
  260. package/dist/hono/sandbox-content.d.ts.map +1 -1
  261. package/dist/hono/sandbox-content.js +8 -4
  262. package/dist/hono/sandbox-content.js.map +1 -1
  263. package/dist/hono/sandbox-content.test.js +20 -1
  264. package/dist/hono/sandbox-content.test.js.map +1 -1
  265. package/dist/hono/server.d.ts.map +1 -1
  266. package/dist/hono/server.js +32 -0
  267. package/dist/hono/server.js.map +1 -1
  268. package/dist/hono/server.test.js +21 -1
  269. package/dist/hono/server.test.js.map +1 -1
  270. package/dist/hono/session-continuity.integration.test.js +10 -1
  271. package/dist/hono/session-continuity.integration.test.js.map +1 -1
  272. package/dist/hono/tag-fields.d.ts.map +1 -1
  273. package/dist/hono/tag-fields.js +2 -0
  274. package/dist/hono/tag-fields.js.map +1 -1
  275. package/dist/hono/tag-input.d.ts +124 -3
  276. package/dist/hono/tag-input.d.ts.map +1 -1
  277. package/dist/hono/tag-input.js +27 -17
  278. package/dist/hono/tag-input.js.map +1 -1
  279. package/dist/hono/tag-input.test.js +19 -1
  280. package/dist/hono/tag-input.test.js.map +1 -1
  281. package/dist/hono/tool-mentions.d.ts.map +1 -1
  282. package/dist/hono/tool-mentions.js +1 -0
  283. package/dist/hono/tool-mentions.js.map +1 -1
  284. package/dist/hono/tool-mentions.test.js +5 -0
  285. package/dist/hono/tool-mentions.test.js.map +1 -1
  286. package/dist/hono/types.d.ts +8 -0
  287. package/dist/hono/types.d.ts.map +1 -1
  288. package/dist/hono/workspace-script.d.ts +10 -0
  289. package/dist/hono/workspace-script.d.ts.map +1 -0
  290. package/dist/hono/workspace-script.js +19 -0
  291. package/dist/hono/workspace-script.js.map +1 -0
  292. package/dist/hono/workspace-settle.d.ts +20 -0
  293. package/dist/hono/workspace-settle.d.ts.map +1 -0
  294. package/dist/hono/workspace-settle.js +50 -0
  295. package/dist/hono/workspace-settle.js.map +1 -0
  296. package/dist/review/digest.d.ts +8 -1
  297. package/dist/review/digest.d.ts.map +1 -1
  298. package/dist/review/digest.js +10 -43
  299. package/dist/review/digest.js.map +1 -1
  300. package/dist/review/digest.test.js +11 -6
  301. package/dist/review/digest.test.js.map +1 -1
  302. package/dist/review/evidence-block.js +1 -1
  303. package/dist/review/evidence-block.js.map +1 -1
  304. package/dist/review/evidence-block.test.js +0 -10
  305. package/dist/review/evidence-block.test.js.map +1 -1
  306. package/dist/review/shell-policy.d.ts +6 -0
  307. package/dist/review/shell-policy.d.ts.map +1 -1
  308. package/dist/review/shell-policy.js +240 -72
  309. package/dist/review/shell-policy.js.map +1 -1
  310. package/dist/review/shell-policy.test.js +114 -0
  311. package/dist/review/shell-policy.test.js.map +1 -1
  312. package/package.json +9 -5
  313. package/dist/hono/latitude-telemetry.d.ts +0 -17
  314. package/dist/hono/latitude-telemetry.d.ts.map +0 -1
  315. package/dist/hono/latitude-telemetry.js +0 -52
  316. package/dist/hono/latitude-telemetry.js.map +0 -1
  317. package/dist/hono/latitude-telemetry.test.d.ts +0 -2
  318. package/dist/hono/latitude-telemetry.test.d.ts.map +0 -1
  319. package/dist/hono/latitude-telemetry.test.js +0 -18
  320. package/dist/hono/latitude-telemetry.test.js.map +0 -1
package/README.md CHANGED
@@ -41,8 +41,10 @@ await server.start();
41
41
  | `BAKER_API_KEY` | API key for Convex callbacks (`bk_...`) |
42
42
  | `BAKER_BASE_BRANCH` | Git base branch for sandbox baseline lookups. Defaults to `main`; `/company`, `/brand`, `/knowledge`, landing definition, and `/flows/:slug` endpoints use the merge-base with this ref when `?baseline=1` is requested |
43
43
  | `INTERNAL_SECRET_KEY` | AES key for encrypting flow `_data.json` config at rest |
44
+ | `BAKER_RUNTIME_COMPANY_ID` | Optional. The company the Runtime works for; stamped into each Form destination the bridge seals. Deliberately not `BAKER_COMPANY_ID`, which the scaffold build reads as a production deploy setting |
44
45
  | `ANTHROPIC_API_KEY` | Anthropic API key (required by the Agent SDK) |
45
46
  | `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` | OpenRouter routing (base URL + per-org key) injected into the sandbox. Forwarded to the Agent SDK subprocess so the agent can reach its model |
47
+ | `OPENROUTER_API_KEY` | The same per-org key under its own name. The bridge reads the user's and the agent's words with it before letting two kinds of call through: a question asking the user to pick an avatar's face (the casting picker shows real portraits), and an avatar accent in a language nobody asked for. Unset or unreachable, both calls go through unchecked |
46
48
  | `CLAUDE_CODE_EXTRA_BODY` | Optional JSON merged by the CLI into every model request body. The bridge sets/extends it per turn with a per-thread `session_id` so OpenRouter sticky-routes the whole conversation to one provider instance and its instance-scoped prompt cache is re-read across the turn's many tool-call requests (see [Prompt-cache pinning](#prompt-cache-pinning)). It also defaults `provider` to Bedrock first (see [Bedrock is the preferred provider](#bedrock-is-the-preferred-provider)). Any inherited value is merged; our `session_id` and the `web`-plugin opt-out win (see [OpenRouter web plugin is forced off](#openrouter-web-plugin-is-forced-off)). An inherited `provider` replaces the Bedrock default |
47
49
  | `BAKER_CHAT_ID` | Present only when running inside a chat sandbox. When set, the bridge relocates Claude Code auto-memory into the cloned repo (`.claude/memory`) — see [Session journal](#session-journal-agent-memory) |
48
50
  | `BAKER_ACTING_USER_ID` | Injected into the Agent SDK subprocess env per turn (when the turn's sender is known) so in-sandbox `baker` CLI calls can scope work to the acting user — e.g. `baker mcp add --scope user` |
@@ -51,10 +53,26 @@ await server.start();
51
53
  | `NO_COLOR` | Set to `1` on every Agent SDK subprocess so `baker --help` output carries no ANSI escapes — see [Session surface](#session-surface) |
52
54
  | `BAKER_AGENT_MODEL` | Optional. Runs every turn in this runtime on the named model instead of the default, and is forwarded to subagents as `CLAUDE_CODE_SUBAGENT_MODEL`. Ignored unless it names a model with a known context window — see [Model selection](#model-selection) |
53
55
  | `CLAUDE_CODE_SUBAGENT_MODEL`, `CLAUDE_CODE_SUBAGENT_MODEL_FORCE` | Set on the Agent SDK subprocess only when `BAKER_AGENT_MODEL` is set. Together they override the `model:` each subagent pins in its own frontmatter |
54
- | `LATITUDE_API_KEY` / `LATITUDE_PROJECT` | Optional, both or neither. When set, every turn is traced to that Latitude project — see [Latitude tracing](#latitude-tracing) |
56
+ | `BAKER_CREDENTIALS` | Set to `1` in a Runtime with Credentials (the credential broker is running). Only then is `request_credential` offered to the agent; otherwise a call to it is refused |
55
57
 
56
58
  All variables are validated at startup via `@t3-oss/env-core` + `zod`.
57
59
 
60
+ ## Credential broker (`baker-credential-broker`)
61
+
62
+ A second bin, run as root in the sandbox before the agent starts, so the agent can use the user's stored credentials without ever seeing them. A third, `baker-credential-broker-supervisor`, takes the same environment, runs the broker and starts it again whenever it exits, stops answering `GET /health`, or fails to start at all; on `SIGTERM` it stops the broker and kills it if it is still running 5 s later; it is what a Runtime starts. It runs out of the OOM killer's reach with the broker's heap capped, and the bridge marks every process its agent starts as the OOM killer's first choice — through a command wrapper it passes as `CLAUDE_CODE_SHELL_PREFIX` (unless the Runtime set one), backed by a 2-second scan — so memory pressure ends one command, never the turn. `src/hono/oom-priority.agent.test.ts` runs the SDK's own agent binary through the wrapper, so an upgrade that changes how it calls `CLAUDE_CODE_SHELL_PREFIX` fails the suite. The agent writes `{{credential:NAME}}` in a request; the broker is an HTTP(S) proxy on `127.0.0.1` that swaps in the real value and swaps it back out of the response.
63
+
64
+ | Variable | Description |
65
+ |---|---|
66
+ | `CREDENTIAL_BROKER_TOKEN` | Required. Authenticates the broker to Baker's credential routes |
67
+ | `BAKER_CONVEX_SITE_URL` | Required. Baker's site URL |
68
+ | `BAKER_CREDENTIAL_PROXY_PORT` | Proxy port (default `8877`) |
69
+ | `BAKER_CA_DIR` | Where the broker's CA key (0600) and cert live (default `/etc/baker-ca`); run as root it also installs the cert into the system bundle |
70
+ | `BAKER_BROKER_SOCKET` | Unix socket for the bridge (default `/run/baker/broker.sock`): `POST /scrub`, `GET /health` |
71
+
72
+ The agent's processes get `agentProxyEnv()`: `HTTP(S)_PROXY` pointing at the broker, `NO_PROXY` for loopback plus the model gateway, `NODE_USE_ENV_PROXY=1`, `NODE_EXTRA_CA_CERTS` set to the broker's CA, and `REQUESTS_CA_BUNDLE` / `SSL_CERT_FILE` / `CURL_CA_BUNDLE` set to the system bundle.
73
+
74
+ Security model: a value is only substituted over https, and only for a host the user allowed for that credential. A placeholder in the URL or a header that is not allowed for the host, or that names no stored credential, is refused (403 / 404, or 503 while credentials can't be fetched) before anything leaves the sandbox. A placeholder that appears only in the body is left as plain text unless it names a credential allowed for that host. Response headers and text bodies up to 10 MB are scrubbed back to placeholders; those responses arrive buffered rather than streamed. The socket never returns a value. Once one fetch has succeeded, `/scrub` answers from the last good table while a refresh is in flight; the bridge retries a failed `/scrub` three times (0.25 s, 1 s, 3 s) before withholding a tool result.
75
+
58
76
  ## Session surface
59
77
 
60
78
  The Agent SDK ships developer-facing surfaces a marketer's session can never
@@ -87,9 +105,10 @@ use, and each one costs context on every turn. The bridge trims them at the
87
105
  - **Tools that reach past one session are disallowed** — `EnterWorktree` and
88
106
  `ExitWorktree` (the workspace's branches belong to the chat-branch and publish
89
107
  model, which has no account of a second checkout), `Workflow` (fans out dozens
90
- of agents in one call on the organization's key), and `SendMessage` (a runtime
91
- holds exactly one session, so there is no peer to address). `Task` itself is
92
- *not* disallowed — it is the subagent spawn the workspace's own agents ride.
108
+ of agents in one call on the organization's key). `Task` itself is *not*
109
+ disallowed — it is the subagent spawn the workspace's own agents ride — and
110
+ neither is `SendMessage`, which continues a subagent the session already ran;
111
+ the turn stays open until the continued subagent finishes.
93
112
 
94
113
  To see what a session actually ends up with, read the tool list off its own
95
114
  `init` message (`--output-format stream-json --verbose`) rather than this list:
@@ -108,19 +127,19 @@ subagents in `.claude/agents/`, discovered from disk via
108
127
  ## Model selection
109
128
 
110
129
  Every turn runs on `claude-opus-5-5` by default — a deliberate quality-over-cost
111
- choice. Opus 5.5 is $4/$20 per MTok on OpenRouter against Sonnet 5's $2/$10, i.e.
130
+ choice. Opus 5.5 is $4/$20 per MTok on OpenRouter against Sonnet 5.5's $2/$10, i.e.
112
131
  2x on input, output *and* cache reads. Two things contain the cost: the main
113
132
  thread runs at `medium` effort (see **Reasoning effort** below), and the shared
114
- subagents pin themselves to `claude-sonnet-5` in their own frontmatter rather
133
+ subagents pin themselves to `claude-sonnet-5-5` in their own frontmatter rather
115
134
  than inheriting this default — without that pin every fan-out worker would move
116
135
  to Opus pricing too.
117
136
 
118
137
  ### Overriding the model for a whole runtime
119
138
 
120
139
  Set `BAKER_AGENT_MODEL` to run every turn in that runtime on another model. It
121
- accepts either form of id (`claude-haiku-4-5` or `anthropic/claude-haiku-4.5`)
140
+ accepts either form of id (`claude-haiku-5-5` or `anthropic/claude-haiku-5.5`)
122
141
  and is normalised to what the Agent SDK expects. Development deployments set it
123
- to `claude-haiku-4-5`, where turnaround matters more than capability.
142
+ to `claude-haiku-5-5`, where turnaround matters more than capability.
124
143
 
125
144
  Two things it does that a plain model option does not:
126
145
 
@@ -157,9 +176,11 @@ prompt text on the newest user message instead; that leaves earlier breakpoints
157
176
  intact.
158
177
 
159
178
  Subagents set their own `effort` (and `model`) in their markdown frontmatter and
160
- do **not** inherit either from the main thread. The three scaffold-shared agents
161
- all run `claude-sonnet-5`: `creative-producer` and `landing-finish-reviewer` at
162
- `effort: low`, `research` at `effort: medium`. Either way it is cache-safe —
179
+ do **not** inherit either from the main thread. The four scaffold-shared agents
180
+ all run `claude-sonnet-5-5`: `landing-finish-reviewer`, `research` and
181
+ `visual-check` at `effort: low`, `creative-producer` at `effort: medium`
182
+ (rebuilding a reference's winning mechanism for a brand is judgment, not a
183
+ mechanical build). Either way it is cache-safe —
163
184
  a subagent carries its own context, so it cannot invalidate the parent's prefix.
164
185
 
165
186
  ## Prompt-cache pinning
@@ -185,23 +206,37 @@ cache stays warm while the Bedrock default is set.
185
206
 
186
207
  ## Bedrock is the preferred provider
187
208
 
188
- The same `CLAUDE_CODE_EXTRA_BODY` value carries
189
- `provider: { order: ["amazon-bedrock"], allow_fallbacks: true }`. Without it,
190
- OpenRouter load-balances Claude across Anthropic, Vertex, Azure and Bedrock by
191
- price and uptime, and the `session_id` pin then holds the conversation on whichever
192
- it picked first. With it, every request goes to Bedrock first and only moves to
193
- another provider when Bedrock is down or rate-limited. An inherited `provider`
194
- (set in the sandbox's `CLAUDE_CODE_EXTRA_BODY`) replaces this default as an
195
- operator override.
209
+ The same `CLAUDE_CODE_EXTRA_BODY` value carries a `provider` with
210
+ `allow_fallbacks: true` and this `order`:
211
+
212
+ ```json
213
+ ["amazon-bedrock", "amazon-bedrock/eu-west-1", "amazon-bedrock/us-east-1"]
214
+ ```
215
+
216
+ Without it, OpenRouter load-balances Claude across Anthropic, Vertex, Azure and
217
+ Bedrock by price and uptime, and the `session_id` pin then holds the conversation
218
+ on whichever it picked first. With it, every request goes to Bedrock first, then
219
+ to the two regional Bedrock endpoints, and only moves to another provider when all
220
+ three fail. The regions are there because Bedrock's failure mode is AWS capacity,
221
+ not our quota: on weekday mornings the default endpoint answers `503` to most Opus
222
+ 5.5 requests for hours, and AWS's advice for a `503` is another region. All three
223
+ run on our own Bedrock key; the providers after them bill OpenRouter credits. An
224
+ inherited `provider` (set in the sandbox's `CLAUDE_CODE_EXTRA_BODY`) replaces this
225
+ default as an operator override.
196
226
 
197
227
  A manual `order` bypasses OpenRouter's sticky routing, so while it is set the
198
228
  `session_id` pin above is inert and the `order` is what keeps the cache warm. The
199
229
  bare `amazon-bedrock` slug is a single endpoint (regional endpoints are separate
200
- slugs), so every request lands on the same one; measured, consecutive requests
201
- read the whole cached prefix back. The cost is on fallback: each request retries
202
- Bedrock first, so a cache warmed on the fallback provider is not reused while
203
- Bedrock is flapping. The `session_id` pin still applies whenever an inherited
204
- `provider` drops the `order`.
230
+ slugs), so while it is healthy every request lands on the same one; measured,
231
+ consecutive requests read the whole cached prefix back. The cost is on fallback.
232
+ Each endpoint keeps its own cache, the regional Bedrock ones included, so a
233
+ request that moves to the next endpoint starts cold and pays its whole prefix;
234
+ and each request retries the default endpoint first, so a cache warmed further
235
+ down the `order` is not reused while Bedrock is flapping. During a capacity
236
+ window every request also waits for the default endpoint's `503` (about 7 s)
237
+ before moving on.
238
+ The `session_id` pin still applies whenever an inherited `provider` drops the
239
+ `order`.
205
240
 
206
241
  The preference only holds for a request Bedrock can serve, and OpenRouter drops
207
242
  Bedrock from the route without an error when it can't. From Opus 5.5 the CLI
@@ -227,19 +262,6 @@ and other server tools are opt-in via `tools` only, so there is nothing to disab
227
262
  for those. An admin who sets "Prevent overrides" on the plugin wins over this field —
228
263
  the account setting is then the only off-switch.
229
264
 
230
- ## Latitude tracing
231
-
232
- With `LATITUDE_API_KEY` and `LATITUDE_PROJECT` in the environment, the bridge
233
- registers Latitude's Claude Code hook (`@latitude-data/claude-code-telemetry`,
234
- pinned) on `Stop` and `SessionEnd` through the per-query `settings`. At the end
235
- of each turn it uploads the turn as a trace: the prompt, every model call with
236
- its tokens, every tool call with its result, subagents and auto-memory. Each
237
- trace carries the workspace's git repo (`metadata.git.repo`, the company) and
238
- branch (`metadata.git.branch`, the chat); the Latitude session is the Claude
239
- session. Session reviews are not traced. Without both
240
- variables nothing is registered. Content is sent in full; mask attributes with
241
- `LATITUDE_REDACT_ATTRIBUTES` if needed.
242
-
243
265
  ## Endpoints
244
266
 
245
267
  | Method | Path | Auth | Description |
@@ -249,8 +271,9 @@ variables nothing is registered. Content is sent in full; mask attributes with
249
271
  | `GET` | `/company` | None | Return COMPANY.md as `{ content, title }`; `?baseline=1` also includes `baselineContent` from the git merge-base when available |
250
272
  | `GET` | `/brand` | None | Return BRAND.md as `{ content, title }`; `?baseline=1` also includes `baselineContent` from the git merge-base when available |
251
273
  | `GET` | `/brand/entry?path=<sub-path>` | None | Return one file inside `src/brand/` by its path (e.g. `logos/logo.webp`). Markdown paths return `{ content, title }` (+ `baselineContent` with `?baseline=1`); every other asset (logos, fonts) streams raw bytes with its `Content-Type`. `?baseline=1` serves the version at the git merge-base (the "before"), used by the per-file Brand changes preview |
252
- | `GET` | `/knowledge/:collection/:entry` or `/knowledge/:entry` | None | Return knowledge entry as `{ content, title }`; title-only Chat Change slugs are resolved by matching entry ids/titles. `?baseline=1` also includes `baselineContent` from the git merge-base when available |
274
+ | `GET` | `/knowledge/:collection/:entry` or `/knowledge/:entry` | None | Return knowledge entry as `{ content, title }` — `content` is the whole file, frontmatter included (the dashboard shows it as properties); title-only Chat Change slugs are resolved by matching entry ids/titles. `?baseline=1` also includes the whole-file `baselineContent` from the git merge-base when available |
253
275
  | `GET` | `/documents/:slug` | None | Serve document binary (pdf, pptx, docx, xlsx) |
276
+ | `GET` | `/checks/:slug` | None | Return a check as `{ slug, scope, manifest, readme, files, related }`: every file of `__tooling__/checks/extensions/<slug>/` (for a base check, only its `waivers.json`) with `content`, `baselineContent` from the git merge-base and a `status` (`added`, `modified`, `deleted`, `unchanged`), plus the `__tooling__/checks/sdk/` modules its scripts import |
254
277
  | `GET` | `/landings/:slug/definition` | None | Return landing `_definition.md` as `{ content, title }`; `?baseline=1` also includes `baselineContent` from the git merge-base when available |
255
278
  | `GET` | `/flows/:slug` | None | Read flow `_data.json` (decrypted). `?baseline=1` returns `{ flow, baselineFlow, baselineStatus }` — used by the dashboard's structural changes view. `baselineStatus` is `found` (baseline decoded, `baselineFlow` set), `new` (flow absent at the merge-base, i.e. not yet published), `unavailable` (no base ref / merge-base to compare against, or an unexpected git failure), or `undecodable` (baseline present at the merge-base but not readable — invalid JSON or not decryptable with the current key); the last two keep the UI from rendering a misleading full "added" diff |
256
279
  | `PUT` | `/flows/:slug` | None | Write flow `_data.json` (encrypts at rest) |
@@ -268,14 +291,20 @@ variables nothing is registered. Content is sent in full; mask attributes with
268
291
 
269
292
  Every turn also carries an in-process SDK MCP server named `baker_ui` hosting interactive dashboard tools. Its first tool, `request_tag_input`, is the **blocking secure tag form**: the agent invokes it when a marketing tag needs secret values (API keys, access tokens, OAuth connections). The bridge intercepts the call in `canUseTool` (same pattern as `AskUserQuestion`), POSTs `/api/chat/tag-input-request` to Convex so the dashboard renders the form, and suspends the turn until Convex delivers the resolution via `POST /tag-input-decision`. Secret values flow dashboard → Convex tag draft directly; the bridge and the model only ever see the redacted result (`{ status, ref, type, identifier, secretFieldsSet }`). Secret values a model puts into `prefilledConfig` are stripped bridge-side (and again backend-side) — their names become requested fields instead. `prefilledConfig` keys must be real config fields of the tag type (the tool description lists them per type); a call that invents field names is denied with the valid ones, because such a key renders nowhere in the form and would otherwise fail at stage time, after the user already approved. Live wait matches `AskUserQuestion` (15 min); like all blocking prompts, the form itself never expires — the thread parks and a later submission recovers the turn. It is **not** a connection route: a `googleTagManager` tag is the container snippet on the site, while the Tag Manager connection that unlocks the container's contents also has to name a container — which this form cannot do — so `oauthProviderId` is not requestable there and the backend rejects it, pointing the agent at `request_connection`.
270
293
 
271
- Its second tool, `request_flow_input`, is the **blocking form-configuration form**: the agent invokes it to configure the confidential parts of a flow ("Form") it cannot author itself — side-effect credentials/connections (`encryptedConfig`, `oauthProviderId`) and widget third-party field definitions (`nodeData.form.external` for HubSpot/Calendly/HighLevel/SavvyCal). The agent authors the surrounding structure in `_data.json` and references it by `nodeId`/`sideEffectId`. Same `canUseTool` park pattern: the bridge POSTs `/api/chat/flow-input-request`, the dashboard renders a tabbed form (secret inputs / OAuth connect+account picker / live resource picker), and on submit Convex delivers the resolved values to `POST /flow-input-decision`. Unlike tags, the resolved values are written **into the branch flow file** through the encrypting `writeFlow` (read → patch node/side-effect → write); the model only ever sees a redacted result: `{ status, nodeId, sideEffectId?, secretFieldsSet, destination?, connected?, fields?, note? }`. `destination` is the non-secret half of what the user chose — the spreadsheet, the sheet, the portal, the Calendly event — so an agent that has just configured an integration does not have to spend a turn on `baker flows show --full` to learn what it configured, and does not map columns against a guess. For a widget node, `fields` carries the autofill keys the picked resource accepts — literally the left-hand side of an `autofilledFields` entry, adapter prefix included, so a SavvyCal custom field arrives as `questions.<id>` because that is what its adapter reads. They come from each provider's own field container (`fieldGroups[].fields[]`, `customQuestions[]`, `customParams.formFields[]`, `customFormFields[]`, `fields[]`, `customFields[]`) rather than from a search for name-shaped strings, so a select's option labels and the resource's own title are not among them (a HubSpot form's `name` is "Contact us"; it is reported under `destination`). A widget with no field list of its own returns none rather than a guess. Both targets go through the same filter: only plain strings and numbers are echoed, and anything in that type's secret list is redacted to a name in `secretFieldsSet`. A few non-secret keys are withheld from `destination` on top of that, because they are not an answer either — an `httpWebhook`'s basic-auth `username` is not a typed credential, so it is not in the secret list, but echoing it hands the model half a credential and nothing on the side effect is addressed by it. Leaked secret values in `prefilledConfig` are stripped bridge-side (and again backend-side). Live wait matches `AskUserQuestion` (15 min); like all blocking prompts, the form itself never expires — the thread parks and a later submission recovers the turn.
294
+ A ChatGPT Ads tag pre-fills its Pixel ID and requests the optional Conversions API key in this secure form. The bridge classifies `apiKey` as a secret, so the agent never sees it and the published page receives only the Pixel ID and a signed server-event reference.
295
+
296
+ Its second tool, `request_flow_input`, is the **blocking form-configuration form**: the agent invokes it to configure the confidential parts of a flow ("Form") it cannot author itself — side-effect credentials/connections (`encryptedConfig`, `oauthProviderId`) and widget third-party field definitions (`nodeData.form.external` for HubSpot/Calendly/HighLevel/SavvyCal). The agent authors the surrounding structure in `_data.json` and references it by `nodeId`/`sideEffectId`. Same `canUseTool` park pattern: the bridge POSTs `/api/chat/flow-input-request`, the dashboard renders a tabbed form (secret inputs / OAuth connect+account picker / live resource picker), and on submit Convex delivers the resolved values to `POST /flow-input-decision`. Unlike tags, the resolved values are written **into the branch flow file** through the encrypting `writeFlow` (read → patch node/side-effect → write); the model only ever sees a redacted result: `{ status, nodeId, sideEffectId?, secretFieldsSet, destination?, connected?, fields?, note? }`. `destination` is the non-secret half of what the user chose — the spreadsheet, the sheet, the portal, the Calendly event — so an agent that has just configured an integration does not have to spend a turn on `baker flows show --full` to learn what it configured, and does not map columns against a guess. For a widget node, `fields` carries the autofill keys the picked resource accepts — literally the left-hand side of an `autofilledFields` entry, adapter prefix included, so a SavvyCal custom field arrives as `questions.<id>` because that is what its adapter reads. They come from each provider's own field container (`fieldGroups[].fields[]`, `customQuestions[]`, `customParams.formFields[]`, `customFormFields[]`, `fields[]`, `customFields[]`) rather than from a search for name-shaped strings, so a select's option labels and the resource's own title are not among them (a HubSpot form's `name` is "Contact us"; it is reported under `destination`). A widget with no field list of its own returns none rather than a guess. Both targets go through the same filter: only plain strings and numbers are echoed, and anything in that type's secret list is redacted to a name in `secretFieldsSet`. A few non-secret keys are withheld from `destination` on top of that, because they are not an answer either — an `httpWebhook`'s basic-auth `username` is not a typed credential, so it is not in the secret list, but echoing it hands the model half a credential and nothing on the side effect is addressed by it. Leaked secret values in `prefilledConfig` are stripped bridge-side (and again backend-side), and a call whose `prefilledConfig` names a key the side effect's card does not render (an email card reads `email` and `subject`, not `to`) is denied with the valid keys, because the card would drop it silently. A request for an `email` side effect is denied outright with the `baker flows connect … --email … --subject …` call that sets it: nothing in an email destination is secret, so it is the agent's to state, never the user's card. Live wait matches `AskUserQuestion` (15 min); like all blocking prompts, the form itself never expires — the thread parks and a later submission recovers the turn.
272
297
 
273
- Its third tool, `request_connection`, is the **blocking connection request**: the agent invokes it when a data platform the task needs — Google Ads, LinkedIn Ads, Meta Ads, X Ads, Google Analytics 4, Google Search Console, or Google Tag Manager — is not connected yet, instead of telling the user to go to Settings. Same `canUseTool` park pattern: the bridge POSTs `/api/chat/connection-input-request`, the dashboard renders one tab per platform with the OAuth connect button and the account/property/site/container picker, and on submit Convex delivers the outcome to `POST /connection-input-decision`. Nothing confidential passes through the bridge or the model: the dashboard writes the selection onto the company's connection, and the resolution carries only `{ status, platform, accountLabel?, selected[] }`. Live wait matches `AskUserQuestion` (15 min).
298
+ Its third tool, `request_connection`, is the **blocking connection request**: the agent invokes it when a data platform the task needs — Google Ads, LinkedIn Ads, Meta Ads, X Ads, Google Analytics 4, Google Search Console, or Google Tag Manager — is not connected yet, instead of telling the user to go to Settings. Same `canUseTool` park pattern: the bridge POSTs `/api/chat/connection-input-request`, the dashboard renders one tab per platform with the OAuth connect button and the account/property/site/container picker, and on submit Convex delivers the outcome to `POST /connection-input-decision`. Nothing confidential passes through the bridge or the model: the dashboard writes the selection onto the company's connection, and the resolution carries only `{ status, platform, accountLabel?, selected[] }`. A Google Drive tab may add `choose: "media-folder"` (kept only on Google Drive): the user picks a folder that Baker imports as a media source, and the connected result also carries its `mediaSourceKey`. An ad or data platform tab may add `pick: true` (kept only on platforms with accounts to choose): when the platform is already connected, the card opens its account picker straight away, for the agent that found the account it needs is not selected. Live wait matches `AskUserQuestion` (15 min). It can also ask for a Shopify store (`platform: "shopify"`): the card asks for the store's address and sends the user to Shopify to approve Baker.
299
+
300
+ The same card adds tools. A `{ server: { name, url, auth?, keyHeader?, scope? } }` entry opens a remote MCP server filled in from the user's instructions (`auth`: `none` \| `key` \| `sign-in`; the key is typed into the card, never into the chat), and an `{ app }` entry connects a managed app from `baker mcp catalog` — the fallback for a vendor with no server of its own. When an app or server comes through, the bridge fetches the turn's MCP servers afresh (`McpServersProvider` with `{ fresh: true }`) and loads them into the running query with `setMcpServers` — `baker_ui` included, since the SDK drops any in-process server left out — before the tool answers, and adds `toolsReady` to the resolution. The provider keeps that set for the next turn, so the tool list changes once. A platform-only resolution never reloads.
274
301
 
275
302
  Its fourth tool, `request_avatar_casting`, is the **blocking face picker**: an avatar invented from a description gets several casting portraits rendered, the user picks one in the dashboard, and the identity sheet every later render grounds on is fused from that one. Same `canUseTool` park pattern — the bridge POSTs `/api/chat/avatar-casting-request`, Convex renders the candidates and parks the thread, and the pick is delivered to `POST /avatar-casting-decision` as `{ status: "cast", avatarHandle, imageId }` or `{ status: "declined", avatarHandle, reason? }`.
276
303
 
277
304
  It is also the one blocking tool the bridge opens **on its own**. The agent forgot to call it in every recorded run, in a new way each time, so the trigger is the EVENT rather than the prose around it: a `baker avatars create` for a synthetic avatar registers a pending casting, and it opens before whatever the agent does next. The create is read off the **assistant message** as well as off `canUseTool`, because `canUseTool` is raised only for the ask path and the scaffold allows `Bash(baker:*)` — so every create the product actually runs bypassed the permission callback entirely, and the avatar sat in `generating` with no portraits and no sheet while the agent reported the picker was open. When the create is the turn's last tool call there is no next call to open it before, so the request is posted at turn end and the thread parks on it. An unattended Session has nobody to ask, so the create is rewritten to `--face auto` instead and nothing is left pending — the flag is inserted right after `avatars create` rather than appended, so a piped or redirected command does not hand it to the next program, and an explicit `--face ask` is replaced rather than joined.
278
305
 
306
+ The picker does not wait forever. Once every face is on screen, Convex schedules a pick 30 seconds out: if the user has not chosen (or pressed "Give me more time", which clears the deadline), the first face is kept and delivered as `{ status: "cast", avatarHandle, imageId, chosenBy: "auto" }`. The pick runs server-side, so a tab left in the background never stalls the job, and the result text tells the agent nobody chose that face so it never credits the user with it.
307
+
279
308
  A decline comes back to the agent with the remedy attached, not as a bare `{"status":"declined"}`. The tool's description says to sharpen the subject with `baker avatars update` and call it again, but that text sits thousands of tokens up the context and was written before the turn began; read against the bare object, the agent replied "the casting portraits should be showing on your screen right now" to a user who had just rejected all four and whose picker was gone. The result now states the outcome and the next move together, the way a CLI `error.fix` does, and says to ask what to change when no reason was given.
280
309
 
281
310
  When the agent *does* call `request_avatar_casting` for the avatar a pending casting is already owed to — the tool's description says the picker opens by itself, but the call is tolerated — that call becomes the casting rather than triggering a second one, so the pick is returned to the tool that asked for it. Every other blocking prompt that follows a create still reaches its own handler; the auto-cast runs first, then the original call is routed normally.
@@ -291,7 +320,7 @@ The fetch is resilient by design: results are cached per `(thread, actor)` for 6
291
320
  A user can `@`-mention a tool in a message (a green chip in the composer). Two kinds arrive, and the bridge treats them differently because the agent has to reach them differently:
292
321
 
293
322
  - `@[Label](mcp:<key>)` — a tool that arrives in the session over MCP. The bridge appends an instruction naming the requested tools so the agent prefers them for that turn. A hint, not a restriction: every other tool stays available and the server map is unchanged.
294
- - `@[Label](integration:<provider>)` — a **Baker integration** (GA4, Search Console, Tag Manager, an ad platform). There is no MCP server for these; they are read with `baker` commands. The bridge appends an instruction naming the command for each one, so an agent told to "read Google Search Console" does not go looking for a tool of that name, find nothing, and report a connected platform as unavailable.
323
+ - `@[Label](integration:<provider>)` — a **Baker integration** (GA4, Search Console, Tag Manager, an ad platform, HubSpot, Shopify). There is no MCP server for these; they are read with `baker` commands. The bridge appends an instruction naming the command for each one, so an agent told to "read Google Search Console" does not go looking for a tool of that name, find nothing, and report a connected platform as unavailable.
295
324
 
296
325
  Both kinds are stripped back to their plain `Label` first — the mention syntax is dashboard serialization and means nothing to the model. The older `mcp:<provider>:<key>` spelling is still read, for messages written before integrations had their own type.
297
326
 
@@ -366,7 +395,9 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
366
395
  the turn mid-tool-call and Claude Code returns an error-during-execution result
367
396
  that surfaces as a failed turn). A turn runs under the SDK `plan` permission mode
368
397
  (read-only until approval) when the dispatch carries `planMode: true` (the
369
- dashboard's plan-mode toggle); a `/plan …` prompt is the fallback trigger. The
398
+ dashboard's plan-mode toggle); a `/plan …` prompt is the fallback trigger. Every
399
+ other turn passes `default` explicitly: an omitted mode starts an OpenRouter
400
+ session in auto mode, where a classifier rather than `canUseTool` decides. The
370
401
  review loop fires only when plan mode is active; if the agent calls
371
402
  `ExitPlanMode` outside plan mode it is a no-op passthrough (no review card).
372
403
  When the SDK omits inline plan text, the bridge reads Claude Code's reported
@@ -431,8 +462,8 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
431
462
  skipped entirely (only `parent_tool_use_id`-free, non-`subagent_type`
432
463
  messages count). The `window` is the model's **real OpenRouter context
433
464
  length**, resolved from the model id (`contextWindowForModel` in
434
- `model-capabilities.ts`): opus 5.5, opus 5 and sonnet 5 are 1M, haiku 4.5
435
- is 200k — and resolved from **the model that actually served the request**,
465
+ `model-capabilities.ts`): opus 5.5, opus 5, sonnet 5.5, sonnet 5 and haiku 5.5 are 1M,
466
+ haiku 4.5 is 200k — and resolved from **the model that actually served the request**,
436
467
  which is not always the session's. A skill can pin its own model for the turn
437
468
  via `model:` in its frontmatter, and measuring such a turn against the session's
438
469
  1M reads a third full when it is in fact over — the mismatch that hid the publish
@@ -457,10 +488,16 @@ Convex action ──POST /message/async──▶ Bridge AgentSession
457
488
  unset: the CLI's own default beats a guess.
458
489
  That ceiling is not the window the meter uses, and the pin cannot raise it — see
459
490
  the 1M context correction below, which does.
491
+ - **Subagent prompt cache TTL.** Outside a Claude subscription the CLI caches
492
+ every request on a 5-minute TTL. The bridge sets
493
+ `settings.subagentPromptCacheTtl: "1h"` so subagents, workflows and helper
494
+ requests keep their cache across a wait longer than that (a render can take up
495
+ to 30 minutes). An hour-long cache write costs 2× base input instead of 1.25×.
496
+ The main conversation is unchanged (`promptCacheTtl` stays unset).
460
497
  - **1M context correction.** The bridge sends the `context-1m-2025-08-07` beta for
461
498
  every 1M model it routes to (`betasForModel`), **Opus 5.5, the default,
462
499
  included**. This is *not* an API requirement: Anthropic serves 1M by default on
463
- Opus 5.5, Opus 5 and Sonnet 5, and that beta is the legacy Sonnet 4/4.5
500
+ Opus 5.5, Opus 5, Sonnet 5.5 and Sonnet 5, and that beta is the legacy Sonnet 4/4.5
464
501
  header — verified against the OpenRouter endpoint as identical HTTP 200s with
465
502
  and without it. What it corrects is the CLI's **client-side** idea of the
466
503
  window: the CLI resolves that locally and reads 1M only for a model id carrying
@@ -749,7 +786,9 @@ findings cite.
749
786
  Three things enforce read-only, because each fails differently: the review key's
750
787
  scope, `REVIEW_DISALLOWED_TOOLS`, and `reviewBashPolicy` (an allowlist of readers
751
788
  that also refuses the ways an allowed binary becomes an arbitrary one — a
752
- redirect, `xargs`, a command substitution).
789
+ redirect, `xargs`, a command substitution). It reads the command with a real
790
+ shell parser, so a `|` or a tool name inside a quoted jq program, grep pattern
791
+ or path is data, not a second command; anything it cannot parse is refused.
753
792
 
754
793
  ## Resilience
755
794
 
@@ -765,8 +804,11 @@ redirect, `xargs`, a command substitution).
765
804
  ends input itself once the turn is really over: a clean result with no
766
805
  in-flight tasks, re-checked when the last task settles (a settle isn't
767
806
  guaranteed to wake the model into a second result). Turns that ran tasks get
768
- a 1s settle window so a just-settled task's queued wake isn't cut off; only
769
- genuine model activity cancels it. Model activity after a result also retires
807
+ a 1s settle window after a result so a just-settled task's queued wake isn't
808
+ cut off, and a 45s window after the last task settles, because the CLI streams
809
+ nothing between that settle and the woken turn's first token; only genuine
810
+ model activity cancels either. The agent's Bash calls may run 30 minutes
811
+ (`BASH_MAX_TIMEOUT_MS`), the longest the workspace's timeout hook grants. Model activity after a result also retires
770
812
  that result: the CLI has woken into a new turn, so the settle re-check waits
771
813
  for the woken turn's own result rather than closing stdin under a model that
772
814
  is still on time-to-first-token. A 60s exit guard closes the query if the
@@ -774,7 +816,17 @@ redirect, `xargs`, a command substitution).
774
816
  BOTH the task_started/task_notification edge bookends and the
775
817
  `background_tasks_changed` level signal — a long foreground command the CLI
776
818
  auto-backgrounds emits only the latter, and missing it was the main source of
777
- the "control-channel closed mid-turn" turn failures.
819
+ the "control-channel closed mid-turn" turn failures. Tasks the SDK flags
820
+ `ambient` (housekeeping, auto-started watchers) are not work and never hold
821
+ the turn; a Monitor the agent asked for is not ambient.
822
+ - Before input ends on a turn that ran tasks, the bridge reads the CLI's running
823
+ cost and model usage once (5s cap). A task can settle without waking the
824
+ model, and then the last result does not include what it spent. A wake or a
825
+ task that lands during that reading cancels the end, as it would a pending one.
826
+ - After a turn that ran tasks has ended, the bridge runs the scaffold's
827
+ markdown flush and publish recovery itself: those Stop hooks defer while
828
+ background work runs, and a task that settles without waking the model leaves
829
+ no later Stop to run them.
778
830
  - A stall detector guards each model request: if the stream yields nothing for
779
831
  3 min while the CLI should be talking to the model (right after the LAST
780
832
  outstanding tool result goes back, or between stream deltas — never during
@@ -783,8 +835,12 @@ redirect, `xargs`, a command substitution).
783
835
  so the dying CLI can't keep writing to the resumed transcript), up to 2
784
836
  retries. A hung upstream request recovers in minutes instead of killing the
785
837
  turn. Outstanding tool_use ids are tracked so parallel tools — one result
786
- back while a sibling render still runs — never read as a stall.
787
- - The SDK iterator is additionally wrapped in a 30 min watchdog: if no event
838
+ back while a sibling render still runs — never read as a stall. A tool's
839
+ `tool_progress` heartbeat is ignored: a background task's arrives while the
840
+ model's stream is stalled.
841
+ - The SDK iterator is additionally wrapped in a 30 min watchdog — 2 h while a
842
+ tool call or background task is known to be running, since a render or a
843
+ long command prints nothing until it ends: if no event
788
844
  arrives for that long, the run is aborted and the thread is marked errored
789
845
  with a legible "lost the connection" message. If the aborted run never
790
846
  unwinds (a wedged stream ignoring the close), a 60s grace timer force-delivers
@@ -0,0 +1,23 @@
1
+ import type { ResolvedCredential } from "./credential-text.ts";
2
+ export type CredentialUse = {
3
+ name: string;
4
+ host: string;
5
+ };
6
+ /**
7
+ * Baker refused the broker's token: Credentials were switched off, the company
8
+ * suspended, or the Runtime is no longer active. Unlike a failed request, this
9
+ * is an answer — the broker must stop using what it holds.
10
+ */
11
+ export declare class CredentialsRefused extends Error {
12
+ readonly status: number;
13
+ constructor(status: number);
14
+ }
15
+ export type BakerBrokerApi = {
16
+ fetchCredentials(): Promise<ResolvedCredential[]>;
17
+ reportUsage(uses: CredentialUse[]): Promise<void>;
18
+ };
19
+ export declare function createBakerBrokerApi(options: {
20
+ siteUrl: string;
21
+ token: string;
22
+ }): BakerBrokerApi;
23
+ //# sourceMappingURL=baker-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baker-api.d.ts","sourceRoot":"","sources":["../../src/broker/baker-api.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAS/D,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM;IAAnC,YAAqB,MAAM,EAAE,MAAM,EAElC;CACF;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,gBAAgB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAClD,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC;AAIF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CA0BhG"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The broker's two calls to Baker: fetch the company's credentials (the only
3
+ * route that ever returns a value) and report which were used where. The
4
+ * shapes mirror `brokerResolveResponseSchema` / `brokerUsageRequestSchema` in
5
+ * `@baker/api/credentials`, which the published bridge cannot import.
6
+ * Success is `{ ok: true, data }`; a failure is any non-2xx status with
7
+ * `{ code, error }`.
8
+ */
9
+ import { z } from "zod";
10
+ const resolveResponseSchema = z.object({
11
+ credentials: z.array(z.object({ name: z.string(), value: z.string(), allowedHosts: z.array(z.string()) })),
12
+ });
13
+ const successSchema = z.object({ ok: z.literal(true), data: z.unknown().optional() });
14
+ const failureSchema = z.object({ code: z.string() });
15
+ /**
16
+ * Baker refused the broker's token: Credentials were switched off, the company
17
+ * suspended, or the Runtime is no longer active. Unlike a failed request, this
18
+ * is an answer — the broker must stop using what it holds.
19
+ */
20
+ export class CredentialsRefused extends Error {
21
+ status;
22
+ constructor(status) {
23
+ super(`Baker refused the broker (${status})`);
24
+ this.status = status;
25
+ }
26
+ }
27
+ const TIMEOUT_MS = 10_000;
28
+ export function createBakerBrokerApi(options) {
29
+ async function post(path, body) {
30
+ const response = await fetch(new URL(path, options.siteUrl), {
31
+ method: "POST",
32
+ headers: { authorization: `Broker ${options.token}`, "content-type": "application/json" },
33
+ body: JSON.stringify(body),
34
+ signal: AbortSignal.timeout(TIMEOUT_MS),
35
+ });
36
+ const payload = await response.json().catch(() => null);
37
+ if (response.status === 401 || response.status === 403)
38
+ throw new CredentialsRefused(response.status);
39
+ if (!response.ok) {
40
+ const code = failureSchema.safeParse(payload).data?.code ?? "UNKNOWN";
41
+ throw new Error(`${path} answered ${response.status} ${code}`);
42
+ }
43
+ return successSchema.parse(payload).data;
44
+ }
45
+ return {
46
+ async fetchCredentials() {
47
+ const data = resolveResponseSchema.parse(await post("/api/runtime/credentials/resolve", {}));
48
+ return data.credentials;
49
+ },
50
+ async reportUsage(uses) {
51
+ await post("/api/runtime/credentials/usage", { uses });
52
+ },
53
+ };
54
+ }
55
+ //# sourceMappingURL=baker-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baker-api.js","sourceRoot":"","sources":["../../src/broker/baker-api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;CAC3G,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AACtF,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAIrD;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACtB,MAAM;IAA3B,YAAqB,MAAc;QACjC,KAAK,CAAC,6BAA6B,MAAM,GAAG,CAAC,CAAC;sBAD3B,MAAM;IAE3B,CAAC;CACF;AAOD,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B,MAAM,UAAU,oBAAoB,CAAC,OAA2C;IAC9E,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,IAAa;QAC7C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;YAC3D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YACzF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAC1B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC;SACxC,CAAC,CAAC;QACH,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;YAAE,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACtG,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,SAAS,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,aAAa,QAAQ,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;IAC3C,CAAC;IAED,OAAO;QACL,KAAK,CAAC,gBAAgB;YACpB,MAAM,IAAI,GAAG,qBAAqB,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,kCAAkC,EAAE,EAAE,CAAC,CAAC,CAAC;YAC7F,OAAO,IAAI,CAAC,WAAW,CAAC;QAC1B,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,IAAI;YACpB,MAAM,IAAI,CAAC,gCAAgC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_BROKER_SOCKET = "/run/baker/broker.sock";
2
+ export type BrokerClientOptions = {
3
+ socketPath?: string;
4
+ timeoutMs?: number;
5
+ };
6
+ /** Whether a broker runs in this sandbox: its socket exists. */
7
+ export declare function brokerRunning(options?: BrokerClientOptions): boolean;
8
+ /** `text` with every stored credential value replaced by its placeholder. */
9
+ export declare function scrubViaBroker(text: string, options?: BrokerClientOptions): Promise<string>;
10
+ //# sourceMappingURL=broker-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-client.d.ts","sourceRoot":"","sources":["../../src/broker/broker-client.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,qBAAqB,2BAA2B,CAAC;AAG9D,MAAM,MAAM,mBAAmB,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAM9E,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAExE;AA+BD,6EAA6E;AAC7E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQrG"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The bridge's side of the broker socket. Dependency-free on purpose: the
3
+ * bridge imports it on the hot path of every tool result.
4
+ *
5
+ * When no broker runs (no socket file — local dev, or a Runtime without
6
+ * credentials) every call is a fast no-op and `scrubViaBroker` returns the
7
+ * text unchanged. Any other failure (timeout, 413, 429, a crashed broker) is
8
+ * thrown so the caller decides whether to hold the text back.
9
+ */
10
+ import { existsSync } from "node:fs";
11
+ import { request } from "node:http";
12
+ export const DEFAULT_BROKER_SOCKET = "/run/baker/broker.sock";
13
+ const DEFAULT_TIMEOUT_MS = 2_000;
14
+ function isMissingSocket(error) {
15
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
16
+ }
17
+ /** Whether a broker runs in this sandbox: its socket exists. */
18
+ export function brokerRunning(options = {}) {
19
+ return existsSync(options.socketPath ?? DEFAULT_BROKER_SOCKET);
20
+ }
21
+ function call(path, body, options) {
22
+ const socketPath = options.socketPath ?? DEFAULT_BROKER_SOCKET;
23
+ if (!brokerRunning(options))
24
+ return Promise.resolve(null);
25
+ return new Promise((resolve, reject) => {
26
+ const req = request({
27
+ socketPath,
28
+ path,
29
+ method: "POST",
30
+ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) },
31
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
32
+ }, (res) => {
33
+ const chunks = [];
34
+ res.on("data", (chunk) => chunks.push(chunk));
35
+ res.on("end", () => {
36
+ const text = Buffer.concat(chunks).toString("utf8");
37
+ if (res.statusCode === 200)
38
+ resolve(text);
39
+ else
40
+ reject(new Error(`credential broker ${path} answered ${res.statusCode}`));
41
+ });
42
+ res.on("error", reject);
43
+ });
44
+ req.on("timeout", () => req.destroy(new Error(`credential broker ${path} timed out`)));
45
+ req.on("error", (error) => (isMissingSocket(error) ? resolve(null) : reject(error)));
46
+ req.end(body);
47
+ });
48
+ }
49
+ /** `text` with every stored credential value replaced by its placeholder. */
50
+ export async function scrubViaBroker(text, options = {}) {
51
+ const response = await call("/scrub", JSON.stringify({ text }), options);
52
+ if (response === null)
53
+ return text;
54
+ const parsed = JSON.parse(response);
55
+ if (typeof parsed === "object" && parsed !== null && "text" in parsed && typeof parsed.text === "string") {
56
+ return parsed.text;
57
+ }
58
+ throw new Error("credential broker /scrub answered without text");
59
+ }
60
+ //# sourceMappingURL=broker-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-client.js","sourceRoot":"","sources":["../../src/broker/broker-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,CAAC,MAAM,qBAAqB,GAAG,wBAAwB,CAAC;AAC9D,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAIjC,SAAS,eAAe,CAAC,KAAc;IACrC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;AAC9E,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,aAAa,CAAC,OAAO,GAAwB,EAAE;IAC7D,OAAO,UAAU,CAAC,OAAO,CAAC,UAAU,IAAI,qBAAqB,CAAC,CAAC;AACjE,CAAC;AAED,SAAS,IAAI,CAAC,IAAY,EAAE,IAAY,EAAE,OAA4B;IACpE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,qBAAqB,CAAC;IAC/D,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,OAAO,CACjB;YACE,UAAU;YACV,IAAI;YACJ,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;YAC1F,OAAO,EAAE,OAAO,CAAC,SAAS,IAAI,kBAAkB;SACjD,EACD,CAAC,GAAG,EAAE,EAAE;YACN,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YACtD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACjB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACpD,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG;oBAAE,OAAO,CAAC,IAAI,CAAC,CAAC;;oBACrC,MAAM,CAAC,IAAI,KAAK,CAAC,qBAAqB,IAAI,aAAa,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YACjF,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,CAAC,CACF,CAAC;QACF,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,qBAAqB,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;QACvF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACrF,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY,EAAE,OAAO,GAAwB,EAAE;IAClF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACzE,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACzG,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;AACpE,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { BrokerProcess } from "./supervisor.ts";
2
+ /**
3
+ * Starts the broker as a child of the supervisor. A spawn that fails (ENOENT,
4
+ * EAGAIN, EMFILE — likeliest under the memory pressure the supervisor exists
5
+ * for) emits `error` and possibly no `exit`; it settles `exited` all the same,
6
+ * so the supervisor's back-off retries it. A synchronous throw is the
7
+ * supervisor's to catch.
8
+ */
9
+ export declare function spawnBroker(command: string, args: readonly string[], env: NodeJS.ProcessEnv, log: (message: string) => void): BrokerProcess;
10
+ //# sourceMappingURL=broker-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-process.d.ts","sourceRoot":"","sources":["../../src/broker/broker-process.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEjE;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC7B,aAAa,CAUf"}
@@ -0,0 +1,20 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Starts the broker as a child of the supervisor. A spawn that fails (ENOENT,
4
+ * EAGAIN, EMFILE — likeliest under the memory pressure the supervisor exists
5
+ * for) emits `error` and possibly no `exit`; it settles `exited` all the same,
6
+ * so the supervisor's back-off retries it. A synchronous throw is the
7
+ * supervisor's to catch.
8
+ */
9
+ export function spawnBroker(command, args, env, log) {
10
+ const child = spawn(command, args, { stdio: "inherit", env });
11
+ const exited = new Promise((resolve) => {
12
+ child.once("exit", (code, signal) => resolve({ code, signal }));
13
+ child.once("error", (error) => {
14
+ log(`could not start broker: ${error.message}`);
15
+ resolve({ code: null, signal: null });
16
+ });
17
+ });
18
+ return { exited, kill: (signal) => void child.kill(signal) };
19
+ }
20
+ //# sourceMappingURL=broker-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-process.js","sourceRoot":"","sources":["../../src/broker/broker-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,OAAe,EACf,IAAuB,EACvB,GAAsB,EACtB,GAA8B;IAE9B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,OAAO,CAAa,CAAC,OAAO,EAAE,EAAE;QACjD,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QAChE,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC5B,GAAG,CAAC,2BAA2B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAChD,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=broker-process.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-process.test.d.ts","sourceRoot":"","sources":["../../src/broker/broker-process.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { spawnBroker } from "./broker-process.js";
3
+ describe("spawnBroker", () => {
4
+ // Node reports ENOENT, EAGAIN and EMFILE as an `error` event, with no `exit`
5
+ // after it: unheard, it crashed the supervisor; heard, it is a broker that exited.
6
+ it("reports a broker that could not be spawned as one that exited, with the reason", async () => {
7
+ const reasons = [];
8
+ const broker = spawnBroker("/nonexistent/node", [], {}, (line) => reasons.push(line));
9
+ await expect(broker.exited).resolves.toEqual({ code: null, signal: null });
10
+ expect(reasons.join("")).toContain("ENOENT");
11
+ });
12
+ });
13
+ //# sourceMappingURL=broker-process.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker-process.test.js","sourceRoot":"","sources":["../../src/broker/broker-process.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,6EAA6E;IAC7E,mFAAmF;IACnF,EAAE,CAAC,gFAAgF,EAAE,KAAK,IAAI,EAAE;QAC9F,MAAM,OAAO,GAAa,EAAE,CAAC;QAE7B,MAAM,MAAM,GAAG,WAAW,CAAC,mBAAmB,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEtF,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3E,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}