@vellumai/assistant 0.7.1 → 0.7.2

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 (535) hide show
  1. package/ARCHITECTURE.md +32 -49
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/security.md +20 -0
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +1 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
  19. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  20. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  21. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  22. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  23. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  24. package/openapi.yaml +565 -12
  25. package/package.json +6 -3
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  27. package/src/__tests__/app-bundler.test.ts +170 -1
  28. package/src/__tests__/app-control-flow.test.ts +374 -0
  29. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  30. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  31. package/src/__tests__/app-executors.test.ts +30 -43
  32. package/src/__tests__/approval-routes-http.test.ts +23 -6
  33. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  34. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  35. package/src/__tests__/assistant-event-hub.test.ts +109 -2
  36. package/src/__tests__/assistant-event.test.ts +10 -0
  37. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
  39. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  40. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  41. package/src/__tests__/btw-routes.test.ts +13 -4
  42. package/src/__tests__/call-controller.test.ts +49 -1
  43. package/src/__tests__/call-domain.test.ts +0 -2
  44. package/src/__tests__/call-routes-http.test.ts +0 -2
  45. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  46. package/src/__tests__/checker.test.ts +3 -4
  47. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  48. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  49. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  50. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  51. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  54. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  55. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  56. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  57. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  58. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  59. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  60. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  61. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  62. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  64. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  65. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  66. package/src/__tests__/credentials-cli.test.ts +5 -12
  67. package/src/__tests__/cu-unified-flow.test.ts +185 -23
  68. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  69. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  70. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  71. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  72. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  73. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  74. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  75. package/src/__tests__/heartbeat-service.test.ts +718 -1
  76. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  77. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  78. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  79. package/src/__tests__/host-bash-proxy.test.ts +246 -47
  80. package/src/__tests__/host-bash-routes.test.ts +294 -0
  81. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  82. package/src/__tests__/host-browser-routes.test.ts +39 -13
  83. package/src/__tests__/host-cu-proxy.test.ts +41 -52
  84. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  85. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  86. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  87. package/src/__tests__/host-file-proxy.test.ts +37 -43
  88. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  89. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  90. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  91. package/src/__tests__/host-proxy-base.test.ts +312 -0
  92. package/src/__tests__/host-shell-tool.test.ts +22 -4
  93. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  94. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  95. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  96. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  97. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  98. package/src/__tests__/identity-routes.test.ts +103 -1
  99. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  100. package/src/__tests__/inline-command-runner.test.ts +0 -1
  101. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  102. package/src/__tests__/integration-status.test.ts +85 -5
  103. package/src/__tests__/intent-routing.test.ts +0 -1
  104. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  105. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  106. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  107. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  108. package/src/__tests__/mcp-cli.test.ts +338 -2
  109. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  110. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  111. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  112. package/src/__tests__/oauth-cli.test.ts +0 -2
  113. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  114. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  115. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  116. package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
  117. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  118. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  119. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  120. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  121. package/src/__tests__/retry-backoff.test.ts +87 -0
  122. package/src/__tests__/runtime-events-sse.test.ts +10 -6
  123. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  124. package/src/__tests__/schedule-retry.test.ts +715 -0
  125. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  126. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  127. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  128. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  129. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  130. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  131. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  132. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  133. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  134. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  135. package/src/__tests__/slack-channel-config.test.ts +9 -14
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  137. package/src/__tests__/system-prompt.test.ts +0 -1
  138. package/src/__tests__/telegram-config.test.ts +0 -1
  139. package/src/__tests__/test-preload.ts +8 -0
  140. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  141. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  142. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  143. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  145. package/src/__tests__/tool-executor.test.ts +0 -1
  146. package/src/__tests__/twilio-config.test.ts +3 -16
  147. package/src/__tests__/twilio-routes.test.ts +3 -5
  148. package/src/__tests__/twilio-validation.test.ts +93 -0
  149. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  150. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  151. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  152. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  153. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  154. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  155. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  156. package/src/backup/__tests__/paths.test.ts +0 -22
  157. package/src/backup/__tests__/restore.test.ts +51 -151
  158. package/src/backup/paths.ts +2 -18
  159. package/src/backup/restore.ts +107 -231
  160. package/src/bundler/app-bundler.ts +51 -3
  161. package/src/calls/relay-server.ts +4 -44
  162. package/src/calls/twilio-config.ts +2 -17
  163. package/src/calls/twilio-rest.ts +33 -105
  164. package/src/calls/twilio-routes.ts +11 -12
  165. package/src/channels/types.ts +8 -7
  166. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  167. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  168. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  169. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  170. package/src/cli/commands/backup.ts +6 -331
  171. package/src/cli/commands/clients.ts +36 -37
  172. package/src/cli/commands/contacts.ts +73 -0
  173. package/src/cli/commands/conversations.ts +2 -5
  174. package/src/cli/commands/credentials.ts +15 -7
  175. package/src/cli/commands/domain.ts +66 -15
  176. package/src/cli/commands/gateway.ts +183 -0
  177. package/src/cli/commands/keys.ts +9 -6
  178. package/src/cli/commands/mcp.ts +116 -156
  179. package/src/cli/commands/memory-v2.ts +296 -1
  180. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  181. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  182. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  183. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  184. package/src/cli/commands/platform/disconnect.ts +5 -4
  185. package/src/cli/commands/platform/index.ts +0 -18
  186. package/src/cli/lib/daemon-credential-client.ts +110 -28
  187. package/src/cli/program.ts +2 -0
  188. package/src/config/assistant-feature-flags.ts +67 -10
  189. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  190. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  191. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  192. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  193. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  194. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  195. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  196. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  197. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  198. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  199. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  200. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  201. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  202. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  203. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  204. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  205. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  206. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  207. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  208. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  209. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  210. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  211. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  212. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  213. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  214. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  215. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  216. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  217. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  218. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  219. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  220. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  221. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  222. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  223. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  224. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  225. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  226. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  227. package/src/config/bundled-tool-registry.ts +21 -0
  228. package/src/config/env-registry.ts +0 -2
  229. package/src/config/env.ts +19 -12
  230. package/src/config/feature-flag-registry.json +21 -133
  231. package/src/config/loader.ts +73 -99
  232. package/src/config/sanitize-for-transfer.ts +2 -0
  233. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  234. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
  235. package/src/config/schemas/calls.ts +0 -9
  236. package/src/config/schemas/heartbeat.ts +63 -0
  237. package/src/config/schemas/ingress.ts +10 -6
  238. package/src/config/schemas/llm.ts +5 -10
  239. package/src/config/schemas/memory-lifecycle.ts +77 -24
  240. package/src/config/schemas/memory-v2.ts +48 -4
  241. package/src/config/schemas/platform.ts +6 -0
  242. package/src/config/schemas/services.ts +1 -15
  243. package/src/config/schemas/skills.ts +0 -6
  244. package/src/config/seed-inference-profiles.ts +1 -1
  245. package/src/contacts/contact-store.ts +0 -30
  246. package/src/contacts/contacts-write.ts +0 -27
  247. package/src/context/window-manager.ts +1 -2
  248. package/src/credential-execution/feature-gates.ts +10 -10
  249. package/src/credential-execution/process-manager.ts +12 -41
  250. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  251. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  252. package/src/daemon/config-watcher.ts +4 -3
  253. package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
  254. package/src/daemon/conversation-agent-loop.ts +32 -28
  255. package/src/daemon/conversation-lifecycle.ts +8 -1
  256. package/src/daemon/conversation-process.ts +16 -11
  257. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  258. package/src/daemon/conversation-surfaces.ts +125 -4
  259. package/src/daemon/conversation-tool-setup.ts +16 -55
  260. package/src/daemon/conversation.ts +21 -2
  261. package/src/daemon/doordash-steps.ts +1 -1
  262. package/src/daemon/handlers/shared.ts +4 -1
  263. package/src/daemon/host-app-control-proxy.ts +293 -0
  264. package/src/daemon/host-bash-proxy.ts +84 -74
  265. package/src/daemon/host-browser-proxy.ts +67 -82
  266. package/src/daemon/host-cu-proxy.ts +81 -86
  267. package/src/daemon/host-file-proxy.ts +93 -69
  268. package/src/daemon/host-proxy-base.ts +294 -0
  269. package/src/daemon/host-proxy-preactivation.ts +82 -0
  270. package/src/daemon/host-transfer-proxy.ts +247 -129
  271. package/src/daemon/lifecycle.ts +115 -117
  272. package/src/daemon/message-protocol.ts +3 -8
  273. package/src/daemon/message-types/contacts.ts +23 -1
  274. package/src/daemon/message-types/conversations.ts +11 -8
  275. package/src/daemon/message-types/host-app-control.ts +150 -0
  276. package/src/daemon/message-types/host-bash.ts +4 -0
  277. package/src/daemon/message-types/host-cu.ts +2 -0
  278. package/src/daemon/message-types/host-file.ts +4 -0
  279. package/src/daemon/message-types/host-transfer.ts +3 -0
  280. package/src/daemon/message-types/schedules.ts +8 -3
  281. package/src/daemon/message-types/skills.ts +2 -2
  282. package/src/daemon/process-message.ts +18 -1
  283. package/src/daemon/shutdown-handlers.ts +0 -3
  284. package/src/daemon/tool-setup-types.ts +51 -0
  285. package/src/daemon/tool-side-effects.ts +1 -1
  286. package/src/events/tool-audit-listener.ts +2 -1
  287. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
  288. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  289. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  290. package/src/heartbeat/heartbeat-service.ts +280 -49
  291. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  292. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  293. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  294. package/src/home/post-connect-feed.ts +68 -0
  295. package/src/home/relationship-state-writer.ts +17 -92
  296. package/src/home/suggested-prompts.ts +46 -10
  297. package/src/inbound/public-ingress-urls.ts +32 -34
  298. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  299. package/src/ipc/assistant-server.ts +14 -1
  300. package/src/ipc/cli-client.ts +32 -1
  301. package/src/live-voice/live-voice-metrics.ts +10 -10
  302. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  303. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  304. package/src/mcp/mcp-auth-state.ts +133 -0
  305. package/src/mcp/mcp-oauth-provider.ts +19 -0
  306. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  307. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  308. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  309. package/src/memory/anisotropy.test.ts +247 -0
  310. package/src/memory/anisotropy.ts +443 -0
  311. package/src/memory/auto-analysis-constants.ts +17 -0
  312. package/src/memory/auto-analysis-guard.ts +5 -15
  313. package/src/memory/canonical-guardian-store.ts +7 -7
  314. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  315. package/src/memory/context-search/agent-protocol.ts +6 -6
  316. package/src/memory/context-search/agent-runner.ts +32 -7
  317. package/src/memory/context-search/sources/memory-v2.ts +17 -5
  318. package/src/memory/conversation-crud.ts +1 -1
  319. package/src/memory/conversation-key-store.ts +2 -15
  320. package/src/memory/db-init.ts +4 -0
  321. package/src/memory/embedding-backend.ts +9 -21
  322. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
  323. package/src/memory/graph/conversation-graph-memory.ts +1 -24
  324. package/src/memory/graph/graph-search.ts +8 -0
  325. package/src/memory/graph/retriever.ts +28 -0
  326. package/src/memory/graph/tools.ts +1 -1
  327. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  328. package/src/memory/jobs/embed-concept-page.ts +28 -2
  329. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  330. package/src/memory/jobs-store.ts +66 -22
  331. package/src/memory/jobs-worker.ts +112 -63
  332. package/src/memory/memory-v2-activation-log-store.ts +1 -1
  333. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  334. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  335. package/src/memory/migrations/index.ts +5 -0
  336. package/src/memory/migrations/registry.ts +8 -0
  337. package/src/memory/pkb/pkb-search.ts +7 -0
  338. package/src/memory/qdrant-client.ts +50 -20
  339. package/src/memory/schema/infrastructure.ts +15 -0
  340. package/src/memory/search/semantic.ts +7 -0
  341. package/src/memory/sparse-tokenize.ts +49 -0
  342. package/src/memory/v2/__tests__/activation.test.ts +77 -95
  343. package/src/memory/v2/__tests__/injection.test.ts +43 -21
  344. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  345. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  346. package/src/memory/v2/__tests__/static-context.test.ts +0 -1
  347. package/src/memory/v2/activation.ts +69 -88
  348. package/src/memory/v2/consolidation-job.ts +3 -5
  349. package/src/memory/v2/constants.ts +7 -0
  350. package/src/memory/v2/injection.ts +86 -53
  351. package/src/memory/v2/prompts/consolidation.ts +312 -91
  352. package/src/memory/v2/qdrant.ts +99 -1
  353. package/src/memory/v2/sim.ts +126 -16
  354. package/src/memory/v2/skill-qdrant.ts +12 -3
  355. package/src/memory/v2/skill-store.ts +16 -1
  356. package/src/memory/v2/sparse-bm25.ts +245 -0
  357. package/src/memory/v2/static-context.ts +6 -5
  358. package/src/messaging/providers/gmail/types.ts +0 -49
  359. package/src/messaging/providers/slack/adapter.ts +1 -31
  360. package/src/messaging/providers/slack/types.ts +0 -32
  361. package/src/notifications/README.md +10 -10
  362. package/src/notifications/broadcaster.ts +1 -1
  363. package/src/notifications/guardian-question-mode.ts +5 -5
  364. package/src/oauth/connect-orchestrator.ts +4 -0
  365. package/src/oauth/credential-token-resolver.ts +1 -3
  366. package/src/oauth/manual-token-connection.ts +0 -4
  367. package/src/outbound-proxy/index.ts +1 -37
  368. package/src/outbound-proxy/logging.ts +1 -1
  369. package/src/outbound-proxy/policy.ts +6 -5
  370. package/src/outbound-proxy/router.ts +2 -1
  371. package/src/permissions/approval-policy.test.ts +6 -275
  372. package/src/permissions/approval-policy.ts +0 -51
  373. package/src/permissions/checker.test.ts +0 -1
  374. package/src/permissions/checker.ts +3 -17
  375. package/src/permissions/gateway-threshold-reader.ts +2 -0
  376. package/src/permissions/prompter.ts +34 -1
  377. package/src/permissions/secret-prompter.ts +6 -2
  378. package/src/prompts/bootstrap-cleanup.ts +27 -0
  379. package/src/prompts/system-prompt.ts +3 -18
  380. package/src/prompts/templates/SOUL.md +13 -1
  381. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  382. package/src/runtime/assistant-event-hub.ts +118 -96
  383. package/src/runtime/assistant-event.ts +1 -0
  384. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  385. package/src/runtime/auth/middleware.ts +0 -96
  386. package/src/runtime/auth/route-policy.ts +19 -0
  387. package/src/runtime/btw-sidechain.ts +2 -3
  388. package/src/runtime/channel-invite-transport.ts +2 -48
  389. package/src/runtime/channel-invite-transports/email.ts +1 -1
  390. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  391. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  392. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  393. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  394. package/src/runtime/channel-invite-types.ts +54 -0
  395. package/src/runtime/channel-readiness-service.ts +32 -13
  396. package/src/runtime/http-server.ts +3 -329
  397. package/src/runtime/http-types.ts +0 -5
  398. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  399. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  400. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  401. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  402. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  403. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  404. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  405. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  406. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  407. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  408. package/src/runtime/migrations/migration-transport.ts +7 -7
  409. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  410. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  411. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  412. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  413. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  414. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  415. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  416. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  417. package/src/runtime/pending-interactions.ts +35 -9
  418. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  419. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  420. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  421. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  422. package/src/runtime/routes/approval-interception-types.ts +13 -0
  423. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  424. package/src/runtime/routes/backup-routes.ts +15 -38
  425. package/src/runtime/routes/btw-routes.ts +14 -37
  426. package/src/runtime/routes/client-routes.ts +1 -0
  427. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  428. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  429. package/src/runtime/routes/conversation-routes.ts +30 -13
  430. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  431. package/src/runtime/routes/documents-routes.ts +30 -0
  432. package/src/runtime/routes/errors.ts +19 -4
  433. package/src/runtime/routes/events-routes.ts +12 -6
  434. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  435. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  436. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  437. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  438. package/src/runtime/routes/host-bash-routes.ts +36 -6
  439. package/src/runtime/routes/host-browser-routes.ts +108 -13
  440. package/src/runtime/routes/host-cu-routes.ts +44 -14
  441. package/src/runtime/routes/host-file-routes.ts +33 -10
  442. package/src/runtime/routes/host-transfer-routes.ts +64 -24
  443. package/src/runtime/routes/http-adapter.ts +1 -0
  444. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  445. package/src/runtime/routes/identity-routes.ts +15 -43
  446. package/src/runtime/routes/inbound-message-handler.ts +1 -9
  447. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  448. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  449. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  450. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  451. package/src/runtime/routes/index.ts +8 -0
  452. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  453. package/src/runtime/routes/memory-item-routes.ts +10 -12
  454. package/src/runtime/routes/memory-v2-routes.ts +441 -1
  455. package/src/runtime/routes/migration-routes.ts +96 -0
  456. package/src/runtime/routes/schedule-routes.ts +7 -0
  457. package/src/runtime/verification-templates.ts +4 -7
  458. package/src/schedule/integration-status.ts +66 -2
  459. package/src/schedule/recurrence-engine.ts +4 -1
  460. package/src/schedule/retry-backoff.ts +18 -0
  461. package/src/schedule/retry-policy.ts +82 -0
  462. package/src/schedule/schedule-recovery.ts +64 -0
  463. package/src/schedule/schedule-store.ts +106 -2
  464. package/src/schedule/scheduler-types.ts +25 -0
  465. package/src/schedule/scheduler.ts +63 -38
  466. package/src/security/oauth-callback-registry.ts +8 -0
  467. package/src/sequence/analytics.ts +5 -5
  468. package/src/sequence/engine.ts +1 -1
  469. package/src/skills/catalog-files.ts +2 -8
  470. package/src/skills/include-graph.ts +5 -5
  471. package/src/skills/remote-skill-policy.ts +5 -5
  472. package/src/skills/skill-file-provider.ts +1 -1
  473. package/src/skills/skill-file-types.ts +13 -0
  474. package/src/skills/skillssh-audit-types.ts +28 -0
  475. package/src/skills/skillssh-registry.ts +8 -21
  476. package/src/telemetry/types.ts +2 -0
  477. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  478. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  479. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  480. package/src/tools/apps/executors.ts +56 -69
  481. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  482. package/src/tools/browser/browser-execution.ts +2 -2
  483. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  484. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  485. package/src/tools/browser/cdp-client/factory.ts +23 -24
  486. package/src/tools/browser/cdp-client/index.ts +1 -14
  487. package/src/tools/computer-use/definitions.ts +42 -20
  488. package/src/tools/executor.ts +2 -0
  489. package/src/tools/host-filesystem/edit.ts +26 -0
  490. package/src/tools/host-filesystem/read.ts +26 -0
  491. package/src/tools/host-filesystem/transfer.ts +31 -1
  492. package/src/tools/host-filesystem/write.ts +26 -0
  493. package/src/tools/host-terminal/host-shell.ts +58 -0
  494. package/src/tools/schedule/create.ts +6 -0
  495. package/src/tools/schedule/list.ts +2 -0
  496. package/src/tools/schedule/update.ts +10 -0
  497. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  498. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  499. package/src/tools/skills/load.ts +0 -32
  500. package/src/tools/tool-approval-handler.ts +1 -5
  501. package/src/tools/types.ts +4 -0
  502. package/src/usage/pricing.ts +1 -1
  503. package/src/workspace/hatched-date.ts +86 -0
  504. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  505. package/src/workspace/migrations/006-services-config.ts +8 -5
  506. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  507. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  508. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  509. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  510. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  511. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  512. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  513. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  514. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  515. package/src/workspace/migrations/AGENTS.md +1 -1
  516. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  517. package/src/workspace/migrations/utils.ts +21 -0
  518. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  519. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  520. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  521. package/src/__tests__/twilio-rest.test.ts +0 -34
  522. package/src/backup/__tests__/backup-key.test.ts +0 -152
  523. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  524. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  525. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  526. package/src/backup/backup-key.ts +0 -137
  527. package/src/backup/backup-worker.ts +0 -472
  528. package/src/backup/offsite-writer.ts +0 -222
  529. package/src/backup/stream-crypt.ts +0 -263
  530. package/src/daemon/message-types/pairing.ts +0 -58
  531. package/src/outbound-proxy/config.ts +0 -20
  532. package/src/outbound-proxy/health.ts +0 -18
  533. package/src/outbound-proxy/types.ts +0 -150
  534. package/src/runtime/capability-tokens.ts +0 -190
  535. package/src/signals/mcp-reload.ts +0 -18
@@ -19,15 +19,17 @@ import {
19
19
  readdirSync,
20
20
  readFileSync,
21
21
  readSync,
22
+ realpathSync,
22
23
  } from "node:fs";
23
24
  import { stat, unlink } from "node:fs/promises";
24
25
  import { tmpdir } from "node:os";
25
- import { join, relative } from "node:path";
26
+ import { dirname, join, relative, resolve, sep } from "node:path";
26
27
  import { Readable } from "node:stream";
27
28
  import { pipeline } from "node:stream/promises";
28
29
  import { createGzip, gzipSync } from "node:zlib";
29
30
 
30
31
  import { sanitizeConfigForTransfer } from "../../config/sanitize-for-transfer.js";
32
+ import { getLogger } from "../../util/logger.js";
31
33
  import type { VBundleOriginMode } from "./origin-mode.js";
32
34
  import type {
33
35
  ManifestFileEntryType,
@@ -41,6 +43,8 @@ import type {
41
43
  export interface VBundleFileEntry {
42
44
  path: string;
43
45
  data: Uint8Array;
46
+ /** When set, `data` is ignored: the entry is emitted as a tar typeflag-2 (symlink) record with empty body, and `linkTarget` is the symlink target encoded relative to the symlink's own directory inside the archive. */
47
+ linkTarget?: string;
44
48
  }
45
49
 
46
50
  /** v1 manifest `assistant` block. */
@@ -109,13 +113,24 @@ interface InMemoryEntry {
109
113
  size: number;
110
114
  }
111
115
 
112
- /** Union of disk-backed and in-memory tar stream entries. */
113
- type TarStreamEntry = FileMetadata | InMemoryEntry;
116
+ /** Symlink entry emitted as a tar typeflag-2 record with empty body. */
117
+ interface SymlinkMetadata {
118
+ archivePath: string;
119
+ linkTarget: string;
120
+ size: 0;
121
+ }
122
+
123
+ /** Union of disk-backed, in-memory, and symlink tar stream entries. */
124
+ type TarStreamEntry = FileMetadata | InMemoryEntry | SymlinkMetadata;
114
125
 
115
126
  function isInMemoryEntry(entry: TarStreamEntry): entry is InMemoryEntry {
116
127
  return "data" in entry;
117
128
  }
118
129
 
130
+ function isSymlinkEntry(entry: TarStreamEntry): entry is SymlinkMetadata {
131
+ return "linkTarget" in entry;
132
+ }
133
+
119
134
  // ---------------------------------------------------------------------------
120
135
  // Hash helpers
121
136
  // ---------------------------------------------------------------------------
@@ -234,7 +249,11 @@ function createPaxPathEntry(name: string): Uint8Array {
234
249
  return result;
235
250
  }
236
251
 
237
- function createTarEntry(name: string, data: Uint8Array): Uint8Array {
252
+ function createTarEntry(
253
+ name: string,
254
+ data: Uint8Array,
255
+ linkTarget?: string,
256
+ ): Uint8Array {
238
257
  const encoder = new TextEncoder();
239
258
  const nameBytes = encoder.encode(name);
240
259
 
@@ -243,6 +262,14 @@ function createTarEntry(name: string, data: Uint8Array): Uint8Array {
243
262
  const needsPax = nameBytes.length > 100;
244
263
  const paxEntry = needsPax ? createPaxPathEntry(name) : null;
245
264
 
265
+ const isSymlink = linkTarget !== undefined;
266
+ const linkTargetBytes = isSymlink ? encoder.encode(linkTarget) : null;
267
+ if (linkTargetBytes && linkTargetBytes.length > 100) {
268
+ throw new Error(
269
+ `Symlink target "${linkTarget}" is ${linkTargetBytes.length} bytes, exceeding the ustar linkname-field 100-byte limit. The walker should guard against this case before calling createTarEntry.`,
270
+ );
271
+ }
272
+
246
273
  const header = new Uint8Array(BLOCK_SIZE);
247
274
 
248
275
  // File name (0-99) — truncated if >100 bytes; PAX header carries the full name
@@ -257,14 +284,19 @@ function createTarEntry(name: string, data: Uint8Array): Uint8Array {
257
284
  // Group ID (116-123)
258
285
  writeOctal(header, 116, 8, 0);
259
286
 
260
- // File size (124-135)
261
- writeOctal(header, 124, 12, data.length);
287
+ // File size (124-135) — symlink entries always carry size 0
288
+ writeOctal(header, 124, 12, isSymlink ? 0 : data.length);
262
289
 
263
290
  // Modification time (136-147)
264
291
  writeOctal(header, 136, 12, Math.floor(Date.now() / 1000));
265
292
 
266
- // Type flag (156): regular file
267
- header[156] = "0".charCodeAt(0);
293
+ // Type flag (156): regular file ("0") or symlink ("2")
294
+ header[156] = (isSymlink ? "2" : "0").charCodeAt(0);
295
+
296
+ // Linkname (157-256) — only set for symlinks; null-padded by default
297
+ if (linkTargetBytes) {
298
+ header.set(linkTargetBytes, 157);
299
+ }
268
300
 
269
301
  // USTAR magic (257-262)
270
302
  const magic = encoder.encode("ustar\0");
@@ -274,16 +306,22 @@ function createTarEntry(name: string, data: Uint8Array): Uint8Array {
274
306
  header[263] = "0".charCodeAt(0);
275
307
  header[264] = "0".charCodeAt(0);
276
308
 
277
- // Compute and write checksum (148-155)
309
+ // Compute and write checksum (148-155) — must be last so the linkname
310
+ // (and every other field) contributes to the sum.
278
311
  const checksum = computeHeaderChecksum(header);
279
312
  writeOctal(header, 148, 7, checksum);
280
313
  header[155] = 0x20; // trailing space
281
314
 
282
- // Combine header + padded data
283
- const paddedData = padToBlock(data);
284
- const fileEntry = new Uint8Array(header.length + paddedData.length);
285
- fileEntry.set(header, 0);
286
- fileEntry.set(paddedData, header.length);
315
+ // Symlink entries are header-only no body, no padding.
316
+ const fileEntry = isSymlink
317
+ ? header
318
+ : (() => {
319
+ const paddedData = padToBlock(data);
320
+ const combined = new Uint8Array(header.length + paddedData.length);
321
+ combined.set(header, 0);
322
+ combined.set(paddedData, header.length);
323
+ return combined;
324
+ })();
287
325
 
288
326
  if (paxEntry) {
289
327
  const result = new Uint8Array(paxEntry.length + fileEntry.length);
@@ -296,11 +334,11 @@ function createTarEntry(name: string, data: Uint8Array): Uint8Array {
296
334
  }
297
335
 
298
336
  function createTarArchive(
299
- entries: Array<{ name: string; data: Uint8Array }>,
337
+ entries: Array<{ name: string; data: Uint8Array; linkTarget?: string }>,
300
338
  ): Uint8Array {
301
339
  const parts: Uint8Array[] = [];
302
340
  for (const entry of entries) {
303
- parts.push(createTarEntry(entry.name, entry.data));
341
+ parts.push(createTarEntry(entry.name, entry.data, entry.linkTarget));
304
342
  }
305
343
  // End-of-archive: two zero blocks
306
344
  parts.push(new Uint8Array(BLOCK_SIZE * 2));
@@ -374,12 +412,22 @@ export function buildVBundle(options: BuildVBundleOptions): BuildVBundleResult {
374
412
  secretsRedacted,
375
413
  } = options;
376
414
 
377
- // Build file entries for the manifest
378
- const fileEntries: ManifestFileEntryType[] = files.map((f) => ({
379
- path: f.path,
380
- sha256: sha256Hex(f.data),
381
- size_bytes: f.data.length,
382
- }));
415
+ // Build file entries for the manifest. Symlink entries hash the link target
416
+ // string (not the empty data buffer) and declare size_bytes: 0.
417
+ const fileEntries: ManifestFileEntryType[] = files.map((f) =>
418
+ f.linkTarget !== undefined
419
+ ? {
420
+ path: f.path,
421
+ sha256: sha256Hex(f.linkTarget),
422
+ size_bytes: 0,
423
+ link_target: f.linkTarget,
424
+ }
425
+ : {
426
+ path: f.path,
427
+ sha256: sha256Hex(f.data),
428
+ size_bytes: f.data.length,
429
+ },
430
+ );
383
431
 
384
432
  const { manifest, manifestData } = buildManifestObject({
385
433
  contents: fileEntries,
@@ -391,10 +439,16 @@ export function buildVBundle(options: BuildVBundleOptions): BuildVBundleResult {
391
439
  now: new Date(),
392
440
  });
393
441
 
394
- // Build tar entries: manifest first, then all files
442
+ // Build tar entries: manifest first, then all files. Symlink entries forward
443
+ // `linkTarget` so createTarEntry emits a typeflag-2 header; `data` is unused
444
+ // in that branch but must still be a valid Uint8Array.
395
445
  const tarEntries = [
396
446
  { name: "manifest.json", data: manifestData },
397
- ...files.map((f) => ({ name: f.path, data: f.data })),
447
+ ...files.map((f) =>
448
+ f.linkTarget !== undefined
449
+ ? { name: f.path, data: new Uint8Array(0), linkTarget: f.linkTarget }
450
+ : { name: f.path, data: f.data },
451
+ ),
398
452
  ];
399
453
 
400
454
  const tar = createTarArchive(tarEntries);
@@ -410,33 +464,143 @@ export function buildVBundle(options: BuildVBundleOptions): BuildVBundleResult {
410
464
  interface WalkDirectoryOptions {
411
465
  /** Include binary files (files containing null bytes). Default: false. */
412
466
  includeBinary?: boolean;
413
- /** Directory names to skip (matched against immediate child name). */
467
+ /** Directory names to skip (matched against relative path from walk root). */
414
468
  skipDirs?: string[];
469
+ /** File names to skip (matched against the entry basename). */
470
+ skipFiles?: string[];
471
+ }
472
+
473
+ /**
474
+ * Resolve and classify a symlink encountered during a walk.
475
+ *
476
+ * Returns one of:
477
+ * { kind: "class1", linkTarget } — emit as a tar typeflag-2 entry whose
478
+ * `linkname` field holds `linkTarget` (the symlink target encoded as a
479
+ * POSIX path relative to the symlink's own directory).
480
+ * { kind: "drop", reason } — drop the symlink. Reasons cover broken
481
+ * links, targets outside the workspace (class 2), targets inside a
482
+ * skipped directory (class 3), directory targets (out of scope), and
483
+ * link targets whose UTF-8 encoding exceeds the 100-byte ustar
484
+ * `linkname` field limit.
485
+ */
486
+ type SymlinkClassification =
487
+ | { kind: "class1"; linkTarget: string }
488
+ | { kind: "drop"; reason: string };
489
+
490
+ function classifySymlink(args: {
491
+ fullPath: string;
492
+ walkRoot: string;
493
+ skipDirs: readonly string[];
494
+ }): SymlinkClassification {
495
+ const { fullPath, walkRoot, skipDirs } = args;
496
+
497
+ let absoluteTarget: string;
498
+ try {
499
+ absoluteTarget = realpathSync(fullPath);
500
+ } catch {
501
+ return { kind: "drop", reason: "broken symlink (realpath failed)" };
502
+ }
503
+
504
+ let targetStat;
505
+ try {
506
+ targetStat = lstatSync(absoluteTarget);
507
+ } catch {
508
+ return { kind: "drop", reason: "broken symlink (target stat failed)" };
509
+ }
510
+ if (!targetStat.isFile()) {
511
+ return { kind: "drop", reason: "target is not a regular file" };
512
+ }
513
+
514
+ let dirAbs: string;
515
+ try {
516
+ dirAbs = realpathSync(walkRoot);
517
+ } catch {
518
+ dirAbs = resolve(walkRoot);
519
+ }
520
+ const targetAbs = resolve(absoluteTarget);
521
+ const insideWorkspace =
522
+ targetAbs === dirAbs || targetAbs.startsWith(dirAbs + sep);
523
+ if (!insideWorkspace) {
524
+ return { kind: "drop", reason: "target outside workspace" };
525
+ }
526
+
527
+ const targetRelToWorkspace = relative(dirAbs, targetAbs);
528
+ if (
529
+ skipDirs.some(
530
+ (s) =>
531
+ targetRelToWorkspace === s || targetRelToWorkspace.startsWith(s + "/"),
532
+ )
533
+ ) {
534
+ return { kind: "drop", reason: "target inside skipDir" };
535
+ }
536
+
537
+ // Canonicalize the symlink's parent directory so the relative linkTarget
538
+ // computation lines up with `absoluteTarget` (which is canonical from
539
+ // realpathSync). On macOS, walking through /var/folders/... and resolving
540
+ // the target through /private/var/folders/... would otherwise produce a
541
+ // long ../../../private/... path that exceeds the 100-byte ustar limit.
542
+ let parentAbs: string;
543
+ try {
544
+ parentAbs = realpathSync(dirname(fullPath));
545
+ } catch {
546
+ parentAbs = resolve(dirname(fullPath));
547
+ }
548
+ const linkTarget = relative(parentAbs, absoluteTarget);
549
+ if (new TextEncoder().encode(linkTarget).length > 100) {
550
+ return {
551
+ kind: "drop",
552
+ reason: "encoded link target exceeds 100-byte ustar limit",
553
+ };
554
+ }
555
+
556
+ return { kind: "class1", linkTarget };
415
557
  }
416
558
 
417
559
  /**
418
- * Recursively walk a directory and return all non-symlink files as
419
- * VBundleFileEntry objects with paths prefixed by `archivePrefix`.
560
+ * Recursively walk a directory and return all regular files (and bundleable
561
+ * symlinks) as VBundleFileEntry objects with paths prefixed by
562
+ * `archivePrefix`. Symlinks that resolve to a regular file inside the walk
563
+ * root and outside any skipDir are emitted as typeflag-2 entries (data
564
+ * empty, `linkTarget` populated). All other symlinks (broken, directory
565
+ * target, target outside workspace, target inside skipDir, encoded
566
+ * linkTarget over 100 bytes) are reported via the returned `droppedSymlinks`
567
+ * array as workspace-relative paths of the symlink itself.
420
568
  *
421
569
  * By default, binary files (detected via null-byte heuristic in the first
422
570
  * 8 KB) are skipped. Pass `includeBinary: true` to include them.
423
571
  */
424
- function walkDirectory(
572
+ export function walkDirectory(
425
573
  dir: string,
426
574
  archivePrefix: string,
427
575
  options: WalkDirectoryOptions = {},
428
- ): VBundleFileEntry[] {
429
- const { includeBinary = false, skipDirs = [] } = options;
576
+ ): { files: VBundleFileEntry[]; droppedSymlinks: string[] } {
577
+ const { includeBinary = false, skipDirs = [], skipFiles = [] } = options;
430
578
  const entries: VBundleFileEntry[] = [];
579
+ const droppedSymlinks: string[] = [];
431
580
 
432
581
  function walk(currentDir: string): void {
433
582
  const dirEntries = readdirSync(currentDir, { withFileTypes: true });
434
583
  for (const entry of dirEntries) {
435
584
  const fullPath = join(currentDir, entry.name);
436
585
 
437
- // Skip symlinks
438
586
  const stat = lstatSync(fullPath);
439
- if (stat.isSymbolicLink()) continue;
587
+ if (stat.isSymbolicLink()) {
588
+ const classification = classifySymlink({
589
+ fullPath,
590
+ walkRoot: dir,
591
+ skipDirs,
592
+ });
593
+ if (classification.kind === "class1") {
594
+ entries.push({
595
+ path: `${archivePrefix}/${relative(dir, fullPath)}`,
596
+ data: new Uint8Array(0),
597
+ linkTarget: classification.linkTarget,
598
+ });
599
+ } else {
600
+ droppedSymlinks.push(relative(dir, fullPath));
601
+ }
602
+ continue;
603
+ }
440
604
 
441
605
  if (stat.isDirectory()) {
442
606
  // Check skip list against the relative path from the walk root
@@ -446,6 +610,9 @@ function walkDirectory(
446
610
  }
447
611
  walk(fullPath);
448
612
  } else if (stat.isFile()) {
613
+ // Skip files by basename (e.g. backup key)
614
+ if (skipFiles.includes(entry.name)) continue;
615
+
449
616
  // Skip SQLite auxiliary files — these are ephemeral and race-prone
450
617
  // with the live DB connection. The WAL is checkpointed before the
451
618
  // walk, so the main .db file has all committed rows.
@@ -482,7 +649,7 @@ function walkDirectory(
482
649
  }
483
650
 
484
651
  walk(dir);
485
- return entries;
652
+ return { files: entries, droppedSymlinks };
486
653
  }
487
654
 
488
655
  // ---------------------------------------------------------------------------
@@ -561,12 +728,22 @@ export function buildExportVBundle(
561
728
  existsSync(workspaceDir) &&
562
729
  lstatSync(workspaceDir).isDirectory()
563
730
  ) {
564
- files.push(
565
- ...walkDirectory(workspaceDir, "workspace", {
731
+ const { files: walkedFiles, droppedSymlinks } = walkDirectory(
732
+ workspaceDir,
733
+ "workspace",
734
+ {
566
735
  includeBinary: true,
567
736
  skipDirs: ["embedding-models", "data/qdrant", "signals", "deprecated"],
568
- }),
737
+ skipFiles: [".backup.key"],
738
+ },
569
739
  );
740
+ files.push(...walkedFiles);
741
+ if (droppedSymlinks.length > 0) {
742
+ getLogger("vbundle-builder").warn(
743
+ { count: droppedSymlinks.length, paths: droppedSymlinks },
744
+ `Dropped ${droppedSymlinks.length} symlinks pointing outside workspace or into skipped directories`,
745
+ );
746
+ }
570
747
  }
571
748
 
572
749
  // Sanitize workspace/config.json to strip environment-specific fields
@@ -601,26 +778,48 @@ export function buildExportVBundle(
601
778
 
602
779
  /**
603
780
  * Walk a directory tree and collect file metadata (paths + sizes) without
604
- * reading file contents into memory. Uses the same filtering logic as
605
- * `walkDirectory` (symlink skip, SQLite auxiliary skip, binary detection,
606
- * skip dirs).
781
+ * reading file contents into memory. Mirrors `walkDirectory`'s filtering
782
+ * logic (SQLite auxiliary skip, binary detection, skipDirs) and symlink
783
+ * classification — bundleable symlinks are emitted as `SymlinkMetadata`
784
+ * entries; non-bundleable symlinks are reported via `droppedSymlinks`.
607
785
  */
608
- function walkDirectoryForMetadata(
786
+ export function walkDirectoryForMetadata(
609
787
  dir: string,
610
788
  archivePrefix: string,
611
789
  options: WalkDirectoryOptions = {},
612
- ): FileMetadata[] {
613
- const { includeBinary = false, skipDirs = [] } = options;
790
+ ): {
791
+ files: FileMetadata[];
792
+ symlinks: SymlinkMetadata[];
793
+ droppedSymlinks: string[];
794
+ } {
795
+ const { includeBinary = false, skipDirs = [], skipFiles = [] } = options;
614
796
  const entries: FileMetadata[] = [];
797
+ const symlinks: SymlinkMetadata[] = [];
798
+ const droppedSymlinks: string[] = [];
615
799
 
616
800
  function walk(currentDir: string): void {
617
801
  const dirEntries = readdirSync(currentDir, { withFileTypes: true });
618
802
  for (const entry of dirEntries) {
619
803
  const fullPath = join(currentDir, entry.name);
620
804
 
621
- // Skip symlinks
622
805
  const fileStat = lstatSync(fullPath);
623
- if (fileStat.isSymbolicLink()) continue;
806
+ if (fileStat.isSymbolicLink()) {
807
+ const classification = classifySymlink({
808
+ fullPath,
809
+ walkRoot: dir,
810
+ skipDirs,
811
+ });
812
+ if (classification.kind === "class1") {
813
+ symlinks.push({
814
+ archivePath: `${archivePrefix}/${relative(dir, fullPath)}`,
815
+ linkTarget: classification.linkTarget,
816
+ size: 0,
817
+ });
818
+ } else {
819
+ droppedSymlinks.push(relative(dir, fullPath));
820
+ }
821
+ continue;
822
+ }
624
823
 
625
824
  if (fileStat.isDirectory()) {
626
825
  // Check skip list against the relative path from the walk root
@@ -630,6 +829,9 @@ function walkDirectoryForMetadata(
630
829
  }
631
830
  walk(fullPath);
632
831
  } else if (fileStat.isFile()) {
832
+ // Skip files by basename (e.g. backup key)
833
+ if (skipFiles.includes(entry.name)) continue;
834
+
633
835
  // Skip SQLite auxiliary files — these are ephemeral and race-prone
634
836
  if (
635
837
  entry.name.endsWith(".db-wal") ||
@@ -673,7 +875,7 @@ function walkDirectoryForMetadata(
673
875
  }
674
876
 
675
877
  walk(dir);
676
- return entries;
878
+ return { files: entries, symlinks, droppedSymlinks };
677
879
  }
678
880
 
679
881
  /**
@@ -699,8 +901,18 @@ async function computeFileSha256(
699
901
  /**
700
902
  * Create just the 512-byte tar header block for a regular file entry.
701
903
  * Extracted from `createTarEntry` logic — does NOT include data or padding.
904
+ *
905
+ * When `linkTarget` is provided, the header is emitted as a tar typeflag-2
906
+ * (symlink) record: typeflag is "2", the link target is written into the
907
+ * `linkname` field (header[157..256], 100-byte limit), and `size` is forced
908
+ * to 0 in the header field. Caller is responsible for not yielding any body
909
+ * or padding bytes for symlink entries.
702
910
  */
703
- function createTarHeaderBlock(name: string, size: number): Uint8Array {
911
+ function createTarHeaderBlock(
912
+ name: string,
913
+ size: number,
914
+ linkTarget?: string,
915
+ ): Uint8Array {
704
916
  const encoder = new TextEncoder();
705
917
  const nameBytes = encoder.encode(name);
706
918
 
@@ -718,14 +930,26 @@ function createTarHeaderBlock(name: string, size: number): Uint8Array {
718
930
  // Group ID (116-123)
719
931
  writeOctal(header, 116, 8, 0);
720
932
 
721
- // File size (124-135)
722
- writeOctal(header, 124, 12, size);
933
+ // File size (124-135) — symlink entries always declare size=0
934
+ writeOctal(header, 124, 12, linkTarget !== undefined ? 0 : size);
723
935
 
724
936
  // Modification time (136-147)
725
937
  writeOctal(header, 136, 12, Math.floor(Date.now() / 1000));
726
938
 
727
- // Type flag (156): regular file
728
- header[156] = "0".charCodeAt(0);
939
+ // Type flag (156): regular file ("0") or symlink ("2")
940
+ header[156] =
941
+ linkTarget !== undefined ? "2".charCodeAt(0) : "0".charCodeAt(0);
942
+
943
+ // Linkname (157-256, 100 bytes) — only set for symlink entries
944
+ if (linkTarget !== undefined) {
945
+ const linkBytes = encoder.encode(linkTarget);
946
+ if (linkBytes.length > 100) {
947
+ throw new Error(
948
+ `symlink target exceeds 100-byte ustar linkname limit (${linkBytes.length} bytes): ${linkTarget}`,
949
+ );
950
+ }
951
+ header.set(linkBytes, 157);
952
+ }
729
953
 
730
954
  // USTAR magic (257-262)
731
955
  const magic = encoder.encode("ustar\0");
@@ -735,7 +959,8 @@ function createTarHeaderBlock(name: string, size: number): Uint8Array {
735
959
  header[263] = "0".charCodeAt(0);
736
960
  header[264] = "0".charCodeAt(0);
737
961
 
738
- // Compute and write checksum (148-155)
962
+ // Compute and write checksum (148-155). Must run AFTER linkname is set
963
+ // so the checksum covers the symlink target bytes.
739
964
  const checksum = computeHeaderChecksum(header);
740
965
  writeOctal(header, 148, 7, checksum);
741
966
  header[155] = 0x20; // trailing space
@@ -747,13 +972,21 @@ function createTarHeaderBlock(name: string, size: number): Uint8Array {
747
972
  * If name exceeds 100 bytes, returns the PAX extended header entry
748
973
  * concatenated with the regular header block. Otherwise returns just
749
974
  * the header block.
975
+ *
976
+ * `linkTarget` is forwarded to `createTarHeaderBlock` so symlink entries
977
+ * still get a PAX path header for long names while emitting a typeflag-2
978
+ * ustar block.
750
979
  */
751
- function createPaxAndHeaderBlocks(name: string, size: number): Uint8Array {
980
+ function createPaxAndHeaderBlocks(
981
+ name: string,
982
+ size: number,
983
+ linkTarget?: string,
984
+ ): Uint8Array {
752
985
  const encoder = new TextEncoder();
753
986
  const nameBytes = encoder.encode(name);
754
987
  const needsPax = nameBytes.length > 100;
755
988
 
756
- const header = createTarHeaderBlock(name, size);
989
+ const header = createTarHeaderBlock(name, size, linkTarget);
757
990
 
758
991
  if (needsPax) {
759
992
  const paxEntry = createPaxPathEntry(name);
@@ -791,7 +1024,15 @@ async function* generateTarStream(
791
1024
 
792
1025
  // File entries
793
1026
  for (const file of files) {
794
- const entrySize = isInMemoryEntry(file) ? file.size : file.size;
1027
+ if (isSymlinkEntry(file)) {
1028
+ // Symlink entry: typeflag-2 header carries the linkname; no body, no
1029
+ // padding. Skip the entrySize/body/padding logic entirely so the
1030
+ // surrounding stream stays block-aligned.
1031
+ yield createPaxAndHeaderBlocks(file.archivePath, 0, file.linkTarget);
1032
+ continue;
1033
+ }
1034
+
1035
+ const entrySize = file.size;
795
1036
  yield createPaxAndHeaderBlocks(file.archivePath, entrySize);
796
1037
 
797
1038
  if (isInMemoryEntry(file)) {
@@ -885,6 +1126,7 @@ export async function streamExportVBundle(
885
1126
  }
886
1127
 
887
1128
  const allFileMetadata: FileMetadata[] = [];
1129
+ const symlinkEntries: SymlinkMetadata[] = [];
888
1130
 
889
1131
  // Walk the entire workspace directory, including binary files
890
1132
  if (
@@ -892,12 +1134,23 @@ export async function streamExportVBundle(
892
1134
  existsSync(workspaceDir) &&
893
1135
  lstatSync(workspaceDir).isDirectory()
894
1136
  ) {
895
- allFileMetadata.push(
896
- ...walkDirectoryForMetadata(workspaceDir, "workspace", {
897
- includeBinary: true,
898
- skipDirs: ["embedding-models", "data/qdrant", "signals", "deprecated"],
899
- }),
900
- );
1137
+ const {
1138
+ files: walkedFiles,
1139
+ symlinks: walkedSymlinks,
1140
+ droppedSymlinks,
1141
+ } = walkDirectoryForMetadata(workspaceDir, "workspace", {
1142
+ includeBinary: true,
1143
+ skipDirs: ["embedding-models", "data/qdrant", "signals", "deprecated"],
1144
+ skipFiles: [".backup.key"],
1145
+ });
1146
+ allFileMetadata.push(...walkedFiles);
1147
+ symlinkEntries.push(...walkedSymlinks);
1148
+ if (droppedSymlinks.length > 0) {
1149
+ getLogger("vbundle-builder").warn(
1150
+ { count: droppedSymlinks.length, paths: droppedSymlinks },
1151
+ `Dropped ${droppedSymlinks.length} symlinks pointing outside workspace or into skipped directories`,
1152
+ );
1153
+ }
901
1154
  }
902
1155
 
903
1156
  // Sanitize workspace/config.json: read from disk, sanitize, and replace the
@@ -960,6 +1213,19 @@ export async function streamExportVBundle(
960
1213
  });
961
1214
  }
962
1215
 
1216
+ // Add symlink entries to the manifest. The sha256 is computed over the
1217
+ // link target string (UTF-8 encoded) so the streaming validator can
1218
+ // verify the manifest declared the same target the tar header carries.
1219
+ // size_bytes is always 0 for symlink entries.
1220
+ for (const entry of symlinkEntries) {
1221
+ fileEntries.push({
1222
+ path: entry.archivePath,
1223
+ sha256: sha256Hex(entry.linkTarget),
1224
+ size_bytes: 0,
1225
+ link_target: entry.linkTarget,
1226
+ });
1227
+ }
1228
+
963
1229
  const { manifest, manifestData } = buildManifestObject({
964
1230
  contents: fileEntries,
965
1231
  assistant,
@@ -980,6 +1246,7 @@ export async function streamExportVBundle(
980
1246
  ...allFileMetadata,
981
1247
  ...sanitizedConfigEntries,
982
1248
  ...inMemoryEntries,
1249
+ ...symlinkEntries,
983
1250
  ];
984
1251
  const tarGenerator = generateTarStream(manifestData, allEntries);
985
1252
  const tarReadable = Readable.from(tarGenerator);
@@ -44,9 +44,9 @@ const LEGACY_USER_MD_ARCHIVE_PATH = "prompts/USER.md";
44
44
  // Public types
45
45
  // ---------------------------------------------------------------------------
46
46
 
47
- export type ImportAction = "create" | "overwrite" | "unchanged" | "skip";
47
+ type ImportAction = "create" | "overwrite" | "unchanged" | "skip";
48
48
 
49
- export interface ImportFileReport {
49
+ interface ImportFileReport {
50
50
  /** Archive path (e.g. "data/db/assistant.db") */
51
51
  path: string;
52
52
  /** What would happen to this file on import */
@@ -61,7 +61,7 @@ export interface ImportFileReport {
61
61
  current_sha256: string | null;
62
62
  }
63
63
 
64
- export interface ImportConflict {
64
+ interface ImportConflict {
65
65
  code: string;
66
66
  message: string;
67
67
  path?: string;
@@ -231,7 +231,7 @@ function sha256Hex(data: Uint8Array): string {
231
231
  // Core analyzer
232
232
  // ---------------------------------------------------------------------------
233
233
 
234
- export interface AnalyzeImportOptions {
234
+ interface AnalyzeImportOptions {
235
235
  /** The parsed and validated manifest from the bundle */
236
236
  manifest: ManifestType;
237
237
  /** Resolves archive paths to disk paths for comparison */