@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
@@ -0,0 +1,518 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import type { ServerMessage } from "../../daemon/message-protocol.js";
4
+ import {
5
+ __resetChromeExtensionRegistryForTests,
6
+ type ChromeExtensionConnection,
7
+ ChromeExtensionRegistry,
8
+ getChromeExtensionRegistry,
9
+ } from "../chrome-extension-registry.js";
10
+
11
+ // Minimal structural stand-in for Bun's ServerWebSocket. Only the methods
12
+ // the registry touches (`send`, `close`) are modeled; the rest of the Bun
13
+ // ServerWebSocket API is out of scope for these unit tests.
14
+ interface FakeWs {
15
+ send: (data: string) => number;
16
+ close: (code?: number, reason?: string) => void;
17
+ sent: string[];
18
+ closed: { code?: number; reason?: string }[];
19
+ sendShouldThrow?: boolean;
20
+ }
21
+
22
+ function makeFakeWs(): FakeWs {
23
+ const sent: string[] = [];
24
+ const closed: { code?: number; reason?: string }[] = [];
25
+ const ws: FakeWs = {
26
+ sent,
27
+ closed,
28
+ send(data: string) {
29
+ if (ws.sendShouldThrow) {
30
+ throw new Error("simulated ws.send failure");
31
+ }
32
+ sent.push(data);
33
+ return data.length;
34
+ },
35
+ close(code?: number, reason?: string) {
36
+ closed.push({ code, reason });
37
+ },
38
+ };
39
+ return ws;
40
+ }
41
+
42
+ function makeConnection(
43
+ guardianId: string,
44
+ id?: string,
45
+ clientInstanceId?: string,
46
+ ): { conn: ChromeExtensionConnection; fakeWs: FakeWs } {
47
+ const fakeWs = makeFakeWs();
48
+ const now = Date.now();
49
+ const conn: ChromeExtensionConnection = {
50
+ id: id ?? crypto.randomUUID(),
51
+ guardianId,
52
+ clientInstanceId,
53
+ ws: fakeWs as unknown as ChromeExtensionConnection["ws"],
54
+ connectedAt: now,
55
+ lastActiveAt: now,
56
+ };
57
+ return { conn, fakeWs };
58
+ }
59
+
60
+ describe("ChromeExtensionRegistry", () => {
61
+ beforeEach(() => {
62
+ __resetChromeExtensionRegistryForTests();
63
+ });
64
+
65
+ test("register stores the connection under the guardianId", () => {
66
+ const registry = new ChromeExtensionRegistry();
67
+ const { conn } = makeConnection("guardian-alpha");
68
+ registry.register(conn);
69
+ expect(registry.get("guardian-alpha")).toBe(conn);
70
+ });
71
+
72
+ test("unregister removes the connection", () => {
73
+ const registry = new ChromeExtensionRegistry();
74
+ const { conn } = makeConnection("guardian-alpha");
75
+ registry.register(conn);
76
+ registry.unregister(conn.id);
77
+ expect(registry.get("guardian-alpha")).toBeUndefined();
78
+ });
79
+
80
+ test("unregister is a no-op when the connectionId is unknown", () => {
81
+ const registry = new ChromeExtensionRegistry();
82
+ // Should not throw even though nothing is registered.
83
+ expect(() => registry.unregister("unknown-connection")).not.toThrow();
84
+ });
85
+
86
+ test("registering a second connection for the same guardianId + instance closes the prior one", () => {
87
+ const registry = new ChromeExtensionRegistry();
88
+ const { conn: conn1, fakeWs: fakeWs1 } = makeConnection(
89
+ "guardian-alpha",
90
+ "conn-1",
91
+ "install-A",
92
+ );
93
+ const { conn: conn2 } = makeConnection(
94
+ "guardian-alpha",
95
+ "conn-2",
96
+ "install-A",
97
+ );
98
+ registry.register(conn1);
99
+ registry.register(conn2);
100
+ // Prior connection (same instance) should have been closed with code 1000.
101
+ expect(fakeWs1.closed).toHaveLength(1);
102
+ expect(fakeWs1.closed[0].code).toBe(1000);
103
+ // Registry should hold the new connection for that instance.
104
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(conn2);
105
+ });
106
+
107
+ test("registering the same connection id twice is idempotent and does not close itself", () => {
108
+ const registry = new ChromeExtensionRegistry();
109
+ const { conn, fakeWs } = makeConnection("guardian-alpha", "conn-1");
110
+ registry.register(conn);
111
+ registry.register(conn);
112
+ expect(fakeWs.closed).toHaveLength(0);
113
+ expect(registry.get("guardian-alpha")).toBe(conn);
114
+ });
115
+
116
+ test("send returns false when no connection exists for the guardian", () => {
117
+ const registry = new ChromeExtensionRegistry();
118
+ const msg: ServerMessage = {
119
+ type: "host_browser_cancel",
120
+ requestId: "req-1",
121
+ } as ServerMessage;
122
+ expect(registry.send("missing-guardian", msg)).toBe(false);
123
+ });
124
+
125
+ test("send returns true and forwards the JSON-serialized message when a connection exists", () => {
126
+ const registry = new ChromeExtensionRegistry();
127
+ const { conn, fakeWs } = makeConnection("guardian-alpha");
128
+ registry.register(conn);
129
+ const msg: ServerMessage = {
130
+ type: "host_browser_cancel",
131
+ requestId: "req-1",
132
+ } as ServerMessage;
133
+ const ok = registry.send("guardian-alpha", msg);
134
+ expect(ok).toBe(true);
135
+ expect(fakeWs.sent).toHaveLength(1);
136
+ const parsed = JSON.parse(fakeWs.sent[0]);
137
+ expect(parsed.type).toBe("host_browser_cancel");
138
+ expect(parsed.requestId).toBe("req-1");
139
+ });
140
+
141
+ test("send returns false when ws.send throws (best-effort delivery)", () => {
142
+ const registry = new ChromeExtensionRegistry();
143
+ const { conn, fakeWs } = makeConnection("guardian-alpha");
144
+ fakeWs.sendShouldThrow = true;
145
+ registry.register(conn);
146
+ const msg: ServerMessage = {
147
+ type: "host_browser_cancel",
148
+ requestId: "req-1",
149
+ } as ServerMessage;
150
+ expect(registry.send("guardian-alpha", msg)).toBe(false);
151
+ });
152
+
153
+ test("getChromeExtensionRegistry returns a module-level singleton", () => {
154
+ const first = getChromeExtensionRegistry();
155
+ const second = getChromeExtensionRegistry();
156
+ expect(first).toBe(second);
157
+ });
158
+
159
+ test("unregister after supersession does not remove the new connection", () => {
160
+ // When a new connection supersedes an older one, the close handler for
161
+ // the older socket will fire later and call unregister with the OLD id.
162
+ // That must not clobber the newer registration.
163
+ const registry = new ChromeExtensionRegistry();
164
+ const { conn: old } = makeConnection(
165
+ "guardian-alpha",
166
+ "old-id",
167
+ "install-A",
168
+ );
169
+ const { conn: fresh } = makeConnection(
170
+ "guardian-alpha",
171
+ "fresh-id",
172
+ "install-A",
173
+ );
174
+ registry.register(old);
175
+ registry.register(fresh);
176
+ registry.unregister("old-id");
177
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(fresh);
178
+ });
179
+
180
+ // ── Multi-instance routing ──────────────────────────────────────────
181
+ //
182
+ // A single guardian may have multiple parallel extension installs
183
+ // connected at once (two Chrome profiles, two desktops sharing a sync
184
+ // identity). The registry keys inner entries by (guardianId,
185
+ // clientInstanceId) so sibling installs don't evict each other on
186
+ // register/unregister, and the default `send()` path routes to
187
+ // whichever instance has the most recent activity.
188
+ describe("multi-instance routing", () => {
189
+ test("two concurrent instances under the same guardian coexist", () => {
190
+ const registry = new ChromeExtensionRegistry();
191
+ const { conn: connA } = makeConnection(
192
+ "guardian-alpha",
193
+ "conn-A",
194
+ "install-A",
195
+ );
196
+ const { conn: connB } = makeConnection(
197
+ "guardian-alpha",
198
+ "conn-B",
199
+ "install-B",
200
+ );
201
+ registry.register(connA);
202
+ registry.register(connB);
203
+ // Both instances remain registered — neither should have been
204
+ // closed by the other's registration.
205
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(connA);
206
+ expect(registry.getInstance("guardian-alpha", "install-B")).toBe(connB);
207
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(2);
208
+ });
209
+
210
+ test("registering a new instance does not close sibling instances", () => {
211
+ const registry = new ChromeExtensionRegistry();
212
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
213
+ "guardian-alpha",
214
+ "conn-A",
215
+ "install-A",
216
+ );
217
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
218
+ "guardian-alpha",
219
+ "conn-B",
220
+ "install-B",
221
+ );
222
+ registry.register(connA);
223
+ registry.register(connB);
224
+ // Neither socket should have been closed by the sibling's
225
+ // registration.
226
+ expect(fakeWsA.closed).toHaveLength(0);
227
+ expect(fakeWsB.closed).toHaveLength(0);
228
+ });
229
+
230
+ test("unregister of one instance leaves the sibling in place", () => {
231
+ const registry = new ChromeExtensionRegistry();
232
+ const { conn: connA } = makeConnection(
233
+ "guardian-alpha",
234
+ "conn-A",
235
+ "install-A",
236
+ );
237
+ const { conn: connB } = makeConnection(
238
+ "guardian-alpha",
239
+ "conn-B",
240
+ "install-B",
241
+ );
242
+ registry.register(connA);
243
+ registry.register(connB);
244
+ registry.unregister("conn-A");
245
+ expect(
246
+ registry.getInstance("guardian-alpha", "install-A"),
247
+ ).toBeUndefined();
248
+ expect(registry.getInstance("guardian-alpha", "install-B")).toBe(connB);
249
+ // Guardian bucket should still exist because install-B is active.
250
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(1);
251
+ });
252
+
253
+ test("default send routes to the most recently active instance", () => {
254
+ const registry = new ChromeExtensionRegistry();
255
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
256
+ "guardian-alpha",
257
+ "conn-A",
258
+ "install-A",
259
+ );
260
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
261
+ "guardian-alpha",
262
+ "conn-B",
263
+ "install-B",
264
+ );
265
+ // Use a fake clock so A's register timestamp is strictly less
266
+ // than B's, ensuring B becomes the "most recently active"
267
+ // instance regardless of host-clock resolution.
268
+ const originalNow = Date.now;
269
+ let fakeNow = originalNow();
270
+ Date.now = () => fakeNow;
271
+ try {
272
+ registry.register(connA);
273
+ fakeNow += 10;
274
+ registry.register(connB);
275
+ const msg: ServerMessage = {
276
+ type: "host_browser_cancel",
277
+ requestId: "req-1",
278
+ } as ServerMessage;
279
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
280
+ } finally {
281
+ Date.now = originalNow;
282
+ }
283
+ // Default send should have landed on instance B, not A.
284
+ expect(fakeWsA.sent).toHaveLength(0);
285
+ expect(fakeWsB.sent).toHaveLength(1);
286
+ });
287
+
288
+ test("default send follows activity — later sendToInstance flips the default", () => {
289
+ const registry = new ChromeExtensionRegistry();
290
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
291
+ "guardian-alpha",
292
+ "conn-A",
293
+ "install-A",
294
+ );
295
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
296
+ "guardian-alpha",
297
+ "conn-B",
298
+ "install-B",
299
+ );
300
+ registry.register(connA);
301
+ registry.register(connB);
302
+ // B was registered last so it starts as the default. Force a
303
+ // send to A via sendToInstance — that bumps A's lastActiveAt and
304
+ // should make it the new default target.
305
+ const msg: ServerMessage = {
306
+ type: "host_browser_cancel",
307
+ requestId: "req-1",
308
+ } as ServerMessage;
309
+ // Nudge the clock forward so A's lastActiveAt strictly exceeds
310
+ // B's register-time stamp even on hosts where Date.now() has
311
+ // millisecond resolution.
312
+ const originalNow = Date.now;
313
+ let fakeNow = originalNow() + 10;
314
+ Date.now = () => fakeNow;
315
+ try {
316
+ expect(
317
+ registry.sendToInstance("guardian-alpha", "install-A", msg),
318
+ ).toBe(true);
319
+ fakeNow += 10;
320
+ // Default send should now route to A.
321
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
322
+ } finally {
323
+ Date.now = originalNow;
324
+ }
325
+ // A received one explicit send and one default send.
326
+ expect(fakeWsA.sent).toHaveLength(2);
327
+ // B only received the initial register (no sends).
328
+ expect(fakeWsB.sent).toHaveLength(0);
329
+ });
330
+
331
+ test("sendToInstance returns false for an unknown instance", () => {
332
+ const registry = new ChromeExtensionRegistry();
333
+ const { conn } = makeConnection("guardian-alpha", "conn-A", "install-A");
334
+ registry.register(conn);
335
+ const msg: ServerMessage = {
336
+ type: "host_browser_cancel",
337
+ requestId: "req-1",
338
+ } as ServerMessage;
339
+ expect(
340
+ registry.sendToInstance("guardian-alpha", "install-missing", msg),
341
+ ).toBe(false);
342
+ expect(
343
+ registry.sendToInstance("guardian-missing", "install-A", msg),
344
+ ).toBe(false);
345
+ });
346
+
347
+ test("get returns undefined after the last instance unregisters", () => {
348
+ const registry = new ChromeExtensionRegistry();
349
+ const { conn } = makeConnection("guardian-alpha", "conn-A", "install-A");
350
+ registry.register(conn);
351
+ registry.unregister("conn-A");
352
+ expect(registry.get("guardian-alpha")).toBeUndefined();
353
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(0);
354
+ });
355
+
356
+ test("ties on lastActiveAt are broken by registrationSeq (newest wins)", () => {
357
+ // Freeze Date.now so both instances stamp the exact same
358
+ // lastActiveAt. Without the registrationSeq tiebreaker, the
359
+ // strict `>` comparison in get() would keep whichever entry
360
+ // Map#values() yields first — i.e. the earlier-inserted one,
361
+ // which is the opposite of the "most recently registered"
362
+ // semantics a caller would expect.
363
+ const registry = new ChromeExtensionRegistry();
364
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
365
+ "guardian-alpha",
366
+ "conn-A",
367
+ "install-A",
368
+ );
369
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
370
+ "guardian-alpha",
371
+ "conn-B",
372
+ "install-B",
373
+ );
374
+ const originalNow = Date.now;
375
+ const frozenNow = originalNow();
376
+ Date.now = () => frozenNow;
377
+ try {
378
+ registry.register(connA);
379
+ registry.register(connB);
380
+ // Both should hold the same lastActiveAt because Date.now is frozen.
381
+ expect(connA.lastActiveAt).toBe(connB.lastActiveAt);
382
+ // connB registered second, so its registrationSeq must be higher.
383
+ expect(connB.registrationSeq).toBeGreaterThan(
384
+ connA.registrationSeq ?? 0,
385
+ );
386
+ const msg: ServerMessage = {
387
+ type: "host_browser_cancel",
388
+ requestId: "req-1",
389
+ } as ServerMessage;
390
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
391
+ } finally {
392
+ Date.now = originalNow;
393
+ }
394
+ // Default send should have landed on the newer instance (B),
395
+ // not the older insertion-order winner (A).
396
+ expect(fakeWsA.sent).toHaveLength(0);
397
+ expect(fakeWsB.sent).toHaveLength(1);
398
+ });
399
+
400
+ test("registrationSeq is monotonically increasing across registrations", () => {
401
+ // Sanity check on the counter itself — new registrations should
402
+ // always stamp a strictly greater sequence number than anything
403
+ // that came before, including re-registrations of the same
404
+ // instance after a supersede.
405
+ const registry = new ChromeExtensionRegistry();
406
+ const { conn: connA } = makeConnection(
407
+ "guardian-alpha",
408
+ "conn-A",
409
+ "install-A",
410
+ );
411
+ const { conn: connB } = makeConnection(
412
+ "guardian-alpha",
413
+ "conn-B",
414
+ "install-B",
415
+ );
416
+ const { conn: connC } = makeConnection(
417
+ "guardian-alpha",
418
+ "conn-C",
419
+ "install-A",
420
+ );
421
+ registry.register(connA);
422
+ registry.register(connB);
423
+ registry.register(connC); // supersedes install-A → conn-A closes
424
+ expect(connA.registrationSeq).toBeDefined();
425
+ expect(connB.registrationSeq).toBeDefined();
426
+ expect(connC.registrationSeq).toBeDefined();
427
+ expect(connB.registrationSeq!).toBeGreaterThan(connA.registrationSeq!);
428
+ expect(connC.registrationSeq!).toBeGreaterThan(connB.registrationSeq!);
429
+ });
430
+
431
+ test("same-millisecond supersede leaves the new connection as default", () => {
432
+ // Re-registering the same install within the same millisecond
433
+ // window must still promote the new connection to the default
434
+ // target — the tie-breaker should apply to re-registrations of
435
+ // the same instance just as it applies to sibling instances.
436
+ const registry = new ChromeExtensionRegistry();
437
+ const { conn: old } = makeConnection(
438
+ "guardian-alpha",
439
+ "old-id",
440
+ "install-A",
441
+ );
442
+ const { conn: fresh, fakeWs: fakeWsFresh } = makeConnection(
443
+ "guardian-alpha",
444
+ "fresh-id",
445
+ "install-A",
446
+ );
447
+ const originalNow = Date.now;
448
+ const frozenNow = originalNow();
449
+ Date.now = () => frozenNow;
450
+ try {
451
+ registry.register(old);
452
+ registry.register(fresh);
453
+ const msg: ServerMessage = {
454
+ type: "host_browser_cancel",
455
+ requestId: "req-1",
456
+ } as ServerMessage;
457
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
458
+ } finally {
459
+ Date.now = originalNow;
460
+ }
461
+ // The fresh connection must receive the default send even though
462
+ // both entries stamped the exact same lastActiveAt.
463
+ expect(fakeWsFresh.sent).toHaveLength(1);
464
+ });
465
+ });
466
+
467
+ // ── Backwards compatibility ─────────────────────────────────────────
468
+ //
469
+ // Connections without a clientInstanceId (older extension builds or
470
+ // dev-bypass paths) synthesize a connection-scoped key so each one
471
+ // lives in its own slot. This gives sibling instances for the same
472
+ // guardian independent lifecycles even without explicit client ids.
473
+ describe("backwards compatibility when clientInstanceId is absent", () => {
474
+ test("two legacy connections under the same guardian coexist", () => {
475
+ const registry = new ChromeExtensionRegistry();
476
+ const { conn: connA } = makeConnection(
477
+ "guardian-alpha",
478
+ "conn-A",
479
+ // clientInstanceId intentionally omitted
480
+ );
481
+ const { conn: connB } = makeConnection(
482
+ "guardian-alpha",
483
+ "conn-B",
484
+ // clientInstanceId intentionally omitted
485
+ );
486
+ registry.register(connA);
487
+ registry.register(connB);
488
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(2);
489
+ });
490
+
491
+ test("legacy unregister does not clobber a newer legacy sibling", () => {
492
+ const registry = new ChromeExtensionRegistry();
493
+ const { conn: oldConn } = makeConnection("guardian-alpha", "old-id");
494
+ const { conn: freshConn } = makeConnection("guardian-alpha", "fresh-id");
495
+ registry.register(oldConn);
496
+ registry.register(freshConn);
497
+ registry.unregister("old-id");
498
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(1);
499
+ // The surviving entry is the newer connection.
500
+ const remaining = registry.listInstances("guardian-alpha");
501
+ expect(remaining[0]).toBe(freshConn);
502
+ });
503
+
504
+ test("legacy and instance-id connections can coexist under the same guardian", () => {
505
+ const registry = new ChromeExtensionRegistry();
506
+ const { conn: legacy } = makeConnection("guardian-alpha", "legacy-id");
507
+ const { conn: modern } = makeConnection(
508
+ "guardian-alpha",
509
+ "modern-id",
510
+ "install-A",
511
+ );
512
+ registry.register(legacy);
513
+ registry.register(modern);
514
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(2);
515
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(modern);
516
+ });
517
+ });
518
+ });
@@ -45,7 +45,7 @@ interface SubscriberEntry {
45
45
  * events that match their `assistantId` (and optionally `conversationId`).
46
46
  *
47
47
  * The hub is intentionally simple: synchronous fanout, no buffering, no
48
- * backpressure. Slow-consumer protection lives in the SSE route (PR 7).
48
+ * backpressure. Slow-consumer protection lives in the SSE route.
49
49
  */
50
50
  export class AssistantEventHub {
51
51
  private readonly subscribers = new Set<SubscriberEntry>();
@@ -156,6 +156,28 @@ export class AssistantEventHub {
156
156
  }
157
157
  }
158
158
 
159
+ /**
160
+ * Returns true when at least one active subscriber would receive the given
161
+ * event based on the same assistant/conversation matching rules as publish().
162
+ */
163
+ hasSubscribersForEvent(
164
+ event: Pick<AssistantEvent, "assistantId" | "conversationId">,
165
+ ): boolean {
166
+ for (const entry of this.subscribers) {
167
+ if (!entry.active) continue;
168
+ if (entry.filter.assistantId !== event.assistantId) continue;
169
+ if (
170
+ event.conversationId != null &&
171
+ entry.filter.conversationId != null &&
172
+ entry.filter.conversationId !== event.conversationId
173
+ ) {
174
+ continue;
175
+ }
176
+ return true;
177
+ }
178
+ return false;
179
+ }
180
+
159
181
  /** Number of currently active subscribers (useful for tests and caps). */
160
182
  subscriberCount(): number {
161
183
  return this.subscribers.size;
@@ -172,6 +194,6 @@ export class AssistantEventHub {
172
194
  /**
173
195
  * Singleton hub shared across the entire runtime process.
174
196
  *
175
- * Import and use this in daemon send paths (PR 3) and the SSE route (PR 5).
197
+ * Import and use this in daemon send paths and the SSE route.
176
198
  */
177
199
  export const assistantEventHub = new AssistantEventHub({ maxSubscribers: 100 });
@@ -63,6 +63,7 @@ describe("route policy coverage", () => {
63
63
  // excluded because they are handled before JWT auth and are not composed
64
64
  // into buildRouteTable().
65
65
  const PRE_AUTH_ROUTE_MODULES = new Set([
66
+ "browser-extension-pair-routes.ts",
66
67
  "guardian-bootstrap-routes.ts",
67
68
  "guardian-refresh-routes.ts",
68
69
  ]);