@vellumai/assistant 0.6.4 → 0.6.6

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 (1008) hide show
  1. package/.prettierignore +5 -0
  2. package/AGENTS.md +9 -1
  3. package/ARCHITECTURE.md +43 -49
  4. package/Dockerfile +17 -3
  5. package/README.md +3 -4
  6. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  7. package/bun.lock +8 -3
  8. package/docs/architecture/integrations.md +33 -59
  9. package/docs/architecture/memory.md +25 -30
  10. package/docs/architecture/security.md +19 -18
  11. package/docs/browser-use-architecture-phase2.md +63 -20
  12. package/docs/error-handling.md +111 -0
  13. package/docs/plugins.md +761 -0
  14. package/docs/skills.md +10 -10
  15. package/docs/stt-provider-onboarding.md +2 -1
  16. package/examples/plugins/echo/README.md +132 -0
  17. package/examples/plugins/echo/package.json +17 -0
  18. package/examples/plugins/echo/register.ts +187 -0
  19. package/knip.json +9 -2
  20. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  21. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  22. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  23. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  24. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  25. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  26. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  27. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  28. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  29. package/openapi.yaml +334 -78
  30. package/package.json +6 -3
  31. package/scripts/generate-openapi.ts +50 -11
  32. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  33. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  34. package/src/__tests__/agent-loop.test.ts +112 -1
  35. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  36. package/src/__tests__/anthropic-provider.test.ts +171 -2
  37. package/src/__tests__/app-compiler.test.ts +57 -0
  38. package/src/__tests__/approval-cascade.test.ts +36 -10
  39. package/src/__tests__/approval-routes-http.test.ts +134 -10
  40. package/src/__tests__/assistant-attachments.test.ts +44 -0
  41. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  42. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  43. package/src/__tests__/avatar-generator.test.ts +4 -2
  44. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  45. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  46. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  47. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  48. package/src/__tests__/btw-routes.test.ts +47 -1
  49. package/src/__tests__/bundled-asset.test.ts +6 -6
  50. package/src/__tests__/call-controller.test.ts +1 -2
  51. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  52. package/src/__tests__/catalog-cache.test.ts +96 -4
  53. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  54. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  55. package/src/__tests__/checker.test.ts +870 -655
  56. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  57. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  58. package/src/__tests__/compaction-events.test.ts +501 -0
  59. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  60. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  61. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  62. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  63. package/src/__tests__/config-analysis.test.ts +11 -28
  64. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  65. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  66. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  67. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  68. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  69. package/src/__tests__/config-schema.test.ts +440 -114
  70. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  71. package/src/__tests__/config-watcher.test.ts +2 -2
  72. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  73. package/src/__tests__/contacts-tools.test.ts +26 -0
  74. package/src/__tests__/contacts-write.test.ts +4 -4
  75. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  76. package/src/__tests__/context-token-estimator.test.ts +191 -1
  77. package/src/__tests__/context-window-manager.test.ts +883 -4
  78. package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
  79. package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
  80. package/src/__tests__/conversation-agent-loop.test.ts +435 -216
  81. package/src/__tests__/conversation-attachments.test.ts +1 -1
  82. package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
  83. package/src/__tests__/conversation-error.test.ts +37 -6
  84. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  85. package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
  86. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  87. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  88. package/src/__tests__/conversation-pairing.test.ts +174 -10
  89. package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
  90. package/src/__tests__/conversation-process-callsite.test.ts +309 -0
  91. package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
  92. package/src/__tests__/conversation-queue.test.ts +68 -38
  93. package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
  94. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  95. package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
  96. package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
  97. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  98. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  99. package/src/__tests__/conversation-slash-queue.test.ts +39 -19
  100. package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
  101. package/src/__tests__/conversation-speed-override.test.ts +36 -12
  102. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  103. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  104. package/src/__tests__/conversation-title-service.test.ts +118 -2
  105. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  106. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  107. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  108. package/src/__tests__/conversation-usage.test.ts +4 -2
  109. package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
  110. package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
  111. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
  112. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  113. package/src/__tests__/credential-health-service.test.ts +78 -9
  114. package/src/__tests__/credential-security-invariants.test.ts +5 -2
  115. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  116. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  117. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  118. package/src/__tests__/credentials-cli.test.ts +1 -9
  119. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  120. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  121. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  122. package/src/__tests__/delete-propagation.test.ts +437 -0
  123. package/src/__tests__/dm-backfill.test.ts +417 -0
  124. package/src/__tests__/dm-persistence.test.ts +227 -0
  125. package/src/__tests__/edit-propagation.test.ts +280 -0
  126. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  127. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  128. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  129. package/src/__tests__/estimator-calibration.test.ts +213 -0
  130. package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
  131. package/src/__tests__/file-write-tool.test.ts +151 -1
  132. package/src/__tests__/filing-service.test.ts +255 -0
  133. package/src/__tests__/first-greeting.test.ts +247 -5
  134. package/src/__tests__/gemini-provider.test.ts +0 -3
  135. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  136. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  137. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  138. package/src/__tests__/heartbeat-service.test.ts +96 -15
  139. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  140. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  141. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  142. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  143. package/src/__tests__/host-shell-tool.test.ts +124 -18
  144. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  145. package/src/__tests__/image-credentials.test.ts +137 -0
  146. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  147. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  148. package/src/__tests__/injector-chain.test.ts +526 -0
  149. package/src/__tests__/intent-routing.test.ts +1 -66
  150. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  151. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  152. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  153. package/src/__tests__/llm-resolver.test.ts +214 -0
  154. package/src/__tests__/llm-schema.test.ts +223 -0
  155. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  156. package/src/__tests__/media-generate-image.test.ts +119 -13
  157. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  158. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  159. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  160. package/src/__tests__/migration-import-from-url.test.ts +621 -0
  161. package/src/__tests__/model-intents.test.ts +11 -83
  162. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  163. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  164. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  165. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  166. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  167. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  168. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  169. package/src/__tests__/oauth-cli.test.ts +14 -12
  170. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  171. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  172. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  173. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  174. package/src/__tests__/oauth-store.test.ts +46 -78
  175. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  176. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  177. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  178. package/src/__tests__/openai-image-service.test.ts +368 -0
  179. package/src/__tests__/openai-provider.test.ts +7 -0
  180. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  181. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  182. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  183. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  184. package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
  185. package/src/__tests__/permission-mode.test.ts +16 -0
  186. package/src/__tests__/permission-types.test.ts +0 -1
  187. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  188. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  189. package/src/__tests__/persona-resolver.test.ts +13 -13
  190. package/src/__tests__/pipeline-runner.test.ts +565 -0
  191. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  192. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  193. package/src/__tests__/platform.test.ts +5 -2
  194. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  195. package/src/__tests__/plugin-registry.test.ts +273 -0
  196. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  197. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  198. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  199. package/src/__tests__/plugin-types.test.ts +320 -0
  200. package/src/__tests__/pricing.test.ts +93 -14
  201. package/src/__tests__/profiler-routes.test.ts +1 -1
  202. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  203. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  204. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  205. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  206. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  207. package/src/__tests__/proxy-approval-callback.test.ts +69 -9
  208. package/src/__tests__/reaction-persistence.test.ts +561 -0
  209. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  210. package/src/__tests__/registry.test.ts +0 -2
  211. package/src/__tests__/relay-server.test.ts +1 -1
  212. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  213. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  214. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  215. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  216. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  217. package/src/__tests__/schedule-routes.test.ts +131 -1
  218. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  219. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  220. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  221. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  222. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  223. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  224. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  225. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  226. package/src/__tests__/server-history-render.test.ts +31 -0
  227. package/src/__tests__/shell-identity.test.ts +0 -134
  228. package/src/__tests__/shell-parser-property.test.ts +13 -13
  229. package/src/__tests__/skill-cache-store.test.ts +182 -0
  230. package/src/__tests__/skills.test.ts +19 -33
  231. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  232. package/src/__tests__/slack-skill.test.ts +3 -8
  233. package/src/__tests__/starter-bundle.test.ts +35 -0
  234. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  235. package/src/__tests__/suggestion-routes.test.ts +259 -3
  236. package/src/__tests__/system-prompt.test.ts +22 -35
  237. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  238. package/src/__tests__/task-runner.test.ts +3 -1
  239. package/src/__tests__/task-scheduler.test.ts +3 -15
  240. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  241. package/src/__tests__/terminal-tools.test.ts +8 -0
  242. package/src/__tests__/test-preload.ts +11 -0
  243. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  244. package/src/__tests__/thread-backfill.test.ts +941 -0
  245. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  246. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  247. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  248. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  249. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
  250. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  251. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  252. package/src/__tests__/tool-executor.test.ts +201 -94
  253. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  254. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  255. package/src/__tests__/trust-store.test.ts +442 -109
  256. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  257. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  258. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  259. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  260. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  261. package/src/__tests__/volume-security-guard.test.ts +3 -2
  262. package/src/__tests__/web-search-history.test.ts +337 -0
  263. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  264. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  265. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  266. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  267. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  268. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  269. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  270. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  271. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  272. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  273. package/src/__tests__/workspace-policy.test.ts +22 -16
  274. package/src/acp/client-handler.ts +1 -2
  275. package/src/agent/loop.ts +545 -115
  276. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  277. package/src/approvals/guardian-request-resolvers.ts +80 -0
  278. package/src/avatar/resvg-lazy.test.ts +136 -0
  279. package/src/avatar/resvg-lazy.ts +82 -9
  280. package/src/avatar/traits-png-sync.ts +21 -1
  281. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  282. package/src/backup/backup-worker.ts +3 -15
  283. package/src/browser/__tests__/operations.test.ts +163 -0
  284. package/src/browser/identifiers.ts +51 -0
  285. package/src/browser/operations.ts +660 -0
  286. package/src/browser/types.ts +81 -0
  287. package/src/bundler/app-compiler.ts +84 -1
  288. package/src/calls/call-state.ts +2 -2
  289. package/src/calls/guardian-question-copy.ts +2 -2
  290. package/src/calls/telephony-stt-routing.ts +1 -1
  291. package/src/calls/voice-session-bridge.ts +1 -0
  292. package/src/channels/__tests__/types.test.ts +3 -3
  293. package/src/channels/types.ts +6 -4
  294. package/src/cli/AGENTS.md +1 -1
  295. package/src/cli/__tests__/notifications.test.ts +87 -211
  296. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  297. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  298. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  299. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  300. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  301. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  302. package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
  303. package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
  304. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  305. package/src/cli/commands/__tests__/task.test.ts +913 -0
  306. package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
  307. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  308. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  309. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  310. package/src/cli/commands/attachment.ts +182 -0
  311. package/src/cli/commands/backup.ts +2 -2
  312. package/src/cli/commands/browser.ts +350 -0
  313. package/src/cli/commands/cache.ts +341 -0
  314. package/src/cli/commands/clients.ts +138 -0
  315. package/src/cli/commands/completions.ts +2 -12
  316. package/src/cli/commands/config.ts +6 -6
  317. package/src/cli/commands/conversations-import.ts +347 -0
  318. package/src/cli/commands/conversations.ts +69 -8
  319. package/src/cli/commands/email.ts +234 -194
  320. package/src/cli/commands/image-generation.ts +299 -0
  321. package/src/cli/commands/inference.ts +200 -0
  322. package/src/cli/commands/memory.ts +127 -17
  323. package/src/cli/commands/notifications.ts +68 -103
  324. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  325. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  326. package/src/cli/commands/oauth/connect.ts +2 -2
  327. package/src/cli/commands/oauth/providers.ts +176 -8
  328. package/src/cli/commands/oauth/status.ts +46 -36
  329. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  330. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  331. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  332. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  333. package/src/cli/commands/skills.ts +3 -4
  334. package/src/cli/commands/stt.ts +339 -0
  335. package/src/cli/commands/task.ts +795 -0
  336. package/src/cli/commands/trust.ts +50 -19
  337. package/src/cli/commands/tts.ts +273 -0
  338. package/src/cli/commands/ui.ts +670 -0
  339. package/src/cli/commands/watchers.ts +509 -0
  340. package/src/cli/lib/daemon-credential-client.ts +0 -19
  341. package/src/cli/program.ts +39 -24
  342. package/src/cli.ts +0 -37
  343. package/src/config/__tests__/backup-schema.test.ts +7 -2
  344. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  345. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  346. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  347. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  348. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  349. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  350. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  351. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  352. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  353. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  354. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  355. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  356. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
  357. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  358. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  359. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
  360. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  361. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  362. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  363. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  364. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  365. package/src/config/bundled-tool-registry.ts +0 -190
  366. package/src/config/env.ts +7 -2
  367. package/src/config/feature-flag-registry.json +42 -10
  368. package/src/config/llm-resolver.ts +128 -0
  369. package/src/config/loader.ts +194 -10
  370. package/src/config/raw-config-utils.ts +30 -2
  371. package/src/config/sanitize-for-transfer.ts +35 -0
  372. package/src/config/schema.ts +49 -41
  373. package/src/config/schemas/analysis.ts +3 -22
  374. package/src/config/schemas/backup.ts +1 -1
  375. package/src/config/schemas/calls.ts +0 -4
  376. package/src/config/schemas/conversations.ts +16 -0
  377. package/src/config/schemas/filing.ts +2 -7
  378. package/src/config/schemas/heartbeat.ts +0 -5
  379. package/src/config/schemas/inference.ts +3 -23
  380. package/src/config/schemas/llm.ts +317 -0
  381. package/src/config/schemas/memory-processing.ts +1 -9
  382. package/src/config/schemas/notifications.ts +4 -11
  383. package/src/config/schemas/platform.ts +3 -9
  384. package/src/config/schemas/security.ts +33 -0
  385. package/src/config/schemas/services.ts +9 -4
  386. package/src/config/schemas/stt.ts +1 -0
  387. package/src/config/schemas/tts.ts +64 -0
  388. package/src/config/schemas/updates.ts +1 -1
  389. package/src/config/schemas/workspace-git.ts +3 -40
  390. package/src/config/skill-state.ts +6 -2
  391. package/src/config/skills.ts +96 -7
  392. package/src/context/__tests__/compact-prompt.test.ts +63 -0
  393. package/src/context/__tests__/microcompact.test.ts +805 -0
  394. package/src/context/estimator-calibration.ts +136 -0
  395. package/src/context/microcompact.ts +443 -0
  396. package/src/context/prompts/compact.md +26 -0
  397. package/src/context/token-estimator.ts +61 -3
  398. package/src/context/tool-result-truncation.ts +3 -63
  399. package/src/context/window-manager.ts +417 -39
  400. package/src/credential-execution/approval-bridge.ts +0 -1
  401. package/src/credential-execution/executable-discovery.ts +19 -8
  402. package/src/credential-execution/process-manager.test.ts +109 -0
  403. package/src/credential-execution/process-manager.ts +65 -2
  404. package/src/credential-health/credential-health-service.ts +19 -6
  405. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  406. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  407. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  408. package/src/daemon/approval-generators.ts +29 -4
  409. package/src/daemon/assistant-attachments.ts +24 -13
  410. package/src/daemon/classifier.ts +2 -2
  411. package/src/daemon/config-watcher.ts +0 -3
  412. package/src/daemon/context-overflow-policy.ts +4 -13
  413. package/src/daemon/context-overflow-reducer.ts +4 -1
  414. package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
  415. package/src/daemon/conversation-agent-loop.ts +1282 -599
  416. package/src/daemon/conversation-attachments.ts +2 -6
  417. package/src/daemon/conversation-error.ts +36 -1
  418. package/src/daemon/conversation-history.ts +10 -19
  419. package/src/daemon/conversation-lifecycle.ts +59 -17
  420. package/src/daemon/conversation-messaging.ts +73 -4
  421. package/src/daemon/conversation-notifiers.ts +2 -110
  422. package/src/daemon/conversation-process.ts +24 -11
  423. package/src/daemon/conversation-queue-manager.ts +3 -0
  424. package/src/daemon/conversation-runtime-assembly.ts +1063 -211
  425. package/src/daemon/conversation-slash.ts +2 -2
  426. package/src/daemon/conversation-surfaces.ts +389 -1
  427. package/src/daemon/conversation-tool-setup.ts +51 -9
  428. package/src/daemon/conversation-usage.ts +1 -1
  429. package/src/daemon/conversation.ts +197 -64
  430. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  431. package/src/daemon/external-skills-bootstrap.ts +41 -0
  432. package/src/daemon/first-greeting.ts +191 -14
  433. package/src/daemon/guardian-action-generators.ts +34 -14
  434. package/src/daemon/handlers/config-model.test.ts +86 -0
  435. package/src/daemon/handlers/config-model.ts +65 -12
  436. package/src/daemon/handlers/conversations.ts +9 -2
  437. package/src/daemon/handlers/shared.ts +39 -11
  438. package/src/daemon/handlers/skills.ts +7 -3
  439. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  440. package/src/daemon/lifecycle.ts +109 -82
  441. package/src/daemon/message-types/computer-use.ts +2 -34
  442. package/src/daemon/message-types/conversations.ts +63 -0
  443. package/src/daemon/message-types/messages.ts +21 -1
  444. package/src/daemon/message-types/trust.ts +0 -2
  445. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  446. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  447. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  448. package/src/daemon/pkb-context-tracker.ts +125 -0
  449. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  450. package/src/daemon/pkb-reminder-builder.ts +31 -0
  451. package/src/daemon/providers-setup.ts +6 -0
  452. package/src/daemon/server.ts +122 -12
  453. package/src/daemon/shutdown-handlers.ts +2 -12
  454. package/src/daemon/tool-side-effects.ts +14 -65
  455. package/src/daemon/web-search-history.ts +126 -0
  456. package/src/events/domain-events.ts +0 -1
  457. package/src/filing/filing-service.ts +9 -10
  458. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  459. package/src/heartbeat/heartbeat-service.ts +99 -28
  460. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  461. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  462. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  463. package/src/home/assistant-feed-authoring.ts +4 -0
  464. package/src/home/emit-feed-event.ts +11 -0
  465. package/src/home/feed-scheduler.ts +20 -4
  466. package/src/home/feed-types.ts +97 -4
  467. package/src/home/relationship-state-writer.ts +2 -2
  468. package/src/home/rewrite-command-preview.ts +66 -0
  469. package/src/home/rollup-producer.ts +34 -5
  470. package/src/home/suggested-prompts.ts +101 -0
  471. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  472. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  473. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  474. package/src/ipc/__tests__/socket-path.test.ts +34 -0
  475. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  476. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  477. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  478. package/src/ipc/cli-client.ts +2 -1
  479. package/src/ipc/cli-server.ts +26 -8
  480. package/src/ipc/gateway-client.ts +6 -3
  481. package/src/ipc/routes/attachment.ts +114 -0
  482. package/src/ipc/routes/browser-context.ts +63 -0
  483. package/src/ipc/routes/browser.ts +97 -0
  484. package/src/ipc/routes/cache.ts +96 -0
  485. package/src/ipc/routes/get-contact.ts +16 -0
  486. package/src/ipc/routes/index.ts +31 -1
  487. package/src/ipc/routes/list-clients.ts +31 -0
  488. package/src/ipc/routes/merge-contacts.ts +17 -0
  489. package/src/ipc/routes/notification.ts +133 -0
  490. package/src/ipc/routes/rename-conversation.ts +59 -0
  491. package/src/ipc/routes/search-contacts.ts +19 -0
  492. package/src/ipc/routes/task-queue.ts +226 -0
  493. package/src/ipc/routes/task.ts +173 -0
  494. package/src/ipc/routes/ui-request.ts +50 -0
  495. package/src/ipc/routes/upsert-contact.ts +25 -0
  496. package/src/ipc/routes/watcher.ts +203 -0
  497. package/src/ipc/socket-path.ts +76 -0
  498. package/src/media/app-icon-generator.ts +23 -46
  499. package/src/media/avatar-router.ts +26 -41
  500. package/src/media/gemini-image-service.ts +8 -41
  501. package/src/media/image-credentials.ts +73 -0
  502. package/src/media/image-service.ts +85 -0
  503. package/src/media/openai-image-service.ts +131 -0
  504. package/src/media/types.ts +46 -0
  505. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  506. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  507. package/src/memory/admin.ts +18 -0
  508. package/src/memory/conversation-analyze-job.ts +14 -13
  509. package/src/memory/conversation-attention-store.ts +13 -6
  510. package/src/memory/conversation-crud.ts +133 -3
  511. package/src/memory/conversation-group-migration.ts +38 -6
  512. package/src/memory/conversation-queries.ts +57 -4
  513. package/src/memory/conversation-title-service.ts +32 -4
  514. package/src/memory/db-init.ts +10 -0
  515. package/src/memory/embedding-backend.ts +1 -1
  516. package/src/memory/embedding-gemini.test.ts +41 -2
  517. package/src/memory/embedding-gemini.ts +6 -1
  518. package/src/memory/graph/bootstrap.test.ts +282 -0
  519. package/src/memory/graph/bootstrap.ts +8 -5
  520. package/src/memory/graph/compaction.ts +299 -0
  521. package/src/memory/graph/consolidation.ts +4 -4
  522. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  523. package/src/memory/graph/extraction.test.ts +272 -2
  524. package/src/memory/graph/extraction.ts +183 -53
  525. package/src/memory/graph/graph-search.test.ts +93 -0
  526. package/src/memory/graph/graph-search.ts +4 -1
  527. package/src/memory/graph/inspect.ts +2 -2
  528. package/src/memory/graph/narrative.ts +2 -2
  529. package/src/memory/graph/pattern-scan.ts +2 -2
  530. package/src/memory/graph/retriever.test.ts +459 -0
  531. package/src/memory/graph/retriever.ts +237 -48
  532. package/src/memory/graph/store.ts +41 -0
  533. package/src/memory/graph/tool-handlers.ts +27 -0
  534. package/src/memory/graph/tools.ts +6 -1
  535. package/src/memory/indexer.ts +5 -5
  536. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  537. package/src/memory/job-handlers/summarization.ts +2 -2
  538. package/src/memory/job-utils.ts +7 -1
  539. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  540. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  541. package/src/memory/jobs-store.ts +44 -3
  542. package/src/memory/jobs-worker.ts +4 -0
  543. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  544. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  545. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  546. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  547. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  548. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  549. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  550. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  551. package/src/memory/migrations/index.ts +5 -0
  552. package/src/memory/pkb/pkb-index.test.ts +369 -0
  553. package/src/memory/pkb/pkb-index.ts +255 -0
  554. package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
  555. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  556. package/src/memory/pkb/pkb-search.test.ts +499 -0
  557. package/src/memory/pkb/pkb-search.ts +159 -0
  558. package/src/memory/pkb/types.ts +53 -0
  559. package/src/memory/qdrant-client.test.ts +60 -0
  560. package/src/memory/qdrant-client.ts +147 -1
  561. package/src/memory/schema/infrastructure.ts +1 -0
  562. package/src/memory/schema/oauth.ts +4 -1
  563. package/src/memory/slack-thread-store.ts +37 -0
  564. package/src/messaging/providers/gmail/adapter.ts +6 -16
  565. package/src/messaging/providers/gmail/client.ts +22 -0
  566. package/src/messaging/providers/gmail/types.ts +7 -0
  567. package/src/messaging/providers/slack/adapter.ts +14 -2
  568. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  569. package/src/messaging/providers/slack/backfill.ts +101 -0
  570. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  571. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  572. package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
  573. package/src/messaging/providers/slack/render-transcript.ts +501 -0
  574. package/src/messaging/style-analyzer.ts +5 -2
  575. package/src/notifications/README.md +9 -5
  576. package/src/notifications/conversation-pairing.ts +78 -19
  577. package/src/notifications/copy-composer.ts +0 -5
  578. package/src/notifications/decision-engine.ts +3 -9
  579. package/src/notifications/emit-signal.ts +1 -1
  580. package/src/notifications/preference-extractor.ts +2 -6
  581. package/src/notifications/signal.ts +1 -2
  582. package/src/oauth/AGENTS.md +1 -1
  583. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  584. package/src/oauth/connect-orchestrator.ts +8 -34
  585. package/src/oauth/connect-types.ts +6 -10
  586. package/src/oauth/manual-token-connection.ts +23 -0
  587. package/src/oauth/oauth-store.ts +31 -14
  588. package/src/oauth/platform-connection.test.ts +47 -0
  589. package/src/oauth/platform-connection.ts +15 -5
  590. package/src/oauth/provider-serializer.ts +6 -1
  591. package/src/oauth/seed-providers.ts +56 -106
  592. package/src/outbound-proxy/http-forwarder.ts +9 -0
  593. package/src/permissions/approval-policy.test.ts +1223 -0
  594. package/src/permissions/approval-policy.ts +309 -0
  595. package/src/permissions/arg-parser.test.ts +161 -0
  596. package/src/permissions/arg-parser.ts +141 -0
  597. package/src/permissions/bash-risk-classifier.test.ts +1620 -0
  598. package/src/permissions/bash-risk-classifier.ts +950 -0
  599. package/src/permissions/checker.ts +348 -711
  600. package/src/permissions/command-registry.test.ts +774 -0
  601. package/src/permissions/command-registry.ts +1005 -0
  602. package/src/permissions/defaults.ts +28 -79
  603. package/src/permissions/file-risk-classifier.test.ts +535 -0
  604. package/src/permissions/file-risk-classifier.ts +274 -0
  605. package/src/permissions/gateway-threshold-reader.ts +196 -0
  606. package/src/permissions/prompter.ts +4 -0
  607. package/src/permissions/risk-types.ts +262 -0
  608. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  609. package/src/permissions/schedule-risk-classifier.ts +85 -0
  610. package/src/permissions/secret-prompter.ts +53 -2
  611. package/src/permissions/shell-identity.ts +2 -42
  612. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  613. package/src/permissions/skill-risk-classifier.ts +214 -0
  614. package/src/permissions/trust-client.ts +52 -25
  615. package/src/permissions/trust-store-interface.ts +1 -6
  616. package/src/permissions/trust-store.ts +161 -62
  617. package/src/permissions/types.ts +25 -14
  618. package/src/permissions/web-risk-classifier.test.ts +170 -0
  619. package/src/permissions/web-risk-classifier.ts +89 -0
  620. package/src/permissions/workspace-policy.ts +9 -19
  621. package/src/platform/client.ts +19 -1
  622. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  623. package/src/plugins/defaults/compaction.ts +145 -0
  624. package/src/plugins/defaults/empty-response.ts +126 -0
  625. package/src/plugins/defaults/history-repair.ts +85 -0
  626. package/src/plugins/defaults/index.ts +116 -0
  627. package/src/plugins/defaults/injectors.ts +491 -0
  628. package/src/plugins/defaults/llm-call.ts +82 -0
  629. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  630. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  631. package/src/plugins/defaults/persistence.ts +129 -0
  632. package/src/plugins/defaults/title-generate.ts +95 -0
  633. package/src/plugins/defaults/token-estimate.ts +104 -0
  634. package/src/plugins/defaults/tool-error.ts +126 -0
  635. package/src/plugins/defaults/tool-execute.ts +89 -0
  636. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  637. package/src/plugins/pipeline.ts +316 -0
  638. package/src/plugins/plugin-skill-contributions.ts +292 -0
  639. package/src/plugins/registry.ts +241 -0
  640. package/src/plugins/types.ts +1134 -0
  641. package/src/plugins/user-loader.ts +177 -0
  642. package/src/prompts/persona-resolver.ts +3 -3
  643. package/src/prompts/system-prompt.ts +19 -20
  644. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  645. package/src/prompts/templates/SOUL.md +2 -2
  646. package/src/prompts/update-bulletin-job.ts +190 -0
  647. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  648. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  649. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  650. package/src/providers/anthropic/client.ts +183 -14
  651. package/src/providers/call-site-routing.ts +71 -0
  652. package/src/providers/gemini/client.ts +65 -2
  653. package/src/providers/managed-proxy/constants.ts +2 -1
  654. package/src/providers/model-catalog.ts +524 -33
  655. package/src/providers/model-intents.ts +4 -4
  656. package/src/providers/openai/chat-completions-provider.ts +57 -1
  657. package/src/providers/openai/responses-provider.ts +86 -9
  658. package/src/providers/openrouter/client.ts +80 -9
  659. package/src/providers/provider-env-vars.ts +56 -0
  660. package/src/providers/provider-send-message.ts +22 -5
  661. package/src/providers/ratelimit.ts +4 -0
  662. package/src/providers/registry.ts +19 -8
  663. package/src/providers/retry.ts +174 -39
  664. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  665. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  666. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  667. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  668. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  669. package/src/providers/speech-to-text/resolve.ts +7 -0
  670. package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
  671. package/src/providers/speech-to-text/xai-realtime.ts +821 -0
  672. package/src/providers/speech-to-text/xai.test.ts +155 -0
  673. package/src/providers/speech-to-text/xai.ts +97 -0
  674. package/src/providers/types.ts +93 -3
  675. package/src/runtime/AGENTS.md +27 -18
  676. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  677. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  678. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  679. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  680. package/src/runtime/agent-wake.ts +63 -22
  681. package/src/runtime/auth/route-policy.ts +4 -0
  682. package/src/runtime/btw-sidechain.ts +13 -3
  683. package/src/runtime/channel-reply-delivery.ts +106 -2
  684. package/src/runtime/client-registry.ts +261 -0
  685. package/src/runtime/decision-token.ts +116 -0
  686. package/src/runtime/gateway-client.ts +2 -2
  687. package/src/runtime/http-router.ts +32 -0
  688. package/src/runtime/http-server.ts +129 -9
  689. package/src/runtime/http-types.ts +23 -3
  690. package/src/runtime/interactive-ui.ts +362 -0
  691. package/src/runtime/invite-instruction-generator.ts +2 -2
  692. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  693. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  694. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  695. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  696. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  697. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  698. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  699. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  700. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  701. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  702. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  703. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  704. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  705. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  706. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  707. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  708. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  709. package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
  710. package/src/runtime/routes/approval-routes.ts +29 -17
  711. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  712. package/src/runtime/routes/avatar-routes.ts +20 -4
  713. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  714. package/src/runtime/routes/btw-routes.ts +1 -4
  715. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  716. package/src/runtime/routes/conversation-routes.ts +351 -138
  717. package/src/runtime/routes/debug-routes.ts +1 -1
  718. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  719. package/src/runtime/routes/events-routes.ts +16 -0
  720. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  721. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  722. package/src/runtime/routes/home-feed-routes.ts +120 -2
  723. package/src/runtime/routes/inbound-message-handler.ts +987 -2
  724. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  725. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  726. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  727. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  728. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  729. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  730. package/src/runtime/routes/migration-routes.ts +720 -127
  731. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  732. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  733. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  734. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  735. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  736. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  737. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  738. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  739. package/src/runtime/routes/playground/deps.ts +56 -0
  740. package/src/runtime/routes/playground/force-compact.ts +73 -0
  741. package/src/runtime/routes/playground/guard.ts +37 -0
  742. package/src/runtime/routes/playground/index.ts +28 -0
  743. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  744. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  745. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  746. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  747. package/src/runtime/routes/playground/state.ts +78 -0
  748. package/src/runtime/routes/schedule-routes.ts +89 -8
  749. package/src/runtime/routes/settings-routes.ts +4 -2
  750. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  751. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  752. package/src/runtime/routes/work-items-routes.ts +3 -2
  753. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  754. package/src/runtime/services/analyze-conversation.ts +12 -16
  755. package/src/runtime/skill-route-registry.ts +97 -15
  756. package/src/schedule/run-script.ts +68 -0
  757. package/src/schedule/schedule-store.ts +7 -1
  758. package/src/schedule/scheduler.ts +56 -8
  759. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  760. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  761. package/src/security/oauth2.ts +98 -35
  762. package/src/security/secure-keys.ts +7 -8
  763. package/src/security/token-manager.ts +27 -13
  764. package/src/security/untrusted-content.ts +102 -0
  765. package/src/skills/catalog-cache.ts +35 -9
  766. package/src/skills/catalog-install.ts +31 -3
  767. package/src/skills/skill-cache-store.ts +97 -0
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  769. package/src/stt/daemon-batch-transcriber.ts +33 -0
  770. package/src/stt/stt-stream-session.ts +8 -1
  771. package/src/stt/types.ts +5 -1
  772. package/src/subagent/manager.ts +41 -13
  773. package/src/tasks/ephemeral-permissions.ts +9 -4
  774. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  775. package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
  776. package/src/tools/browser/browser-execution.ts +150 -54
  777. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  778. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  779. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  780. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  781. package/src/tools/browser/cdp-client/factory.ts +15 -4
  782. package/src/tools/credentials/tool-policy.ts +39 -5
  783. package/src/tools/credentials/vault.ts +9 -4
  784. package/src/tools/executor.ts +129 -73
  785. package/src/tools/filesystem/write.ts +52 -0
  786. package/src/tools/host-terminal/host-shell.ts +45 -5
  787. package/src/tools/memory/register.test.ts +185 -0
  788. package/src/tools/memory/register.ts +3 -1
  789. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  790. package/src/tools/network/web-fetch.ts +20 -10
  791. package/src/tools/network/web-search.ts +19 -4
  792. package/src/tools/permission-checker.ts +116 -46
  793. package/src/tools/policy-context.ts +29 -8
  794. package/src/tools/registry.ts +195 -6
  795. package/src/tools/schedule/create.ts +23 -8
  796. package/src/tools/schedule/update.ts +3 -1
  797. package/src/tools/secret-detection-handler.ts +0 -51
  798. package/src/tools/side-effects.ts +0 -11
  799. package/src/tools/skills/execute.ts +2 -2
  800. package/src/tools/skills/sandbox-runner.ts +5 -2
  801. package/src/tools/system/avatar-generator.ts +6 -2
  802. package/src/tools/terminal/backends/native.ts +51 -2
  803. package/src/tools/terminal/safe-env.ts +3 -2
  804. package/src/tools/terminal/shell.ts +1 -0
  805. package/src/tools/tool-manifest.ts +6 -21
  806. package/src/tools/types.ts +40 -5
  807. package/src/tools/verification-control-plane-policy.ts +1 -1
  808. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  809. package/src/tts/provider-catalog.ts +18 -0
  810. package/src/tts/providers/index.ts +2 -0
  811. package/src/tts/providers/xai-provider.ts +224 -0
  812. package/src/tts/types.ts +46 -0
  813. package/src/types/tar-stream.d.ts +66 -0
  814. package/src/util/json.ts +17 -0
  815. package/src/util/platform.ts +9 -4
  816. package/src/util/pricing.ts +41 -8
  817. package/src/watcher/engine.ts +1 -1
  818. package/src/watcher/providers/google-calendar.ts +134 -8
  819. package/src/watcher/providers/outlook-calendar.ts +42 -2
  820. package/src/workspace/git-service.ts +23 -4
  821. package/src/workspace/migrations/006-services-config.ts +2 -4
  822. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  823. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  824. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  825. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  826. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
  827. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  828. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  829. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  830. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  831. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  832. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  833. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  834. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  835. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  836. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  837. package/src/workspace/migrations/AGENTS.md +1 -1
  838. package/src/workspace/migrations/registry.ts +28 -0
  839. package/src/workspace/provider-commit-message-generator.ts +19 -38
  840. package/tsconfig.json +1 -1
  841. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  842. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  843. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  844. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  845. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  846. package/src/__tests__/gmail-preferences.test.ts +0 -117
  847. package/src/__tests__/hooks-blocking.test.ts +0 -178
  848. package/src/__tests__/hooks-cli.test.ts +0 -182
  849. package/src/__tests__/hooks-config.test.ts +0 -108
  850. package/src/__tests__/hooks-discovery.test.ts +0 -211
  851. package/src/__tests__/hooks-integration.test.ts +0 -196
  852. package/src/__tests__/hooks-manager.test.ts +0 -226
  853. package/src/__tests__/hooks-runner.test.ts +0 -175
  854. package/src/__tests__/hooks-settings.test.ts +0 -160
  855. package/src/__tests__/hooks-templates.test.ts +0 -169
  856. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  857. package/src/__tests__/hooks-watch.test.ts +0 -112
  858. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  859. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  860. package/src/__tests__/outlook-attachments.test.ts +0 -301
  861. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  862. package/src/__tests__/outlook-categories.test.ts +0 -212
  863. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  864. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  865. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  866. package/src/__tests__/outlook-trash.test.ts +0 -77
  867. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  868. package/src/__tests__/send-notification-tool.test.ts +0 -83
  869. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  870. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  871. package/src/__tests__/update-bulletin.test.ts +0 -478
  872. package/src/__tests__/update-template-contract.test.ts +0 -29
  873. package/src/cli/commands/doctor.ts +0 -341
  874. package/src/cli/commands/shotgun.ts +0 -266
  875. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  876. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  877. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  878. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  879. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  880. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  881. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  882. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  883. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  884. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  885. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  886. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  887. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  888. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  889. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  890. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  891. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  892. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  893. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  894. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  895. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  896. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  897. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  898. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  899. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
  900. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  901. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  902. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  903. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  904. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  905. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  906. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  907. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  908. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  909. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  910. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  911. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  912. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  913. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  914. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  915. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  916. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  917. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  918. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  919. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  920. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  921. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  922. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  923. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  924. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  925. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  926. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  927. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  928. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  929. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  930. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  931. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  932. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  933. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  934. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  935. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  936. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  937. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  938. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  939. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  940. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  941. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  942. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  943. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  944. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  945. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  946. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  947. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  948. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  949. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  950. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  951. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  952. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  953. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  954. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  955. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  956. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  957. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  958. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  959. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  960. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  961. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  962. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  963. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  964. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  965. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  966. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  967. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  968. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  969. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  970. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  971. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  972. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  973. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  974. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  975. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  976. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  977. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  978. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  979. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  980. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  981. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  982. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  983. package/src/daemon/context-overflow-approval.ts +0 -52
  984. package/src/daemon/watch-handler.ts +0 -399
  985. package/src/hooks/cli.ts +0 -253
  986. package/src/hooks/config.ts +0 -100
  987. package/src/hooks/discovery.ts +0 -135
  988. package/src/hooks/manager.ts +0 -179
  989. package/src/hooks/runner.ts +0 -117
  990. package/src/hooks/templates.ts +0 -77
  991. package/src/hooks/types.ts +0 -75
  992. package/src/oauth/scope-policy.ts +0 -89
  993. package/src/prompts/templates/UPDATES.md +0 -50
  994. package/src/prompts/update-bulletin-format.ts +0 -85
  995. package/src/prompts/update-bulletin-state.ts +0 -58
  996. package/src/prompts/update-bulletin-template-path.ts +0 -13
  997. package/src/prompts/update-bulletin.ts +0 -139
  998. package/src/runtime/gateway-internal-client.ts +0 -94
  999. package/src/runtime/routes/watch-routes.ts +0 -156
  1000. package/src/shared/provider-env-vars.ts +0 -19
  1001. package/src/signals/shotgun.ts +0 -203
  1002. package/src/tools/watch/screen-watch.ts +0 -144
  1003. package/src/tools/watch/watch-state.ts +0 -142
  1004. package/src/tools/watcher/create.ts +0 -86
  1005. package/src/tools/watcher/delete.ts +0 -36
  1006. package/src/tools/watcher/digest.ts +0 -54
  1007. package/src/tools/watcher/list.ts +0 -83
  1008. package/src/tools/watcher/update.ts +0 -71
@@ -0,0 +1,1540 @@
1
+ /**
2
+ * Tests for `streamCommitImport` — the streaming `.vbundle` importer.
3
+ *
4
+ * Covered:
5
+ * - Happy path: multi-file bundle lands in workspace; report shape matches
6
+ * buffer-based `commitImport`.
7
+ * - Manifest-first-failure: non-manifest first entry → validation_failed,
8
+ * temp dir cleaned, real workspace untouched.
9
+ * - Mid-stream hash failure: tampered manifest sha → validation_failed,
10
+ * temp dir cleaned, real workspace untouched.
11
+ * - Missing entry: manifest declares a file that's absent from the tar →
12
+ * validation_failed with offending path surfaced.
13
+ * - Extra entry (manifest_mismatch): tar carries a file the manifest does
14
+ * not declare → validation_failed.
15
+ * - Memory ceiling: 100 MB fixture streams through without pushing heap
16
+ * past ~64 MB, proving we're not buffering the whole bundle.
17
+ * - Sanity parity: buffer-based `commitImport` and `streamCommitImport`
18
+ * produce report objects with the same field shape for the same input.
19
+ */
20
+
21
+ import { createHash } from "node:crypto";
22
+ import {
23
+ createReadStream,
24
+ existsSync,
25
+ mkdirSync,
26
+ mkdtempSync,
27
+ readdirSync,
28
+ readFileSync,
29
+ realpathSync,
30
+ rmSync,
31
+ writeFileSync,
32
+ } from "node:fs";
33
+ import { tmpdir } from "node:os";
34
+ import { join } from "node:path";
35
+ import { Readable } from "node:stream";
36
+ import { gunzipSync, gzipSync } from "node:zlib";
37
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
38
+
39
+ import { buildVBundle } from "../vbundle-builder.js";
40
+ import { DefaultPathResolver } from "../vbundle-import-analyzer.js";
41
+ import { commitImport } from "../vbundle-importer.js";
42
+ import { streamCommitImport } from "../vbundle-streaming-importer.js";
43
+ import { canonicalizeJson } from "../vbundle-validator.js";
44
+
45
+ /**
46
+ * Fixed "customized" guardian persona content used by the USER.md-skip
47
+ * test. Has user-authored content past the bare scaffold, so
48
+ * `isGuardianPersonaCustomized` returns true.
49
+ */
50
+ const CUSTOMIZED_PERSONA_FIXTURE = `_ Lines starting with _ are comments - they won't appear in the system prompt
51
+
52
+ # User Profile
53
+
54
+ - Preferred name/reference: Real User
55
+ - Pronouns: she/her
56
+ - Locale: en-US
57
+ - Work role: Staff Engineer
58
+ - Goals: Ship drop-user-md
59
+ - Hobbies/fun: Reading papers
60
+ - Daily tools: Terminal, Vellum
61
+ `;
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Fixture helpers
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Build a temp workspace dir whose parent we own, so the atomic-swap rename
69
+ * (`workspaceDir` → `workspaceDir.pre-import-<ts>`) stays inside the test
70
+ * sandbox instead of polluting $TMPDIR with stale siblings.
71
+ */
72
+ function freshWorkspace(): string {
73
+ const parent = realpathSync(
74
+ mkdtempSync(join(tmpdir(), "vbundle-stream-import-")),
75
+ );
76
+ const workspaceDir = join(parent, "workspace");
77
+ // Don't mkdir — leaving it absent lets us verify "real workspace untouched"
78
+ // semantics clearly. Individual tests that need an existing workspace
79
+ // create it themselves.
80
+ return workspaceDir;
81
+ }
82
+
83
+ function readableFrom(buf: Uint8Array): Readable {
84
+ return Readable.from([Buffer.from(buf)]);
85
+ }
86
+
87
+ function sha256Hex(data: Uint8Array | string): string {
88
+ return createHash("sha256").update(data).digest("hex");
89
+ }
90
+
91
+ /**
92
+ * Strip a specific ustar entry from an already-built archive. Keeps the
93
+ * manifest (first entry) intact and drops the entry whose name matches
94
+ * `entryName`. Assumes no PAX/longname entries precede the target.
95
+ */
96
+ function removeEntry(archive: Uint8Array, entryName: string): Uint8Array {
97
+ const raw = gunzipSync(archive);
98
+
99
+ let offset = 0;
100
+ while (offset + 512 <= raw.length) {
101
+ const block = raw.subarray(offset, offset + 512);
102
+ if (block.every((b) => b === 0)) break;
103
+
104
+ // Entry name is at offset 0..100 of the header, null-terminated.
105
+ let nameEnd = 0;
106
+ while (nameEnd < 100 && block[nameEnd] !== 0) nameEnd += 1;
107
+ const name = new TextDecoder().decode(block.subarray(0, nameEnd));
108
+
109
+ const sizeStr = new TextDecoder()
110
+ .decode(block.subarray(124, 136))
111
+ .replace(/\0.*$/, "")
112
+ .trim();
113
+ const size = parseInt(sizeStr, 8) || 0;
114
+ const dataBlocks = Math.ceil(size / 512);
115
+ const entryLen = 512 + dataBlocks * 512;
116
+
117
+ if (name === entryName) {
118
+ const out = new Uint8Array(raw.length - entryLen);
119
+ out.set(raw.subarray(0, offset), 0);
120
+ out.set(raw.subarray(offset + entryLen), offset);
121
+ return gzipSync(out);
122
+ }
123
+
124
+ offset += entryLen;
125
+ }
126
+
127
+ throw new Error(
128
+ `removeEntry: test helper could not find entry "${entryName}" in archive`,
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Update manifest.json in place to drop the entry with the given archive
134
+ * path AND recompute manifest_sha256 so the manifest itself stays valid.
135
+ * Used to craft the "extra entry" (manifest_mismatch) fixture — the tar
136
+ * has the file, but the manifest does not.
137
+ */
138
+ function dropFromManifestAndRepack(
139
+ archive: Uint8Array,
140
+ pathToDrop: string,
141
+ ): Uint8Array {
142
+ const raw = gunzipSync(archive);
143
+ const sizeStr = new TextDecoder()
144
+ .decode(raw.subarray(124, 136))
145
+ .replace(/\0.*$/, "")
146
+ .trim();
147
+ const origSize = parseInt(sizeStr, 8);
148
+ const manifestJson = new TextDecoder().decode(
149
+ raw.subarray(512, 512 + origSize),
150
+ );
151
+ const manifest = JSON.parse(manifestJson) as {
152
+ files: Array<{ path: string; sha256: string; size: number }>;
153
+ manifest_sha256: string;
154
+ [k: string]: unknown;
155
+ };
156
+ manifest.files = manifest.files.filter((f) => f.path !== pathToDrop);
157
+ // Recompute manifest_sha256.
158
+ const withoutChecksum: Record<string, unknown> = { ...manifest };
159
+ delete withoutChecksum.manifest_sha256;
160
+ manifest.manifest_sha256 = sha256Hex(canonicalizeJson(withoutChecksum));
161
+
162
+ const newJson = JSON.stringify(manifest);
163
+ const newBytes = new TextEncoder().encode(newJson);
164
+
165
+ // The manifest has almost certainly changed length — rebuild the tar.
166
+ // Rewrite the first entry's size field and pad the body to the next
167
+ // 512-byte boundary, then concatenate everything after the old manifest.
168
+ const header = new Uint8Array(512);
169
+ header.set(raw.subarray(0, 512), 0);
170
+ const newSizeOctal = newBytes.length.toString(8).padStart(11, "0");
171
+ for (let i = 0; i < 11; i++) {
172
+ header[124 + i] = newSizeOctal.charCodeAt(i);
173
+ }
174
+ header[135] = 0;
175
+ // Zero out the old checksum field before recomputing.
176
+ for (let i = 148; i < 156; i++) header[i] = 0x20;
177
+ let sum = 0;
178
+ for (let i = 0; i < 512; i++) sum += header[i];
179
+ const cksum = sum.toString(8).padStart(6, "0");
180
+ for (let i = 0; i < 6; i++) header[148 + i] = cksum.charCodeAt(i);
181
+ header[154] = 0;
182
+ header[155] = 0x20;
183
+
184
+ const oldPaddedLen = 512 + Math.ceil(origSize / 512) * 512;
185
+ const newPadded = Math.ceil(newBytes.length / 512) * 512;
186
+ const out = new Uint8Array(
187
+ header.length + newPadded + (raw.length - oldPaddedLen),
188
+ );
189
+ out.set(header, 0);
190
+ out.set(newBytes, 512);
191
+ out.set(raw.subarray(oldPaddedLen), 512 + newPadded);
192
+ return gzipSync(out);
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Happy path
197
+ // ---------------------------------------------------------------------------
198
+
199
+ describe("streamCommitImport — happy path", () => {
200
+ let workspaceDir: string;
201
+ beforeEach(() => {
202
+ workspaceDir = freshWorkspace();
203
+ });
204
+ afterEach(() => {
205
+ // Clean up any sibling temp/backup dirs left under the workspace parent.
206
+ const parent = join(workspaceDir, "..");
207
+ try {
208
+ rmSync(parent, { recursive: true, force: true });
209
+ } catch {
210
+ // best-effort
211
+ }
212
+ });
213
+
214
+ test("writes every file into the workspace and returns a report with the expected shape", async () => {
215
+ const fileA = new TextEncoder().encode("alpha alpha alpha\n");
216
+ const fileB = new TextEncoder().encode("beta beta\n");
217
+ const fileC = new TextEncoder().encode("gamma payload\n");
218
+
219
+ const { archive } = buildVBundle({
220
+ files: [
221
+ { path: "workspace/a.txt", data: fileA },
222
+ { path: "workspace/sub/b.txt", data: fileB },
223
+ { path: "workspace/sub/c.txt", data: fileC },
224
+ ],
225
+ source: "test-happy-path",
226
+ });
227
+
228
+ const result = await streamCommitImport({
229
+ source: readableFrom(archive),
230
+ pathResolver: new DefaultPathResolver(workspaceDir),
231
+ workspaceDir,
232
+ });
233
+
234
+ expect(result.ok).toBe(true);
235
+ if (!result.ok) throw new Error("unreachable");
236
+
237
+ expect(existsSync(join(workspaceDir, "a.txt"))).toBe(true);
238
+ expect(readFileSync(join(workspaceDir, "a.txt"))).toEqual(
239
+ Buffer.from(fileA),
240
+ );
241
+ expect(readFileSync(join(workspaceDir, "sub/b.txt"))).toEqual(
242
+ Buffer.from(fileB),
243
+ );
244
+ expect(readFileSync(join(workspaceDir, "sub/c.txt"))).toEqual(
245
+ Buffer.from(fileC),
246
+ );
247
+
248
+ expect(result.report.success).toBe(true);
249
+ expect(result.report.summary.total_files).toBe(3);
250
+ expect(result.report.summary.files_created).toBe(3);
251
+ expect(result.report.manifest.files).toHaveLength(3);
252
+ for (const f of result.report.files) {
253
+ expect(f.action).toBe("created");
254
+ expect(f.backup_path).toBeNull();
255
+ expect(typeof f.sha256).toBe("string");
256
+ expect(f.disk_path.startsWith(workspaceDir)).toBe(true);
257
+ }
258
+ });
259
+
260
+ test("invokes onProgress after each file entry finishes", async () => {
261
+ const { archive } = buildVBundle({
262
+ files: [
263
+ {
264
+ path: "workspace/a.txt",
265
+ data: new TextEncoder().encode("one"),
266
+ },
267
+ {
268
+ path: "workspace/b.txt",
269
+ data: new TextEncoder().encode("two!"),
270
+ },
271
+ ],
272
+ });
273
+
274
+ const events: Array<{
275
+ archivePath: string;
276
+ bytesWritten: number;
277
+ entryIndex: number;
278
+ }> = [];
279
+ const result = await streamCommitImport({
280
+ source: readableFrom(archive),
281
+ pathResolver: new DefaultPathResolver(workspaceDir),
282
+ workspaceDir,
283
+ onProgress: (e) => events.push(e),
284
+ });
285
+
286
+ expect(result.ok).toBe(true);
287
+ expect(events.map((e) => e.archivePath)).toEqual([
288
+ "workspace/a.txt",
289
+ "workspace/b.txt",
290
+ ]);
291
+ expect(events[0]?.bytesWritten).toBe(3);
292
+ expect(events[1]?.bytesWritten).toBe(4);
293
+ expect(events[0]?.entryIndex).toBeLessThan(events[1]?.entryIndex ?? -1);
294
+ });
295
+
296
+ test("forwards credentials to importCredentials callback but never writes them to disk", async () => {
297
+ const { archive } = buildVBundle({
298
+ files: [
299
+ {
300
+ path: "workspace/config.json",
301
+ data: new TextEncoder().encode("{}"),
302
+ },
303
+ {
304
+ path: "credentials/openai-key",
305
+ data: new TextEncoder().encode("sk-test-1"),
306
+ },
307
+ {
308
+ path: "credentials/anthropic-key",
309
+ data: new TextEncoder().encode("sk-ant-2"),
310
+ },
311
+ ],
312
+ });
313
+
314
+ const received: Array<{ account: string; value: string }> = [];
315
+ const result = await streamCommitImport({
316
+ source: readableFrom(archive),
317
+ pathResolver: new DefaultPathResolver(workspaceDir),
318
+ workspaceDir,
319
+ importCredentials: async (creds) => {
320
+ received.push(...creds);
321
+ },
322
+ });
323
+
324
+ expect(result.ok).toBe(true);
325
+ expect(received).toHaveLength(2);
326
+ expect(received).toContainEqual({
327
+ account: "openai-key",
328
+ value: "sk-test-1",
329
+ });
330
+ expect(received).toContainEqual({
331
+ account: "anthropic-key",
332
+ value: "sk-ant-2",
333
+ });
334
+ // Credentials must NOT appear on disk.
335
+ expect(existsSync(join(workspaceDir, "credentials"))).toBe(false);
336
+ });
337
+ });
338
+
339
+ // ---------------------------------------------------------------------------
340
+ // Failure modes — every one must leave the real workspace untouched and
341
+ // clean up the sibling temp dir.
342
+ // ---------------------------------------------------------------------------
343
+
344
+ describe("streamCommitImport — failure modes", () => {
345
+ let workspaceDir: string;
346
+ beforeEach(() => {
347
+ workspaceDir = freshWorkspace();
348
+ });
349
+ afterEach(() => {
350
+ const parent = join(workspaceDir, "..");
351
+ try {
352
+ rmSync(parent, { recursive: true, force: true });
353
+ } catch {
354
+ // best-effort
355
+ }
356
+ });
357
+
358
+ /** Ensure no sibling temp/backup dirs for this workspace remain. */
359
+ function assertNoLeftoverTempDirs(): void {
360
+ const parent = join(workspaceDir, "..");
361
+ const base = workspaceDir.split("/").pop()!;
362
+ const siblings = readdirSync(parent);
363
+ const leftover = siblings.filter(
364
+ (name) =>
365
+ name.startsWith(`${base}.import-`) ||
366
+ name.startsWith(`${base}.pre-import-`),
367
+ );
368
+ expect(leftover).toEqual([]);
369
+ }
370
+
371
+ test("manifest-first failure: non-manifest first entry → validation_failed, real workspace untouched", async () => {
372
+ // Seed the real workspace with a marker file so we can verify it's
373
+ // untouched after the failed import.
374
+ mkdirSync(workspaceDir, { recursive: true });
375
+ writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
376
+
377
+ // Hand-roll a gzipped tar whose first entry is NOT manifest.json.
378
+ // Reuse buildVBundle for a valid archive, then strip the manifest
379
+ // entry using removeEntry — the remaining archive opens with a
380
+ // workspace/ file as entry #1.
381
+ const { archive } = buildVBundle({
382
+ files: [
383
+ {
384
+ path: "workspace/a.txt",
385
+ data: new TextEncoder().encode("hello"),
386
+ },
387
+ ],
388
+ });
389
+ const noManifest = removeEntry(archive, "manifest.json");
390
+
391
+ const result = await streamCommitImport({
392
+ source: readableFrom(noManifest),
393
+ pathResolver: new DefaultPathResolver(workspaceDir),
394
+ workspaceDir,
395
+ });
396
+
397
+ expect(result.ok).toBe(false);
398
+ if (result.ok) throw new Error("unreachable");
399
+ expect(result.reason).toBe("validation_failed");
400
+
401
+ // Real workspace's pre-existing content is still there, unmodified.
402
+ expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
403
+ "keep me\n",
404
+ );
405
+ assertNoLeftoverTempDirs();
406
+ });
407
+
408
+ test("mid-stream hash failure: tampered manifest sha → validation_failed, cleanup intact", async () => {
409
+ mkdirSync(workspaceDir, { recursive: true });
410
+ writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
411
+
412
+ // Build a valid bundle with one file whose data is 32 bytes long.
413
+ const body = new TextEncoder().encode("x".repeat(32));
414
+ const { archive } = buildVBundle({
415
+ files: [{ path: "workspace/victim.txt", data: body }],
416
+ });
417
+
418
+ // Tamper the manifest sha256 for workspace/victim.txt by substituting
419
+ // one hex character. Keeps the manifest valid (the substitution is
420
+ // same-length) — but because manifest_sha256 is recomputed over the
421
+ // declared data, we ALSO need to tamper manifest_sha256 to keep the
422
+ // manifest itself valid. Otherwise the manifest will fail its
423
+ // self-checksum and the test exercises the wrong path.
424
+ //
425
+ // Easier approach: build a NEW valid manifest that declares the wrong
426
+ // hash for victim.txt. We hand-rebuild the archive via
427
+ // `dropFromManifestAndRepack`-style logic: replace the existing entry
428
+ // in manifest.files with a different sha256, recompute manifest_sha256.
429
+ const raw = gunzipSync(archive);
430
+ const sizeStr = new TextDecoder()
431
+ .decode(raw.subarray(124, 136))
432
+ .replace(/\0.*$/, "")
433
+ .trim();
434
+ const origSize = parseInt(sizeStr, 8);
435
+ const manifestJson = new TextDecoder().decode(
436
+ raw.subarray(512, 512 + origSize),
437
+ );
438
+ const manifest = JSON.parse(manifestJson) as {
439
+ files: Array<{ path: string; sha256: string; size: number }>;
440
+ manifest_sha256: string;
441
+ [k: string]: unknown;
442
+ };
443
+ manifest.files = manifest.files.map((f) =>
444
+ f.path === "workspace/victim.txt"
445
+ ? {
446
+ ...f,
447
+ // Deterministic-but-wrong sha: flip the high bit of char 0.
448
+ sha256: "0" + f.sha256.slice(1),
449
+ }
450
+ : f,
451
+ );
452
+ const withoutChecksum: Record<string, unknown> = { ...manifest };
453
+ delete withoutChecksum.manifest_sha256;
454
+ manifest.manifest_sha256 = sha256Hex(canonicalizeJson(withoutChecksum));
455
+
456
+ const newJson = JSON.stringify(manifest);
457
+ const newBytes = new TextEncoder().encode(newJson);
458
+ if (newBytes.length !== origSize) {
459
+ throw new Error(
460
+ `hash-failure test fixture: manifest length drifted (${newBytes.length} vs ${origSize})`,
461
+ );
462
+ }
463
+ const tampered = new Uint8Array(raw.length);
464
+ tampered.set(raw);
465
+ tampered.set(newBytes, 512);
466
+ const tamperedArchive = gzipSync(tampered);
467
+
468
+ const result = await streamCommitImport({
469
+ source: readableFrom(tamperedArchive),
470
+ pathResolver: new DefaultPathResolver(workspaceDir),
471
+ workspaceDir,
472
+ });
473
+
474
+ expect(result.ok).toBe(false);
475
+ if (result.ok) throw new Error("unreachable");
476
+ expect(result.reason).toBe("validation_failed");
477
+
478
+ // Existing workspace content preserved, no temp dir hanging around.
479
+ expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
480
+ "keep me\n",
481
+ );
482
+ assertNoLeftoverTempDirs();
483
+ });
484
+
485
+ test("missing entry: manifest declares a path absent from the tar → validation_failed", async () => {
486
+ mkdirSync(workspaceDir, { recursive: true });
487
+ writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
488
+
489
+ const { archive } = buildVBundle({
490
+ files: [
491
+ {
492
+ path: "workspace/present.txt",
493
+ data: new TextEncoder().encode("here"),
494
+ },
495
+ {
496
+ path: "workspace/missing.txt",
497
+ data: new TextEncoder().encode("gone"),
498
+ },
499
+ ],
500
+ });
501
+ const stripped = removeEntry(archive, "workspace/missing.txt");
502
+
503
+ const result = await streamCommitImport({
504
+ source: readableFrom(stripped),
505
+ pathResolver: new DefaultPathResolver(workspaceDir),
506
+ workspaceDir,
507
+ });
508
+
509
+ expect(result.ok).toBe(false);
510
+ if (result.ok) throw new Error("unreachable");
511
+ expect(result.reason).toBe("validation_failed");
512
+ // The error payload should surface the missing path.
513
+ const combined = JSON.stringify(result);
514
+ expect(combined).toContain("workspace/missing.txt");
515
+
516
+ expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
517
+ "keep me\n",
518
+ );
519
+ assertNoLeftoverTempDirs();
520
+ });
521
+
522
+ test("extra entry: tar contains a file the manifest does not declare → validation_failed", async () => {
523
+ mkdirSync(workspaceDir, { recursive: true });
524
+ writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
525
+
526
+ const { archive } = buildVBundle({
527
+ files: [
528
+ {
529
+ path: "workspace/declared.txt",
530
+ data: new TextEncoder().encode("fine"),
531
+ },
532
+ {
533
+ path: "workspace/extra.txt",
534
+ data: new TextEncoder().encode("surprise"),
535
+ },
536
+ ],
537
+ });
538
+ const extraPresent = dropFromManifestAndRepack(
539
+ archive,
540
+ "workspace/extra.txt",
541
+ );
542
+
543
+ const result = await streamCommitImport({
544
+ source: readableFrom(extraPresent),
545
+ pathResolver: new DefaultPathResolver(workspaceDir),
546
+ workspaceDir,
547
+ });
548
+
549
+ expect(result.ok).toBe(false);
550
+ if (result.ok) throw new Error("unreachable");
551
+ expect(result.reason).toBe("validation_failed");
552
+
553
+ expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
554
+ "keep me\n",
555
+ );
556
+ assertNoLeftoverTempDirs();
557
+ });
558
+ });
559
+
560
+ // ---------------------------------------------------------------------------
561
+ // Memory ceiling — the point of the streaming path.
562
+ // ---------------------------------------------------------------------------
563
+
564
+ /**
565
+ * Materialize a ~100 MB .vbundle fixture on disk and return its path.
566
+ * Wrapping the build in its own function lets the intermediate Uint8Arrays
567
+ * go out of scope before we start measuring heap — the fixture itself
568
+ * must not count against the importer's working-set budget.
569
+ */
570
+ function writeLargeFixtureToDisk(archivePath: string): void {
571
+ const CHUNK = 25 * 1024 * 1024;
572
+ const files = [0, 1, 2, 3].map((i) => ({
573
+ path: `workspace/big-${i}.bin`,
574
+ data: new Uint8Array(CHUNK).fill(0x41 + i),
575
+ }));
576
+ const { archive } = buildVBundle({ files });
577
+ writeFileSync(archivePath, archive);
578
+ }
579
+
580
+ describe("streamCommitImport — memory ceiling", () => {
581
+ test("100 MB fixture streams in without pushing RSS past ~64 MB over baseline", async () => {
582
+ const workspaceDir = freshWorkspace();
583
+ const parent = join(workspaceDir, "..");
584
+ const archivePath = join(parent, "fixture.vbundle");
585
+
586
+ try {
587
+ // Build the fixture in an isolated scope so intermediate buffers go
588
+ // out of scope before we start measuring.
589
+ writeLargeFixtureToDisk(archivePath);
590
+
591
+ // Bun's `process.memoryUsage().heapUsed` can include accounting for
592
+ // off-heap Buffer backing stores, so a strict heap ceiling is noisy
593
+ // across engines. Use RSS instead — that's the actual "did the
594
+ // process grow" signal. If the importer were buffering the full 100
595
+ // MB archive, RSS would spike by at least 100 MB; a streaming
596
+ // importer's per-entry working set is bounded by ~one tar entry's
597
+ // internal buffers (a few MB).
598
+ const baselineRss = process.memoryUsage().rss;
599
+ let peakRss = baselineRss;
600
+ let progressCount = 0;
601
+
602
+ const result = await streamCommitImport({
603
+ source: createReadStream(archivePath),
604
+ pathResolver: new DefaultPathResolver(workspaceDir),
605
+ workspaceDir,
606
+ onProgress: () => {
607
+ progressCount += 1;
608
+ const cur = process.memoryUsage().rss;
609
+ if (cur > peakRss) peakRss = cur;
610
+ },
611
+ });
612
+
613
+ expect(result.ok).toBe(true);
614
+ // We expect onProgress to fire at least 4 times (one per big file) —
615
+ // spot-check that we actually sampled during import.
616
+ expect(progressCount).toBeGreaterThanOrEqual(4);
617
+
618
+ // The 64 MB delta bound is a rough guard proving "it doesn't buffer
619
+ // the whole bundle" — if the importer were accumulating the 100 MB
620
+ // archive in memory, RSS would jump well past this threshold.
621
+ const delta = peakRss - baselineRss;
622
+ expect(delta).toBeLessThan(64 * 1024 * 1024);
623
+ } finally {
624
+ try {
625
+ rmSync(parent, { recursive: true, force: true });
626
+ } catch {
627
+ // best-effort
628
+ }
629
+ }
630
+ }, 60_000);
631
+ });
632
+
633
+ // ---------------------------------------------------------------------------
634
+ // Sanity parity with buffer-based commitImport.
635
+ // ---------------------------------------------------------------------------
636
+
637
+ describe("streamCommitImport — report parity with commitImport", () => {
638
+ test("buffer-based and streaming importer produce report objects with the same field shape", async () => {
639
+ const bufferWorkspace = freshWorkspace();
640
+ const streamWorkspace = freshWorkspace();
641
+
642
+ const files = [
643
+ {
644
+ path: "workspace/a.txt",
645
+ data: new TextEncoder().encode("alpha"),
646
+ },
647
+ {
648
+ path: "workspace/sub/b.txt",
649
+ data: new TextEncoder().encode("beta beta"),
650
+ },
651
+ ];
652
+ const { archive } = buildVBundle({ files });
653
+
654
+ // Buffer-based path.
655
+ mkdirSync(bufferWorkspace, { recursive: true });
656
+ const bufferResult = commitImport({
657
+ archiveData: archive,
658
+ pathResolver: new DefaultPathResolver(bufferWorkspace),
659
+ workspaceDir: bufferWorkspace,
660
+ });
661
+
662
+ // Streaming path.
663
+ const streamResult = await streamCommitImport({
664
+ source: readableFrom(archive),
665
+ pathResolver: new DefaultPathResolver(streamWorkspace),
666
+ workspaceDir: streamWorkspace,
667
+ });
668
+
669
+ try {
670
+ expect(bufferResult.ok).toBe(true);
671
+ expect(streamResult.ok).toBe(true);
672
+ if (!bufferResult.ok || !streamResult.ok) throw new Error("unreachable");
673
+
674
+ // The shapes must match key-for-key.
675
+ expect(Object.keys(streamResult.report).sort()).toEqual(
676
+ Object.keys(bufferResult.report).sort(),
677
+ );
678
+ expect(Object.keys(streamResult.report.summary).sort()).toEqual(
679
+ Object.keys(bufferResult.report.summary).sort(),
680
+ );
681
+ expect(streamResult.report.files.length).toBe(
682
+ bufferResult.report.files.length,
683
+ );
684
+ for (let i = 0; i < streamResult.report.files.length; i++) {
685
+ expect(Object.keys(streamResult.report.files[i]).sort()).toEqual(
686
+ Object.keys(bufferResult.report.files[i]).sort(),
687
+ );
688
+ }
689
+
690
+ // Manifest payload itself should match — the streaming path parses it
691
+ // directly from the same bytes.
692
+ expect(streamResult.report.manifest.manifest_sha256).toBe(
693
+ bufferResult.report.manifest.manifest_sha256,
694
+ );
695
+ } finally {
696
+ for (const ws of [bufferWorkspace, streamWorkspace]) {
697
+ const parent = join(ws, "..");
698
+ try {
699
+ rmSync(parent, { recursive: true, force: true });
700
+ } catch {
701
+ // best-effort
702
+ }
703
+ }
704
+ }
705
+ });
706
+ });
707
+
708
+ // ---------------------------------------------------------------------------
709
+ // Parity with commitImport: workspace-swap gating, config sanitization,
710
+ // legacy USER.md skip when persona is customized. Each of these regressed
711
+ // when the streaming path was first introduced.
712
+ // ---------------------------------------------------------------------------
713
+
714
+ describe("streamCommitImport — no workspace entries means no swap", () => {
715
+ let workspaceDir: string;
716
+ beforeEach(() => {
717
+ workspaceDir = freshWorkspace();
718
+ });
719
+ afterEach(() => {
720
+ const parent = join(workspaceDir, "..");
721
+ try {
722
+ rmSync(parent, { recursive: true, force: true });
723
+ } catch {
724
+ // best-effort
725
+ }
726
+ });
727
+
728
+ test("bundle with only credentials leaves the real workspace untouched", async () => {
729
+ // Seed the real workspace with a marker file. A successful import that
730
+ // had workspace entries would wipe-and-swap this file out of existence,
731
+ // so its survival post-import proves we skipped the rename pair.
732
+ mkdirSync(workspaceDir, { recursive: true });
733
+ writeFileSync(
734
+ join(workspaceDir, "marker-please-preserve.txt"),
735
+ "do not touch\n",
736
+ );
737
+ mkdirSync(join(workspaceDir, "deep", "nested"), { recursive: true });
738
+ writeFileSync(
739
+ join(workspaceDir, "deep", "nested", "file.txt"),
740
+ "nested content\n",
741
+ );
742
+
743
+ // Credential entries resolve to null via DefaultPathResolver — they are
744
+ // buffered in memory for CES, never land on disk. A bundle consisting
745
+ // entirely of credentials therefore has zero workspace-targeted writes.
746
+ const { archive } = buildVBundle({
747
+ files: [
748
+ {
749
+ path: "credentials/openai-key",
750
+ data: new TextEncoder().encode("sk-test-creds-only"),
751
+ },
752
+ ],
753
+ });
754
+
755
+ const received: Array<{ account: string; value: string }> = [];
756
+ const result = await streamCommitImport({
757
+ source: readableFrom(archive),
758
+ pathResolver: new DefaultPathResolver(workspaceDir),
759
+ workspaceDir,
760
+ importCredentials: async (creds) => {
761
+ received.push(...creds);
762
+ },
763
+ });
764
+
765
+ expect(result.ok).toBe(true);
766
+ if (!result.ok) throw new Error("unreachable");
767
+
768
+ // Real workspace's pre-existing files are STILL THERE — the temp tree
769
+ // was not swapped in.
770
+ expect(
771
+ readFileSync(join(workspaceDir, "marker-please-preserve.txt"), "utf8"),
772
+ ).toBe("do not touch\n");
773
+ expect(
774
+ readFileSync(join(workspaceDir, "deep", "nested", "file.txt"), "utf8"),
775
+ ).toBe("nested content\n");
776
+
777
+ // Credentials still flowed through post-commit as they should.
778
+ expect(received).toEqual([
779
+ { account: "openai-key", value: "sk-test-creds-only" },
780
+ ]);
781
+
782
+ // Report should reflect "nothing imported into the workspace".
783
+ expect(result.report.summary.files_created).toBe(0);
784
+ expect(result.report.summary.files_overwritten).toBe(0);
785
+
786
+ // Cleanup removed the temp dir — no sibling left behind.
787
+ const parent = join(workspaceDir, "..");
788
+ const base = workspaceDir.split("/").pop()!;
789
+ const siblings = readdirSync(parent);
790
+ const leftover = siblings.filter(
791
+ (name) =>
792
+ name.startsWith(`${base}.import-`) ||
793
+ name.startsWith(`${base}.pre-import-`),
794
+ );
795
+ expect(leftover).toEqual([]);
796
+ });
797
+
798
+ test("bundle with only out-of-workspace resolved targets leaves real workspace untouched", async () => {
799
+ mkdirSync(workspaceDir, { recursive: true });
800
+ writeFileSync(
801
+ join(workspaceDir, "marker-please-preserve.txt"),
802
+ "survive\n",
803
+ );
804
+
805
+ // A resolver that resolves paths OUTSIDE the workspace dir — the
806
+ // streaming importer drains these through the verifier and records
807
+ // them as skipped, so no writes land in the temp workspace.
808
+ const outOfWorkspaceDir = realpathSync(
809
+ mkdtempSync(join(tmpdir(), "oow-target-")),
810
+ );
811
+ const externalResolver = {
812
+ resolve(archivePath: string): string | null {
813
+ if (archivePath.startsWith("credentials/")) return null;
814
+ return join(outOfWorkspaceDir, archivePath.replace(/\//g, "_"));
815
+ },
816
+ };
817
+
818
+ const { archive } = buildVBundle({
819
+ files: [
820
+ {
821
+ path: "workspace/something.txt",
822
+ data: new TextEncoder().encode("ignored"),
823
+ },
824
+ ],
825
+ });
826
+
827
+ try {
828
+ const result = await streamCommitImport({
829
+ source: readableFrom(archive),
830
+ pathResolver: externalResolver,
831
+ workspaceDir,
832
+ });
833
+
834
+ expect(result.ok).toBe(true);
835
+ if (!result.ok) throw new Error("unreachable");
836
+
837
+ // Everything was skipped as "outside workspace".
838
+ expect(result.report.summary.files_skipped).toBeGreaterThanOrEqual(1);
839
+ expect(result.report.summary.files_created).toBe(0);
840
+
841
+ // Real workspace is still intact.
842
+ expect(
843
+ readFileSync(join(workspaceDir, "marker-please-preserve.txt"), "utf8"),
844
+ ).toBe("survive\n");
845
+ } finally {
846
+ try {
847
+ rmSync(outOfWorkspaceDir, { recursive: true, force: true });
848
+ } catch {
849
+ // best-effort
850
+ }
851
+ }
852
+ });
853
+ });
854
+
855
+ describe("streamCommitImport — config sanitization parity", () => {
856
+ let workspaceDir: string;
857
+ beforeEach(() => {
858
+ workspaceDir = freshWorkspace();
859
+ });
860
+ afterEach(() => {
861
+ const parent = join(workspaceDir, "..");
862
+ try {
863
+ rmSync(parent, { recursive: true, force: true });
864
+ } catch {
865
+ // best-effort
866
+ }
867
+ });
868
+
869
+ test("workspace/config.json is sanitized before being written", async () => {
870
+ // sanitizeConfigForTransfer strips `daemon` entirely, clears
871
+ // `ingress.publicBaseUrl`, deletes `ingress.enabled`, and zeros
872
+ // `skills.load.extraDirs`. We plant all of these in the archived
873
+ // config and assert they're gone on disk.
874
+ const tainted = JSON.stringify({
875
+ daemon: { pid: 1234, host: "private.example.com" },
876
+ ingress: { publicBaseUrl: "https://leaky.example", enabled: true },
877
+ skills: { load: { extraDirs: ["/tmp/leak-a", "/tmp/leak-b"] } },
878
+ unrelated: "keep-me",
879
+ });
880
+
881
+ const { archive } = buildVBundle({
882
+ files: [
883
+ {
884
+ path: "workspace/config.json",
885
+ data: new TextEncoder().encode(tainted),
886
+ },
887
+ ],
888
+ });
889
+
890
+ const result = await streamCommitImport({
891
+ source: readableFrom(archive),
892
+ pathResolver: new DefaultPathResolver(workspaceDir),
893
+ workspaceDir,
894
+ });
895
+
896
+ expect(result.ok).toBe(true);
897
+ if (!result.ok) throw new Error("unreachable");
898
+
899
+ const writtenPath = join(workspaceDir, "config.json");
900
+ expect(existsSync(writtenPath)).toBe(true);
901
+ const writtenJson = JSON.parse(readFileSync(writtenPath, "utf8")) as Record<
902
+ string,
903
+ unknown
904
+ >;
905
+
906
+ // Environment-specific fields have been stripped or reset.
907
+ expect(writtenJson.daemon).toBeUndefined();
908
+ expect((writtenJson.ingress as Record<string, unknown>).publicBaseUrl).toBe(
909
+ "",
910
+ );
911
+ expect(
912
+ (writtenJson.ingress as Record<string, unknown>).enabled,
913
+ ).toBeUndefined();
914
+ expect(
915
+ (
916
+ (writtenJson.skills as Record<string, unknown>).load as Record<
917
+ string,
918
+ unknown
919
+ >
920
+ ).extraDirs,
921
+ ).toEqual([]);
922
+
923
+ // Unrelated content is preserved verbatim.
924
+ expect(writtenJson.unrelated).toBe("keep-me");
925
+ });
926
+ });
927
+
928
+ describe("streamCommitImport — legacy USER.md skip on customized persona", () => {
929
+ let workspaceDir: string;
930
+ beforeEach(() => {
931
+ workspaceDir = freshWorkspace();
932
+ });
933
+ afterEach(() => {
934
+ const parent = join(workspaceDir, "..");
935
+ try {
936
+ rmSync(parent, { recursive: true, force: true });
937
+ } catch {
938
+ // best-effort
939
+ }
940
+ });
941
+
942
+ test("skips writing prompts/USER.md when guardian persona is customized", async () => {
943
+ // Seed the live workspace with a customized guardian persona file.
944
+ // `isGuardianPersonaCustomized` inspects its content vs the bare
945
+ // scaffold template; customized content must prevent the write.
946
+ //
947
+ // NOTE: the streaming importer uses an atomic temp-dir swap. The swap
948
+ // REPLACES the entire live workspace with the temp workspace — so the
949
+ // customized file at users/captain.md is expected to be gone after
950
+ // import regardless (its contents are not re-materialized into the
951
+ // temp tree). The behavior under test here is narrower: we verify
952
+ // the legacy bundle's USER.md content was NEVER written into the
953
+ // temp workspace's guardian path, and that the entry is reported as
954
+ // `"skipped"` with a warning. This matches commitImport's semantics
955
+ // for the legacy entry itself.
956
+ const guardianPath = join(workspaceDir, "users", "captain.md");
957
+ mkdirSync(join(workspaceDir, "users"), { recursive: true });
958
+ writeFileSync(guardianPath, CUSTOMIZED_PERSONA_FIXTURE, "utf8");
959
+
960
+ const legacyContent = new TextEncoder().encode(
961
+ "# Legacy bundle persona — should NOT be written over a customized file\n",
962
+ );
963
+
964
+ const resolver = new DefaultPathResolver(
965
+ workspaceDir,
966
+ undefined,
967
+ () => guardianPath,
968
+ );
969
+
970
+ const { archive } = buildVBundle({
971
+ files: [
972
+ {
973
+ path: "prompts/USER.md",
974
+ data: legacyContent,
975
+ },
976
+ // Second entry ensures there's at least one workspace-targeted
977
+ // write, so the atomic swap runs and we're exercising the skip
978
+ // branch on the full flow rather than the no-swap short circuit.
979
+ {
980
+ path: "workspace/other.txt",
981
+ data: new TextEncoder().encode("other content"),
982
+ },
983
+ ],
984
+ });
985
+
986
+ const result = await streamCommitImport({
987
+ source: readableFrom(archive),
988
+ pathResolver: resolver,
989
+ workspaceDir,
990
+ });
991
+
992
+ expect(result.ok).toBe(true);
993
+ if (!result.ok) throw new Error("unreachable");
994
+
995
+ // The other file in the bundle was written normally (proves the swap
996
+ // happened — we're not accidentally hitting the no-swap short circuit).
997
+ expect(readFileSync(join(workspaceDir, "other.txt"), "utf8")).toBe(
998
+ "other content",
999
+ );
1000
+
1001
+ // Legacy content was NEVER written to the guardian target path. Because
1002
+ // the USER.md entry was skipped (not written into the temp tree) and
1003
+ // the swap replaces the entire workspace with the temp tree, the
1004
+ // guardian path should simply not exist after import.
1005
+ if (existsSync(guardianPath)) {
1006
+ // If something did write there, it must not be the bundle's legacy
1007
+ // content — that's the crucial regression to prevent.
1008
+ expect(readFileSync(guardianPath, "utf8")).not.toBe(
1009
+ new TextDecoder().decode(legacyContent),
1010
+ );
1011
+ } else {
1012
+ expect(existsSync(guardianPath)).toBe(false);
1013
+ }
1014
+
1015
+ // The legacy entry is present in the report as "skipped".
1016
+ const legacyEntry = result.report.files.find(
1017
+ (f) => f.path === "prompts/USER.md",
1018
+ );
1019
+ expect(legacyEntry).toBeDefined();
1020
+ expect(legacyEntry!.action).toBe("skipped");
1021
+
1022
+ // A warning surfaces the skip reason.
1023
+ expect(
1024
+ result.report.warnings.some((w) => w.includes("prompts/USER.md")),
1025
+ ).toBe(true);
1026
+ });
1027
+ });
1028
+
1029
+ // ---------------------------------------------------------------------------
1030
+ // Carry-over parity: live workspace paths that the buffer-based importer
1031
+ // preserves (data/db, data/qdrant, embedding-models, deprecated) must also
1032
+ // survive the streaming importer's temp-dir atomic swap when the bundle
1033
+ // does not carry them. Without this behavior, a partial bundle (e.g. one
1034
+ // that only ships prompts + config) would wipe the user's SQLite DB and
1035
+ // Qdrant vector store when imported through the streaming path.
1036
+ // ---------------------------------------------------------------------------
1037
+
1038
+ describe("streamCommitImport — preserves live workspace paths when bundle omits them", () => {
1039
+ let workspaceDir: string;
1040
+ beforeEach(() => {
1041
+ workspaceDir = freshWorkspace();
1042
+ });
1043
+ afterEach(() => {
1044
+ const parent = join(workspaceDir, "..");
1045
+ try {
1046
+ rmSync(parent, { recursive: true, force: true });
1047
+ } catch {
1048
+ // best-effort
1049
+ }
1050
+ });
1051
+
1052
+ test("keeps the live data/db/assistant.db when the bundle omits data/db/*", async () => {
1053
+ // Seed the live workspace with a fake SQLite DB whose contents we
1054
+ // can identify post-import.
1055
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
1056
+ const dbContent = Buffer.from("SQLite-format-3\0live-db-payload");
1057
+ writeFileSync(join(workspaceDir, "data", "db", "assistant.db"), dbContent);
1058
+
1059
+ // A bundle that writes a config file but carries nothing under
1060
+ // workspace/data/db/.
1061
+ const { archive } = buildVBundle({
1062
+ files: [
1063
+ {
1064
+ path: "workspace/skills/example.md",
1065
+ data: new TextEncoder().encode("# skill\n"),
1066
+ },
1067
+ ],
1068
+ });
1069
+
1070
+ const result = await streamCommitImport({
1071
+ source: readableFrom(archive),
1072
+ pathResolver: new DefaultPathResolver(workspaceDir),
1073
+ workspaceDir,
1074
+ });
1075
+
1076
+ expect(result.ok).toBe(true);
1077
+ if (!result.ok) throw new Error("unreachable");
1078
+
1079
+ // Live DB survived the atomic swap with its exact original bytes.
1080
+ const postDbPath = join(workspaceDir, "data", "db", "assistant.db");
1081
+ expect(existsSync(postDbPath)).toBe(true);
1082
+ expect(readFileSync(postDbPath)).toEqual(dbContent);
1083
+
1084
+ // The bundle-provided file also landed.
1085
+ expect(
1086
+ readFileSync(join(workspaceDir, "skills", "example.md"), "utf8"),
1087
+ ).toBe("# skill\n");
1088
+ });
1089
+
1090
+ test("keeps the live data/qdrant/ directory when the bundle omits qdrant entries", async () => {
1091
+ // Populate a fake qdrant store with a nested file.
1092
+ mkdirSync(join(workspaceDir, "data", "qdrant", "segments"), {
1093
+ recursive: true,
1094
+ });
1095
+ const segmentBytes = Buffer.from("qdrant-segment-bytes");
1096
+ writeFileSync(
1097
+ join(workspaceDir, "data", "qdrant", "segments", "0.seg"),
1098
+ segmentBytes,
1099
+ );
1100
+
1101
+ const { archive } = buildVBundle({
1102
+ files: [
1103
+ {
1104
+ path: "workspace/config.json",
1105
+ data: new TextEncoder().encode("{}"),
1106
+ },
1107
+ ],
1108
+ });
1109
+
1110
+ const result = await streamCommitImport({
1111
+ source: readableFrom(archive),
1112
+ pathResolver: new DefaultPathResolver(workspaceDir),
1113
+ workspaceDir,
1114
+ });
1115
+
1116
+ expect(result.ok).toBe(true);
1117
+ if (!result.ok) throw new Error("unreachable");
1118
+
1119
+ const postSegPath = join(
1120
+ workspaceDir,
1121
+ "data",
1122
+ "qdrant",
1123
+ "segments",
1124
+ "0.seg",
1125
+ );
1126
+ expect(existsSync(postSegPath)).toBe(true);
1127
+ expect(readFileSync(postSegPath)).toEqual(segmentBytes);
1128
+ });
1129
+
1130
+ test("lets the bundle overwrite data/db when it does carry an assistant.db entry", async () => {
1131
+ // Seed the live workspace with OLD content so we can tell whether the
1132
+ // carry-over logic accidentally kept it instead of honoring the
1133
+ // bundle's new DB file.
1134
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
1135
+ writeFileSync(
1136
+ join(workspaceDir, "data", "db", "assistant.db"),
1137
+ "OLD-LIVE-DB",
1138
+ );
1139
+
1140
+ const newDbBytes = new TextEncoder().encode("NEW-BUNDLE-DB");
1141
+ const { archive } = buildVBundle({
1142
+ files: [
1143
+ {
1144
+ path: "workspace/data/db/assistant.db",
1145
+ data: newDbBytes,
1146
+ },
1147
+ ],
1148
+ });
1149
+
1150
+ const result = await streamCommitImport({
1151
+ source: readableFrom(archive),
1152
+ pathResolver: new DefaultPathResolver(workspaceDir),
1153
+ workspaceDir,
1154
+ });
1155
+
1156
+ expect(result.ok).toBe(true);
1157
+ if (!result.ok) throw new Error("unreachable");
1158
+
1159
+ const postDbPath = join(workspaceDir, "data", "db", "assistant.db");
1160
+ expect(readFileSync(postDbPath)).toEqual(Buffer.from(newDbBytes));
1161
+ });
1162
+ });
1163
+
1164
+ // ---------------------------------------------------------------------------
1165
+ // Gap A — legacy-only bundle (no `workspace/*` entries) writes each file in
1166
+ // place WITHOUT triggering the atomic workspace swap. This matches
1167
+ // commitImport's legacy branch: a `data/db/assistant.db`- or
1168
+ // `config/settings.json`- style bundle preserves arbitrary live files
1169
+ // outside WORKSPACE_PRESERVE_PATHS (where the swap path would have wiped
1170
+ // them). Regression test for the self-review Gap A finding.
1171
+ // ---------------------------------------------------------------------------
1172
+
1173
+ describe("streamCommitImport — legacy-only bundle writes in place", () => {
1174
+ let workspaceDir: string;
1175
+ beforeEach(() => {
1176
+ workspaceDir = freshWorkspace();
1177
+ });
1178
+ afterEach(() => {
1179
+ const parent = join(workspaceDir, "..");
1180
+ try {
1181
+ rmSync(parent, { recursive: true, force: true });
1182
+ } catch {
1183
+ // best-effort
1184
+ }
1185
+ });
1186
+
1187
+ test("bundle carrying only `data/db/assistant.db` writes the DB in place and leaves unrelated live files untouched", async () => {
1188
+ // Seed the live workspace with BOTH a pre-existing `data/db/assistant.db`
1189
+ // (which the bundle will overwrite) AND an unrelated marker file at
1190
+ // top-level that is not in WORKSPACE_PRESERVE_PATHS — proving the old
1191
+ // atomic-swap path would have wiped it. Under the legacy-only branch
1192
+ // that file must survive.
1193
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
1194
+ writeFileSync(
1195
+ join(workspaceDir, "data", "db", "assistant.db"),
1196
+ "OLD-LIVE-DB",
1197
+ );
1198
+ writeFileSync(
1199
+ join(workspaceDir, "unrelated-top-level.txt"),
1200
+ "must-survive-legacy-import\n",
1201
+ );
1202
+ // A sibling directory — also outside preserve paths — that the atomic
1203
+ // swap would have wiped.
1204
+ mkdirSync(join(workspaceDir, "custom-user-dir"), { recursive: true });
1205
+ writeFileSync(
1206
+ join(workspaceDir, "custom-user-dir", "note.md"),
1207
+ "custom note\n",
1208
+ );
1209
+
1210
+ const newDbBytes = new TextEncoder().encode("NEW-LEGACY-BUNDLE-DB");
1211
+ const { archive } = buildVBundle({
1212
+ files: [
1213
+ {
1214
+ // Legacy archive path — NO `workspace/` prefix. This is what
1215
+ // older bundle exports produce.
1216
+ path: "data/db/assistant.db",
1217
+ data: newDbBytes,
1218
+ },
1219
+ ],
1220
+ });
1221
+
1222
+ const result = await streamCommitImport({
1223
+ source: readableFrom(archive),
1224
+ pathResolver: new DefaultPathResolver(workspaceDir),
1225
+ workspaceDir,
1226
+ });
1227
+
1228
+ expect(result.ok).toBe(true);
1229
+ if (!result.ok) throw new Error("unreachable");
1230
+
1231
+ // Bundle's DB landed at the right live location with the new bytes.
1232
+ const postDbPath = join(workspaceDir, "data", "db", "assistant.db");
1233
+ expect(readFileSync(postDbPath)).toEqual(Buffer.from(newDbBytes));
1234
+
1235
+ // Unrelated live files still present — the atomic-swap path would
1236
+ // have wiped them. Their survival here proves we took the in-place
1237
+ // legacy branch.
1238
+ expect(
1239
+ readFileSync(join(workspaceDir, "unrelated-top-level.txt"), "utf8"),
1240
+ ).toBe("must-survive-legacy-import\n");
1241
+ expect(
1242
+ readFileSync(join(workspaceDir, "custom-user-dir", "note.md"), "utf8"),
1243
+ ).toBe("custom note\n");
1244
+
1245
+ // The legacy entry is reported as "overwritten" (pre-existing DB),
1246
+ // with a backup path captured alongside.
1247
+ const dbEntry = result.report.files.find(
1248
+ (f) => f.path === "data/db/assistant.db",
1249
+ );
1250
+ expect(dbEntry).toBeDefined();
1251
+ expect(dbEntry!.action).toBe("overwritten");
1252
+ expect(dbEntry!.backup_path).not.toBeNull();
1253
+ // And the report summary reflects the same.
1254
+ expect(result.report.summary.files_overwritten).toBe(1);
1255
+ expect(result.report.summary.backups_created).toBe(1);
1256
+ });
1257
+ });
1258
+
1259
+ // ---------------------------------------------------------------------------
1260
+ // Gap B — carry-over does per-file merge inside preserved directories.
1261
+ // A bundle that writes ONE file under `workspace/data/qdrant/` must not
1262
+ // cause the rest of the live `data/qdrant/` tree to be wiped by the
1263
+ // atomic swap. Regression test for the self-review Gap B finding.
1264
+ // ---------------------------------------------------------------------------
1265
+
1266
+ describe("streamCommitImport — preserved-path carry-over is per-file", () => {
1267
+ let workspaceDir: string;
1268
+ beforeEach(() => {
1269
+ workspaceDir = freshWorkspace();
1270
+ });
1271
+ afterEach(() => {
1272
+ const parent = join(workspaceDir, "..");
1273
+ try {
1274
+ rmSync(parent, { recursive: true, force: true });
1275
+ } catch {
1276
+ // best-effort
1277
+ }
1278
+ });
1279
+
1280
+ test("bundle touching one file in data/qdrant/ does not wipe other live files in the same preserved dir", async () => {
1281
+ // Populate the live qdrant store with several nested files / dirs.
1282
+ // Only one of these paths overlaps with what the bundle will write;
1283
+ // the rest must survive the atomic swap via per-file carry-over.
1284
+ mkdirSync(join(workspaceDir, "data", "qdrant", "segments"), {
1285
+ recursive: true,
1286
+ });
1287
+ writeFileSync(
1288
+ join(workspaceDir, "data", "qdrant", "segments", "0.seg"),
1289
+ "seg-0-live",
1290
+ );
1291
+ writeFileSync(
1292
+ join(workspaceDir, "data", "qdrant", "segments", "1.seg"),
1293
+ "seg-1-live",
1294
+ );
1295
+ // A top-level file inside the preserved dir that the bundle will
1296
+ // overwrite.
1297
+ writeFileSync(
1298
+ join(workspaceDir, "data", "qdrant", "meta.json"),
1299
+ '{"stale":true}',
1300
+ );
1301
+ // A sibling directory the bundle never touches — must survive.
1302
+ mkdirSync(join(workspaceDir, "data", "qdrant", "wal"), {
1303
+ recursive: true,
1304
+ });
1305
+ writeFileSync(
1306
+ join(workspaceDir, "data", "qdrant", "wal", "wal-000"),
1307
+ "wal-entry",
1308
+ );
1309
+
1310
+ // Bundle carries exactly ONE file under `data/qdrant/` AND at least
1311
+ // one other `workspace/*` entry so the atomic-swap path fires (not
1312
+ // the legacy in-place path).
1313
+ const newMeta = new TextEncoder().encode('{"fresh":true}');
1314
+ const { archive } = buildVBundle({
1315
+ files: [
1316
+ {
1317
+ path: "workspace/data/qdrant/meta.json",
1318
+ data: newMeta,
1319
+ },
1320
+ {
1321
+ path: "workspace/marker.txt",
1322
+ data: new TextEncoder().encode("marker\n"),
1323
+ },
1324
+ ],
1325
+ });
1326
+
1327
+ const result = await streamCommitImport({
1328
+ source: readableFrom(archive),
1329
+ pathResolver: new DefaultPathResolver(workspaceDir),
1330
+ workspaceDir,
1331
+ });
1332
+
1333
+ expect(result.ok).toBe(true);
1334
+ if (!result.ok) throw new Error("unreachable");
1335
+
1336
+ // Bundle's overwrite landed.
1337
+ expect(
1338
+ readFileSync(join(workspaceDir, "data", "qdrant", "meta.json")),
1339
+ ).toEqual(Buffer.from(newMeta));
1340
+
1341
+ // All the other live files in the preserved dir survived.
1342
+ expect(
1343
+ readFileSync(
1344
+ join(workspaceDir, "data", "qdrant", "segments", "0.seg"),
1345
+ "utf8",
1346
+ ),
1347
+ ).toBe("seg-0-live");
1348
+ expect(
1349
+ readFileSync(
1350
+ join(workspaceDir, "data", "qdrant", "segments", "1.seg"),
1351
+ "utf8",
1352
+ ),
1353
+ ).toBe("seg-1-live");
1354
+ expect(
1355
+ readFileSync(
1356
+ join(workspaceDir, "data", "qdrant", "wal", "wal-000"),
1357
+ "utf8",
1358
+ ),
1359
+ ).toBe("wal-entry");
1360
+
1361
+ // And the workspace-targeted file outside the preserved dir landed.
1362
+ expect(readFileSync(join(workspaceDir, "marker.txt"), "utf8")).toBe(
1363
+ "marker\n",
1364
+ );
1365
+ });
1366
+ });
1367
+
1368
+ // ---------------------------------------------------------------------------
1369
+ // Gap C — resource ceilings. Bundles declaring too many entries or too
1370
+ // many bytes of data should abort with validation_failed before we commit
1371
+ // anything to disk. Uses the test-only `maxBundleEntries` /
1372
+ // `maxBundleBytes` knobs so we can exercise the abort with tiny
1373
+ // fixtures. Regression test for the self-review Gap C finding.
1374
+ // ---------------------------------------------------------------------------
1375
+
1376
+ describe("streamCommitImport — bundle resource ceilings", () => {
1377
+ let workspaceDir: string;
1378
+ beforeEach(() => {
1379
+ workspaceDir = freshWorkspace();
1380
+ });
1381
+ afterEach(() => {
1382
+ const parent = join(workspaceDir, "..");
1383
+ try {
1384
+ rmSync(parent, { recursive: true, force: true });
1385
+ } catch {
1386
+ // best-effort
1387
+ }
1388
+ });
1389
+
1390
+ /** Assert no sibling temp/backup dirs remain. */
1391
+ function assertNoLeftoverTempDirs(): void {
1392
+ const parent = join(workspaceDir, "..");
1393
+ const base = workspaceDir.split("/").pop()!;
1394
+ const siblings = readdirSync(parent);
1395
+ const leftover = siblings.filter(
1396
+ (name) =>
1397
+ name.startsWith(`${base}.import-`) ||
1398
+ name.startsWith(`${base}.pre-import-`),
1399
+ );
1400
+ expect(leftover).toEqual([]);
1401
+ }
1402
+
1403
+ test("bundle declaring more entries than maxBundleEntries → validation_failed with bundle_too_many_entries", async () => {
1404
+ mkdirSync(workspaceDir, { recursive: true });
1405
+ writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
1406
+
1407
+ // Three small files — we'll set the cap to 2 so the manifest's
1408
+ // declared-count check trips before any tar entry is processed.
1409
+ const { archive } = buildVBundle({
1410
+ files: [
1411
+ { path: "workspace/a.txt", data: new TextEncoder().encode("a") },
1412
+ { path: "workspace/b.txt", data: new TextEncoder().encode("b") },
1413
+ { path: "workspace/c.txt", data: new TextEncoder().encode("c") },
1414
+ ],
1415
+ });
1416
+
1417
+ const result = await streamCommitImport({
1418
+ source: readableFrom(archive),
1419
+ pathResolver: new DefaultPathResolver(workspaceDir),
1420
+ workspaceDir,
1421
+ maxBundleEntries: 2,
1422
+ });
1423
+
1424
+ expect(result.ok).toBe(false);
1425
+ if (result.ok) throw new Error("unreachable");
1426
+ expect(result.reason).toBe("validation_failed");
1427
+ if (result.reason !== "validation_failed") throw new Error("unreachable");
1428
+ expect(result.errors[0]!.code).toBe("bundle_too_many_entries");
1429
+
1430
+ // Real workspace untouched, temp dir cleaned up.
1431
+ expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
1432
+ "keep me\n",
1433
+ );
1434
+ assertNoLeftoverTempDirs();
1435
+ });
1436
+
1437
+ test("bundle exceeding maxBundleBytes → validation_failed with bundle_too_large", async () => {
1438
+ mkdirSync(workspaceDir, { recursive: true });
1439
+ writeFileSync(join(workspaceDir, "existing.txt"), "keep me\n");
1440
+
1441
+ // Two files totaling 200 bytes of user data. Cap at 128 bytes so the
1442
+ // second file's streamed size pushes totalBytesStreamed over.
1443
+ const big = new Uint8Array(100).fill(0x41);
1444
+ const { archive } = buildVBundle({
1445
+ files: [
1446
+ { path: "workspace/big1.bin", data: big },
1447
+ { path: "workspace/big2.bin", data: big },
1448
+ ],
1449
+ });
1450
+
1451
+ const result = await streamCommitImport({
1452
+ source: readableFrom(archive),
1453
+ pathResolver: new DefaultPathResolver(workspaceDir),
1454
+ workspaceDir,
1455
+ maxBundleBytes: 128,
1456
+ });
1457
+
1458
+ expect(result.ok).toBe(false);
1459
+ if (result.ok) throw new Error("unreachable");
1460
+ expect(result.reason).toBe("validation_failed");
1461
+ if (result.reason !== "validation_failed") throw new Error("unreachable");
1462
+ expect(result.errors[0]!.code).toBe("bundle_too_large");
1463
+
1464
+ expect(readFileSync(join(workspaceDir, "existing.txt"), "utf8")).toBe(
1465
+ "keep me\n",
1466
+ );
1467
+ assertNoLeftoverTempDirs();
1468
+ });
1469
+ });
1470
+
1471
+ // ---------------------------------------------------------------------------
1472
+ // Gap D — sanitized config reports sha256 of the BYTES ACTUALLY WRITTEN,
1473
+ // not the manifest-declared sha of the raw archive content. commitImport
1474
+ // already does this; the streaming importer regressed to reporting the
1475
+ // manifest sha, so downstream integrity re-checks failed. Regression test
1476
+ // for the self-review Gap D finding.
1477
+ // ---------------------------------------------------------------------------
1478
+
1479
+ describe("streamCommitImport — report.sha256 reflects post-sanitization bytes", () => {
1480
+ let workspaceDir: string;
1481
+ beforeEach(() => {
1482
+ workspaceDir = freshWorkspace();
1483
+ });
1484
+ afterEach(() => {
1485
+ const parent = join(workspaceDir, "..");
1486
+ try {
1487
+ rmSync(parent, { recursive: true, force: true });
1488
+ } catch {
1489
+ // best-effort
1490
+ }
1491
+ });
1492
+
1493
+ test("workspace/config.json report.sha256 equals sha256 of the on-disk sanitized bytes, not the raw archive sha", async () => {
1494
+ // Plant content that sanitizeConfigForTransfer will demonstrably
1495
+ // change (`daemon` is deleted, `ingress.publicBaseUrl` cleared, etc).
1496
+ // The raw archive bytes and the post-sanitization bytes therefore
1497
+ // differ — a correctly implemented importer reports the latter's sha.
1498
+ const tainted = JSON.stringify({
1499
+ daemon: { pid: 9999, host: "private.example.com" },
1500
+ ingress: { publicBaseUrl: "https://leak.example", enabled: true },
1501
+ unrelated: "keep-me",
1502
+ });
1503
+ const rawArchiveBytes = new TextEncoder().encode(tainted);
1504
+ const rawArchiveSha = sha256Hex(rawArchiveBytes);
1505
+
1506
+ const { archive } = buildVBundle({
1507
+ files: [
1508
+ {
1509
+ path: "workspace/config.json",
1510
+ data: rawArchiveBytes,
1511
+ },
1512
+ ],
1513
+ });
1514
+
1515
+ const result = await streamCommitImport({
1516
+ source: readableFrom(archive),
1517
+ pathResolver: new DefaultPathResolver(workspaceDir),
1518
+ workspaceDir,
1519
+ });
1520
+
1521
+ expect(result.ok).toBe(true);
1522
+ if (!result.ok) throw new Error("unreachable");
1523
+
1524
+ const writtenPath = join(workspaceDir, "config.json");
1525
+ const onDiskBytes = readFileSync(writtenPath);
1526
+ const onDiskSha = sha256Hex(onDiskBytes);
1527
+
1528
+ // Sanity: sanitization actually changed the bytes (otherwise the test
1529
+ // would be tautologically true).
1530
+ expect(onDiskSha).not.toBe(rawArchiveSha);
1531
+
1532
+ const configEntry = result.report.files.find(
1533
+ (f) => f.path === "workspace/config.json",
1534
+ );
1535
+ expect(configEntry).toBeDefined();
1536
+ expect(configEntry!.sha256).toBe(onDiskSha);
1537
+ // Reported size is also the post-sanitization size, not raw.
1538
+ expect(configEntry!.size).toBe(onDiskBytes.length);
1539
+ });
1540
+ });