@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
@@ -1,7 +1,13 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, describe, expect, jest, test } from "bun:test";
2
2
 
3
3
  const { HostFileProxy } = await import("../daemon/host-file-proxy.js");
4
4
 
5
+ // Minimal PNG header
6
+ const PNG_HEADER = Buffer.from([
7
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
8
+ 0x48, 0x44, 0x52,
9
+ ]);
10
+
5
11
  describe("HostFileProxy", () => {
6
12
  let proxy: InstanceType<typeof HostFileProxy>;
7
13
  let sentMessages: unknown[];
@@ -77,6 +83,39 @@ describe("HostFileProxy", () => {
77
83
  expect(result.content).toContain("ENOENT");
78
84
  });
79
85
 
86
+ test("rebuilds image tool results from proxied image payloads", async () => {
87
+ setup();
88
+
89
+ const resultPromise = proxy.request(
90
+ {
91
+ operation: "read",
92
+ path: "/Users/test/Desktop/screenshot.png",
93
+ },
94
+ "session-1",
95
+ );
96
+
97
+ const sent = sentMessages[0] as Record<string, unknown>;
98
+ const requestId = sent.requestId as string;
99
+
100
+ proxy.resolve(requestId, {
101
+ content: "Image loaded on host",
102
+ isError: false,
103
+ imageData: PNG_HEADER.toString("base64"),
104
+ });
105
+
106
+ const result = await resultPromise;
107
+ expect(result.isError).toBe(false);
108
+ expect(result.content).toContain("Image loaded");
109
+ expect(result.content).toContain("/Users/test/Desktop/screenshot.png");
110
+ expect(result.contentBlocks).toHaveLength(1);
111
+ expect(result.contentBlocks?.[0]).toMatchObject({
112
+ type: "image",
113
+ source: {
114
+ media_type: "image/png",
115
+ },
116
+ });
117
+ });
118
+
80
119
  test("handles write operations", async () => {
81
120
  setup();
82
121
 
@@ -377,6 +416,151 @@ describe("HostFileProxy", () => {
377
416
  });
378
417
  });
379
418
 
419
+ describe("abort listener lifecycle", () => {
420
+ // Helper that wraps an AbortSignal to observe add/removeEventListener
421
+ // invocations without tripping over tsc's strict overload matching on
422
+ // AbortSignal itself.
423
+ type Spied = {
424
+ signal: AbortSignal;
425
+ addCalls: string[];
426
+ removeCalls: string[];
427
+ };
428
+ function spySignal(source: AbortSignal): Spied {
429
+ const addCalls: string[] = [];
430
+ const removeCalls: string[] = [];
431
+
432
+ const s = source as any;
433
+ const origAdd = source.addEventListener.bind(source);
434
+ const origRemove = source.removeEventListener.bind(source);
435
+ s.addEventListener = (
436
+ type: string,
437
+
438
+ ...rest: any[]
439
+ ) => {
440
+ addCalls.push(type);
441
+
442
+ return (origAdd as any)(type, ...rest);
443
+ };
444
+ s.removeEventListener = (
445
+ type: string,
446
+
447
+ ...rest: any[]
448
+ ) => {
449
+ removeCalls.push(type);
450
+
451
+ return (origRemove as any)(type, ...rest);
452
+ };
453
+ return { signal: source, addCalls, removeCalls };
454
+ }
455
+
456
+ test("removes abort listener from signal after resolve completes", async () => {
457
+ setup();
458
+ const controller = new AbortController();
459
+ const spy = spySignal(controller.signal);
460
+
461
+ const resultPromise = proxy.request(
462
+ { operation: "read", path: "/tmp/test.txt" },
463
+ "session-1",
464
+ spy.signal,
465
+ );
466
+
467
+ expect(spy.addCalls).toEqual(["abort"]);
468
+ expect(spy.removeCalls).toEqual([]);
469
+
470
+ const requestId = (sentMessages[0] as Record<string, unknown>)
471
+ .requestId as string;
472
+ proxy.resolve(requestId, { content: "file contents", isError: false });
473
+ await resultPromise;
474
+
475
+ // Listener is detached after normal completion.
476
+ expect(spy.removeCalls).toEqual(["abort"]);
477
+
478
+ // Subsequent aborts are harmless no-ops (no side effects on the proxy).
479
+ controller.abort();
480
+ // No additional emitted envelopes from the late abort.
481
+ expect(sentMessages).toHaveLength(1);
482
+ });
483
+
484
+ test("removes abort listener from signal on timer timeout", async () => {
485
+ setup();
486
+
487
+ jest.useFakeTimers();
488
+ try {
489
+ const controller = new AbortController();
490
+ const spy = spySignal(controller.signal);
491
+
492
+ const resultPromise = proxy.request(
493
+ { operation: "read", path: "/tmp/slow.txt" },
494
+ "session-1",
495
+ spy.signal,
496
+ );
497
+
498
+ expect(spy.addCalls).toEqual(["abort"]);
499
+ expect(spy.removeCalls).toEqual([]);
500
+
501
+ const requestId = (sentMessages[0] as Record<string, unknown>)
502
+ .requestId as string;
503
+ expect(proxy.hasPendingRequest(requestId)).toBe(true);
504
+
505
+ // Advance past the 30s internal timeout.
506
+ jest.advanceTimersByTime(31 * 1000);
507
+
508
+ const result = await resultPromise;
509
+ expect(result.isError).toBe(true);
510
+ expect(result.content).toContain("Host file proxy timed out");
511
+ expect(proxy.hasPendingRequest(requestId)).toBe(false);
512
+
513
+ // Listener is detached after the timer fires.
514
+ expect(spy.removeCalls).toEqual(["abort"]);
515
+
516
+ // Subsequent aborts should be harmless — no cancel emitted.
517
+ controller.abort();
518
+ expect(sentMessages).toHaveLength(1);
519
+ } finally {
520
+ jest.useRealTimers();
521
+ }
522
+ });
523
+ });
524
+
525
+ describe("sender throws synchronously", () => {
526
+ test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
527
+ const resolvedIds: string[] = [];
528
+ sentMessages = [];
529
+ sendToClient = () => {
530
+ throw new Error("transport down");
531
+ };
532
+ proxy = new HostFileProxy(sendToClient, (id) => resolvedIds.push(id));
533
+
534
+ const resultPromise = proxy.request(
535
+ { operation: "read", path: "/tmp/test.txt" },
536
+ "session-1",
537
+ );
538
+
539
+ await expect(resultPromise).rejects.toThrow("transport down");
540
+
541
+ // The internal resolve should fire exactly once as part of cleanup.
542
+ expect(resolvedIds).toHaveLength(1);
543
+
544
+ // Issue a new request on a fresh (non-throwing) sender and verify
545
+ // the proxy is still functional — no stale timers or bookkeeping
546
+ // from the failed request.
547
+ sentMessages = [];
548
+ proxy.updateSender((msg) => sentMessages.push(msg), true);
549
+ const okPromise = proxy.request(
550
+ { operation: "read", path: "/tmp/ok.txt" },
551
+ "session-1",
552
+ );
553
+ expect(sentMessages).toHaveLength(1);
554
+ const okRequestId = (sentMessages[0] as Record<string, unknown>)
555
+ .requestId as string;
556
+ expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
557
+ proxy.resolve(okRequestId, { content: "ok", isError: false });
558
+ const okResult = await okPromise;
559
+ expect(okResult.content).toBe("ok");
560
+ expect(okResult.isError).toBe(false);
561
+ });
562
+ });
563
+
380
564
  describe("onInternalResolve callback", () => {
381
565
  test("fires on abort", async () => {
382
566
  const resolvedIds: string[] = [];
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, describe, expect, test } from "bun:test";
5
5
 
6
+ import type { HostFileInput } from "../daemon/host-file-proxy.js";
6
7
  import { hostFileReadTool } from "../tools/host-filesystem/read.js";
7
8
  import type { ToolContext } from "../tools/types.js";
8
9
 
@@ -165,6 +166,57 @@ describe("host_file_read tool", () => {
165
166
  });
166
167
 
167
168
  describe("host_file_read image support", () => {
169
+ test("uses host proxy for image reads when available", async () => {
170
+ const requests: Array<{
171
+ input: HostFileInput;
172
+ conversationId: string;
173
+ signal?: AbortSignal;
174
+ }> = [];
175
+ const proxyContext: ToolContext = {
176
+ ...makeContext(),
177
+ hostFileProxy: {
178
+ isAvailable: () => true,
179
+ request: async (input, conversationId, signal) => {
180
+ requests.push({ input, conversationId, signal });
181
+ return {
182
+ content: "Image loaded: /host/screenshot.png",
183
+ isError: false,
184
+ contentBlocks: [
185
+ {
186
+ type: "image",
187
+ source: {
188
+ type: "base64",
189
+ media_type: "image/png",
190
+ data: PNG_HEADER.toString("base64"),
191
+ },
192
+ },
193
+ ],
194
+ };
195
+ },
196
+ } as ToolContext["hostFileProxy"],
197
+ };
198
+
199
+ const result = await hostFileReadTool.execute(
200
+ { path: "/host/screenshot.png" },
201
+ proxyContext,
202
+ );
203
+
204
+ expect(result.isError).toBe(false);
205
+ expect(result.contentBlocks).toHaveLength(1);
206
+ expect(requests).toEqual([
207
+ {
208
+ input: {
209
+ operation: "read",
210
+ path: "/host/screenshot.png",
211
+ offset: undefined,
212
+ limit: undefined,
213
+ },
214
+ conversationId: "test-conversation",
215
+ signal: undefined,
216
+ },
217
+ ]);
218
+ });
219
+
168
220
  test("returns image content block for .png file", async () => {
169
221
  const dir = makeTempDir();
170
222
  const filePath = join(dir, "screenshot.png");
@@ -0,0 +1,165 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { HostProxyInterfaceId, InterfaceId } from "../channels/types.js";
4
+ import { supportsHostProxy } from "../channels/types.js";
5
+ import type {
6
+ ConversationTransportMetadata,
7
+ HostProxyTransportMetadata,
8
+ NonHostProxyTransportMetadata,
9
+ } from "../daemon/message-types/conversations.js";
10
+ import { isHostProxyTransport } from "../daemon/message-types/conversations.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // supportsHostProxy — runtime behavior
14
+ // ---------------------------------------------------------------------------
15
+
16
+ describe("supportsHostProxy (runtime)", () => {
17
+ test("no-arg form returns true for host-proxy interfaces", () => {
18
+ expect(supportsHostProxy("macos")).toBe(true);
19
+ });
20
+
21
+ test("no-arg form returns false for interfaces without host-proxy support", () => {
22
+ const nonHostProxyIds: InterfaceId[] = [
23
+ "ios",
24
+ "cli",
25
+ "telegram",
26
+ "phone",
27
+ "vellum",
28
+ "whatsapp",
29
+ "slack",
30
+ "email",
31
+ "chrome-extension",
32
+ ];
33
+ for (const id of nonHostProxyIds) {
34
+ expect(supportsHostProxy(id)).toBe(false);
35
+ }
36
+ });
37
+
38
+ test("capability form grants host_browser to chrome-extension", () => {
39
+ expect(supportsHostProxy("chrome-extension", "host_browser")).toBe(true);
40
+ expect(supportsHostProxy("chrome-extension", "host_bash")).toBe(false);
41
+ expect(supportsHostProxy("chrome-extension", "host_file")).toBe(false);
42
+ expect(supportsHostProxy("chrome-extension", "host_cu")).toBe(false);
43
+ });
44
+
45
+ test("capability form grants host_bash/file/cu to macOS but not host_browser", () => {
46
+ expect(supportsHostProxy("macos", "host_bash")).toBe(true);
47
+ expect(supportsHostProxy("macos", "host_file")).toBe(true);
48
+ expect(supportsHostProxy("macos", "host_cu")).toBe(true);
49
+ expect(supportsHostProxy("macos", "host_browser")).toBe(false);
50
+ });
51
+
52
+ test("capability form rejects everything for non-host-proxy interfaces", () => {
53
+ expect(supportsHostProxy("ios", "host_bash")).toBe(false);
54
+ expect(supportsHostProxy("cli", "host_file")).toBe(false);
55
+ expect(supportsHostProxy("telegram", "host_browser")).toBe(false);
56
+ });
57
+ });
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // supportsHostProxy — type predicate (compile-time contract)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ describe("supportsHostProxy (type predicate)", () => {
64
+ test("no-arg form narrows InterfaceId to HostProxyInterfaceId", () => {
65
+ const id: InterfaceId = "macos";
66
+ if (supportsHostProxy(id)) {
67
+ // Inside this branch, TypeScript narrows `id` to HostProxyInterfaceId.
68
+ // If the overload were wrong, this assignment would fail to type-check
69
+ // and the test file wouldn't compile.
70
+ const narrowed: HostProxyInterfaceId = id;
71
+ expect(narrowed).toBe("macos");
72
+ } else {
73
+ throw new Error("expected narrowing branch to be taken for macos");
74
+ }
75
+ });
76
+
77
+ test("narrowing reaches through discriminated transport union", () => {
78
+ // Build a value typed as the full union so TypeScript can't cheat.
79
+ const transport: ConversationTransportMetadata = {
80
+ channelId: "vellum",
81
+ interfaceId: "macos",
82
+ hostHomeDir: "/Users/alice",
83
+ hostUsername: "alice",
84
+ };
85
+
86
+ if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
87
+ // Narrowing the discriminant narrows the union member — after this
88
+ // check, `transport` should be HostProxyTransportMetadata and the
89
+ // host-env fields are directly accessible.
90
+ const narrowed: HostProxyTransportMetadata = transport;
91
+ expect(narrowed.hostHomeDir).toBe("/Users/alice");
92
+ expect(narrowed.hostUsername).toBe("alice");
93
+ } else {
94
+ throw new Error("expected host-proxy branch for macos transport");
95
+ }
96
+ });
97
+
98
+ test("non-host-proxy branch narrows to NonHostProxyTransportMetadata", () => {
99
+ const transport: ConversationTransportMetadata = {
100
+ channelId: "vellum",
101
+ interfaceId: "ios",
102
+ };
103
+
104
+ if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
105
+ throw new Error("expected non-host-proxy branch for ios transport");
106
+ } else {
107
+ // `transport` is NonHostProxyTransportMetadata here.
108
+ const narrowed: NonHostProxyTransportMetadata = transport;
109
+ expect(narrowed.interfaceId).toBe("ios");
110
+ }
111
+ });
112
+ });
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // isHostProxyTransport — type guard on ConversationTransportMetadata
116
+ // ---------------------------------------------------------------------------
117
+
118
+ describe("isHostProxyTransport", () => {
119
+ test("returns true for macOS transport and narrows to HostProxyTransportMetadata", () => {
120
+ const transport: ConversationTransportMetadata = {
121
+ channelId: "vellum",
122
+ interfaceId: "macos",
123
+ hostHomeDir: "/Users/alice",
124
+ hostUsername: "alice",
125
+ };
126
+
127
+ expect(isHostProxyTransport(transport)).toBe(true);
128
+
129
+ if (isHostProxyTransport(transport)) {
130
+ const narrowed: HostProxyTransportMetadata = transport;
131
+ expect(narrowed.hostHomeDir).toBe("/Users/alice");
132
+ expect(narrowed.hostUsername).toBe("alice");
133
+ } else {
134
+ throw new Error("narrowing branch not taken");
135
+ }
136
+ });
137
+
138
+ test("returns false for every non-host-proxy interface", () => {
139
+ const nonHostProxyIds: Array<Exclude<InterfaceId, HostProxyInterfaceId>> = [
140
+ "ios",
141
+ "cli",
142
+ "telegram",
143
+ "phone",
144
+ "vellum",
145
+ "whatsapp",
146
+ "slack",
147
+ "email",
148
+ "chrome-extension",
149
+ ];
150
+ for (const interfaceId of nonHostProxyIds) {
151
+ const transport: ConversationTransportMetadata = {
152
+ channelId: "vellum",
153
+ interfaceId,
154
+ };
155
+ expect(isHostProxyTransport(transport)).toBe(false);
156
+ }
157
+ });
158
+
159
+ test("returns false when interfaceId is absent", () => {
160
+ const transport: ConversationTransportMetadata = {
161
+ channelId: "vellum",
162
+ };
163
+ expect(isHostProxyTransport(transport)).toBe(false);
164
+ });
165
+ });
@@ -36,7 +36,6 @@ const mockConfig = {
36
36
  entropyThreshold: 4.0,
37
37
  },
38
38
  auditLog: { retentionDays: 0 },
39
- sandbox: { enabled: true },
40
39
  };
41
40
 
42
41
  // Track whether wrapCommand was ever called — host_bash must never invoke it
@@ -149,10 +148,6 @@ describe("host_bash tool", () => {
149
148
  const dir = mkdtempSync(join(tmpdir(), "host-shell-plain-"));
150
149
  testDirs.push(dir);
151
150
 
152
- // Verify the tool executes successfully even when sandbox is enabled in config,
153
- // proving it bypasses the sandbox entirely
154
- expect(mockConfig.sandbox.enabled).toBe(true);
155
-
156
151
  spawnCalls.length = 0;
157
152
 
158
153
  const result = await hostShellTool.execute(
@@ -236,10 +231,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
236
231
  expect(spawnCalls[0].args[2]).toBe("ls -la /tmp");
237
232
  });
238
233
 
239
- test("sandbox config being enabled does not affect host_bash", async () => {
240
- // The mock config has sandbox.enabled = true
241
- expect(mockConfig.sandbox.enabled).toBe(true);
242
-
234
+ test("host_bash always spawns plain bash without wrapCommand", async () => {
243
235
  const dir = mkdtempSync(join(tmpdir(), "host-shell-sandbox-cfg-"));
244
236
  testDirs.push(dir);
245
237
 
@@ -255,9 +247,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
255
247
  );
256
248
 
257
249
  expect(result.isError).toBe(false);
258
- // Must never call wrapCommand regardless of config
259
250
  expect(wrapCommandCallCount).toBe(0);
260
- // Must still spawn plain bash
261
251
  expect(spawnCalls[0].command).toBe("bash");
262
252
  });
263
253
  });
@@ -178,6 +178,7 @@ function makeConversation(overrides: Record<string, unknown> = {}) {
178
178
  setTrustContext: () => {},
179
179
  updateClient: () => {},
180
180
  setHostBashProxy: () => {},
181
+ setHostBrowserProxy: () => {},
181
182
  setHostFileProxy: () => {},
182
183
  setHostCuProxy: () => {},
183
184
  addPreactivatedSkillId: () => {},
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Tests for initFeatureFlagOverrides() — the async gateway fetch that
3
+ * pre-populates the feature flag cache before CLI program construction.
4
+ */
5
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
6
+
7
+ import {
8
+ clearFeatureFlagOverridesCache,
9
+ initFeatureFlagOverrides,
10
+ isAssistantFeatureFlagEnabled,
11
+ } from "../config/assistant-feature-flags.js";
12
+ import * as tokenService from "../runtime/auth/token-service.js";
13
+ import { getMockFetchCalls, mockFetch, resetMockFetch } from "./mock-fetch.js";
14
+
15
+ const VALID_HEX_KEY = "ab".repeat(32);
16
+
17
+ beforeEach(() => {
18
+ clearFeatureFlagOverridesCache();
19
+ tokenService._resetSigningKeyForTesting();
20
+
21
+ // Set up a signing key so mintEdgeRelayToken() works
22
+ process.env.ACTOR_TOKEN_SIGNING_KEY = VALID_HEX_KEY;
23
+ tokenService.initAuthSigningKey(tokenService.resolveSigningKey());
24
+ });
25
+
26
+ afterEach(() => {
27
+ resetMockFetch();
28
+ clearFeatureFlagOverridesCache();
29
+ tokenService._resetSigningKeyForTesting();
30
+ delete process.env.ACTOR_TOKEN_SIGNING_KEY;
31
+ });
32
+
33
+ describe("initFeatureFlagOverrides", () => {
34
+ it("populates cache from gateway fetch response", async () => {
35
+ mockFetch(
36
+ "/v1/feature-flags",
37
+ { method: "GET" },
38
+ {
39
+ body: {
40
+ flags: [
41
+ {
42
+ key: "foo-enabled",
43
+ enabled: true,
44
+ label: "Foo",
45
+ defaultEnabled: false,
46
+ description: "",
47
+ },
48
+ {
49
+ key: "bar-enabled",
50
+ enabled: true,
51
+ label: "Bar",
52
+ defaultEnabled: true,
53
+ description: "",
54
+ },
55
+ ],
56
+ },
57
+ status: 200,
58
+ },
59
+ );
60
+
61
+ await initFeatureFlagOverrides();
62
+
63
+ const config = {} as any;
64
+ expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
65
+ expect(isAssistantFeatureFlagEnabled("bar-enabled", config)).toBe(true);
66
+
67
+ // Verify fetch was called with correct URL and auth header
68
+ const calls = getMockFetchCalls();
69
+ expect(calls.length).toBe(1);
70
+ expect(calls[0].path).toContain("/v1/feature-flags");
71
+ const headers = calls[0].init.headers as Record<string, string> | undefined;
72
+ expect(headers).toHaveProperty("Authorization");
73
+ });
74
+
75
+ it("sends a valid Bearer JWT in the Authorization header", async () => {
76
+ mockFetch(
77
+ "/v1/feature-flags",
78
+ { method: "GET" },
79
+ { body: { flags: [] }, status: 200 },
80
+ );
81
+
82
+ await initFeatureFlagOverrides();
83
+
84
+ const calls = getMockFetchCalls();
85
+ expect(calls.length).toBe(1);
86
+ const headers = calls[0].init.headers as Record<string, string> | undefined;
87
+ const authHeader = headers?.Authorization;
88
+
89
+ expect(authHeader).toBeDefined();
90
+ expect(authHeader).toMatch(/^Bearer /);
91
+
92
+ // Verify it's a valid JWT (three dot-separated base64url segments)
93
+ const token = authHeader!.replace("Bearer ", "");
94
+ const parts = token.split(".");
95
+ expect(parts.length).toBe(3);
96
+ });
97
+
98
+ it("falls back gracefully when gateway is unreachable", async () => {
99
+ mockFetch("/v1/feature-flags", { method: "GET" }, { status: 500 });
100
+
101
+ // Should not throw
102
+ await initFeatureFlagOverrides();
103
+
104
+ // Without gateway data or file, undeclared flags default to true
105
+ const config = {} as any;
106
+ expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
107
+ });
108
+
109
+ it("falls back gracefully on non-OK HTTP status", async () => {
110
+ mockFetch(
111
+ "/v1/feature-flags",
112
+ { method: "GET" },
113
+ { body: "Unauthorized", status: 401 },
114
+ );
115
+
116
+ await initFeatureFlagOverrides();
117
+
118
+ // Undeclared flags default to true without overrides
119
+ const config = {} as any;
120
+ expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
121
+ });
122
+
123
+ it("initializes signing key lazily when not yet set", async () => {
124
+ // Reset signing key to simulate fresh CLI subprocess
125
+ tokenService._resetSigningKeyForTesting();
126
+ delete process.env.ACTOR_TOKEN_SIGNING_KEY;
127
+
128
+ expect(tokenService.isSigningKeyInitialized()).toBe(false);
129
+
130
+ mockFetch(
131
+ "/v1/feature-flags",
132
+ { method: "GET" },
133
+ {
134
+ body: {
135
+ flags: [{ key: "expected-enabled", enabled: true }],
136
+ },
137
+ status: 200,
138
+ },
139
+ );
140
+
141
+ await initFeatureFlagOverrides();
142
+
143
+ // Signing key should have been initialized during the fetch
144
+ expect(tokenService.isSigningKeyInitialized()).toBe(true);
145
+
146
+ // And the flag should be resolved correctly
147
+ const config = {} as any;
148
+ expect(isAssistantFeatureFlagEnabled("expected-enabled", config)).toBe(
149
+ true,
150
+ );
151
+ });
152
+
153
+ it("does not cache empty gateway response", async () => {
154
+ mockFetch(
155
+ "/v1/feature-flags",
156
+ { method: "GET" },
157
+ { body: { flags: [] }, status: 200 },
158
+ );
159
+
160
+ await initFeatureFlagOverrides();
161
+
162
+ // Undeclared flags without overrides default to true (not false from
163
+ // a cached empty map)
164
+ const config = {} as any;
165
+ expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
166
+ });
167
+ });
@@ -11,7 +11,6 @@ const mockConfig = {
11
11
  model: "test",
12
12
  maxTokens: 4096,
13
13
  dataDir: "/tmp",
14
- sandbox: { enabled: true },
15
14
  timeouts: {
16
15
  shellDefaultTimeoutSec: 120,
17
16
  shellMaxTimeoutSec: 600,
@@ -104,20 +103,23 @@ describe("runInlineCommand", () => {
104
103
  // ── Sandbox enforcement ──────────────────────────────────────────────────
105
104
 
106
105
  describe("sandbox enforcement", () => {
107
- test("always passes sandbox config with enabled=true", async () => {
106
+ test("always passes sandbox config with enabled=false", async () => {
108
107
  lastWrapCall = null;
109
108
  await runInlineCommand("echo sandbox-check", CWD);
110
109
 
111
110
  expect(lastWrapCall).not.toBeNull();
112
- expect(lastWrapCall!.config.enabled).toBe(true);
111
+ expect(lastWrapCall!.config.enabled).toBe(false);
113
112
  });
114
113
 
115
- test("always passes networkMode=off", async () => {
114
+ test("does not pass networkMode when sandbox is disabled", async () => {
116
115
  lastWrapCall = null;
117
116
  await runInlineCommand("echo network-check", CWD);
118
117
 
119
118
  expect(lastWrapCall).not.toBeNull();
120
- expect(lastWrapCall!.options?.networkMode).toBe("off");
119
+ // networkMode is a no-op when sandbox is disabled (wrapCommand returns
120
+ // a plain bash invocation), so it is not passed. Network isolation is
121
+ // provided by the Docker/platform-managed container.
122
+ expect(lastWrapCall!.options).toBeUndefined();
121
123
  });
122
124
 
123
125
  test("uses the provided workingDir as cwd", async () => {