@vellumai/assistant 0.6.1 → 0.6.3

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 (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -21,17 +21,16 @@ mock.module("../config/loader.js", () => ({
21
21
  }));
22
22
 
23
23
  mock.module("../oauth/oauth-store.js", () => ({
24
- isProviderConnected: (providerKey: string) =>
25
- connectedProviders.has(providerKey),
26
- getConnectionByProvider: (providerKey: string) =>
27
- connectedProviders.has(providerKey)
28
- ? { id: `conn-${providerKey}`, status: "active" }
24
+ isProviderConnected: (provider: string) => connectedProviders.has(provider),
25
+ getConnectionByProvider: (provider: string) =>
26
+ connectedProviders.has(provider)
27
+ ? { id: `conn-${provider}`, status: "active" }
29
28
  : undefined,
30
29
  }));
31
30
 
32
31
  /** Mark a provider as fully connected (active row + access token). */
33
- function setOAuthConnected(providerKey: string): void {
34
- connectedProviders.add(providerKey);
32
+ function setOAuthConnected(provider: string): void {
33
+ connectedProviders.add(provider);
35
34
  }
36
35
 
37
36
  const { getIntegrationSummary, formatIntegrationSummary, hasCapability } =
@@ -84,7 +84,11 @@ describe("handleListMessages tool_result merging", () => {
84
84
  conv.id,
85
85
  "user",
86
86
  JSON.stringify([
87
- { type: "tool_result", tool_use_id: "tu1", content: "file1.txt\nfile2.txt" },
87
+ {
88
+ type: "tool_result",
89
+ tool_use_id: "tu1",
90
+ content: "file1.txt\nfile2.txt",
91
+ },
88
92
  ]),
89
93
  );
90
94
 
@@ -117,7 +121,12 @@ describe("handleListMessages tool_result merging", () => {
117
121
  JSON.stringify([
118
122
  { type: "tool_use", id: "tu1", name: "bash", input: { command: "ls" } },
119
123
  { type: "text", text: "and also" },
120
- { type: "tool_use", id: "tu2", name: "file_read", input: { path: "/tmp/a" } },
124
+ {
125
+ type: "tool_use",
126
+ id: "tu2",
127
+ name: "file_read",
128
+ input: { path: "/tmp/a" },
129
+ },
121
130
  ]),
122
131
  );
123
132
  await addMessage(
@@ -176,7 +185,11 @@ describe("handleListMessages tool_result merging", () => {
176
185
  conv.id,
177
186
  "user",
178
187
  JSON.stringify([
179
- { type: "tool_result", tool_use_id: "tu_orphan", content: "stale result" },
188
+ {
189
+ type: "tool_result",
190
+ tool_use_id: "tu_orphan",
191
+ content: "stale result",
192
+ },
180
193
  ]),
181
194
  );
182
195
  await addMessage(
@@ -228,7 +241,12 @@ describe("handleListMessages tool_result merging", () => {
228
241
  "assistant",
229
242
  JSON.stringify([
230
243
  { type: "text", text: "Now reading:" },
231
- { type: "tool_use", id: "tu2", name: "file_read", input: { path: "/x" } },
244
+ {
245
+ type: "tool_use",
246
+ id: "tu2",
247
+ name: "file_read",
248
+ input: { path: "/x" },
249
+ },
232
250
  ]),
233
251
  );
234
252
  await addMessage(
@@ -248,17 +266,19 @@ describe("handleListMessages tool_result merging", () => {
248
266
  const response = handleListMessages(createTestUrl(conv.id), null);
249
267
  const body = (await response.json()) as { messages: MessagePayload[] };
250
268
 
251
- // user("list files"), assistant(bash), assistant(file_read), user("thanks")
252
- expect(body.messages).toHaveLength(4);
269
+ // Consecutive assistant messages are merged at query time so the client
270
+ // sees one grouped message (matching the streaming path behavior).
271
+ // user("list files"), merged-assistant(bash + file_read), user("thanks")
272
+ expect(body.messages).toHaveLength(3);
253
273
  expect(body.messages[0].role).toBe("user");
254
274
  expect(body.messages[1].role).toBe("assistant");
275
+ expect(body.messages[1].toolCalls).toHaveLength(2);
255
276
  expect(body.messages[1].toolCalls![0].name).toBe("bash");
256
277
  expect(body.messages[1].toolCalls![0].result).toBe("files");
257
- expect(body.messages[2].role).toBe("assistant");
258
- expect(body.messages[2].toolCalls![0].name).toBe("file_read");
259
- expect(body.messages[2].toolCalls![0].result).toBe("file data");
260
- expect(body.messages[3].role).toBe("user");
261
- expect(body.messages[3].content).toBe("thanks");
278
+ expect(body.messages[1].toolCalls![1].name).toBe("file_read");
279
+ expect(body.messages[1].toolCalls![1].result).toBe("file data");
280
+ expect(body.messages[2].role).toBe("user");
281
+ expect(body.messages[2].content).toBe("thanks");
262
282
  });
263
283
 
264
284
  test("tool_result with is_error propagates error status", async () => {
@@ -272,7 +292,12 @@ describe("handleListMessages tool_result merging", () => {
272
292
  conv.id,
273
293
  "assistant",
274
294
  JSON.stringify([
275
- { type: "tool_use", id: "tu1", name: "bash", input: { command: "fail" } },
295
+ {
296
+ type: "tool_use",
297
+ id: "tu1",
298
+ name: "bash",
299
+ input: { command: "fail" },
300
+ },
276
301
  ]),
277
302
  );
278
303
  await addMessage(
@@ -93,6 +93,35 @@ writeFileSync(
93
93
  JSON.stringify({ provider: "anthropic" }),
94
94
  );
95
95
 
96
+ // Conversation directories — used for workspace allowlist tests
97
+ const conversationsDir = join(testWorkspaceDir, "conversations");
98
+ mkdirSync(conversationsDir, { recursive: true });
99
+
100
+ function seedConversation(name: string, body: string) {
101
+ const dir = join(conversationsDir, name);
102
+ mkdirSync(dir, { recursive: true });
103
+ writeFileSync(join(dir, "meta.json"), "{}\n");
104
+ writeFileSync(join(dir, "messages.jsonl"), body);
105
+ }
106
+
107
+ seedConversation(
108
+ "2025-01-10T00-00-00.000Z_conv-jan10",
109
+ '{"role":"user","content":"jan 10"}\n',
110
+ );
111
+ seedConversation(
112
+ "2025-01-15T00-00-00.000Z_conv-jan15",
113
+ '{"role":"user","content":"jan 15"}\n',
114
+ );
115
+ seedConversation(
116
+ "2025-01-20T00-00-00.000Z_conv-jan20",
117
+ '{"role":"user","content":"jan 20"}\n',
118
+ );
119
+ seedConversation(
120
+ "2025-01-25T00-00-00.000Z_conv-jan25",
121
+ '{"role":"user","content":"jan 25"}\n',
122
+ );
123
+ seedConversation("malformed-name", '{"role":"user","content":"x"}\n');
124
+
96
125
  // Daemon log files — used for date filtering tests
97
126
  const logsDir = join(testWorkspaceDir, "data", "logs");
98
127
  mkdirSync(logsDir, { recursive: true });
@@ -241,3 +270,164 @@ describe("POST /v1/export — daemon log date filtering", () => {
241
270
  }
242
271
  });
243
272
  });
273
+
274
+ describe("POST /v1/export — workspace allowlist", () => {
275
+ test("includes all valid conversation dirs by default", async () => {
276
+ const res = await callExport();
277
+ const dir = await extractArchive(res);
278
+ try {
279
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
280
+ expect(convs).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
281
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
282
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
283
+ expect(convs).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
284
+ expect(convs).not.toContain("malformed-name");
285
+ } finally {
286
+ rmSync(dir, { recursive: true, force: true });
287
+ }
288
+ });
289
+
290
+ test("skips malformed conversation dir names", async () => {
291
+ const res = await callExport();
292
+ const dir = await extractArchive(res);
293
+ try {
294
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
295
+ expect(convs).not.toContain("malformed-name");
296
+ } finally {
297
+ rmSync(dir, { recursive: true, force: true });
298
+ }
299
+ });
300
+
301
+ test("filters conversation dirs by startTime", async () => {
302
+ const startTime = Date.parse("2025-01-14T00:00:00Z");
303
+ const res = await callExport({ startTime });
304
+ const dir = await extractArchive(res);
305
+ try {
306
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
307
+ expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
308
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
309
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
310
+ expect(convs).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
311
+ } finally {
312
+ rmSync(dir, { recursive: true, force: true });
313
+ }
314
+ });
315
+
316
+ test("filters conversation dirs by endTime", async () => {
317
+ const endTime = Date.parse("2025-01-22T00:00:00Z");
318
+ const res = await callExport({ endTime });
319
+ const dir = await extractArchive(res);
320
+ try {
321
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
322
+ expect(convs).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
323
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
324
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
325
+ expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
326
+ } finally {
327
+ rmSync(dir, { recursive: true, force: true });
328
+ }
329
+ });
330
+
331
+ test("filters conversation dirs by both startTime and endTime", async () => {
332
+ const startTime = Date.parse("2025-01-14T00:00:00Z");
333
+ const endTime = Date.parse("2025-01-22T00:00:00Z");
334
+ const res = await callExport({ startTime, endTime });
335
+ const dir = await extractArchive(res);
336
+ try {
337
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
338
+ expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
339
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
340
+ expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
341
+ expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
342
+ } finally {
343
+ rmSync(dir, { recursive: true, force: true });
344
+ }
345
+ });
346
+
347
+ test("filters conversation dirs by conversationId", async () => {
348
+ const res = await callExport({ conversationId: "conv-jan15" });
349
+ const dir = await extractArchive(res);
350
+ try {
351
+ const convs = readdirSync(join(dir, "workspace", "conversations"));
352
+ expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
353
+ expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
354
+ expect(convs).not.toContain("2025-01-20T00-00-00.000Z_conv-jan20");
355
+ expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
356
+ expect(convs).not.toContain("malformed-name");
357
+ } finally {
358
+ rmSync(dir, { recursive: true, force: true });
359
+ }
360
+ });
361
+
362
+ test("conversationId + time filter intersect", async () => {
363
+ const res = await callExport({
364
+ conversationId: "conv-jan15",
365
+ startTime: Date.parse("2025-02-01T00:00:00Z"),
366
+ });
367
+ const dir = await extractArchive(res);
368
+ try {
369
+ const conversationsPath = join(dir, "workspace", "conversations");
370
+ let convs: string[] = [];
371
+ try {
372
+ convs = readdirSync(conversationsPath);
373
+ } catch {
374
+ // Directory does not exist — acceptable per the test contract.
375
+ }
376
+ expect(convs).toEqual([]);
377
+ } finally {
378
+ rmSync(dir, { recursive: true, force: true });
379
+ }
380
+ });
381
+
382
+ test("conversation dir contents survive the round trip", async () => {
383
+ const res = await callExport();
384
+ const dir = await extractArchive(res);
385
+ try {
386
+ const messagesPath = join(
387
+ dir,
388
+ "workspace",
389
+ "conversations",
390
+ "2025-01-15T00-00-00.000Z_conv-jan15",
391
+ "messages.jsonl",
392
+ );
393
+ const content = readFileSync(messagesPath, "utf-8");
394
+ expect(content).toBe('{"role":"user","content":"jan 15"}\n');
395
+ } finally {
396
+ rmSync(dir, { recursive: true, force: true });
397
+ }
398
+ });
399
+
400
+ test("treats empty-string conversationId as no filter", async () => {
401
+ const res = await callExport({ conversationId: "" });
402
+ const dir = await extractArchive(res);
403
+ try {
404
+ // With conversationId === "" (which the rest of handleExport treats as
405
+ // unfiltered), workspace conversations should also be unfiltered. All
406
+ // four canonical conversation dirs should be present.
407
+ const conversationsDir = join(dir, "workspace", "conversations");
408
+ const entries = readdirSync(conversationsDir);
409
+ expect(entries).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
410
+ expect(entries).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
411
+ expect(entries).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
412
+ expect(entries).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
413
+ } finally {
414
+ rmSync(dir, { recursive: true, force: true });
415
+ }
416
+ });
417
+
418
+ test("treats startTime=0 and endTime=0 as no filter", async () => {
419
+ const res = await callExport({ startTime: 0, endTime: 0 });
420
+ const dir = await extractArchive(res);
421
+ try {
422
+ const conversationsDir = join(dir, "workspace", "conversations");
423
+ const entries = readdirSync(conversationsDir);
424
+ // All four canonical conversation dirs should be present (no filtering).
425
+ expect(entries).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
426
+ expect(entries).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
427
+ expect(entries).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
428
+ expect(entries).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
429
+ } finally {
430
+ rmSync(dir, { recursive: true, force: true });
431
+ }
432
+ });
433
+ });
@@ -45,7 +45,10 @@ mock.module("../security/secure-keys.js", () => ({
45
45
 
46
46
  // Mock global fetch
47
47
  const _originalFetch = globalThis.fetch;
48
- const mockFetch = async (input: string | URL | Request, init?: RequestInit) => {
48
+ const mockFetch = async (
49
+ input: string | URL | Request,
50
+ _init?: RequestInit,
51
+ ) => {
49
52
  const url =
50
53
  typeof input === "string"
51
54
  ? input
@@ -57,14 +60,6 @@ const mockFetch = async (input: string | URL | Request, init?: RequestInit) => {
57
60
  return new Response("Not Found", { status: 404 });
58
61
  }
59
62
 
60
- // Verify the Authorization header never leaks secrets into the request path
61
- const authHeader = init?.headers
62
- ? (init.headers as Record<string, string>)["Authorization"]
63
- : undefined;
64
- if (authHeader && !authHeader.startsWith("Api-Key ")) {
65
- throw new Error("Unexpected auth header format");
66
- }
67
-
68
63
  return new Response(JSON.stringify(entry.body), {
69
64
  status: entry.status,
70
65
  headers: { "Content-Type": "application/json" },
@@ -266,17 +261,17 @@ describe("fetchManagedCatalog", () => {
266
261
  expect(descriptor.handle).toBe("platform_oauth:conn_minimal");
267
262
  });
268
263
 
269
- test("error messages never contain API key values", async () => {
264
+ test("error messages never contain sensitive details", async () => {
270
265
  mockPlatformBaseUrl = "https://platform.example.com";
271
266
  mockAssistantApiKey = "sk-super-secret-key-12345";
272
267
  mockPlatformAssistantId = "ast-uuid-1234";
273
268
 
274
- // Simulate a network error
269
+ // Simulate a network error whose message contains sensitive data
275
270
  const savedFetch = globalThis.fetch;
276
271
  const errorFetch: typeof fetch = Object.assign(
277
272
  async () => {
278
273
  throw new Error(
279
- "Connect failed to https://platform.example.com/v1/assistants/ast-uuid-1234/oauth/managed/catalog/ with Api-Key sk-super-secret-key-12345",
274
+ "Connect failed to https://platform.example.com/v1/assistants/ast-uuid-1234/oauth/managed/catalog/ with Bearer sk-super-secret-key-12345",
280
275
  );
281
276
  },
282
277
  { preconnect: savedFetch.preconnect },
@@ -287,9 +282,12 @@ describe("fetchManagedCatalog", () => {
287
282
  const result = await fetchManagedCatalog();
288
283
  expect(result.ok).toBe(false);
289
284
  expect(result.error).toBeDefined();
290
- // Ensure the raw API key is not in the error message
285
+ // Raw error message (with URL, API key, etc.) must not leak
291
286
  expect(result.error).not.toContain("sk-super-secret-key-12345");
292
- expect(result.error).toContain("[REDACTED]");
287
+ expect(result.error).not.toContain("platform.example.com");
288
+ expect(result.error).not.toContain("Connect failed");
289
+ // Should only contain the error class name
290
+ expect(result.error).toContain("Error");
293
291
  } finally {
294
292
  globalThis.fetch = savedFetch;
295
293
  }
@@ -13,6 +13,7 @@ mock.module("../inbound/platform-callback-registration.js", () => ({
13
13
  }));
14
14
 
15
15
  const { McpClient } = await import("../mcp/client.js");
16
+ const { McpOAuthProvider } = await import("../mcp/mcp-oauth-provider.js");
16
17
 
17
18
  /**
18
19
  * Mimics the SDK's StreamableHTTPError which has a `.code` property
@@ -67,7 +68,7 @@ describe("McpClient auth error detection", () => {
67
68
  expect(client.isConnected).toBe(false);
68
69
  });
69
70
 
70
- test("rethrows non-auth StreamableHTTPError for HTTP transports", async () => {
71
+ test("swallows non-auth StreamableHTTPError (connect never throws)", async () => {
71
72
  const client = new McpClient("test-server");
72
73
 
73
74
  (client as any).createTransport = () => ({});
@@ -78,9 +79,9 @@ describe("McpClient auth error detection", () => {
78
79
  close: async () => {},
79
80
  };
80
81
 
81
- await expect(client.connect(httpTransport)).rejects.toThrow(
82
- "Internal Server Error",
83
- );
82
+ // Non-auth errors are logged but never propagated — daemon keeps running
83
+ await client.connect(httpTransport);
84
+ expect(client.isConnected).toBe(false);
84
85
  });
85
86
 
86
87
  test("treats error message containing 'unauthorized' as auth error", async () => {
@@ -97,4 +98,39 @@ describe("McpClient auth error detection", () => {
97
98
  await client.connect(httpTransport);
98
99
  expect(client.isConnected).toBe(false);
99
100
  });
101
+
102
+ test("treats SDK fetchToken 'authorizationCode is required' error as auth error", async () => {
103
+ const client = new McpClient("test-server");
104
+
105
+ (client as any).createTransport = () => ({});
106
+ (client as any).client = {
107
+ connect: () => {
108
+ throw new Error(
109
+ "Either provider.prepareTokenRequest() or authorizationCode is required",
110
+ );
111
+ },
112
+ close: async () => {},
113
+ };
114
+
115
+ await client.connect(httpTransport);
116
+ expect(client.isConnected).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe("McpOAuthProvider redirectUrl", () => {
121
+ test("redirectUrl is undefined until startCallbackServer() is called", () => {
122
+ const nonInteractive = new McpOAuthProvider(
123
+ "test-server",
124
+ "https://example.com/mcp",
125
+ /* interactive */ false,
126
+ );
127
+ expect(nonInteractive.redirectUrl).toBeUndefined();
128
+
129
+ const interactive = new McpOAuthProvider(
130
+ "test-server",
131
+ "https://example.com/mcp",
132
+ /* interactive */ true,
133
+ );
134
+ expect(interactive.redirectUrl).toBeUndefined();
135
+ });
100
136
  });
@@ -3,12 +3,16 @@ import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
3
3
  const mockConnect = jest.fn();
4
4
  const mockDisconnect = jest.fn();
5
5
  let mockIsConnected = true;
6
+ let mockLastError: Error | null = null;
6
7
 
7
8
  mock.module("../mcp/client.js", () => ({
8
9
  McpClient: class {
9
10
  get isConnected() {
10
11
  return mockIsConnected;
11
12
  }
13
+ get lastError() {
14
+ return mockLastError;
15
+ }
12
16
  connect = mockConnect;
13
17
  disconnect = mockDisconnect;
14
18
  },
@@ -32,6 +36,7 @@ describe("checkServerHealth", () => {
32
36
  mockConnect.mockReset();
33
37
  mockDisconnect.mockReset();
34
38
  mockIsConnected = true;
39
+ mockLastError = null;
35
40
  });
36
41
 
37
42
  test("returns Connected when server connects successfully", async () => {
@@ -43,7 +48,7 @@ describe("checkServerHealth", () => {
43
48
  expect(mockDisconnect).toHaveBeenCalled();
44
49
  });
45
50
 
46
- test("returns Needs authentication when isConnected is false", async () => {
51
+ test("returns Needs authentication when isConnected is false and no lastError", async () => {
47
52
  mockConnect.mockResolvedValue(undefined);
48
53
  mockIsConnected = false;
49
54
 
@@ -51,8 +56,10 @@ describe("checkServerHealth", () => {
51
56
  expect(result).toContain("Needs authentication");
52
57
  });
53
58
 
54
- test("returns Error when connect throws", async () => {
55
- mockConnect.mockRejectedValue(new Error("Connection refused"));
59
+ test("returns Error when connect fails with lastError", async () => {
60
+ mockConnect.mockResolvedValue(undefined);
61
+ mockIsConnected = false;
62
+ mockLastError = new Error("Connection refused");
56
63
  mockDisconnect.mockResolvedValue(undefined);
57
64
 
58
65
  const result = await checkServerHealth("test", serverConfig());
@@ -834,7 +834,9 @@ describe("round-trip: export -> validate -> preflight -> import", () => {
834
834
  expect(sha256Hex(writtenDb)).toBe(sha256Hex(dbData));
835
835
 
836
836
  const writtenConfig = readFileSync(testConfigPath, "utf8");
837
- expect(writtenConfig).toBe('{"model":"test-round-trip"}');
837
+ expect(writtenConfig).toBe(
838
+ JSON.stringify({ model: "test-round-trip" }, null, 2) + "\n",
839
+ );
838
840
  }
839
841
  });
840
842
 
@@ -75,7 +75,7 @@ beforeAll(() => {
75
75
  // Write test fixture files so the export reads real data
76
76
  mkdirSync(testDbDir, { recursive: true });
77
77
  writeFileSync(testDbPath, SQLITE_HEADER);
78
- writeFileSync(testConfigPath, JSON.stringify(TEST_CONFIG, null, 2));
78
+ writeFileSync(testConfigPath, JSON.stringify(TEST_CONFIG, null, 2) + "\n");
79
79
  });
80
80
 
81
81
  // ---------------------------------------------------------------------------
@@ -323,7 +323,7 @@ describe("export data population", () => {
323
323
  );
324
324
  expect(configFile).toBeDefined();
325
325
  const expectedConfigSize = Buffer.byteLength(
326
- JSON.stringify(TEST_CONFIG, null, 2),
326
+ JSON.stringify(TEST_CONFIG, null, 2) + "\n",
327
327
  );
328
328
  expect(configFile!.size).toBe(expectedConfigSize);
329
329
  });
@@ -428,6 +428,65 @@ describe("export graceful fallback", () => {
428
428
  });
429
429
  });
430
430
 
431
+ // ---------------------------------------------------------------------------
432
+ // Config sanitization tests
433
+ // ---------------------------------------------------------------------------
434
+
435
+ describe("export config sanitization", () => {
436
+ test("exported config.json has environment-specific fields stripped", async () => {
437
+ // Write a config with environment-specific fields that should be stripped
438
+ const configWithEnvFields = {
439
+ provider: "anthropic",
440
+ model: "test-model",
441
+ ingress: {
442
+ publicBaseUrl: "https://my-tunnel.example.com",
443
+ enabled: true,
444
+ port: 8080,
445
+ },
446
+ daemon: {
447
+ autoStart: true,
448
+ logLevel: "debug",
449
+ },
450
+ skills: {
451
+ load: {
452
+ extraDirs: ["/home/user/custom-skills", "/opt/skills"],
453
+ autoReload: true,
454
+ },
455
+ },
456
+ memory: { enabled: true },
457
+ };
458
+ writeFileSync(testConfigPath, JSON.stringify(configWithEnvFields, null, 2));
459
+
460
+ const req = new Request("http://localhost/v1/migrations/export", {
461
+ method: "POST",
462
+ });
463
+
464
+ const res = await handleMigrationExport(req);
465
+ const archiveData = new Uint8Array(await res.arrayBuffer());
466
+ const entries = parseTarEntries(archiveData);
467
+
468
+ const configEntry = entries.find((e) => e.name === "workspace/config.json");
469
+ expect(configEntry).toBeDefined();
470
+
471
+ const parsedConfig = JSON.parse(
472
+ new TextDecoder().decode(configEntry!.data),
473
+ );
474
+
475
+ // Environment-specific fields should be stripped/reset
476
+ expect(parsedConfig.ingress.publicBaseUrl).toBe("");
477
+ expect(parsedConfig.ingress.enabled).toBeUndefined();
478
+ expect(parsedConfig.daemon).toBeUndefined();
479
+ expect(parsedConfig.skills.load.extraDirs).toEqual([]);
480
+
481
+ // Non-environment-specific fields should be preserved
482
+ expect(parsedConfig.provider).toBe("anthropic");
483
+ expect(parsedConfig.model).toBe("test-model");
484
+ expect(parsedConfig.ingress.port).toBe(8080);
485
+ expect(parsedConfig.skills.load.autoReload).toBe(true);
486
+ expect(parsedConfig.memory.enabled).toBe(true);
487
+ });
488
+ });
489
+
431
490
  // ---------------------------------------------------------------------------
432
491
  // Auth policy registration tests
433
492
  // ---------------------------------------------------------------------------
@@ -285,6 +285,72 @@ describe("streamExportVBundle round-trip", () => {
285
285
  });
286
286
  });
287
287
 
288
+ // ---------------------------------------------------------------------------
289
+ // Streaming config sanitization test
290
+ // ---------------------------------------------------------------------------
291
+
292
+ describe("streamExportVBundle config sanitization", () => {
293
+ test("exported config.json has environment-specific fields stripped", async () => {
294
+ // Write a config with environment-specific fields that should be stripped
295
+ const configWithEnvFields = {
296
+ provider: "anthropic",
297
+ model: "test-model",
298
+ ingress: {
299
+ publicBaseUrl: "https://my-tunnel.example.com",
300
+ enabled: true,
301
+ port: 8080,
302
+ },
303
+ daemon: {
304
+ autoStart: true,
305
+ logLevel: "debug",
306
+ },
307
+ skills: {
308
+ load: {
309
+ extraDirs: ["/home/user/custom-skills", "/opt/skills"],
310
+ autoReload: true,
311
+ },
312
+ },
313
+ memory: { enabled: true },
314
+ };
315
+ writeFileSync(testConfigPath, JSON.stringify(configWithEnvFields, null, 2));
316
+
317
+ const result = await streamExportVBundle({
318
+ workspaceDir: testDir,
319
+ });
320
+
321
+ try {
322
+ const archiveData = new Uint8Array(readFileSync(result.tempPath));
323
+ const entries = parseTarEntries(archiveData);
324
+
325
+ const configEntry = entries.find(
326
+ (e) => e.name === "workspace/config.json",
327
+ );
328
+ expect(configEntry).toBeDefined();
329
+
330
+ const parsedConfig = JSON.parse(
331
+ new TextDecoder().decode(configEntry!.data),
332
+ );
333
+
334
+ // Environment-specific fields should be stripped/reset
335
+ expect(parsedConfig.ingress.publicBaseUrl).toBe("");
336
+ expect(parsedConfig.ingress.enabled).toBeUndefined();
337
+ expect(parsedConfig.daemon).toBeUndefined();
338
+ expect(parsedConfig.skills.load.extraDirs).toEqual([]);
339
+
340
+ // Non-environment-specific fields should be preserved
341
+ expect(parsedConfig.provider).toBe("anthropic");
342
+ expect(parsedConfig.model).toBe("test-model");
343
+ expect(parsedConfig.ingress.port).toBe(8080);
344
+ expect(parsedConfig.skills.load.autoReload).toBe(true);
345
+ expect(parsedConfig.memory.enabled).toBe(true);
346
+ } finally {
347
+ // Restore original config for subsequent tests
348
+ writeFileSync(testConfigPath, JSON.stringify(TEST_CONFIG, null, 2));
349
+ await result.cleanup();
350
+ }
351
+ });
352
+ });
353
+
288
354
  // ---------------------------------------------------------------------------
289
355
  // Cleanup idempotency test
290
356
  // ---------------------------------------------------------------------------