@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
@@ -81,6 +81,8 @@ export interface HistorySurface {
81
81
  data: Record<string, unknown>;
82
82
  actions?: Array<{ id: string; label: string; style?: string }>;
83
83
  display?: string;
84
+ completed?: boolean;
85
+ completionSummary?: string;
84
86
  }
85
87
 
86
88
  export interface RenderedHistoryContent {
@@ -108,6 +110,11 @@ export interface ConversationCreateOptions {
108
110
  transport?: ConversationTransportMetadata;
109
111
  assistantId?: string;
110
112
  trustContext?: TrustContext;
113
+ /**
114
+ * Active task-run scope for this turn. Cleared when omitted so background
115
+ * task permissions do not leak into later turns on a reused conversation.
116
+ */
117
+ taskRunId?: string;
111
118
  /** Normalized auth context for the conversation. */
112
119
  authContext?: AuthContext;
113
120
  /** Whether this turn can block on interactive approval prompts. */
@@ -281,6 +288,11 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
281
288
  : {},
282
289
  actions: Array.isArray(block.actions) ? block.actions : undefined,
283
290
  display: typeof block.display === "string" ? block.display : undefined,
291
+ completed: block.completed === true ? true : undefined,
292
+ completionSummary:
293
+ typeof block.completionSummary === "string"
294
+ ? block.completionSummary
295
+ : undefined,
284
296
  };
285
297
  surfaces.push(surface);
286
298
  contentOrder.push(`surface:${surfaces.length - 1}`);
@@ -337,6 +349,59 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
337
349
  }
338
350
  continue;
339
351
  }
352
+ if (block.type === "server_tool_use") {
353
+ finalizeSegment();
354
+ const name = typeof block.name === "string" ? block.name : "unknown";
355
+ const input = isRecord(block.input)
356
+ ? (block.input as Record<string, unknown>)
357
+ : {};
358
+ const id = typeof block.id === "string" ? block.id : "";
359
+ const entry: HistoryToolCall = { name, input };
360
+ toolCalls.push(entry);
361
+ if (id) pendingToolUses.set(id, entry);
362
+ contentOrder.push(`tool:${toolCalls.length - 1}`);
363
+ if (!seenToolUse) {
364
+ seenToolUse = true;
365
+ if (!seenText) toolCallsBeforeText = true;
366
+ }
367
+ continue;
368
+ }
369
+ if (block.type === "web_search_tool_result") {
370
+ const toolUseId =
371
+ typeof block.tool_use_id === "string" ? block.tool_use_id : "";
372
+ const isError =
373
+ isRecord(block.content) &&
374
+ (block.content as { type?: string }).type ===
375
+ "web_search_tool_result_error";
376
+
377
+ // Format search results into readable text.
378
+ let resultContent = "";
379
+ if (Array.isArray(block.content)) {
380
+ resultContent = (block.content as unknown[])
381
+ .filter(
382
+ (r): r is { type: string; title: string; url: string } =>
383
+ typeof r === "object" &&
384
+ r != null &&
385
+ (r as { type?: string }).type === "web_search_result",
386
+ )
387
+ .map((r) => `${r.title}\n${r.url}`)
388
+ .join("\n\n");
389
+ }
390
+
391
+ const matched = toolUseId ? pendingToolUses.get(toolUseId) : null;
392
+ if (matched) {
393
+ matched.result = resultContent;
394
+ matched.isError = isError;
395
+ } else {
396
+ toolCalls.push({
397
+ name: "web_search",
398
+ input: {},
399
+ result: resultContent,
400
+ isError,
401
+ });
402
+ }
403
+ continue;
404
+ }
340
405
  if (block.type === "tool_result") {
341
406
  const toolUseId =
342
407
  typeof block.tool_use_id === "string" ? block.tool_use_id : "";
@@ -9,7 +9,7 @@ import {
9
9
  statSync,
10
10
  } from "node:fs";
11
11
  import { homedir } from "node:os";
12
- import { join, relative } from "node:path";
12
+ import { basename, join, relative, sep } from "node:path";
13
13
 
14
14
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
15
15
  import {
@@ -31,9 +31,21 @@ import {
31
31
  getConfiguredProvider,
32
32
  userMessage,
33
33
  } from "../../providers/provider-send-message.js";
34
- import { isTextMimeType as isTextMime } from "../../runtime/routes/workspace-utils.js";
34
+ import {
35
+ isTextMimeType as isTextMime,
36
+ MAX_INLINE_TEXT_SIZE,
37
+ } from "../../runtime/routes/workspace-utils.js";
35
38
  import { getCatalog } from "../../skills/catalog-cache.js";
36
39
  import {
40
+ hasHiddenOrSkippedSegment,
41
+ readCatalogSkillFileContent,
42
+ readCatalogSkillFiles,
43
+ sanitizeRelativePath,
44
+ type SkillFileEntry,
45
+ SKIP_DIRS,
46
+ } from "../../skills/catalog-files.js";
47
+ import {
48
+ type CatalogSkill,
37
49
  installSkillLocally,
38
50
  upsertSkillsIndex,
39
51
  } from "../../skills/catalog-install.js";
@@ -64,6 +76,7 @@ import {
64
76
  import { getWorkspaceSkillsDir } from "../../util/platform.js";
65
77
  import type {
66
78
  SkillDetailResponse,
79
+ SkillFileContentResponse,
67
80
  SlimSkillResponse,
68
81
  } from "../message-types/skills.js";
69
82
  import {
@@ -73,10 +86,6 @@ import {
73
86
  log,
74
87
  } from "./shared.js";
75
88
 
76
- // ─── MIME detection helpers ───────────────────────────────────────────────────
77
-
78
- const MAX_INLINE_SIZE = 2 * 1024 * 1024; // 2 MB
79
-
80
89
  // ─── Shared context for standalone functions ─────────────────────────────────
81
90
 
82
91
  /**
@@ -364,7 +373,7 @@ export async function listSkillsWithCatalog(
364
373
  const installed = listSkills(ctx);
365
374
  const installedIds = new Set(installed.map((s) => s.id));
366
375
 
367
- let catalogSkills: import("../../skills/catalog-install.js").CatalogSkill[];
376
+ let catalogSkills: CatalogSkill[];
368
377
  try {
369
378
  catalogSkills = await getCatalog();
370
379
  } catch {
@@ -376,15 +385,7 @@ export async function listSkillsWithCatalog(
376
385
  // Create SlimSkillResponses for catalog skills not already installed.
377
386
  const available: SlimSkillResponse[] = catalogSkills
378
387
  .filter((cs) => !installedIds.has(cs.id))
379
- .map((cs) => ({
380
- id: cs.id,
381
- name: cs.metadata?.vellum?.["display-name"] ?? cs.name,
382
- description: cs.description,
383
- emoji: cs.emoji,
384
- kind: "catalog" as const,
385
- origin: "vellum" as const,
386
- status: "available" as const,
387
- }));
388
+ .map((cs) => catalogSkillToSlim(cs));
388
389
 
389
390
  const merged = [...installed, ...available];
390
391
 
@@ -494,16 +495,12 @@ export async function getSkill(
494
495
 
495
496
  // ─── Skill file listing ──────────────────────────────────────────────────────
496
497
 
497
- export interface SkillFileEntry {
498
- path: string; // relative to skill directory root (e.g. "SKILL.md", "tools/foo.ts")
499
- name: string; // basename
500
- size: number;
501
- mimeType: string;
502
- isBinary: boolean;
503
- content: string | null; // inline text if ≤ 2 MB and text MIME, else null
504
- }
505
-
506
- const SKIP_DIRS = new Set(["node_modules", "__pycache__", ".git"]);
498
+ // `SkillFileEntry` lives in `../../skills/catalog-files.ts` to keep a single
499
+ // source of truth for the shape and avoid a circular import (catalog-files
500
+ // depends on `catalog-cache.ts`, which would otherwise be reachable via this
501
+ // handler module). Re-exported here so handlers can import it alongside
502
+ // the other skill handler exports.
503
+ export type { SkillFileEntry } from "../../skills/catalog-files.js";
507
504
 
508
505
  /**
509
506
  * Returns true if `filePath` is a symlink whose resolved real path escapes
@@ -550,7 +547,7 @@ function readDirRecursive(dir: string, rootDir: string): SkillFileEntry[] {
550
547
  const mimeType = Bun.file(fullPath).type;
551
548
  const isText = isTextMime(mimeType, dirent.name);
552
549
  let content: string | null = null;
553
- if (isText && stat.size <= MAX_INLINE_SIZE) {
550
+ if (isText && stat.size <= MAX_INLINE_TEXT_SIZE) {
554
551
  content = readFileSync(fullPath, "utf-8");
555
552
  }
556
553
  entries.push({
@@ -568,26 +565,224 @@ function readDirRecursive(dir: string, rootDir: string): SkillFileEntry[] {
568
565
  return entries;
569
566
  }
570
567
 
571
- export function getSkillFiles(
568
+ /**
569
+ * Map a `CatalogSkill` (from the Vellum platform API) to a `SlimSkillResponse`
570
+ * shaped for the "available catalog skill" case. Shared between
571
+ * `listSkillsWithCatalog` (merging catalog entries into the installed list)
572
+ * and `getSkillFiles` (catalog fallback for preview listings). Keeping the
573
+ * mapping in one place avoids divergence between the list and detail paths.
574
+ */
575
+ function catalogSkillToSlim(cs: CatalogSkill): SlimSkillResponse {
576
+ return {
577
+ id: cs.id,
578
+ name: cs.metadata?.vellum?.["display-name"] ?? cs.name,
579
+ description: cs.description,
580
+ emoji: cs.emoji,
581
+ kind: "catalog",
582
+ origin: "vellum",
583
+ status: "available",
584
+ };
585
+ }
586
+
587
+ /**
588
+ * Read a single file's content from an installed or catalog skill.
589
+ *
590
+ * Installed-skill path (eager): reads the file directly from the skill's
591
+ * on-disk directory. Applies lexical containment, symlink rejection, and
592
+ * realpath containment checks for defense in depth.
593
+ *
594
+ * Catalog fallback: when the skill id is not backed by a local directory
595
+ * (e.g. an uninstalled Vellum catalog skill), delegates to
596
+ * `readCatalogSkillFileContent`, which handles both the dev-mode repo
597
+ * checkout path and the platform preview API path internally.
598
+ */
599
+ export async function getSkillFileContent(
572
600
  skillId: string,
601
+ relativePath: string,
573
602
  _ctx: SkillOperationContext,
574
- ):
603
+ ): Promise<SkillFileContentResponse | { error: string; status: number }> {
604
+ const sanitized = sanitizeRelativePath(relativePath);
605
+ if (!sanitized) {
606
+ return { error: "Invalid path", status: 400 };
607
+ }
608
+
609
+ // Reject any sanitized path that references a hidden segment (dotfiles
610
+ // like `.env`, dot-dirs like `.git`) or a SKIP_DIRS segment (e.g.
611
+ // `node_modules`, `__pycache__`). Both file-listing endpoints (installed
612
+ // and catalog) intentionally omit these entries, so allowing the content
613
+ // endpoint to read them would create a data-exposure path and break
614
+ // parity with the visible file list. This check runs BEFORE both the
615
+ // installed-skill disk read and the catalog fallback so the rejection
616
+ // is uniform regardless of source.
617
+ if (hasHiddenOrSkippedSegment(sanitized)) {
618
+ return { error: "Invalid path", status: 400 };
619
+ }
620
+
621
+ const found = findSkillById(skillId);
622
+ if (found) {
623
+ if (!existsSync(found.summary.directoryPath)) {
624
+ // Resolver lists the skill as installed but the directory is missing
625
+ // on disk (corrupted install, mid-delete race, external unmount, etc.).
626
+ // Return a distinct 404 instead of falling through to the catalog path
627
+ // so the content response stays consistent with `listSkillsWithCatalog`
628
+ // and `getSkillFiles`, which classify the same id as `kind: "installed"`.
629
+ return {
630
+ error: `Skill directory missing for "${skillId}"`,
631
+ status: 404,
632
+ };
633
+ }
634
+ const dir = found.summary.directoryPath;
635
+ const abs = join(dir, sanitized);
636
+
637
+ // Lexical containment: the resolved absolute path must stay inside the
638
+ // skill directory even after `join` normalization. Cheap short-circuit
639
+ // before any fs calls.
640
+ if (!(abs === dir || abs.startsWith(dir + sep))) {
641
+ return { error: "Invalid path", status: 400 };
642
+ }
643
+
644
+ // Defense-in-depth symlink rejection: refuse to follow a symlinked file
645
+ // inside the skill dir that could point outside the root. Also catches
646
+ // symlinked parent directories via a realpath containment check.
647
+ let lstat;
648
+ try {
649
+ lstat = lstatSync(abs);
650
+ } catch {
651
+ return { error: "File not found", status: 404 };
652
+ }
653
+ if (lstat.isSymbolicLink()) {
654
+ return { error: "File not found", status: 404 };
655
+ }
656
+ if (!lstat.isFile()) {
657
+ return { error: "File not found", status: 404 };
658
+ }
659
+
660
+ let realAbs: string;
661
+ let realDir: string;
662
+ try {
663
+ realAbs = realpathSync(abs);
664
+ realDir = realpathSync(dir);
665
+ } catch {
666
+ return { error: "File not found", status: 404 };
667
+ }
668
+ if (!(realAbs === realDir || realAbs.startsWith(realDir + sep))) {
669
+ return { error: "File not found", status: 404 };
670
+ }
671
+
672
+ let stat;
673
+ try {
674
+ stat = statSync(abs);
675
+ } catch {
676
+ return { error: "File not found", status: 404 };
677
+ }
678
+ if (!stat.isFile()) {
679
+ return { error: "File not found", status: 404 };
680
+ }
681
+
682
+ const name = basename(sanitized);
683
+ const mimeType = Bun.file(abs).type;
684
+ const isText = isTextMime(mimeType, name);
685
+ const isBinary = !isText;
686
+ let content: string | null = null;
687
+ if (isText && stat.size <= MAX_INLINE_TEXT_SIZE) {
688
+ try {
689
+ content = readFileSync(abs, "utf-8");
690
+ } catch {
691
+ content = null;
692
+ }
693
+ }
694
+ return {
695
+ path: sanitized,
696
+ name,
697
+ size: stat.size,
698
+ mimeType,
699
+ isBinary,
700
+ content,
701
+ };
702
+ }
703
+
704
+ // Catalog fallback: skill is not installed locally. Try the catalog
705
+ // preview helper, which handles both dev-mode repo checkouts and the
706
+ // platform preview API.
707
+ let catalog: Awaited<ReturnType<typeof getCatalog>> = [];
708
+ try {
709
+ catalog = await getCatalog();
710
+ } catch {
711
+ catalog = [];
712
+ }
713
+ const inCatalog = catalog.some((s) => s.id === skillId);
714
+ if (!inCatalog) {
715
+ return { error: "Skill not found", status: 404 };
716
+ }
717
+
718
+ const result = await readCatalogSkillFileContent(skillId, sanitized);
719
+ if (!result) {
720
+ return { error: "File not found", status: 404 };
721
+ }
722
+ return {
723
+ path: result.path,
724
+ name: result.name,
725
+ size: result.size,
726
+ mimeType: result.mimeType,
727
+ isBinary: result.isBinary,
728
+ content: result.content,
729
+ };
730
+ }
731
+
732
+ export async function getSkillFiles(
733
+ skillId: string,
734
+ _ctx: SkillOperationContext,
735
+ ): Promise<
575
736
  | { skill: SlimSkillResponse; files: SkillFileEntry[] }
576
- | { error: string; status: number } {
737
+ | { error: string; status: number }
738
+ > {
739
+ // Preferred path: the skill is resolved locally (bundled, managed,
740
+ // workspace, or extra) AND its directory exists on disk. Read files
741
+ // eagerly with inline content.
577
742
  const found = findSkillById(skillId);
578
- if (!found) {
743
+ if (found) {
744
+ if (existsSync(found.summary.directoryPath)) {
745
+ const dirPath = found.summary.directoryPath;
746
+ const files = readDirRecursive(dirPath, dirPath);
747
+ files.sort((a, b) => a.path.localeCompare(b.path));
748
+ return { skill: found.item, files };
749
+ }
750
+ // Resolver lists the skill as installed but the directory is missing
751
+ // on disk (corrupted install, mid-delete race, external unmount, etc.).
752
+ // Return a distinct 404 instead of falling through to the catalog path
753
+ // so the detail response stays consistent with `listSkillsWithCatalog`,
754
+ // which classifies the same id as `kind: "installed"`.
755
+ return {
756
+ error: `Skill directory missing for "${skillId}"`,
757
+ status: 404,
758
+ };
759
+ }
760
+
761
+ // Fallback: skill is not installed. Try the Vellum catalog — this covers
762
+ // previewing files for an uninstalled catalog skill without touching the
763
+ // install flow.
764
+ let catalog: CatalogSkill[];
765
+ try {
766
+ catalog = await getCatalog();
767
+ } catch {
768
+ return { error: `Skill "${skillId}" not found`, status: 404 };
769
+ }
770
+ const cs = catalog.find((c) => c.id === skillId);
771
+ if (!cs) {
579
772
  return { error: `Skill "${skillId}" not found`, status: 404 };
580
773
  }
581
774
 
582
- const dirPath = found.summary.directoryPath;
583
- if (!existsSync(dirPath)) {
584
- return { error: `Skill directory not found for "${skillId}"`, status: 404 };
775
+ const files = await readCatalogSkillFiles(skillId);
776
+ if (files === null) {
777
+ return {
778
+ error: `Skill files unavailable for "${skillId}"`,
779
+ status: 404,
780
+ };
585
781
  }
586
782
 
587
- const files = readDirRecursive(dirPath, dirPath);
783
+ const skill = catalogSkillToSlim(cs);
588
784
  files.sort((a, b) => a.path.localeCompare(b.path));
589
-
590
- return { skill: found.item, files };
785
+ return { skill, files };
591
786
  }
592
787
 
593
788
  export function enableSkill(
@@ -14,6 +14,8 @@ interface PendingRequest {
14
14
  reject: (err: Error) => void;
15
15
  timer: ReturnType<typeof setTimeout>;
16
16
  timeoutSec: number;
17
+ /** Detach the abort listener from the caller's signal. No-op when no signal was passed. */
18
+ detachAbort: () => void;
17
19
  }
18
20
 
19
21
  export class HostBashProxy {
@@ -60,8 +62,14 @@ export class HostBashProxy {
60
62
  const timeoutSec = input.timeout_seconds ?? shellMaxTimeoutSec;
61
63
  // Proxy timeout: slightly after client-side timeout, but before executor's outer timeout
62
64
  const proxyTimeoutSec = timeoutSec + 3;
65
+
66
+ // Declared up-front so onAbort (defined before detachAbort is assigned)
67
+ // can close over a stable reference once it's wired below.
68
+ let detachAbort: () => void = () => {};
69
+
63
70
  const timer = setTimeout(() => {
64
71
  this.pending.delete(requestId);
72
+ detachAbort();
65
73
  this.onInternalResolve?.(requestId);
66
74
  log.warn(
67
75
  { requestId, command: input.command },
@@ -78,13 +86,14 @@ export class HostBashProxy {
78
86
  );
79
87
  }, proxyTimeoutSec * 1000);
80
88
 
81
- this.pending.set(requestId, { resolve, reject, timer, timeoutSec });
82
-
83
89
  if (signal) {
84
90
  const onAbort = () => {
85
91
  if (this.pending.has(requestId)) {
86
92
  clearTimeout(timer);
87
93
  this.pending.delete(requestId);
94
+ // Abort fired — nothing to detach, but call the no-op for symmetry
95
+ // so callers can rely on detachAbort being idempotent.
96
+ detachAbort();
88
97
  this.onInternalResolve?.(requestId);
89
98
  try {
90
99
  this.sendToClient({
@@ -98,19 +107,43 @@ export class HostBashProxy {
98
107
  }
99
108
  };
100
109
  signal.addEventListener("abort", onAbort, { once: true });
110
+ detachAbort = () => signal.removeEventListener("abort", onAbort);
101
111
  }
102
112
 
103
- this.sendToClient({
104
- type: "host_bash_request",
105
- requestId,
106
- conversationId,
107
- command: input.command,
108
- working_dir: input.working_dir,
109
- timeout_seconds: input.timeout_seconds,
110
- ...(input.env && Object.keys(input.env).length > 0
111
- ? { env: input.env }
112
- : {}),
113
- } as ServerMessage);
113
+ this.pending.set(requestId, {
114
+ resolve,
115
+ reject,
116
+ timer,
117
+ timeoutSec,
118
+ detachAbort,
119
+ });
120
+
121
+ try {
122
+ this.sendToClient({
123
+ type: "host_bash_request",
124
+ requestId,
125
+ conversationId,
126
+ command: input.command,
127
+ working_dir: input.working_dir,
128
+ timeout_seconds: input.timeout_seconds,
129
+ ...(input.env && Object.keys(input.env).length > 0
130
+ ? { env: input.env }
131
+ : {}),
132
+ } as ServerMessage);
133
+ } catch (err) {
134
+ // Sender threw synchronously (e.g. client transport error during
135
+ // event emission). Clean up pending state and timer so we don't
136
+ // leak an in-flight entry that nothing will ever resolve.
137
+ clearTimeout(timer);
138
+ this.pending.delete(requestId);
139
+ detachAbort();
140
+ this.onInternalResolve?.(requestId);
141
+ log.warn(
142
+ { requestId, command: input.command, err },
143
+ "Host bash proxy send failed",
144
+ );
145
+ reject(err instanceof Error ? err : new Error(String(err)));
146
+ }
114
147
  });
115
148
  }
116
149
 
@@ -129,6 +162,7 @@ export class HostBashProxy {
129
162
  return;
130
163
  }
131
164
  clearTimeout(entry.timer);
165
+ entry.detachAbort();
132
166
  this.pending.delete(requestId);
133
167
  const result = formatShellOutput(
134
168
  response.stdout,
@@ -151,6 +185,7 @@ export class HostBashProxy {
151
185
  dispose(): void {
152
186
  for (const [requestId, entry] of this.pending) {
153
187
  clearTimeout(entry.timer);
188
+ entry.detachAbort();
154
189
  this.onInternalResolve?.(requestId);
155
190
  try {
156
191
  this.sendToClient({