@vellumai/assistant 0.6.2 → 0.6.4

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 (895) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +41 -49
  4. package/bunfig.toml +3 -0
  5. package/docs/architecture/memory.md +1 -1
  6. package/docs/backup-troubleshooting.md +52 -0
  7. package/docs/browser-use-architecture-phase2.md +174 -0
  8. package/docs/stt-provider-onboarding.md +120 -0
  9. package/knip.json +12 -2
  10. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  11. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  12. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  13. package/openapi.yaml +1111 -86
  14. package/package.json +40 -42
  15. package/scripts/generate-openapi.ts +0 -2
  16. package/scripts/test.sh +73 -18
  17. package/src/__tests__/acp-session.test.ts +43 -0
  18. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  19. package/src/__tests__/agent-loop.test.ts +123 -0
  20. package/src/__tests__/anthropic-provider.test.ts +263 -10
  21. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  22. package/src/__tests__/app-executors.test.ts +1 -0
  23. package/src/__tests__/app-source-watcher.test.ts +37 -11
  24. package/src/__tests__/approval-routes-http.test.ts +178 -1
  25. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  26. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +240 -94
  28. package/src/__tests__/browser-manager.test.ts +40 -27
  29. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  30. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  31. package/src/__tests__/btw-routes.test.ts +7 -0
  32. package/src/__tests__/call-controller.test.ts +581 -20
  33. package/src/__tests__/catalog-files.test.ts +1000 -0
  34. package/src/__tests__/channel-approvals.test.ts +53 -0
  35. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  36. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  37. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  38. package/src/__tests__/checker.test.ts +157 -10
  39. package/src/__tests__/clawhub-files.test.ts +347 -0
  40. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  41. package/src/__tests__/config-analysis.test.ts +100 -0
  42. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  43. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  44. package/src/__tests__/config-schema.test.ts +1248 -224
  45. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  46. package/src/__tests__/config-watcher.test.ts +43 -8
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  48. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  49. package/src/__tests__/contacts-write.test.ts +197 -0
  50. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  51. package/src/__tests__/context-window-manager.test.ts +88 -0
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  54. package/src/__tests__/conversation-agent-loop.test.ts +99 -3
  55. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  56. package/src/__tests__/conversation-attachments.test.ts +80 -4
  57. package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
  58. package/src/__tests__/conversation-error.test.ts +70 -0
  59. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  60. package/src/__tests__/conversation-history-web-search.test.ts +12 -4
  61. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  62. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  63. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  64. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  65. package/src/__tests__/conversation-list-source.test.ts +145 -0
  66. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  67. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  68. package/src/__tests__/conversation-queue.test.ts +946 -62
  69. package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
  73. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  74. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  76. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  77. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  78. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  79. package/src/__tests__/conversation-store.test.ts +195 -0
  80. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  81. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  82. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  83. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  84. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  85. package/src/__tests__/credential-health-service.test.ts +352 -0
  86. package/src/__tests__/credential-security-invariants.test.ts +6 -3
  87. package/src/__tests__/credential-vault-unit.test.ts +383 -7
  88. package/src/__tests__/credential-vault.test.ts +152 -13
  89. package/src/__tests__/credentials-cli.test.ts +42 -18
  90. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  91. package/src/__tests__/date-context.test.ts +4 -4
  92. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  93. package/src/__tests__/device-id.test.ts +112 -0
  94. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  95. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  96. package/src/__tests__/email-html-renderer.test.ts +71 -0
  97. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  98. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  99. package/src/__tests__/emit-event-signal.test.ts +71 -0
  100. package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
  101. package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
  102. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -0
  104. package/src/__tests__/gemini-provider.test.ts +66 -2
  105. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  106. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  107. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  108. package/src/__tests__/gmail-preferences.test.ts +117 -0
  109. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  110. package/src/__tests__/headless-browser-interactions.test.ts +738 -359
  111. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  112. package/src/__tests__/headless-browser-navigate.test.ts +528 -49
  113. package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
  114. package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
  115. package/src/__tests__/heartbeat-service.test.ts +70 -17
  116. package/src/__tests__/home-state-routes.test.ts +162 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +145 -1
  118. package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
  119. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  120. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  121. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  122. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  123. package/src/__tests__/host-browser-routes.test.ts +198 -0
  124. package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
  125. package/src/__tests__/host-cu-proxy.test.ts +166 -1
  126. package/src/__tests__/host-file-proxy.test.ts +185 -1
  127. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  128. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  129. package/src/__tests__/host-shell-tool.test.ts +1 -11
  130. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  131. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  132. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  133. package/src/__tests__/integration-status.test.ts +6 -7
  134. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  135. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  136. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  137. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  138. package/src/__tests__/llm-usage-store.test.ts +363 -0
  139. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  140. package/src/__tests__/mcp-health-check.test.ts +10 -3
  141. package/src/__tests__/media-stream-output.test.ts +555 -0
  142. package/src/__tests__/media-stream-parser.test.ts +374 -0
  143. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  144. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  145. package/src/__tests__/media-turn-detector.test.ts +440 -0
  146. package/src/__tests__/message-queue.test.ts +125 -0
  147. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  148. package/src/__tests__/migration-export-http.test.ts +67 -8
  149. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  150. package/src/__tests__/migration-import-commit-http.test.ts +109 -7
  151. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  152. package/src/__tests__/migration-validate-http.test.ts +3 -3
  153. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  154. package/src/__tests__/model-intents.test.ts +2 -2
  155. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  156. package/src/__tests__/oauth-apps-routes.test.ts +18 -12
  157. package/src/__tests__/oauth-cli.test.ts +709 -60
  158. package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  160. package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
  161. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  162. package/src/__tests__/oauth-providers-routes.test.ts +52 -14
  163. package/src/__tests__/oauth-store.test.ts +1465 -176
  164. package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
  165. package/src/__tests__/onboarding-template-contract.test.ts +81 -70
  166. package/src/__tests__/openai-provider.test.ts +178 -2
  167. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  168. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  169. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  170. package/src/__tests__/outlook-categories.test.ts +1 -1
  171. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  172. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  173. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  174. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  175. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  176. package/src/__tests__/outlook-trash.test.ts +1 -1
  177. package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
  178. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  179. package/src/__tests__/permission-mode.test.ts +28 -56
  180. package/src/__tests__/persona-resolver.test.ts +251 -0
  181. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  182. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  183. package/src/__tests__/platform.test.ts +92 -1
  184. package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
  185. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  186. package/src/__tests__/pricing.test.ts +174 -0
  187. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  188. package/src/__tests__/qdrant-manager.test.ts +29 -8
  189. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  190. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  191. package/src/__tests__/relay-server.test.ts +423 -5
  192. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  193. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  194. package/src/__tests__/schedule-routes.test.ts +162 -0
  195. package/src/__tests__/search-skills-unified.test.ts +118 -0
  196. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  197. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  198. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  199. package/src/__tests__/secure-keys.test.ts +107 -0
  200. package/src/__tests__/send-endpoint-busy.test.ts +8 -1
  201. package/src/__tests__/sequence-store.test.ts +1 -1
  202. package/src/__tests__/server-history-render.test.ts +49 -0
  203. package/src/__tests__/set-permission-mode.test.ts +13 -250
  204. package/src/__tests__/settings-routes.test.ts +201 -0
  205. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
  208. package/src/__tests__/skills.test.ts +5 -2
  209. package/src/__tests__/skillssh-files.test.ts +446 -0
  210. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  211. package/src/__tests__/slack-channel-config.test.ts +576 -16
  212. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  213. package/src/__tests__/stt-stream-session.test.ts +535 -0
  214. package/src/__tests__/subagent-detail.test.ts +44 -2
  215. package/src/__tests__/subagent-disposal.test.ts +1 -0
  216. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  217. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  218. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  219. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  220. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  221. package/src/__tests__/subagent-tools.test.ts +1 -0
  222. package/src/__tests__/subagent-types.test.ts +1 -0
  223. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  224. package/src/__tests__/system-prompt.test.ts +184 -27
  225. package/src/__tests__/task-scheduler.test.ts +32 -6
  226. package/src/__tests__/telegram-config.test.ts +10 -13
  227. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  228. package/src/__tests__/terminal-tools.test.ts +25 -5
  229. package/src/__tests__/test-preload.ts +18 -0
  230. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  231. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  232. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  233. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  234. package/src/__tests__/tool-executor.test.ts +33 -24
  235. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  236. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  237. package/src/__tests__/top-level-renderer.test.ts +73 -1
  238. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  239. package/src/__tests__/trust-store.test.ts +7 -1
  240. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  241. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  242. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  243. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  244. package/src/__tests__/twilio-routes.test.ts +376 -0
  245. package/src/__tests__/unicode.test.ts +293 -0
  246. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  247. package/src/__tests__/update-bulletin.test.ts +206 -5
  248. package/src/__tests__/usage-routes.test.ts +25 -4
  249. package/src/__tests__/user-reference.test.ts +46 -61
  250. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  251. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  252. package/src/__tests__/voice-config-update.test.ts +403 -0
  253. package/src/__tests__/voice-quality.test.ts +434 -19
  254. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  255. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  256. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  257. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  258. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  259. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  260. package/src/__tests__/workspace-policy.test.ts +2 -0
  261. package/src/acp/client-handler.ts +30 -4
  262. package/src/agent/image-optimize.ts +24 -12
  263. package/src/agent/loop.ts +55 -9
  264. package/src/approvals/guardian-request-resolvers.ts +21 -15
  265. package/src/backup/__tests__/backup-key.test.ts +152 -0
  266. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  267. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  268. package/src/backup/__tests__/local-writer.test.ts +218 -0
  269. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  270. package/src/backup/__tests__/paths.test.ts +300 -0
  271. package/src/backup/__tests__/restore.test.ts +498 -0
  272. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  273. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  274. package/src/backup/backup-key.ts +137 -0
  275. package/src/backup/backup-worker.ts +459 -0
  276. package/src/backup/list-snapshots.ts +147 -0
  277. package/src/backup/local-writer.ts +133 -0
  278. package/src/backup/offsite-writer.ts +222 -0
  279. package/src/backup/paths.ts +226 -0
  280. package/src/backup/restore.ts +322 -0
  281. package/src/backup/snapshot-lock.ts +431 -0
  282. package/src/backup/stream-crypt.ts +263 -0
  283. package/src/browser-session/__tests__/manager.test.ts +297 -0
  284. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  285. package/src/browser-session/backends/extension.ts +26 -0
  286. package/src/browser-session/backends/local.ts +24 -0
  287. package/src/browser-session/events.ts +164 -0
  288. package/src/browser-session/index.ts +27 -0
  289. package/src/browser-session/manager.ts +159 -0
  290. package/src/browser-session/types.ts +28 -0
  291. package/src/bundler/package-resolver.ts +4 -0
  292. package/src/calls/audio-store.ts +11 -5
  293. package/src/calls/call-controller.ts +226 -71
  294. package/src/calls/call-domain.ts +9 -0
  295. package/src/calls/call-speech-output.ts +190 -0
  296. package/src/calls/call-transport.ts +77 -0
  297. package/src/calls/media-stream-audio-transcode.ts +173 -0
  298. package/src/calls/media-stream-output.ts +660 -0
  299. package/src/calls/media-stream-parser.ts +300 -0
  300. package/src/calls/media-stream-protocol.ts +166 -0
  301. package/src/calls/media-stream-server.ts +592 -0
  302. package/src/calls/media-stream-stt-session.ts +460 -0
  303. package/src/calls/media-turn-detector.ts +230 -0
  304. package/src/calls/relay-server.ts +90 -75
  305. package/src/calls/resolve-call-tts-provider.ts +136 -0
  306. package/src/calls/telephony-stt-routing.ts +145 -0
  307. package/src/calls/tts-call-strategy.ts +161 -0
  308. package/src/calls/tts-text-sanitizer.ts +32 -16
  309. package/src/calls/twilio-routes.ts +281 -17
  310. package/src/calls/voice-quality.ts +78 -35
  311. package/src/calls/voice-session-bridge.ts +8 -1
  312. package/src/channels/__tests__/types.test.ts +134 -0
  313. package/src/channels/types.ts +69 -3
  314. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  315. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  316. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  317. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  318. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  319. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  320. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  321. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  322. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  323. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  324. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  325. package/src/cli/commands/backup.ts +993 -0
  326. package/src/cli/commands/conversations.ts +77 -0
  327. package/src/cli/commands/credentials.ts +3 -4
  328. package/src/cli/commands/domain.ts +210 -0
  329. package/src/cli/commands/email.ts +273 -16
  330. package/src/cli/commands/mcp.ts +16 -4
  331. package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
  332. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  333. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  334. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  335. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
  336. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
  337. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
  338. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  339. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  340. package/src/cli/commands/oauth/apps.ts +7 -4
  341. package/src/cli/commands/oauth/connect.ts +6 -3
  342. package/src/cli/commands/oauth/disconnect.ts +1 -1
  343. package/src/cli/commands/oauth/mode.ts +12 -3
  344. package/src/cli/commands/oauth/providers.ts +215 -36
  345. package/src/cli/commands/oauth/shared.ts +7 -6
  346. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
  347. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  348. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  349. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  350. package/src/cli/commands/platform/index.ts +107 -10
  351. package/src/cli/commands/usage.ts +10 -9
  352. package/src/cli/lib/daemon-credential-client.ts +4 -0
  353. package/src/cli/program.ts +30 -4
  354. package/src/config/__tests__/backup-schema.test.ts +134 -0
  355. package/src/config/assistant-feature-flags.ts +61 -62
  356. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  357. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
  358. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  359. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  360. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  361. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  362. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  363. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  364. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  365. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  366. package/src/config/bundled-skills/contacts/SKILL.md +5 -2
  367. package/src/config/bundled-skills/document/SKILL.md +4 -0
  368. package/src/config/bundled-skills/gmail/SKILL.md +54 -8
  369. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  370. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  371. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  372. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  373. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  374. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  375. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  376. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  377. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  378. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  379. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  380. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  381. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  382. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  383. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  384. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  385. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  386. package/src/config/bundled-skills/outlook/SKILL.md +9 -2
  387. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  388. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  389. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  390. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  391. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  392. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  393. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  394. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  395. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  396. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  397. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  398. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  399. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  400. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  401. package/src/config/bundled-tool-registry.ts +8 -0
  402. package/src/config/env-registry.ts +38 -0
  403. package/src/config/env.ts +49 -4
  404. package/src/config/feature-flag-registry.json +85 -14
  405. package/src/config/loader.ts +82 -13
  406. package/src/config/sanitize-for-transfer.ts +47 -0
  407. package/src/config/schema.ts +81 -15
  408. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  409. package/src/config/schemas/analysis.ts +51 -0
  410. package/src/config/schemas/backup.ts +72 -0
  411. package/src/config/schemas/calls.ts +1 -26
  412. package/src/config/schemas/elevenlabs.ts +0 -59
  413. package/src/config/schemas/filing.ts +47 -7
  414. package/src/config/schemas/heartbeat.ts +27 -5
  415. package/src/config/schemas/host-browser.ts +112 -0
  416. package/src/config/schemas/inference.ts +1 -1
  417. package/src/config/schemas/memory-lifecycle.ts +14 -2
  418. package/src/config/schemas/memory-retrieval.ts +103 -0
  419. package/src/config/schemas/security.ts +0 -6
  420. package/src/config/schemas/services.ts +52 -0
  421. package/src/config/schemas/stt.ts +59 -0
  422. package/src/config/schemas/tts.ts +230 -0
  423. package/src/config/schemas/updates.ts +14 -0
  424. package/src/config/skills.ts +4 -0
  425. package/src/config/types.ts +4 -1
  426. package/src/contacts/contact-store.ts +56 -11
  427. package/src/contacts/contacts-write.ts +38 -1
  428. package/src/context/post-turn-tool-result-truncation.ts +177 -0
  429. package/src/context/tool-result-truncation.ts +2 -1
  430. package/src/context/window-manager.ts +61 -10
  431. package/src/credential-execution/approval-bridge.ts +49 -15
  432. package/src/credential-execution/executable-discovery.ts +12 -2
  433. package/src/credential-execution/process-manager.ts +33 -2
  434. package/src/credential-health/credential-health-service.ts +366 -0
  435. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  436. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  437. package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
  438. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  439. package/src/daemon/app-source-watcher.ts +35 -0
  440. package/src/daemon/config-watcher.ts +99 -5
  441. package/src/daemon/context-overflow-approval.ts +5 -0
  442. package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
  443. package/src/daemon/conversation-agent-loop.ts +153 -42
  444. package/src/daemon/conversation-attachments.ts +40 -0
  445. package/src/daemon/conversation-error.ts +11 -0
  446. package/src/daemon/conversation-history.ts +40 -6
  447. package/src/daemon/conversation-launch.ts +220 -0
  448. package/src/daemon/conversation-lifecycle.ts +59 -9
  449. package/src/daemon/conversation-messaging.ts +37 -3
  450. package/src/daemon/conversation-notifiers.ts +5 -0
  451. package/src/daemon/conversation-process.ts +622 -13
  452. package/src/daemon/conversation-queue-manager.ts +24 -0
  453. package/src/daemon/conversation-runtime-assembly.ts +128 -36
  454. package/src/daemon/conversation-slash.ts +36 -0
  455. package/src/daemon/conversation-surfaces.ts +131 -40
  456. package/src/daemon/conversation-tool-setup.ts +99 -8
  457. package/src/daemon/conversation-usage.ts +7 -4
  458. package/src/daemon/conversation-workspace.ts +12 -0
  459. package/src/daemon/conversation.ts +292 -16
  460. package/src/daemon/date-context.ts +10 -10
  461. package/src/daemon/first-greeting.ts +3 -2
  462. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  463. package/src/daemon/handlers/conversations.ts +13 -141
  464. package/src/daemon/handlers/shared.ts +80 -0
  465. package/src/daemon/handlers/skills.ts +483 -44
  466. package/src/daemon/host-bash-proxy.ts +48 -13
  467. package/src/daemon/host-browser-proxy.ts +192 -0
  468. package/src/daemon/host-cu-proxy.ts +36 -11
  469. package/src/daemon/host-file-proxy.ts +57 -9
  470. package/src/daemon/lifecycle.ts +179 -28
  471. package/src/daemon/message-protocol.ts +13 -0
  472. package/src/daemon/message-types/conversations.ts +89 -14
  473. package/src/daemon/message-types/home.ts +40 -0
  474. package/src/daemon/message-types/host-browser.ts +100 -0
  475. package/src/daemon/message-types/meet.ts +143 -0
  476. package/src/daemon/message-types/messages.ts +19 -5
  477. package/src/daemon/message-types/schedules.ts +34 -2
  478. package/src/daemon/message-types/skills.ts +26 -0
  479. package/src/daemon/message-types/subagents.ts +2 -0
  480. package/src/daemon/message-types/surfaces.ts +2 -0
  481. package/src/daemon/server.ts +439 -14
  482. package/src/daemon/shutdown-handlers.ts +32 -4
  483. package/src/daemon/shutdown-registry.ts +40 -0
  484. package/src/daemon/tool-side-effects.ts +15 -0
  485. package/src/daemon/transport-hints.ts +5 -24
  486. package/src/email/html-renderer.ts +76 -0
  487. package/src/heartbeat/heartbeat-service.ts +93 -7
  488. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  489. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  490. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  491. package/src/home/__tests__/feed-types.test.ts +275 -0
  492. package/src/home/__tests__/feed-writer.test.ts +688 -0
  493. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  494. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  495. package/src/home/__tests__/progress-formula.test.ts +213 -0
  496. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  497. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  498. package/src/home/assistant-feed-authoring.ts +124 -0
  499. package/src/home/emit-feed-event.ts +158 -0
  500. package/src/home/feed-scheduler.ts +247 -0
  501. package/src/home/feed-types.ts +181 -0
  502. package/src/home/feed-writer.ts +469 -0
  503. package/src/home/platform-gmail-digest.ts +163 -0
  504. package/src/home/progress-formula.ts +86 -0
  505. package/src/home/relationship-state-writer.ts +824 -0
  506. package/src/home/relationship-state.ts +143 -0
  507. package/src/home/rollup-producer.ts +384 -0
  508. package/src/hooks/runner.ts +7 -0
  509. package/src/inbound/platform-callback-registration.ts +30 -20
  510. package/src/inbound/public-ingress-urls.ts +12 -0
  511. package/src/instrument.ts +1 -1
  512. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  513. package/src/ipc/cli-client.ts +151 -0
  514. package/src/ipc/cli-server.ts +234 -0
  515. package/src/ipc/gateway-client.ts +180 -0
  516. package/src/ipc/routes/index.ts +5 -0
  517. package/src/ipc/routes/wake-conversation.ts +19 -0
  518. package/src/mcp/client.ts +59 -24
  519. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  520. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  521. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  522. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  523. package/src/memory/app-store.ts +31 -1
  524. package/src/memory/attachments-store.ts +70 -0
  525. package/src/memory/auto-analysis-enqueue.ts +127 -0
  526. package/src/memory/auto-analysis-guard.ts +27 -0
  527. package/src/memory/cleanup-schedule-state.ts +37 -0
  528. package/src/memory/conversation-analyze-job.ts +73 -0
  529. package/src/memory/conversation-crud.ts +122 -0
  530. package/src/memory/conversation-disk-view.ts +7 -0
  531. package/src/memory/conversation-group-migration.ts +34 -2
  532. package/src/memory/conversation-queries.ts +6 -5
  533. package/src/memory/conversation-starters-cadence.ts +76 -0
  534. package/src/memory/conversation-title-service.ts +5 -2
  535. package/src/memory/db-init.ts +18 -0
  536. package/src/memory/db-maintenance.ts +108 -0
  537. package/src/memory/db.ts +1 -0
  538. package/src/memory/embedding-backend.test.ts +75 -0
  539. package/src/memory/embedding-backend.ts +131 -5
  540. package/src/memory/embedding-gemini.test.ts +54 -0
  541. package/src/memory/embedding-gemini.ts +20 -9
  542. package/src/memory/embedding-local.ts +176 -17
  543. package/src/memory/graph/consolidation.ts +10 -23
  544. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  545. package/src/memory/graph/extraction-job.ts +15 -0
  546. package/src/memory/graph/extraction.test.ts +23 -0
  547. package/src/memory/graph/extraction.ts +8 -0
  548. package/src/memory/graph/retriever.ts +67 -40
  549. package/src/memory/graph/scoring.test.ts +186 -0
  550. package/src/memory/graph/scoring.ts +31 -1
  551. package/src/memory/graph/store.test.ts +7 -3
  552. package/src/memory/graph/store.ts +47 -12
  553. package/src/memory/graph/tools.ts +1 -1
  554. package/src/memory/group-crud.ts +6 -1
  555. package/src/memory/indexer.ts +95 -16
  556. package/src/memory/job-handlers/cleanup.ts +11 -8
  557. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  558. package/src/memory/jobs-store.ts +64 -4
  559. package/src/memory/jobs-worker.ts +22 -9
  560. package/src/memory/llm-usage-store.ts +137 -60
  561. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  562. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  563. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  564. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  565. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  566. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  567. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  568. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  569. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  570. package/src/memory/migrations/index.ts +12 -0
  571. package/src/memory/migrations/registry.ts +16 -0
  572. package/src/memory/qdrant-manager.ts +43 -16
  573. package/src/memory/schema/conversations.ts +3 -0
  574. package/src/memory/schema/oauth.ts +21 -13
  575. package/src/memory/usage-buckets.ts +396 -0
  576. package/src/messaging/providers/gmail/client.ts +57 -6
  577. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  578. package/src/messaging/providers/slack/adapter.ts +143 -38
  579. package/src/messaging/providers/slack/client.ts +16 -0
  580. package/src/messaging/providers/slack/types.ts +4 -0
  581. package/src/notifications/decision-engine.ts +3 -3
  582. package/src/notifications/signal.ts +5 -0
  583. package/src/oauth/AGENTS.md +76 -0
  584. package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
  585. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  586. package/src/oauth/byo-connection.test.ts +26 -9
  587. package/src/oauth/byo-connection.ts +10 -8
  588. package/src/oauth/connect-orchestrator.ts +25 -21
  589. package/src/oauth/connect-types.ts +3 -3
  590. package/src/oauth/connection-resolver.test.ts +17 -4
  591. package/src/oauth/connection-resolver.ts +22 -18
  592. package/src/oauth/connection.ts +3 -1
  593. package/src/oauth/manual-token-connection.ts +13 -13
  594. package/src/oauth/oauth-store.ts +223 -100
  595. package/src/oauth/platform-connection.test.ts +101 -3
  596. package/src/oauth/platform-connection.ts +56 -35
  597. package/src/oauth/provider-serializer.ts +31 -5
  598. package/src/oauth/revoke.ts +76 -0
  599. package/src/oauth/seed-providers.ts +133 -87
  600. package/src/oauth/token-persistence.ts +1 -1
  601. package/src/permissions/checker.ts +16 -6
  602. package/src/permissions/defaults.ts +49 -1
  603. package/src/permissions/permission-mode.ts +4 -11
  604. package/src/permissions/prompter.ts +13 -1
  605. package/src/permissions/trust-store.ts +3 -3
  606. package/src/permissions/v2-consent-policy.ts +87 -0
  607. package/src/permissions/workspace-policy.ts +3 -0
  608. package/src/platform/client.test.ts +10 -0
  609. package/src/platform/sync-identity.ts +129 -0
  610. package/src/prompts/persona-resolver.ts +126 -2
  611. package/src/prompts/system-prompt.ts +76 -38
  612. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  613. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  614. package/src/prompts/templates/SOUL.md +3 -1
  615. package/src/prompts/templates/UPDATES.md +12 -0
  616. package/src/prompts/templates/channels/slack.md +20 -0
  617. package/src/prompts/update-bulletin-format.ts +26 -9
  618. package/src/prompts/update-bulletin.ts +34 -23
  619. package/src/prompts/user-reference.ts +20 -17
  620. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  621. package/src/providers/anthropic/client.ts +157 -60
  622. package/src/providers/fireworks/client.ts +2 -2
  623. package/src/providers/gemini/client.ts +9 -1
  624. package/src/providers/model-catalog.ts +6 -0
  625. package/src/providers/model-intents.ts +4 -4
  626. package/src/providers/ollama/client.ts +2 -2
  627. package/src/providers/openai/chat-completions-provider.ts +474 -0
  628. package/src/providers/openai/client.ts +25 -440
  629. package/src/providers/openai/responses-provider.ts +502 -0
  630. package/src/providers/openrouter/client.ts +101 -4
  631. package/src/providers/provider-secret-catalog.ts +139 -0
  632. package/src/providers/registry.ts +2 -2
  633. package/src/providers/retry.ts +14 -3
  634. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  635. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  636. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  637. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  638. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  639. package/src/providers/speech-to-text/deepgram.ts +115 -0
  640. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  641. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  642. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  643. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  644. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  645. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  646. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  647. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  648. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  649. package/src/providers/speech-to-text/resolve.ts +386 -6
  650. package/src/providers/types.ts +10 -1
  651. package/src/runtime/AGENTS.md +65 -0
  652. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  653. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  654. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  655. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  656. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  657. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  658. package/src/runtime/agent-wake.ts +512 -0
  659. package/src/runtime/assistant-event-hub.ts +2 -2
  660. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  661. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  662. package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
  663. package/src/runtime/auth/middleware.ts +98 -0
  664. package/src/runtime/auth/route-policy.ts +33 -9
  665. package/src/runtime/auth/token-service.ts +56 -1
  666. package/src/runtime/btw-sidechain.ts +2 -0
  667. package/src/runtime/capability-tokens.ts +414 -0
  668. package/src/runtime/channel-approvals.ts +18 -5
  669. package/src/runtime/channel-invite-transport.ts +1 -1
  670. package/src/runtime/channel-invite-transports/email.ts +14 -6
  671. package/src/runtime/channel-readiness-service.ts +12 -22
  672. package/src/runtime/chrome-extension-registry.ts +368 -0
  673. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  674. package/src/runtime/guardian-decision-types.ts +7 -0
  675. package/src/runtime/http-server.ts +815 -75
  676. package/src/runtime/http-types.ts +6 -2
  677. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  678. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  679. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
  680. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  681. package/src/runtime/migrations/migration-transport.ts +7 -0
  682. package/src/runtime/migrations/migration-wizard.ts +23 -2
  683. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  684. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  685. package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
  686. package/src/runtime/migrations/vbundle-importer.ts +89 -5
  687. package/src/runtime/pending-interactions.ts +18 -13
  688. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  689. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  690. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  691. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  692. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  693. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  694. package/src/runtime/routes/app-management-routes.ts +12 -18
  695. package/src/runtime/routes/approval-routes.ts +90 -16
  696. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  697. package/src/runtime/routes/attachment-routes.ts +216 -17
  698. package/src/runtime/routes/backup-routes.ts +519 -0
  699. package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
  700. package/src/runtime/routes/btw-routes.ts +8 -6
  701. package/src/runtime/routes/contact-routes.test.ts +298 -0
  702. package/src/runtime/routes/contact-routes.ts +132 -5
  703. package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
  704. package/src/runtime/routes/conversation-management-routes.ts +223 -0
  705. package/src/runtime/routes/conversation-routes.ts +598 -103
  706. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  707. package/src/runtime/routes/filing-routes.ts +93 -0
  708. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  709. package/src/runtime/routes/home-feed-routes.ts +334 -0
  710. package/src/runtime/routes/home-state-routes.ts +138 -0
  711. package/src/runtime/routes/host-browser-routes.ts +268 -0
  712. package/src/runtime/routes/host-file-routes.ts +9 -1
  713. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  714. package/src/runtime/routes/identity-routes.ts +262 -33
  715. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  716. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  717. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  718. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  719. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  720. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  721. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  722. package/src/runtime/routes/log-export-routes.ts +42 -22
  723. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  724. package/src/runtime/routes/memory-item-routes.ts +1 -7
  725. package/src/runtime/routes/migration-routes.ts +122 -2
  726. package/src/runtime/routes/oauth-apps.ts +15 -17
  727. package/src/runtime/routes/oauth-providers.ts +4 -0
  728. package/src/runtime/routes/schedule-routes.ts +24 -11
  729. package/src/runtime/routes/settings-routes.ts +31 -102
  730. package/src/runtime/routes/skills-routes.ts +128 -9
  731. package/src/runtime/routes/stt-routes.ts +233 -0
  732. package/src/runtime/routes/subagents-routes.ts +14 -10
  733. package/src/runtime/routes/surface-action-routes.ts +41 -2
  734. package/src/runtime/routes/tts-routes.ts +108 -24
  735. package/src/runtime/routes/usage-routes.ts +38 -9
  736. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  737. package/src/runtime/routes/user-routes.ts +13 -1
  738. package/src/runtime/routes/work-items-routes.ts +8 -1
  739. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  740. package/src/runtime/routes/workspace-routes.ts +8 -1
  741. package/src/runtime/routes/workspace-utils.ts +2 -0
  742. package/src/runtime/runtime-mode.ts +33 -0
  743. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  744. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  745. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  746. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  747. package/src/runtime/services/analyze-conversation.ts +344 -0
  748. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  749. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  750. package/src/runtime/skill-route-registry.ts +49 -0
  751. package/src/runtime/slack-block-formatting.ts +437 -10
  752. package/src/schedule/scheduler.ts +57 -5
  753. package/src/security/ces-credential-client.ts +20 -0
  754. package/src/security/ces-rpc-credential-backend.ts +17 -0
  755. package/src/security/credential-backend.ts +5 -0
  756. package/src/security/oauth2.ts +68 -29
  757. package/src/security/secure-keys.ts +143 -27
  758. package/src/security/token-manager.ts +31 -10
  759. package/src/sequence/engine.ts +23 -0
  760. package/src/sequence/types.ts +1 -1
  761. package/src/skills/catalog-files.ts +554 -0
  762. package/src/skills/category-inference.ts +122 -0
  763. package/src/skills/clawhub-files.ts +213 -0
  764. package/src/skills/clawhub.ts +84 -23
  765. package/src/skills/skill-file-provider.ts +40 -0
  766. package/src/skills/skillssh-files.ts +395 -0
  767. package/src/skills/skillssh-registry.ts +4 -4
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  769. package/src/stt/__tests__/types.test.ts +89 -0
  770. package/src/stt/daemon-batch-transcriber.ts +195 -0
  771. package/src/stt/stt-stream-session.ts +499 -0
  772. package/src/stt/types.ts +330 -0
  773. package/src/stt/wav-encoder.test.ts +373 -0
  774. package/src/stt/wav-encoder.ts +175 -0
  775. package/src/subagent/manager.ts +169 -40
  776. package/src/subagent/types.ts +19 -0
  777. package/src/tools/apps/executors.ts +11 -2
  778. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  779. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  780. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  781. package/src/tools/browser/auth-detector.ts +43 -12
  782. package/src/tools/browser/browser-execution.ts +1787 -342
  783. package/src/tools/browser/browser-manager.ts +81 -12
  784. package/src/tools/browser/browser-mode-constants.ts +12 -0
  785. package/src/tools/browser/browser-mode.ts +92 -0
  786. package/src/tools/browser/browser-status-constants.ts +33 -0
  787. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  788. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  789. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
  790. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
  791. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
  792. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  793. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  794. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  795. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  796. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  797. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  798. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
  799. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  800. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
  801. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  802. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
  803. package/src/tools/browser/cdp-client/errors.ts +49 -0
  804. package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
  805. package/src/tools/browser/cdp-client/factory.ts +914 -0
  806. package/src/tools/browser/cdp-client/index.ts +28 -0
  807. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  808. package/src/tools/browser/cdp-client/types.ts +120 -0
  809. package/src/tools/credentials/vault.ts +35 -6
  810. package/src/tools/filesystem/edit.ts +1 -1
  811. package/src/tools/filesystem/list.ts +1 -1
  812. package/src/tools/filesystem/read.ts +1 -1
  813. package/src/tools/filesystem/write.ts +2 -1
  814. package/src/tools/host-filesystem/edit.ts +1 -1
  815. package/src/tools/host-filesystem/read.ts +12 -15
  816. package/src/tools/host-filesystem/write.ts +1 -1
  817. package/src/tools/host-terminal/host-shell.ts +21 -16
  818. package/src/tools/network/web-fetch.ts +5 -2
  819. package/src/tools/network/web-search.ts +5 -2
  820. package/src/tools/permission-checker.ts +77 -82
  821. package/src/tools/registry.ts +0 -2
  822. package/src/tools/secret-detection-handler.ts +34 -0
  823. package/src/tools/shared/filesystem/image-read.ts +61 -40
  824. package/src/tools/shared/shell-output.ts +3 -1
  825. package/src/tools/side-effects.ts +2 -0
  826. package/src/tools/skills/sandbox-runner.ts +3 -2
  827. package/src/tools/subagent/spawn.ts +47 -3
  828. package/src/tools/subagent/status.ts +2 -0
  829. package/src/tools/system/register.ts +2 -16
  830. package/src/tools/terminal/safe-env.ts +15 -0
  831. package/src/tools/terminal/shell.ts +36 -20
  832. package/src/tools/tool-approval-handler.ts +48 -2
  833. package/src/tools/tool-manifest.ts +21 -0
  834. package/src/tools/types.ts +19 -0
  835. package/src/tools/ui-surface/definitions.ts +6 -1
  836. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  837. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  838. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  839. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  840. package/src/tts/provider-catalog.ts +201 -0
  841. package/src/tts/provider-registry.ts +73 -0
  842. package/src/tts/providers/deepgram-provider.ts +219 -0
  843. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  844. package/src/tts/providers/fish-audio-provider.ts +183 -0
  845. package/src/tts/providers/index.ts +42 -0
  846. package/src/tts/providers/register-builtins.ts +130 -0
  847. package/src/tts/synthesize-text.ts +110 -0
  848. package/src/tts/tts-config-resolver.ts +78 -0
  849. package/src/tts/types.ts +153 -0
  850. package/src/types/onboarding-context.ts +7 -0
  851. package/src/util/abort-reasons.ts +58 -0
  852. package/src/util/device-id.ts +32 -16
  853. package/src/util/errors.ts +9 -1
  854. package/src/util/platform.ts +63 -24
  855. package/src/util/pricing.ts +66 -3
  856. package/src/util/spawn.ts +1 -1
  857. package/src/util/truncate.ts +4 -2
  858. package/src/util/unicode.ts +201 -0
  859. package/src/version.ts +19 -24
  860. package/src/watcher/engine.ts +23 -0
  861. package/src/watcher/watcher-store.ts +31 -0
  862. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  863. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  864. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  865. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  866. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  867. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  868. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  869. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  870. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  871. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  872. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  873. package/src/workspace/migrations/registry.ts +16 -0
  874. package/src/workspace/top-level-renderer.ts +31 -1
  875. package/src/workspace/turn-commit.ts +31 -0
  876. package/src/__tests__/chrome-cdp.test.ts +0 -419
  877. package/src/__tests__/email-cli.test.ts +0 -297
  878. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  879. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  880. package/src/__tests__/permission-mode-store.test.ts +0 -277
  881. package/src/browser-extension-relay/protocol.ts +0 -63
  882. package/src/browser-extension-relay/server.ts +0 -203
  883. package/src/cli/commands/browser-relay.ts +0 -536
  884. package/src/config/schemas/sandbox.ts +0 -14
  885. package/src/email/guardrails.ts +0 -221
  886. package/src/email/provider.ts +0 -117
  887. package/src/email/providers/agentmail.ts +0 -361
  888. package/src/email/providers/index.ts +0 -65
  889. package/src/email/service.ts +0 -384
  890. package/src/email/types.ts +0 -126
  891. package/src/permissions/permission-mode-store.ts +0 -180
  892. package/src/prompts/templates/USER.md +0 -13
  893. package/src/providers/speech-to-text/types.ts +0 -17
  894. package/src/tools/browser/chrome-cdp.ts +0 -239
  895. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,1000 @@
1
+ /**
2
+ * Unit tests for `skills/catalog-files.ts` — preview listings and single-file
3
+ * content for catalog skills (installed or not).
4
+ *
5
+ * Covers:
6
+ * - sanitizeRelativePath (accepts safe, rejects traversal / absolute / null)
7
+ * - readCatalogSkillFiles dev-mode (reads from a temp fake repo skills dir,
8
+ * does NOT touch fetch)
9
+ * - readCatalogSkillFiles platform-mode (stubbed fetch with success + 500
10
+ * + 404 + network-error)
11
+ * - readCatalogSkillFileContent dev-mode (text, traversal rejection,
12
+ * binary, oversized)
13
+ * - readCatalogSkillFileContent platform-mode (text, binary, oversized,
14
+ * 404, pre-fetch traversal rejection)
15
+ * - catalog-miss short-circuit (no fetch call when id unknown)
16
+ */
17
+
18
+ import {
19
+ mkdirSync,
20
+ mkdtempSync,
21
+ rmSync,
22
+ symlinkSync,
23
+ writeFileSync,
24
+ } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { join } from "node:path";
27
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
28
+
29
+ import type { CatalogSkill } from "../skills/catalog-install.js";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Mocks — must be declared before importing the module under test
33
+ // ---------------------------------------------------------------------------
34
+
35
+ // Suppress logger output
36
+ mock.module("../util/logger.js", () => ({
37
+ getLogger: () =>
38
+ new Proxy({} as Record<string, unknown>, {
39
+ get: () => () => {},
40
+ }),
41
+ }));
42
+
43
+ let mockCatalog: CatalogSkill[] = [];
44
+ let mockRepoSkillsDir: string | undefined = undefined;
45
+
46
+ mock.module("../skills/catalog-cache.js", () => ({
47
+ getCatalog: async () => mockCatalog,
48
+ getCachedCatalogSync: () => mockCatalog,
49
+ }));
50
+
51
+ mock.module("../skills/catalog-install.js", () => ({
52
+ getRepoSkillsDir: () => mockRepoSkillsDir,
53
+ }));
54
+
55
+ let mockPlatformToken: string | null = null;
56
+ mock.module("../util/platform.ts", () => ({
57
+ readPlatformToken: () => mockPlatformToken,
58
+ }));
59
+ mock.module("../util/platform.js", () => ({
60
+ readPlatformToken: () => mockPlatformToken,
61
+ }));
62
+
63
+ let mockPlatformBaseUrl = "https://platform.test";
64
+ mock.module("../config/env.ts", () => ({
65
+ getPlatformBaseUrl: () => mockPlatformBaseUrl,
66
+ }));
67
+ mock.module("../config/env.js", () => ({
68
+ getPlatformBaseUrl: () => mockPlatformBaseUrl,
69
+ }));
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Imports (after mocks)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ import {
76
+ catalogSkillToSlim,
77
+ createVellumCatalogProvider,
78
+ readCatalogSkillFileContent,
79
+ readCatalogSkillFiles,
80
+ sanitizeRelativePath,
81
+ } from "../skills/catalog-files.js";
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Helpers
85
+ // ---------------------------------------------------------------------------
86
+
87
+ type FetchFn = typeof globalThis.fetch;
88
+
89
+ interface FetchCall {
90
+ url: string;
91
+ init?: RequestInit;
92
+ }
93
+
94
+ let originalFetch: FetchFn;
95
+ let fetchCalls: FetchCall[] = [];
96
+
97
+ function installFetchMock(
98
+ handler: (url: string, init?: RequestInit) => Response | Promise<Response>,
99
+ ): void {
100
+ fetchCalls = [];
101
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
102
+ const url =
103
+ typeof input === "string"
104
+ ? input
105
+ : input instanceof URL
106
+ ? input.toString()
107
+ : input.url;
108
+ fetchCalls.push({ url, init });
109
+ return handler(url, init);
110
+ }) as unknown as FetchFn;
111
+ }
112
+
113
+ function installFetchThrow(error: Error): void {
114
+ fetchCalls = [];
115
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
116
+ const url =
117
+ typeof input === "string"
118
+ ? input
119
+ : input instanceof URL
120
+ ? input.toString()
121
+ : input.url;
122
+ fetchCalls.push({ url, init });
123
+ throw error;
124
+ }) as unknown as FetchFn;
125
+ }
126
+
127
+ function installFetchForbidden(): void {
128
+ fetchCalls = [];
129
+ globalThis.fetch = (async () => {
130
+ throw new Error("fetch should not have been called");
131
+ }) as unknown as FetchFn;
132
+ }
133
+
134
+ // Temp directories created during tests, cleaned up in afterEach.
135
+ const tempDirs: string[] = [];
136
+
137
+ function makeTempSkillsDir(): string {
138
+ const dir = mkdtempSync(join(tmpdir(), "catalog-files-test-"));
139
+ tempDirs.push(dir);
140
+ return dir;
141
+ }
142
+
143
+ function writeSkill(
144
+ root: string,
145
+ skillId: string,
146
+ files: Record<string, string | Buffer>,
147
+ ): string {
148
+ const skillDir = join(root, skillId);
149
+ mkdirSync(skillDir, { recursive: true });
150
+ for (const [relPath, content] of Object.entries(files)) {
151
+ const abs = join(skillDir, relPath);
152
+ mkdirSync(join(abs, ".."), { recursive: true });
153
+ writeFileSync(abs, content);
154
+ }
155
+ return skillDir;
156
+ }
157
+
158
+ function skill(id: string): CatalogSkill {
159
+ return { id, name: id, description: id };
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Setup / teardown
164
+ // ---------------------------------------------------------------------------
165
+
166
+ beforeEach(() => {
167
+ originalFetch = globalThis.fetch;
168
+ fetchCalls = [];
169
+ mockCatalog = [];
170
+ mockRepoSkillsDir = undefined;
171
+ mockPlatformToken = null;
172
+ mockPlatformBaseUrl = "https://platform.test";
173
+ });
174
+
175
+ afterEach(() => {
176
+ globalThis.fetch = originalFetch;
177
+ for (const dir of tempDirs) {
178
+ try {
179
+ rmSync(dir, { recursive: true, force: true });
180
+ } catch {
181
+ // best effort
182
+ }
183
+ }
184
+ tempDirs.length = 0;
185
+ });
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // sanitizeRelativePath
189
+ // ---------------------------------------------------------------------------
190
+
191
+ describe("sanitizeRelativePath", () => {
192
+ test("accepts simple posix paths", () => {
193
+ expect(sanitizeRelativePath("SKILL.md")).toBe("SKILL.md");
194
+ expect(sanitizeRelativePath("tools/run.sh")).toBe("tools/run.sh");
195
+ expect(sanitizeRelativePath("a/b/c.txt")).toBe("a/b/c.txt");
196
+ });
197
+
198
+ test("normalizes leading ./", () => {
199
+ expect(sanitizeRelativePath("./x")).toBe("x");
200
+ expect(sanitizeRelativePath("./tools/run.sh")).toBe("tools/run.sh");
201
+ });
202
+
203
+ test("normalizes backslashes to forward slashes", () => {
204
+ expect(sanitizeRelativePath("tools\\run.sh")).toBe("tools/run.sh");
205
+ });
206
+
207
+ test("rejects empty strings", () => {
208
+ expect(sanitizeRelativePath("")).toBeNull();
209
+ });
210
+
211
+ test("rejects parent-traversal", () => {
212
+ expect(sanitizeRelativePath("..")).toBeNull();
213
+ expect(sanitizeRelativePath("../x")).toBeNull();
214
+ expect(sanitizeRelativePath("../../etc/passwd")).toBeNull();
215
+ });
216
+
217
+ test("rejects absolute unix paths", () => {
218
+ expect(sanitizeRelativePath("/etc/passwd")).toBeNull();
219
+ expect(sanitizeRelativePath("/")).toBeNull();
220
+ });
221
+
222
+ test("rejects windows drive-prefixed paths", () => {
223
+ expect(sanitizeRelativePath("C:/win")).toBeNull();
224
+ expect(sanitizeRelativePath("C:\\Windows")).toBeNull();
225
+ });
226
+
227
+ test("rejects paths that become absolute after ./ stripping", () => {
228
+ // `sanitizeRelativePath` performs a post-normalization absolute-path
229
+ // check so inputs like `.//etc/passwd` cannot reach the filesystem:
230
+ // the leading `./` strip loop leaves `/etc/passwd`, which
231
+ // `posix.normalize` would otherwise pass through as an absolute path.
232
+ expect(sanitizeRelativePath(".//etc/passwd")).toBeNull();
233
+ expect(sanitizeRelativePath("./././/etc/passwd")).toBeNull();
234
+ // The backslash normalizes to `/`, so `.\\/etc/passwd` becomes
235
+ // `.//etc/passwd` before the strip loop, then `/etc/passwd`.
236
+ expect(sanitizeRelativePath(".\\/etc/passwd")).toBeNull();
237
+ // Windows-drive bypass via the same mechanism.
238
+ expect(sanitizeRelativePath(".//C:/Windows/system32")).toBeNull();
239
+ });
240
+
241
+ test("rejects paths containing null bytes", () => {
242
+ expect(sanitizeRelativePath("SKILL.md\0.png")).toBeNull();
243
+ expect(sanitizeRelativePath("\0")).toBeNull();
244
+ });
245
+ });
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // readCatalogSkillFiles — dev mode
249
+ // ---------------------------------------------------------------------------
250
+
251
+ describe("readCatalogSkillFiles (dev mode)", () => {
252
+ test("lists files from the repo skills dir without touching fetch", async () => {
253
+ const root = makeTempSkillsDir();
254
+ writeSkill(root, "my-skill", {
255
+ "SKILL.md": "# hello",
256
+ "tools/run.sh": "#!/bin/sh\necho hi\n",
257
+ "data/img.png": Buffer.from([0x89, 0x50, 0x4e, 0x47]),
258
+ });
259
+ mockRepoSkillsDir = root;
260
+ mockCatalog = [skill("my-skill")];
261
+ installFetchForbidden();
262
+
263
+ const entries = await readCatalogSkillFiles("my-skill");
264
+ expect(entries).not.toBeNull();
265
+ const paths = entries!.map((e) => e.path).sort();
266
+ expect(paths).toEqual(["SKILL.md", "data/img.png", "tools/run.sh"]);
267
+
268
+ const md = entries!.find((e) => e.path === "SKILL.md")!;
269
+ expect(md.name).toBe("SKILL.md");
270
+ expect(md.size).toBe("# hello".length);
271
+ expect(md.isBinary).toBe(false);
272
+ expect(md.content).toBeNull();
273
+
274
+ const png = entries!.find((e) => e.path === "data/img.png")!;
275
+ expect(png.name).toBe("img.png");
276
+ expect(png.isBinary).toBe(true);
277
+ expect(png.content).toBeNull();
278
+
279
+ expect(fetchCalls.length).toBe(0);
280
+ });
281
+
282
+ test("returns null when skill id is not in the catalog (dev mode)", async () => {
283
+ const root = makeTempSkillsDir();
284
+ writeSkill(root, "my-skill", { "SKILL.md": "x" });
285
+ mockRepoSkillsDir = root;
286
+ mockCatalog = []; // not in catalog
287
+ installFetchForbidden();
288
+
289
+ expect(await readCatalogSkillFiles("my-skill")).toBeNull();
290
+ expect(fetchCalls.length).toBe(0);
291
+ });
292
+
293
+ test("rejects a symlinked skill root and falls through to platform mode", async () => {
294
+ // An attacker (or a misconfigured dev) creates
295
+ // <repoSkillsDir>/my-skill as a symlink pointing at an external
296
+ // directory. `resolveCatalogSource` rejects symlinked skill roots so
297
+ // the dev-mode branch never walks the external directory — the
298
+ // realpath containment check downstream would otherwise derive
299
+ // `realRoot` from the already-resolved symlink target and become a
300
+ // no-op.
301
+ //
302
+ // Expected behavior: the dev-mode shortcut is rejected up-front, and
303
+ // we fall through to platform mode — which in this test is stubbed
304
+ // to return an empty file list. So `readCatalogSkillFiles` returns
305
+ // the empty platform response, and `fetch` MUST be called (proving
306
+ // the fall-through happened, rather than the dev-mode shortcut
307
+ // silently reading from the external directory).
308
+ const root = makeTempSkillsDir();
309
+ const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
310
+ tempDirs.push(externalRoot);
311
+
312
+ // External directory populated with a real SKILL.md + a secret file
313
+ // that must NOT be exposed by the listing.
314
+ writeFileSync(join(externalRoot, "SKILL.md"), "# EXTERNAL SKILL");
315
+ writeFileSync(join(externalRoot, "secret.txt"), "EXTERNAL_SECRET");
316
+
317
+ // Symlink the skill root at `<root>/my-skill` to the external dir.
318
+ symlinkSync(externalRoot, join(root, "my-skill"));
319
+
320
+ mockRepoSkillsDir = root;
321
+ mockCatalog = [skill("my-skill")];
322
+ installFetchMock(() => Response.json({ skill_id: "my-skill", files: [] }));
323
+
324
+ const entries = await readCatalogSkillFiles("my-skill");
325
+
326
+ // Fall-through should produce the platform response (empty array),
327
+ // NOT the external directory contents.
328
+ expect(entries).toEqual([]);
329
+
330
+ // Confirm the dev-mode shortcut was bypassed: platform fetch was
331
+ // called against the preview endpoint.
332
+ expect(fetchCalls.length).toBe(1);
333
+ expect(fetchCalls[0]!.url).toBe(
334
+ "https://platform.test/v1/skills/my-skill/files/",
335
+ );
336
+ });
337
+
338
+ test("rejects a symlinked skill root for content reads and falls through to platform mode", async () => {
339
+ // Direct reproduction for `readCatalogSkillFileContent`: with a
340
+ // symlinked skill root, the dev branch must not read the external
341
+ // file. Instead we should fall through to the platform endpoint and
342
+ // return whatever the platform says.
343
+ const root = makeTempSkillsDir();
344
+ const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
345
+ tempDirs.push(externalRoot);
346
+
347
+ writeFileSync(join(externalRoot, "SKILL.md"), "EXTERNAL_SKILL_CONTENT");
348
+ symlinkSync(externalRoot, join(root, "my-skill"));
349
+
350
+ mockRepoSkillsDir = root;
351
+ mockCatalog = [skill("my-skill")];
352
+ installFetchMock(() =>
353
+ Response.json({
354
+ path: "SKILL.md",
355
+ name: "SKILL.md",
356
+ size: 14,
357
+ mime_type: "text/markdown",
358
+ is_binary: false,
359
+ content: "PLATFORM_CONTENT",
360
+ }),
361
+ );
362
+
363
+ const entry = await readCatalogSkillFileContent("my-skill", "SKILL.md");
364
+ expect(entry).not.toBeNull();
365
+ // Content must be the platform payload, NOT the external file's
366
+ // bytes — otherwise the fall-through didn't happen.
367
+ expect(entry!.content).toBe("PLATFORM_CONTENT");
368
+ expect(entry!.mimeType).toBe("text/markdown");
369
+
370
+ // And fetch was called, confirming dev-mode was bypassed.
371
+ expect(fetchCalls.length).toBe(1);
372
+ const url = fetchCalls[0]!.url;
373
+ expect(
374
+ url.startsWith("https://platform.test/v1/skills/my-skill/files/content/"),
375
+ ).toBe(true);
376
+ });
377
+
378
+ test("non-symlinked skill root still uses the dev-mode shortcut", async () => {
379
+ // Sanity check: a normal directory-based skill root must still be
380
+ // served from disk without touching fetch. This guards against the
381
+ // symlink-rejection path collateral-damaging regular dev flows.
382
+ const root = makeTempSkillsDir();
383
+ writeSkill(root, "normal-skill", { "SKILL.md": "# normal\n" });
384
+ mockRepoSkillsDir = root;
385
+ mockCatalog = [skill("normal-skill")];
386
+ installFetchForbidden();
387
+
388
+ const entries = await readCatalogSkillFiles("normal-skill");
389
+ expect(entries).not.toBeNull();
390
+ const paths = entries!.map((e) => e.path).sort();
391
+ expect(paths).toEqual(["SKILL.md"]);
392
+ // No platform round-trip happened.
393
+ expect(fetchCalls.length).toBe(0);
394
+ });
395
+
396
+ test("filters hidden files and SKIP_DIRS from the listing", async () => {
397
+ // Simulates a dev working on a catalog skill locally who has a
398
+ // node_modules/, a .git/, and a .hidden.md file sitting next to
399
+ // SKILL.md. The preview listing must only show SKILL.md — matching
400
+ // the behavior of `readDirRecursive` in `daemon/handlers/skills.ts`
401
+ // for installed skills.
402
+ const root = makeTempSkillsDir();
403
+ writeSkill(root, "my-skill", {
404
+ "SKILL.md": "# hello",
405
+ "node_modules/foo.js": "module.exports = {};",
406
+ "node_modules/nested/bar.js": "module.exports = {};",
407
+ "__pycache__/cached.pyc": Buffer.from([0x00, 0x01, 0x02]),
408
+ ".git/HEAD": "ref: refs/heads/main\n",
409
+ ".git/config": "[core]\n",
410
+ ".hidden.md": "secret",
411
+ ".DS_Store": Buffer.from([0x00, 0x00]),
412
+ });
413
+ mockRepoSkillsDir = root;
414
+ mockCatalog = [skill("my-skill")];
415
+ installFetchForbidden();
416
+
417
+ const entries = await readCatalogSkillFiles("my-skill");
418
+ expect(entries).not.toBeNull();
419
+ const paths = entries!.map((e) => e.path).sort();
420
+ expect(paths).toEqual(["SKILL.md"]);
421
+ expect(fetchCalls.length).toBe(0);
422
+ });
423
+ });
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // readCatalogSkillFiles — platform mode
427
+ // ---------------------------------------------------------------------------
428
+
429
+ describe("readCatalogSkillFiles (platform mode)", () => {
430
+ test("fetches file listing from the platform and maps it", async () => {
431
+ mockRepoSkillsDir = undefined;
432
+ mockCatalog = [skill("remote-skill")];
433
+ mockPlatformToken = "tok-123";
434
+ installFetchMock(() =>
435
+ Response.json({
436
+ skill_id: "remote-skill",
437
+ files: [
438
+ { path: "SKILL.md", name: "SKILL.md", size: 12, sha: "a" },
439
+ { path: "data/img.png", name: "img.png", size: 200, sha: "b" },
440
+ ],
441
+ }),
442
+ );
443
+
444
+ const entries = await readCatalogSkillFiles("remote-skill");
445
+ expect(entries).not.toBeNull();
446
+ expect(entries!.length).toBe(2);
447
+
448
+ // URL + headers
449
+ expect(fetchCalls.length).toBe(1);
450
+ expect(fetchCalls[0]!.url).toBe(
451
+ "https://platform.test/v1/skills/remote-skill/files/",
452
+ );
453
+ const headers = (fetchCalls[0]!.init?.headers ?? {}) as Record<
454
+ string,
455
+ string
456
+ >;
457
+ expect(headers["Accept"]).toBe("application/json");
458
+ expect(headers["X-Conversation-Token"]).toBe("tok-123");
459
+
460
+ // Mapped entries: always content === null, isBinary from filename.
461
+ const md = entries!.find((e) => e.path === "SKILL.md")!;
462
+ expect(md.isBinary).toBe(false);
463
+ expect(md.content).toBeNull();
464
+ expect(md.mimeType).toBe("");
465
+
466
+ const png = entries!.find((e) => e.path === "data/img.png")!;
467
+ expect(png.isBinary).toBe(true);
468
+ expect(png.content).toBeNull();
469
+ expect(png.mimeType).toBe("");
470
+ });
471
+
472
+ test("does not set X-Conversation-Token when no token is present", async () => {
473
+ mockCatalog = [skill("remote-skill")];
474
+ mockPlatformToken = null;
475
+ installFetchMock(() =>
476
+ Response.json({ skill_id: "remote-skill", files: [] }),
477
+ );
478
+
479
+ await readCatalogSkillFiles("remote-skill");
480
+ const headers = (fetchCalls[0]!.init?.headers ?? {}) as Record<
481
+ string,
482
+ string
483
+ >;
484
+ expect(headers["X-Conversation-Token"]).toBeUndefined();
485
+ });
486
+
487
+ test("returns null on 500", async () => {
488
+ mockCatalog = [skill("remote-skill")];
489
+ installFetchMock(
490
+ () => new Response("boom", { status: 500, statusText: "Server Error" }),
491
+ );
492
+ expect(await readCatalogSkillFiles("remote-skill")).toBeNull();
493
+ expect(fetchCalls.length).toBe(1);
494
+ });
495
+
496
+ test("returns null on 404", async () => {
497
+ mockCatalog = [skill("remote-skill")];
498
+ installFetchMock(
499
+ () => new Response("missing", { status: 404, statusText: "Not Found" }),
500
+ );
501
+ expect(await readCatalogSkillFiles("remote-skill")).toBeNull();
502
+ });
503
+
504
+ test("returns null on network error", async () => {
505
+ mockCatalog = [skill("remote-skill")];
506
+ installFetchThrow(new Error("ECONNRESET"));
507
+ expect(await readCatalogSkillFiles("remote-skill")).toBeNull();
508
+ });
509
+
510
+ test("returns null without fetching when skill id missing from catalog", async () => {
511
+ mockCatalog = [];
512
+ installFetchForbidden();
513
+ expect(await readCatalogSkillFiles("unknown")).toBeNull();
514
+ expect(fetchCalls.length).toBe(0);
515
+ });
516
+ });
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // readCatalogSkillFileContent — dev mode
520
+ // ---------------------------------------------------------------------------
521
+
522
+ describe("readCatalogSkillFileContent (dev mode)", () => {
523
+ test("returns inline UTF-8 content for a text file", async () => {
524
+ const root = makeTempSkillsDir();
525
+ writeSkill(root, "my-skill", { "SKILL.md": "# hello world\n" });
526
+ mockRepoSkillsDir = root;
527
+ mockCatalog = [skill("my-skill")];
528
+ installFetchForbidden();
529
+
530
+ const entry = await readCatalogSkillFileContent("my-skill", "SKILL.md");
531
+ expect(entry).not.toBeNull();
532
+ expect(entry!.path).toBe("SKILL.md");
533
+ expect(entry!.name).toBe("SKILL.md");
534
+ expect(entry!.isBinary).toBe(false);
535
+ expect(entry!.content).toBe("# hello world\n");
536
+ expect(fetchCalls.length).toBe(0);
537
+ });
538
+
539
+ test("rejects traversal paths without touching the filesystem", async () => {
540
+ const root = makeTempSkillsDir();
541
+ writeSkill(root, "my-skill", { "SKILL.md": "ok" });
542
+ mockRepoSkillsDir = root;
543
+ mockCatalog = [skill("my-skill")];
544
+ installFetchForbidden();
545
+
546
+ expect(
547
+ await readCatalogSkillFileContent("my-skill", "../escape"),
548
+ ).toBeNull();
549
+ expect(
550
+ await readCatalogSkillFileContent("my-skill", "/etc/passwd"),
551
+ ).toBeNull();
552
+ expect(await readCatalogSkillFileContent("my-skill", "..")).toBeNull();
553
+ expect(await readCatalogSkillFileContent("my-skill", "")).toBeNull();
554
+ });
555
+
556
+ test("returns content=null for a binary file", async () => {
557
+ const root = makeTempSkillsDir();
558
+ writeSkill(root, "my-skill", {
559
+ "img.png": Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
560
+ });
561
+ mockRepoSkillsDir = root;
562
+ mockCatalog = [skill("my-skill")];
563
+ installFetchForbidden();
564
+
565
+ const entry = await readCatalogSkillFileContent("my-skill", "img.png");
566
+ expect(entry).not.toBeNull();
567
+ expect(entry!.isBinary).toBe(true);
568
+ expect(entry!.content).toBeNull();
569
+ });
570
+
571
+ test("returns content=null for oversized text files", async () => {
572
+ const root = makeTempSkillsDir();
573
+ // Just over 2 MB of 'a'
574
+ const oversized = "a".repeat(2 * 1024 * 1024 + 1);
575
+ writeSkill(root, "my-skill", { "big.txt": oversized });
576
+ mockRepoSkillsDir = root;
577
+ mockCatalog = [skill("my-skill")];
578
+ installFetchForbidden();
579
+
580
+ const entry = await readCatalogSkillFileContent("my-skill", "big.txt");
581
+ expect(entry).not.toBeNull();
582
+ expect(entry!.isBinary).toBe(false);
583
+ expect(entry!.content).toBeNull();
584
+ expect(entry!.size).toBe(oversized.length);
585
+ });
586
+
587
+ test("returns null for a missing file", async () => {
588
+ const root = makeTempSkillsDir();
589
+ writeSkill(root, "my-skill", { "SKILL.md": "ok" });
590
+ mockRepoSkillsDir = root;
591
+ mockCatalog = [skill("my-skill")];
592
+ installFetchForbidden();
593
+
594
+ expect(
595
+ await readCatalogSkillFileContent("my-skill", "does/not/exist.txt"),
596
+ ).toBeNull();
597
+ });
598
+
599
+ test("returns null without fetching when skill id missing from catalog", async () => {
600
+ mockCatalog = [];
601
+ installFetchForbidden();
602
+ expect(await readCatalogSkillFileContent("unknown", "SKILL.md")).toBeNull();
603
+ expect(fetchCalls.length).toBe(0);
604
+ });
605
+
606
+ test("rejects symlinked files that point outside the skill root", async () => {
607
+ // Create a temp skill root AND a separate external directory.
608
+ const root = makeTempSkillsDir();
609
+ const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
610
+ tempDirs.push(externalRoot);
611
+
612
+ // Write an "external secret" file completely outside the skill tree.
613
+ const externalSecret = join(externalRoot, "secret.txt");
614
+ writeFileSync(externalSecret, "EXTERNAL_SECRET");
615
+
616
+ // Create the skill directory itself with a legitimate file.
617
+ const skillDir = writeSkill(root, "my-skill", { "SKILL.md": "ok" });
618
+
619
+ // Create a symlink INSIDE the skill dir pointing at the external file.
620
+ const linkPath = join(skillDir, "link-to-secret.md");
621
+ symlinkSync(externalSecret, linkPath);
622
+
623
+ mockRepoSkillsDir = root;
624
+ mockCatalog = [skill("my-skill")];
625
+ installFetchForbidden();
626
+
627
+ const entry = await readCatalogSkillFileContent(
628
+ "my-skill",
629
+ "link-to-secret.md",
630
+ );
631
+ expect(entry).toBeNull();
632
+
633
+ // And the legitimate file is still readable, so the check didn't
634
+ // collateral-damage normal requests.
635
+ const ok = await readCatalogSkillFileContent("my-skill", "SKILL.md");
636
+ expect(ok).not.toBeNull();
637
+ expect(ok!.content).toBe("ok");
638
+ });
639
+
640
+ test("rejects files accessed through a symlinked parent directory", async () => {
641
+ const root = makeTempSkillsDir();
642
+ const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
643
+ tempDirs.push(externalRoot);
644
+
645
+ // External directory with a real file inside it.
646
+ const externalDir = join(externalRoot, "external-dir");
647
+ mkdirSync(externalDir, { recursive: true });
648
+ writeFileSync(join(externalDir, "payload.txt"), "EXTERNAL_PAYLOAD");
649
+
650
+ // Legitimate skill dir with a normal file.
651
+ const skillDir = writeSkill(root, "my-skill", { "SKILL.md": "ok" });
652
+
653
+ // Inside the skill dir, create a symlinked subdirectory that points at
654
+ // the external directory. Then try to request
655
+ // `escape/payload.txt` — lexically this is inside the skill root, but
656
+ // the physical file lives outside.
657
+ const escapeLink = join(skillDir, "escape");
658
+ symlinkSync(externalDir, escapeLink);
659
+
660
+ mockRepoSkillsDir = root;
661
+ mockCatalog = [skill("my-skill")];
662
+ installFetchForbidden();
663
+
664
+ const entry = await readCatalogSkillFileContent(
665
+ "my-skill",
666
+ "escape/payload.txt",
667
+ );
668
+ expect(entry).toBeNull();
669
+ });
670
+
671
+ test("rejects dotfile paths and returns null without reading disk", async () => {
672
+ // `.env` is a valid sanitized path (sanitizeRelativePath accepts it),
673
+ // but the hidden-segment check must reject it so the catalog content
674
+ // reader never exposes dotfiles, matching the listing API that hides
675
+ // them. This preserves parity between listing and content endpoints.
676
+ const root = makeTempSkillsDir();
677
+ writeSkill(root, "my-skill", {
678
+ "SKILL.md": "ok",
679
+ ".env": "SECRET=abc\n",
680
+ });
681
+ mockRepoSkillsDir = root;
682
+ mockCatalog = [skill("my-skill")];
683
+ installFetchForbidden();
684
+
685
+ expect(await readCatalogSkillFileContent("my-skill", ".env")).toBeNull();
686
+ expect(
687
+ await readCatalogSkillFileContent("my-skill", ".git/config"),
688
+ ).toBeNull();
689
+ expect(
690
+ await readCatalogSkillFileContent("my-skill", "docs/.hidden/file.md"),
691
+ ).toBeNull();
692
+ });
693
+
694
+ test("rejects SKIP_DIRS paths and returns null without reading disk", async () => {
695
+ const root = makeTempSkillsDir();
696
+ writeSkill(root, "my-skill", {
697
+ "SKILL.md": "ok",
698
+ "node_modules/foo/index.js": "module.exports = {};",
699
+ });
700
+ mockRepoSkillsDir = root;
701
+ mockCatalog = [skill("my-skill")];
702
+ installFetchForbidden();
703
+
704
+ expect(
705
+ await readCatalogSkillFileContent(
706
+ "my-skill",
707
+ "node_modules/foo/index.js",
708
+ ),
709
+ ).toBeNull();
710
+ expect(
711
+ await readCatalogSkillFileContent("my-skill", "__pycache__/cached.pyc"),
712
+ ).toBeNull();
713
+ expect(
714
+ await readCatalogSkillFileContent(
715
+ "my-skill",
716
+ "nested/node_modules/foo.js",
717
+ ),
718
+ ).toBeNull();
719
+ });
720
+
721
+ test("regular docs/readme.md still returns content (sanity)", async () => {
722
+ const root = makeTempSkillsDir();
723
+ writeSkill(root, "my-skill", {
724
+ "docs/readme.md": "# readme\n",
725
+ });
726
+ mockRepoSkillsDir = root;
727
+ mockCatalog = [skill("my-skill")];
728
+ installFetchForbidden();
729
+
730
+ const entry = await readCatalogSkillFileContent(
731
+ "my-skill",
732
+ "docs/readme.md",
733
+ );
734
+ expect(entry).not.toBeNull();
735
+ expect(entry!.content).toBe("# readme\n");
736
+ expect(entry!.name).toBe("readme.md");
737
+ });
738
+ });
739
+
740
+ // ---------------------------------------------------------------------------
741
+ // readCatalogSkillFileContent — platform mode
742
+ // ---------------------------------------------------------------------------
743
+
744
+ describe("readCatalogSkillFileContent (platform mode)", () => {
745
+ test("maps snake_case text response to camelCase entry", async () => {
746
+ mockCatalog = [skill("remote-skill")];
747
+ installFetchMock(() =>
748
+ Response.json({
749
+ path: "SKILL.md",
750
+ name: "SKILL.md",
751
+ size: 14,
752
+ mime_type: "text/markdown",
753
+ is_binary: false,
754
+ content: "# hello world\n",
755
+ }),
756
+ );
757
+
758
+ const entry = await readCatalogSkillFileContent("remote-skill", "SKILL.md");
759
+ expect(entry).not.toBeNull();
760
+ expect(entry!.path).toBe("SKILL.md");
761
+ expect(entry!.name).toBe("SKILL.md");
762
+ expect(entry!.size).toBe(14);
763
+ expect(entry!.mimeType).toBe("text/markdown");
764
+ expect(entry!.isBinary).toBe(false);
765
+ expect(entry!.content).toBe("# hello world\n");
766
+
767
+ expect(fetchCalls.length).toBe(1);
768
+ const url = fetchCalls[0]!.url;
769
+ expect(
770
+ url.startsWith(
771
+ "https://platform.test/v1/skills/remote-skill/files/content/",
772
+ ),
773
+ ).toBe(true);
774
+ expect(url).toContain("path=SKILL.md");
775
+ });
776
+
777
+ test("preserves binary response (content=null, isBinary=true)", async () => {
778
+ mockCatalog = [skill("remote-skill")];
779
+ installFetchMock(() =>
780
+ Response.json({
781
+ path: "img.png",
782
+ name: "img.png",
783
+ size: 1024,
784
+ mime_type: "image/png",
785
+ is_binary: true,
786
+ content: null,
787
+ }),
788
+ );
789
+
790
+ const entry = await readCatalogSkillFileContent("remote-skill", "img.png");
791
+ expect(entry).not.toBeNull();
792
+ expect(entry!.isBinary).toBe(true);
793
+ expect(entry!.content).toBeNull();
794
+ expect(entry!.mimeType).toBe("image/png");
795
+ });
796
+
797
+ test("preserves oversized text response (content=null)", async () => {
798
+ mockCatalog = [skill("remote-skill")];
799
+ installFetchMock(() =>
800
+ Response.json({
801
+ path: "big.txt",
802
+ name: "big.txt",
803
+ size: 3 * 1024 * 1024,
804
+ mime_type: "text/plain",
805
+ is_binary: false,
806
+ content: null,
807
+ }),
808
+ );
809
+
810
+ const entry = await readCatalogSkillFileContent("remote-skill", "big.txt");
811
+ expect(entry).not.toBeNull();
812
+ expect(entry!.isBinary).toBe(false);
813
+ expect(entry!.content).toBeNull();
814
+ expect(entry!.size).toBe(3 * 1024 * 1024);
815
+ });
816
+
817
+ test("returns null on 404", async () => {
818
+ mockCatalog = [skill("remote-skill")];
819
+ installFetchMock(
820
+ () => new Response("missing", { status: 404, statusText: "Not Found" }),
821
+ );
822
+ expect(
823
+ await readCatalogSkillFileContent("remote-skill", "ghost.md"),
824
+ ).toBeNull();
825
+ });
826
+
827
+ test("rejects traversal BEFORE making any fetch call", async () => {
828
+ mockCatalog = [skill("remote-skill")];
829
+ installFetchForbidden();
830
+ expect(await readCatalogSkillFileContent("remote-skill", "..")).toBeNull();
831
+ expect(
832
+ await readCatalogSkillFileContent("remote-skill", "../etc/passwd"),
833
+ ).toBeNull();
834
+ expect(fetchCalls.length).toBe(0);
835
+ });
836
+
837
+ test("rejects hidden / SKIP_DIRS paths BEFORE making any fetch call", async () => {
838
+ // Platform-mode defense in depth: even though the platform endpoint
839
+ // would refuse these reads server-side, we must short-circuit in
840
+ // `readCatalogSkillFileContent` so an attacker cannot use the daemon
841
+ // as a probe channel (and so we avoid unnecessary network traffic).
842
+ mockCatalog = [skill("remote-skill")];
843
+ installFetchForbidden();
844
+ expect(
845
+ await readCatalogSkillFileContent("remote-skill", ".env"),
846
+ ).toBeNull();
847
+ expect(
848
+ await readCatalogSkillFileContent("remote-skill", ".git/config"),
849
+ ).toBeNull();
850
+ expect(
851
+ await readCatalogSkillFileContent(
852
+ "remote-skill",
853
+ "node_modules/foo/index.js",
854
+ ),
855
+ ).toBeNull();
856
+ expect(fetchCalls.length).toBe(0);
857
+ });
858
+
859
+ test("returns null without fetching when skill id missing from catalog", async () => {
860
+ mockCatalog = [];
861
+ installFetchForbidden();
862
+ expect(await readCatalogSkillFileContent("unknown", "SKILL.md")).toBeNull();
863
+ expect(fetchCalls.length).toBe(0);
864
+ });
865
+ });
866
+
867
+ // ---------------------------------------------------------------------------
868
+ // catalogSkillToSlim
869
+ // ---------------------------------------------------------------------------
870
+
871
+ describe("catalogSkillToSlim", () => {
872
+ test("maps CatalogSkill to SlimSkillResponse with vellum origin", () => {
873
+ const cs: CatalogSkill = {
874
+ id: "test-skill",
875
+ name: "test-skill",
876
+ description: "A test skill",
877
+ emoji: "🧪",
878
+ };
879
+ const slim = catalogSkillToSlim(cs);
880
+ expect(slim.id).toBe("test-skill");
881
+ expect(slim.name).toBe("test-skill");
882
+ expect(slim.description).toBe("A test skill");
883
+ expect(slim.emoji).toBe("🧪");
884
+ expect(slim.kind).toBe("catalog");
885
+ expect(slim.origin).toBe("vellum");
886
+ expect(slim.status).toBe("available");
887
+ });
888
+
889
+ test("uses display-name from metadata when available", () => {
890
+ const cs: CatalogSkill = {
891
+ id: "test-skill",
892
+ name: "test-skill",
893
+ description: "A test skill",
894
+ metadata: { vellum: { "display-name": "Pretty Name" } },
895
+ };
896
+ const slim = catalogSkillToSlim(cs);
897
+ expect(slim.name).toBe("Pretty Name");
898
+ });
899
+ });
900
+
901
+ // ---------------------------------------------------------------------------
902
+ // createVellumCatalogProvider
903
+ // ---------------------------------------------------------------------------
904
+
905
+ describe("createVellumCatalogProvider", () => {
906
+ test("canHandle returns true when skill is in the cached catalog", () => {
907
+ mockCatalog = [skill("my-skill"), skill("other-skill")];
908
+ const provider = createVellumCatalogProvider();
909
+ expect(provider.canHandle("my-skill")).toBe(true);
910
+ expect(provider.canHandle("other-skill")).toBe(true);
911
+ });
912
+
913
+ test("canHandle returns false when skill is NOT in the cached catalog", () => {
914
+ mockCatalog = [skill("my-skill")];
915
+ const provider = createVellumCatalogProvider();
916
+ expect(provider.canHandle("unknown-skill")).toBe(false);
917
+ });
918
+
919
+ test("canHandle returns false when catalog cache is empty", () => {
920
+ mockCatalog = [];
921
+ const provider = createVellumCatalogProvider();
922
+ expect(provider.canHandle("any-skill")).toBe(false);
923
+ });
924
+
925
+ test("listFiles delegates to readCatalogSkillFiles", async () => {
926
+ const root = makeTempSkillsDir();
927
+ writeSkill(root, "my-skill", {
928
+ "SKILL.md": "# hello",
929
+ "tools/run.sh": "#!/bin/sh\necho hi\n",
930
+ });
931
+ mockRepoSkillsDir = root;
932
+ mockCatalog = [skill("my-skill")];
933
+ installFetchForbidden();
934
+
935
+ const provider = createVellumCatalogProvider();
936
+ const entries = await provider.listFiles("my-skill");
937
+ expect(entries).not.toBeNull();
938
+ const paths = entries!.map((e) => e.path).sort();
939
+ expect(paths).toEqual(["SKILL.md", "tools/run.sh"]);
940
+ });
941
+
942
+ test("listFiles returns null for unknown skill", async () => {
943
+ mockCatalog = [];
944
+ installFetchForbidden();
945
+
946
+ const provider = createVellumCatalogProvider();
947
+ expect(await provider.listFiles("unknown")).toBeNull();
948
+ });
949
+
950
+ test("readFileContent delegates to readCatalogSkillFileContent", async () => {
951
+ const root = makeTempSkillsDir();
952
+ writeSkill(root, "my-skill", { "SKILL.md": "# hello world\n" });
953
+ mockRepoSkillsDir = root;
954
+ mockCatalog = [skill("my-skill")];
955
+ installFetchForbidden();
956
+
957
+ const provider = createVellumCatalogProvider();
958
+ const entry = await provider.readFileContent("my-skill", "SKILL.md");
959
+ expect(entry).not.toBeNull();
960
+ expect(entry!.content).toBe("# hello world\n");
961
+ expect(entry!.path).toBe("SKILL.md");
962
+ });
963
+
964
+ test("readFileContent returns null for unknown skill", async () => {
965
+ mockCatalog = [];
966
+ installFetchForbidden();
967
+
968
+ const provider = createVellumCatalogProvider();
969
+ expect(await provider.readFileContent("unknown", "SKILL.md")).toBeNull();
970
+ });
971
+
972
+ test("toSlimSkill returns SlimSkillResponse for catalog skill", async () => {
973
+ mockCatalog = [
974
+ {
975
+ id: "my-skill",
976
+ name: "my-skill",
977
+ description: "A skill",
978
+ emoji: "🔧",
979
+ metadata: { vellum: { "display-name": "My Skill" } },
980
+ },
981
+ ];
982
+
983
+ const provider = createVellumCatalogProvider();
984
+ const slim = await provider.toSlimSkill("my-skill");
985
+ expect(slim).not.toBeNull();
986
+ expect(slim!.id).toBe("my-skill");
987
+ expect(slim!.name).toBe("My Skill");
988
+ expect(slim!.description).toBe("A skill");
989
+ expect(slim!.kind).toBe("catalog");
990
+ expect(slim!.origin).toBe("vellum");
991
+ expect(slim!.status).toBe("available");
992
+ });
993
+
994
+ test("toSlimSkill returns null for unknown skill", async () => {
995
+ mockCatalog = [];
996
+
997
+ const provider = createVellumCatalogProvider();
998
+ expect(await provider.toSlimSkill("unknown")).toBeNull();
999
+ });
1000
+ });