@vellumai/assistant 0.6.2 → 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 (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -18,14 +18,14 @@ export interface TemporalContextOptions {
18
18
  userTimeZone?: string | null;
19
19
  }
20
20
 
21
- const WEEKDAY_SHORT = [
22
- "Sun",
23
- "Mon",
24
- "Tue",
25
- "Wed",
26
- "Thu",
27
- "Fri",
28
- "Sat",
21
+ const WEEKDAY_LONG = [
22
+ "Sunday",
23
+ "Monday",
24
+ "Tuesday",
25
+ "Wednesday",
26
+ "Thursday",
27
+ "Friday",
28
+ "Saturday",
29
29
  ] as const;
30
30
  const UTC_GMT_OFFSET_TOKEN_RE = /^(?:UTC|GMT)([+-])(\d{1,2})(?::?(\d{2}))?$/i;
31
31
 
@@ -295,7 +295,7 @@ function formatLocalDate(date: Date, timeZone: string): string {
295
295
  * Uses the timezone resolution cascade:
296
296
  * explicit override → configured user tz → profile user tz → host fallback.
297
297
  *
298
- * Returns format: `2026-04-02 (Thu) 01:52:33 -05:00 (America/Chicago)`
298
+ * Returns format: `2026-04-02 (Thursday) 01:52:33 -05:00 (America/Chicago)`
299
299
  */
300
300
  export function formatTurnTimestamp(
301
301
  options: TemporalContextOptions = {},
@@ -322,7 +322,7 @@ export function formatTurnTimestamp(
322
322
 
323
323
  const dateStr = formatLocalDate(now, timeZone);
324
324
  const todayParts = localDateParts(now, timeZone);
325
- const dayName = WEEKDAY_SHORT[todayParts.weekday];
325
+ const dayName = WEEKDAY_LONG[todayParts.weekday];
326
326
 
327
327
  const fmt = new Intl.DateTimeFormat("en-US", {
328
328
  timeZone,
@@ -5,10 +5,11 @@ import { getWorkspacePromptPath } from "../util/platform.js";
5
5
  /**
6
6
  * The canned assistant response for the wake-up greeting on a fresh workspace.
7
7
  * Warm, non-presumptuous greeting that communicates "I'm new," "I improve over
8
- * time," "I'm ready to be useful," and "you're in control."
8
+ * time," and invites the user to lead with whatever they want — a task, a
9
+ * question, or getting to know each other.
9
10
  */
10
11
  export const CANNED_FIRST_GREETING =
11
- "Hey, I'm brand new. No name, no memories, nothing yet. Think of me like a new colleague on their first day: I'll get better the more we work together. First things first, let's figure out how we work best. What should I call you?";
12
+ "Hey I'm brand new. No name, no memories, no idea who you are yet. I'll get sharper the more we work together. What can I do for you?";
12
13
 
13
14
  /**
14
15
  * Returns `true` when all of the following are true:
@@ -1,11 +1,5 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
 
3
- import {
4
- type InterfaceId,
5
- parseChannelId,
6
- parseInterfaceId,
7
- supportsHostProxy,
8
- } from "../../channels/types.js";
9
3
  import { getConfig } from "../../config/loader.js";
10
4
  import {
11
5
  createCanonicalGuardianRequest,
@@ -14,28 +8,18 @@ import {
14
8
  import {
15
9
  batchSetDisplayOrders,
16
10
  clearAll,
17
- createConversation,
18
11
  getConversation,
19
12
  updateConversationTitle,
20
13
  } from "../../memory/conversation-crud.js";
21
14
  import { resolveConversationId } from "../../memory/conversation-key-store.js";
22
- import {
23
- GENERATING_TITLE,
24
- queueGenerateConversationTitle,
25
- UNTITLED_FALLBACK,
26
- } from "../../memory/conversation-title-service.js";
27
15
  import * as pendingInteractions from "../../runtime/pending-interactions.js";
28
16
  import { redactSecrets } from "../../security/secret-scanner.js";
29
17
  import { getSubagentManager } from "../../subagent/index.js";
30
18
  import { summarizeToolInput } from "../../tools/tool-input-summary.js";
31
19
  import { truncate } from "../../util/truncate.js";
32
20
  import type { Conversation } from "../conversation.js";
33
- import { HostBashProxy } from "../host-bash-proxy.js";
34
- import { HostCuProxy } from "../host-cu-proxy.js";
35
- import { HostFileProxy } from "../host-file-proxy.js";
36
21
  import type {
37
22
  ConfirmationResponse,
38
- ConversationCreateRequest,
39
23
  ConversationRenameRequest,
40
24
  ConversationSwitchRequest,
41
25
  DeleteQueuedMessage,
@@ -132,6 +116,12 @@ export function makeEventSender(params: {
132
116
  conversationId,
133
117
  kind: "host_bash",
134
118
  });
119
+ } else if (event.type === "host_browser_request") {
120
+ pendingInteractions.register(event.requestId, {
121
+ conversation,
122
+ conversationId,
123
+ kind: "host_browser",
124
+ });
135
125
  } else if (event.type === "host_file_request") {
136
126
  pendingInteractions.register(event.requestId, {
137
127
  conversation,
@@ -227,130 +217,6 @@ export function clearAllConversations(ctx: HandlerContext): number {
227
217
  return cleared;
228
218
  }
229
219
 
230
- export async function handleConversationCreate(
231
- msg: ConversationCreateRequest,
232
- ctx: HandlerContext,
233
- ): Promise<void> {
234
- const conversationType = normalizeConversationType(msg.conversationType);
235
- const title =
236
- msg.title ?? (msg.initialMessage ? GENERATING_TITLE : "New Conversation");
237
- const conversation = createConversation({
238
- title,
239
- conversationType,
240
- });
241
- const conversationObj = await ctx.getOrCreateConversation(conversation.id, {
242
- systemPromptOverride: msg.systemPromptOverride,
243
- maxResponseTokens: msg.maxResponseTokens,
244
- transport: msg.transport,
245
- });
246
-
247
- // Pre-activate skills before sending conversation_info so they're available
248
- // for the initial message processing.
249
- if (msg.preactivatedSkillIds?.length) {
250
- conversationObj.setPreactivatedSkillIds(msg.preactivatedSkillIds);
251
- }
252
-
253
- ctx.send({
254
- type: "conversation_info",
255
- conversationId: conversation.id,
256
- title: conversation.title ?? "New Conversation",
257
- ...(msg.correlationId ? { correlationId: msg.correlationId } : {}),
258
- conversationType: normalizeConversationType(conversation.conversationType),
259
- });
260
-
261
- // Auto-send the initial message if provided, kick-starting the skill.
262
- if (msg.initialMessage) {
263
- // Queue title generation eagerly — some processMessage paths (guardian
264
- // replies, unknown slash commands) bypass the agent loop entirely, so
265
- // we can't rely on the agent loop's early title generation alone.
266
- // The agent loop also queues title generation, but isReplaceableTitle
267
- // prevents double-writes since the first to complete sets a real title.
268
- if (title === GENERATING_TITLE) {
269
- queueGenerateConversationTitle({
270
- conversationId: conversation.id,
271
- context: { origin: "local" },
272
- userMessage: msg.initialMessage,
273
- onTitleUpdated: (newTitle) => {
274
- ctx.send({
275
- type: "conversation_title_updated",
276
- conversationId: conversation.id,
277
- title: newTitle,
278
- });
279
- },
280
- });
281
- }
282
-
283
- const requestId = uuid();
284
- const transportChannel =
285
- parseChannelId(msg.transport?.channelId) ?? "vellum";
286
- const sendEvent = makeEventSender({
287
- ctx,
288
- conversation: conversationObj,
289
- conversationId: conversation.id,
290
- sourceChannel: transportChannel,
291
- });
292
- conversationObj.setTurnChannelContext({
293
- userMessageChannel: transportChannel,
294
- assistantMessageChannel: transportChannel,
295
- });
296
- const transportInterface: InterfaceId =
297
- parseInterfaceId(msg.transport?.interfaceId) ?? "vellum";
298
- conversationObj.setTurnInterfaceContext({
299
- userMessageInterface: transportInterface,
300
- assistantMessageInterface: transportInterface,
301
- });
302
- // Only create the host bash proxy for desktop client interfaces that can
303
- // execute commands on the user's machine. Set before updateClient so
304
- // updateClient's call to hostBashProxy.updateSender targets the new proxy.
305
- if (supportsHostProxy(transportInterface)) {
306
- const proxy = new HostBashProxy(sendEvent, (requestId) => {
307
- pendingInteractions.resolve(requestId);
308
- });
309
- conversationObj.setHostBashProxy(proxy);
310
- const fileProxy = new HostFileProxy(sendEvent, (requestId) => {
311
- pendingInteractions.resolve(requestId);
312
- });
313
- conversationObj.setHostFileProxy(fileProxy);
314
- const cuProxy = new HostCuProxy(sendEvent, (requestId) => {
315
- pendingInteractions.resolve(requestId);
316
- });
317
- conversationObj.setHostCuProxy(cuProxy);
318
- conversationObj.addPreactivatedSkillId("computer-use");
319
- }
320
- conversationObj.updateClient(sendEvent, false);
321
- conversationObj
322
- .processMessage(msg.initialMessage, [], sendEvent, requestId)
323
- .catch((err) => {
324
- const message = err instanceof Error ? err.message : String(err);
325
- log.error(
326
- { err, conversationId: conversation.id },
327
- "Error processing initial message",
328
- );
329
- ctx.send({
330
- type: "error",
331
- message: `Failed to process initial message: ${message}`,
332
- });
333
-
334
- // Replace stuck loading placeholder with a stable fallback title
335
- // if title generation hasn't already completed or been renamed.
336
- try {
337
- const current = getConversation(conversation.id);
338
- if (current && current.title === GENERATING_TITLE) {
339
- const fallback = UNTITLED_FALLBACK;
340
- updateConversationTitle(conversation.id, fallback);
341
- ctx.send({
342
- type: "conversation_title_updated",
343
- conversationId: conversation.id,
344
- title: fallback,
345
- });
346
- }
347
- } catch {
348
- // Best-effort fallback
349
- }
350
- });
351
- }
352
- }
353
-
354
220
  /**
355
221
  * Switch to an existing conversation. Returns conversation info on success,
356
222
  * or throws/returns an error result when the conversation is not found.
@@ -362,6 +228,7 @@ export async function switchConversation(
362
228
  conversationId: string;
363
229
  title: string;
364
230
  conversationType: ReturnType<typeof normalizeConversationType>;
231
+ hostAccess: boolean;
365
232
  } | null> {
366
233
  const conversation = getConversation(conversationId);
367
234
  if (!conversation) {
@@ -384,6 +251,7 @@ export async function switchConversation(
384
251
  conversationId: conversation.id,
385
252
  title: conversation.title ?? "Untitled",
386
253
  conversationType: normalizeConversationType(conversation.conversationType),
254
+ hostAccess: conversation.hostAccess === 1,
387
255
  };
388
256
  }
389
257
 
@@ -405,6 +273,7 @@ export async function handleConversationSwitch(
405
273
  conversationId: result.conversationId,
406
274
  title: result.title,
407
275
  conversationType: result.conversationType,
276
+ hostAccess: result.hostAccess,
408
277
  });
409
278
  }
410
279
 
@@ -110,6 +110,11 @@ export interface ConversationCreateOptions {
110
110
  transport?: ConversationTransportMetadata;
111
111
  assistantId?: string;
112
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;
113
118
  /** Normalized auth context for the conversation. */
114
119
  authContext?: AuthContext;
115
120
  /** Whether this turn can block on interactive approval prompts. */
@@ -344,6 +349,59 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
344
349
  }
345
350
  continue;
346
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
+ }
347
405
  if (block.type === "tool_result") {
348
406
  const toolUseId =
349
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(