@vellumai/assistant 0.7.0 → 0.7.1

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 (666) hide show
  1. package/ARCHITECTURE.md +6 -7
  2. package/Dockerfile +1 -0
  3. package/README.md +2 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +79 -139
  5. package/bun.lock +3 -0
  6. package/docs/architecture/security.md +18 -16
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +1 -5
  9. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -5
  10. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -16
  11. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +1 -9
  12. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +12 -12
  13. package/node_modules/@vellumai/slack-text/bun.lock +24 -0
  14. package/node_modules/@vellumai/slack-text/package.json +18 -0
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +153 -0
  16. package/node_modules/@vellumai/slack-text/src/index.ts +235 -0
  17. package/node_modules/@vellumai/slack-text/tsconfig.json +20 -0
  18. package/openapi.yaml +294 -107
  19. package/package.json +4 -2
  20. package/scripts/generate-openapi.ts +16 -111
  21. package/src/__tests__/agent-wake-override-profile.test.ts +23 -1
  22. package/src/__tests__/anthropic-provider.test.ts +56 -13
  23. package/src/__tests__/app-conversation-ids-backfill.test.ts +278 -0
  24. package/src/__tests__/app-conversation-ids.test.ts +151 -0
  25. package/src/__tests__/approval-cascade.test.ts +0 -15
  26. package/src/__tests__/approval-routes-http.test.ts +6 -17
  27. package/src/__tests__/assistant-event-hub.test.ts +126 -77
  28. package/src/__tests__/assistant-event.test.ts +0 -5
  29. package/src/__tests__/assistant-events-sse-hardening.test.ts +37 -15
  30. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -29
  31. package/src/__tests__/background-shell-host-bash.test.ts +34 -43
  32. package/src/__tests__/call-controller.test.ts +1 -1
  33. package/src/__tests__/call-site-routing-provider.test.ts +193 -0
  34. package/src/__tests__/channel-approval-routes.test.ts +10 -296
  35. package/src/__tests__/channel-approvals.test.ts +25 -17
  36. package/src/__tests__/channel-guardian.test.ts +100 -146
  37. package/src/__tests__/checker.test.ts +20 -34
  38. package/src/__tests__/compact-event-conversation-id-guard.test.ts +50 -0
  39. package/src/__tests__/compaction-events.test.ts +2 -0
  40. package/src/__tests__/config-schema.test.ts +6 -48
  41. package/src/__tests__/config-watcher.test.ts +12 -0
  42. package/src/__tests__/connection-policy.test.ts +1 -52
  43. package/src/__tests__/contacts-write.test.ts +2 -64
  44. package/src/__tests__/context-image-dimensions.test.ts +1 -1
  45. package/src/__tests__/context-search-memory-source.test.ts +120 -1
  46. package/src/__tests__/context-search-memory-v2-source.test.ts +383 -0
  47. package/src/__tests__/context-search-pkb-source.test.ts +49 -0
  48. package/src/__tests__/context-search-workspace-source.test.ts +9 -22
  49. package/src/__tests__/context-window-manager.test.ts +46 -0
  50. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  51. package/src/__tests__/conversation-agent-loop-overflow.test.ts +102 -29
  52. package/src/__tests__/conversation-agent-loop.test.ts +980 -13
  53. package/src/__tests__/conversation-analysis-routes.test.ts +12 -10
  54. package/src/__tests__/conversation-attention-telegram.test.ts +11 -3
  55. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -291
  56. package/src/__tests__/conversation-history-web-search.test.ts +4 -3
  57. package/src/__tests__/conversation-inference-profile-route.test.ts +12 -23
  58. package/src/__tests__/conversation-lifecycle.test.ts +4 -4
  59. package/src/__tests__/conversation-process-callsite.test.ts +79 -2
  60. package/src/__tests__/conversation-queue.test.ts +3 -8
  61. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -161
  62. package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -32
  63. package/src/__tests__/conversation-routes-slash-commands.test.ts +75 -66
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +257 -3
  65. package/src/__tests__/conversation-slash-commands.test.ts +24 -4
  66. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  67. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  68. package/src/__tests__/conversation-starter-routes.test.ts +79 -2
  69. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -5
  70. package/src/__tests__/conversation-surfaces-standalone.test.ts +18 -14
  71. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -2
  72. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +8 -46
  73. package/src/__tests__/conversation-usage.test.ts +253 -3
  74. package/src/__tests__/credential-execution-shell-lockdown.test.ts +0 -39
  75. package/src/__tests__/credential-health-service.test.ts +68 -0
  76. package/src/__tests__/credential-security-e2e.test.ts +4 -3
  77. package/src/__tests__/credential-security-invariants.test.ts +1 -5
  78. package/src/__tests__/credential-token-resolver.test.ts +180 -0
  79. package/src/__tests__/cu-unified-flow.test.ts +33 -16
  80. package/src/__tests__/daemon-assistant-events.test.ts +34 -21
  81. package/src/__tests__/daemon-credential-client.test.ts +4 -1
  82. package/src/__tests__/db-connection-isolation.test.ts +125 -0
  83. package/src/__tests__/db-migration-rollback.test.ts +101 -0
  84. package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +169 -0
  85. package/src/__tests__/deterministic-verification-control-plane.test.ts +7 -80
  86. package/src/__tests__/document-conversations.test.ts +332 -0
  87. package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
  88. package/src/__tests__/emit-event-signal.test.ts +4 -6
  89. package/src/__tests__/events-client-registration.test.ts +193 -49
  90. package/src/__tests__/filing-service.test.ts +58 -7
  91. package/src/__tests__/first-greeting.test.ts +156 -150
  92. package/src/__tests__/fixtures/mock-chrome-extension.ts +108 -66
  93. package/src/__tests__/get-skill-detail-audit.test.ts +3 -8
  94. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  95. package/src/__tests__/guardian-dispatch.test.ts +1 -1
  96. package/src/__tests__/guardian-grant-minting.test.ts +7 -2
  97. package/src/__tests__/guardian-routing-invariants.test.ts +7 -2
  98. package/src/__tests__/guardian-routing-state.test.ts +1 -1
  99. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +32 -11
  100. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -83
  101. package/src/__tests__/headless-browser-mode.test.ts +4 -9
  102. package/src/__tests__/headless-browser-navigate.test.ts +21 -20
  103. package/src/__tests__/heartbeat-service.test.ts +289 -7
  104. package/src/__tests__/helpers/channel-test-adapter.ts +2 -2
  105. package/src/__tests__/helpers/create-guardian-binding.ts +91 -0
  106. package/src/__tests__/host-bash-proxy.test.ts +46 -122
  107. package/src/__tests__/host-browser-e2e-cloud.test.ts +36 -497
  108. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +26 -96
  109. package/src/__tests__/host-browser-proxy.test.ts +111 -185
  110. package/src/__tests__/host-browser-routes.test.ts +45 -75
  111. package/src/__tests__/host-browser-ws-events-e2e.test.ts +26 -30
  112. package/src/__tests__/host-cu-proxy.test.ts +56 -111
  113. package/src/__tests__/host-file-proxy.test.ts +44 -98
  114. package/src/__tests__/host-file-read-tool.test.ts +42 -21
  115. package/src/__tests__/host-shell-tool.test.ts +33 -68
  116. package/src/__tests__/host-transfer-pending-interactions.test.ts +2 -18
  117. package/src/__tests__/host-transfer-proxy.test.ts +43 -53
  118. package/src/__tests__/http-user-message-parity.test.ts +0 -6
  119. package/src/__tests__/inbound-slack-persistence.test.ts +31 -0
  120. package/src/__tests__/injector-chain.test.ts +10 -5
  121. package/src/__tests__/injector-pkb-v2-silenced.test.ts +124 -0
  122. package/src/__tests__/inline-command-runner.test.ts +0 -66
  123. package/src/__tests__/inline-skill-load-permissions.test.ts +0 -2
  124. package/src/__tests__/install-skill-routing.test.ts +1 -13
  125. package/src/__tests__/llm-callsite-catalog.test.ts +34 -0
  126. package/src/__tests__/llm-catalog-parity.test.ts +90 -0
  127. package/src/__tests__/llm-context-resolution.test.ts +180 -0
  128. package/src/__tests__/llm-resolver.test.ts +80 -12
  129. package/src/__tests__/llm-usage-store.test.ts +269 -4
  130. package/src/__tests__/log-export-routes.test.ts +89 -0
  131. package/src/__tests__/managed-profile-guard.test.ts +225 -0
  132. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -10
  133. package/src/__tests__/manual-token-reconciliation.test.ts +334 -0
  134. package/src/__tests__/memory-v2-static-injector.test.ts +95 -0
  135. package/src/__tests__/migration-cross-version-compatibility.test.ts +197 -291
  136. package/src/__tests__/migration-export-http.test.ts +33 -26
  137. package/src/__tests__/migration-export-streaming.test.ts +18 -10
  138. package/src/__tests__/migration-export-to-gcs.test.ts +49 -9
  139. package/src/__tests__/migration-import-commit-http.test.ts +66 -21
  140. package/src/__tests__/migration-import-from-gcs.test.ts +50 -9
  141. package/src/__tests__/migration-import-from-url.test.ts +20 -6
  142. package/src/__tests__/migration-import-preflight-http.test.ts +95 -95
  143. package/src/__tests__/migration-parity-persistence.test.ts +62 -25
  144. package/src/__tests__/migration-transport.test.ts +115 -23
  145. package/src/__tests__/migration-validate-http.test.ts +105 -80
  146. package/src/__tests__/migration-wizard.test.ts +133 -27
  147. package/src/__tests__/non-member-access-request.test.ts +1 -1
  148. package/src/__tests__/notification-guardian-path.test.ts +1 -1
  149. package/src/__tests__/oauth-store.test.ts +19 -0
  150. package/src/__tests__/platform-bash-auto-approve.test.ts +21 -12
  151. package/src/__tests__/prechat-onboarding-contract.test.ts +31 -7
  152. package/src/__tests__/pricing.test.ts +68 -4
  153. package/src/__tests__/process-message-background-slack.test.ts +331 -0
  154. package/src/__tests__/provider-managed-proxy-integration.test.ts +153 -17
  155. package/src/__tests__/provider-send-message-override-profile.test.ts +50 -0
  156. package/src/__tests__/provider-usage-tracking.test.ts +208 -0
  157. package/src/__tests__/reaction-persistence.test.ts +9 -6
  158. package/src/__tests__/rebind-secrets-screen.test.ts +53 -16
  159. package/src/__tests__/recording-handler.test.ts +64 -81
  160. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +4 -3
  161. package/src/__tests__/relay-server.test.ts +18 -13
  162. package/src/__tests__/require-fresh-approval.test.ts +13 -22
  163. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
  164. package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
  165. package/src/__tests__/runtime-events-sse.test.ts +3 -12
  166. package/src/__tests__/search-skills-unified.test.ts +9 -15
  167. package/src/__tests__/secret-ingress-cli.test.ts +2 -5
  168. package/src/__tests__/secret-ingress-http.test.ts +0 -4
  169. package/src/__tests__/secret-onetime-send.test.ts +4 -2
  170. package/src/__tests__/secret-prompt-log-hygiene.test.ts +24 -7
  171. package/src/__tests__/secret-prompter-channel-fallback.test.ts +42 -47
  172. package/src/__tests__/secret-response-routing.test.ts +29 -15
  173. package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -1
  174. package/src/__tests__/secret-scanner.test.ts +2 -545
  175. package/src/__tests__/send-endpoint-busy.test.ts +9 -24
  176. package/src/__tests__/settings-routes.test.ts +1 -1
  177. package/src/__tests__/shell-credential-ref.test.ts +0 -8
  178. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -56
  179. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -11
  180. package/src/__tests__/skill-tool-factory.test.ts +97 -0
  181. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -30
  182. package/src/__tests__/skills-files-catalog-fallback.test.ts +11 -17
  183. package/src/__tests__/slack-inbound-verification.test.ts +1 -62
  184. package/src/__tests__/subagent-fork-notifications.test.ts +57 -47
  185. package/src/__tests__/subagent-manager-notify.test.ts +70 -70
  186. package/src/__tests__/subagent-notify-parent.test.ts +80 -83
  187. package/src/__tests__/system-prompt.test.ts +115 -13
  188. package/src/__tests__/terminal-tools.test.ts +0 -89
  189. package/src/__tests__/thread-backfill.test.ts +945 -31
  190. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -36
  191. package/src/__tests__/tool-execute-pipeline.test.ts +0 -6
  192. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -16
  193. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +9 -19
  194. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -7
  195. package/src/__tests__/tool-executor.test.ts +12 -19
  196. package/src/__tests__/tool-metrics-listener.test.ts +0 -35
  197. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  198. package/src/__tests__/tool-trace-listener.test.ts +0 -17
  199. package/src/__tests__/transfer-progress-screen.test.ts +63 -26
  200. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -149
  201. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -4
  202. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  203. package/src/__tests__/tts-catalog-parity.test.ts +16 -5
  204. package/src/__tests__/usage-attribution.test.ts +247 -0
  205. package/src/__tests__/usage-cli.test.ts +143 -0
  206. package/src/__tests__/usage-grouped-buckets.test.ts +155 -0
  207. package/src/__tests__/usage-routes.test.ts +150 -0
  208. package/src/__tests__/validation-results-screen.test.ts +39 -16
  209. package/src/__tests__/vbundle-pax-and-symlink.test.ts +12 -3
  210. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +49 -137
  211. package/src/__tests__/verification-control-plane-policy.test.ts +4 -7
  212. package/src/__tests__/voice-session-bridge.test.ts +5 -5
  213. package/src/__tests__/workspace-migration-062-drop-memory-v2-edges-json.test.ts +103 -0
  214. package/src/__tests__/workspace-migration-063-release-notes-dynamic-model-context.test.ts +77 -0
  215. package/src/__tests__/workspace-migration-064-unwind-main-agent-opus-seed.test.ts +225 -0
  216. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +8 -30
  217. package/src/acp/index.ts +0 -15
  218. package/src/acp/session-manager.ts +37 -34
  219. package/src/agent/loop.ts +16 -1
  220. package/src/approvals/AGENTS.md +4 -0
  221. package/src/approvals/__tests__/guardian-feed-event.test.ts +10 -3
  222. package/src/approvals/guardian-request-resolvers.ts +10 -2
  223. package/src/backup/__tests__/backup-worker.test.ts +36 -8
  224. package/src/backup/__tests__/paths.test.ts +2 -2
  225. package/src/backup/__tests__/restore.test.ts +45 -28
  226. package/src/backup/backup-worker.ts +36 -2
  227. package/src/backup/paths.ts +9 -6
  228. package/src/browser-session/events.ts +0 -9
  229. package/src/calls/call-store.ts +1 -34
  230. package/src/calls/guardian-question-copy.ts +0 -108
  231. package/src/calls/relay-server.ts +0 -24
  232. package/src/calls/twilio-rest.ts +0 -38
  233. package/src/calls/twilio-routes.ts +1 -1
  234. package/src/calls/voice-session-bridge.ts +7 -38
  235. package/src/channels/types.ts +1 -36
  236. package/src/cli/commands/__tests__/cache.test.ts +152 -5
  237. package/src/cli/commands/__tests__/memory-v2.test.ts +14 -28
  238. package/src/cli/commands/__tests__/trust.test.ts +21 -387
  239. package/src/cli/commands/backup.ts +4 -4
  240. package/src/cli/commands/cache-fs.ts +8 -0
  241. package/src/cli/commands/cache.ts +153 -82
  242. package/src/cli/commands/clients.ts +63 -5
  243. package/src/cli/commands/completions.ts +3 -3
  244. package/src/cli/commands/contacts.ts +231 -76
  245. package/src/cli/commands/keys.ts +4 -1
  246. package/src/cli/commands/memory-v2.ts +24 -52
  247. package/src/cli/commands/oauth/shared.ts +2 -29
  248. package/src/cli/commands/pending.ts +102 -0
  249. package/src/cli/commands/skills.ts +77 -35
  250. package/src/cli/commands/trust.ts +70 -430
  251. package/src/cli/commands/usage.ts +25 -16
  252. package/src/cli/lib/daemon-credential-client.ts +14 -0
  253. package/src/cli/program.ts +2 -0
  254. package/src/cli.ts +0 -21
  255. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  256. package/src/config/bundled-skills/messaging/TOOLS.json +14 -4
  257. package/src/config/env-registry.ts +12 -2
  258. package/src/config/env.ts +3 -14
  259. package/src/config/feature-flag-registry.json +30 -30
  260. package/src/config/llm-callsite-catalog.ts +12 -0
  261. package/src/config/llm-context-resolution.ts +80 -0
  262. package/src/config/llm-resolver.ts +58 -22
  263. package/src/config/loader.ts +3 -3
  264. package/src/config/schema.ts +2 -158
  265. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  266. package/src/config/schemas/call-site-catalog.ts +271 -0
  267. package/src/config/schemas/calls.ts +5 -5
  268. package/src/config/schemas/inference.ts +1 -1
  269. package/src/config/schemas/ingress.ts +1 -1
  270. package/src/config/schemas/llm.ts +31 -3
  271. package/src/config/schemas/memory-retrieval.ts +2 -2
  272. package/src/config/schemas/memory-v2.ts +9 -0
  273. package/src/config/schemas/security.ts +1 -42
  274. package/src/config/schemas/services.ts +6 -6
  275. package/src/config/schemas/skills.ts +5 -5
  276. package/src/config/schemas/tts.ts +1 -1
  277. package/src/config/seed-inference-profiles.ts +117 -0
  278. package/src/config/skills.ts +0 -90
  279. package/src/config/types.ts +3 -6
  280. package/src/contacts/contact-store.ts +0 -17
  281. package/src/contacts/contacts-write.ts +1 -105
  282. package/src/context/window-manager.ts +44 -5
  283. package/src/credential-execution/process-manager.ts +34 -10
  284. package/src/credential-health/credential-health-service.ts +21 -16
  285. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +75 -82
  286. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -9
  287. package/src/daemon/connection-policy.ts +1 -26
  288. package/src/daemon/conversation-agent-loop-handlers.ts +53 -4
  289. package/src/daemon/conversation-agent-loop.ts +277 -36
  290. package/src/daemon/conversation-history.ts +8 -8
  291. package/src/daemon/conversation-launch.ts +20 -135
  292. package/src/daemon/conversation-lifecycle.ts +1 -1
  293. package/src/daemon/conversation-messaging.ts +1 -0
  294. package/src/daemon/conversation-process.ts +83 -163
  295. package/src/daemon/conversation-runtime-assembly.ts +219 -76
  296. package/src/daemon/conversation-slash.ts +47 -5
  297. package/src/daemon/conversation-store.ts +7 -31
  298. package/src/daemon/conversation-surfaces.ts +22 -28
  299. package/src/daemon/conversation-tool-setup.ts +3 -33
  300. package/src/daemon/conversation-usage.ts +36 -0
  301. package/src/daemon/conversation.ts +117 -233
  302. package/src/daemon/daemon-control.ts +3 -71
  303. package/src/daemon/daemon-skill-host.ts +8 -11
  304. package/src/daemon/dictation-profile-store.ts +2 -26
  305. package/src/daemon/first-greeting.ts +44 -156
  306. package/src/daemon/handlers/config-channels.ts +12 -12
  307. package/src/daemon/handlers/config-ingress.ts +4 -165
  308. package/src/daemon/handlers/config-model.ts +1 -1
  309. package/src/daemon/handlers/config-voice.ts +0 -42
  310. package/src/daemon/handlers/conversations.ts +11 -190
  311. package/src/daemon/handlers/recording.ts +26 -158
  312. package/src/daemon/handlers/shared.ts +23 -71
  313. package/src/daemon/handlers/skills.ts +42 -93
  314. package/src/daemon/host-bash-proxy.ts +67 -45
  315. package/src/daemon/host-browser-proxy.ts +65 -27
  316. package/src/daemon/host-cu-proxy.ts +40 -39
  317. package/src/daemon/host-file-proxy.ts +58 -37
  318. package/src/daemon/host-transfer-proxy.ts +84 -46
  319. package/src/daemon/lifecycle.ts +49 -15
  320. package/src/daemon/message-types/conversations.ts +7 -0
  321. package/src/daemon/message-types/host-bash.ts +1 -0
  322. package/src/daemon/message-types/host-cu.ts +1 -0
  323. package/src/daemon/message-types/host-file.ts +1 -0
  324. package/src/daemon/message-types/host-transfer.ts +1 -0
  325. package/src/daemon/message-types/messages.ts +10 -9
  326. package/src/daemon/message-types/workspace.ts +1 -1
  327. package/src/daemon/process-message.ts +102 -239
  328. package/src/daemon/server.ts +13 -462
  329. package/src/daemon/shutdown-handlers.ts +2 -2
  330. package/src/daemon/tool-side-effects.ts +125 -107
  331. package/src/daemon/trust-context.ts +13 -0
  332. package/src/daemon/wake-target-adapter.ts +4 -9
  333. package/src/events/domain-events.ts +0 -8
  334. package/src/events/tool-audit-listener.ts +3 -1
  335. package/src/events/tool-domain-event-publisher.ts +0 -10
  336. package/src/events/tool-metrics-listener.ts +0 -17
  337. package/src/events/tool-trace-listener.ts +0 -14
  338. package/src/filing/filing-service.ts +13 -1
  339. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +6 -2
  340. package/src/heartbeat/heartbeat-service.ts +23 -5
  341. package/src/home/__tests__/feed-writer.test.ts +0 -4
  342. package/src/home/__tests__/relationship-state-writer.test.ts +30 -0
  343. package/src/home/feed-writer.ts +1 -2
  344. package/src/home/relationship-state-writer.ts +16 -3
  345. package/src/ipc/__tests__/browser-ipc.test.ts +2 -12
  346. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -1
  347. package/src/ipc/assistant-server.ts +3 -10
  348. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +39 -20
  349. package/src/ipc/routes/route-adapter.ts +1 -1
  350. package/src/ipc/routes/trust-rules.test.ts +0 -95
  351. package/src/ipc/skill-ipc-types.ts +41 -0
  352. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +13 -27
  353. package/src/ipc/skill-routes/__tests__/identity.test.ts +4 -23
  354. package/src/ipc/skill-routes/events.ts +12 -23
  355. package/src/ipc/skill-routes/identity.ts +4 -17
  356. package/src/ipc/skill-routes/index.ts +1 -1
  357. package/src/ipc/skill-server.ts +6 -39
  358. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +0 -8
  359. package/src/live-voice/protocol.ts +4 -13
  360. package/src/mcp/manager.ts +0 -5
  361. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +55 -0
  362. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +127 -0
  363. package/src/memory/app-git-service.ts +0 -32
  364. package/src/memory/app-store.ts +154 -0
  365. package/src/memory/attachments-store.ts +6 -0
  366. package/src/memory/context-search/sources/memory-v2.ts +578 -0
  367. package/src/memory/context-search/sources/memory.ts +5 -0
  368. package/src/memory/context-search/sources/pkb.ts +10 -1
  369. package/src/memory/context-search/sources/workspace.ts +3 -2
  370. package/src/memory/conversation-crud.ts +29 -4
  371. package/src/memory/conversation-disk-view.ts +1 -5
  372. package/src/memory/conversation-starter-checkpoints.ts +63 -0
  373. package/src/memory/db-connection.ts +62 -0
  374. package/src/memory/db-init.ts +14 -0
  375. package/src/memory/embedding-backend.ts +3 -21
  376. package/src/memory/embedding-gemini.ts +0 -2
  377. package/src/memory/embedding-local.ts +6 -6
  378. package/src/memory/embedding-ollama.ts +6 -6
  379. package/src/memory/embedding-openai.ts +6 -6
  380. package/src/memory/embedding-types.ts +21 -0
  381. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +3 -7
  382. package/src/memory/graph/conversation-graph-memory.ts +35 -13
  383. package/src/memory/graph/injection.test.ts +2 -2
  384. package/src/memory/graph/injection.ts +1 -1
  385. package/src/memory/guardian-action-store.ts +0 -83
  386. package/src/memory/guardian-approvals.ts +0 -48
  387. package/src/memory/indexer.ts +1 -15
  388. package/src/memory/job-handlers/conversation-starters.ts +36 -53
  389. package/src/memory/job-utils.ts +0 -6
  390. package/src/memory/jobs-store.ts +0 -1
  391. package/src/memory/jobs-worker.ts +2 -16
  392. package/src/memory/llm-request-log-store.ts +0 -41
  393. package/src/memory/llm-usage-store.ts +129 -43
  394. package/src/memory/memory-v2-activation-log-store.ts +115 -0
  395. package/src/memory/migrations/233-document-conversations.ts +54 -0
  396. package/src/memory/migrations/234-memory-v2-activation-logs.ts +55 -0
  397. package/src/memory/migrations/235-llm-usage-attribution.ts +31 -0
  398. package/src/memory/migrations/235-slack-compaction-watermark.ts +44 -0
  399. package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +26 -0
  400. package/src/memory/migrations/__tests__/234-memory-v2-activation-logs.test.ts +182 -0
  401. package/src/memory/migrations/index.ts +14 -0
  402. package/src/memory/migrations/registry.ts +24 -0
  403. package/src/memory/raw-query.ts +2 -68
  404. package/src/memory/schema/conversations.ts +7 -0
  405. package/src/memory/schema/infrastructure.ts +25 -0
  406. package/src/memory/search/semantic.ts +5 -16
  407. package/src/memory/tool-usage-store.ts +2 -0
  408. package/src/memory/usage-buckets.ts +40 -1
  409. package/src/memory/usage-grouped-buckets.ts +127 -0
  410. package/src/memory/v2/__tests__/activation.test.ts +289 -90
  411. package/src/memory/v2/__tests__/backfill-jobs.test.ts +2 -129
  412. package/src/memory/v2/__tests__/consolidation-job.test.ts +28 -11
  413. package/src/memory/v2/__tests__/edge-index.test.ts +278 -0
  414. package/src/memory/v2/__tests__/injection.test.ts +384 -15
  415. package/src/memory/v2/__tests__/migration.test.ts +64 -36
  416. package/src/memory/v2/__tests__/page-store.test.ts +191 -8
  417. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +181 -0
  418. package/src/memory/v2/__tests__/skill-store.test.ts +115 -3
  419. package/src/memory/v2/__tests__/static-context.test.ts +153 -0
  420. package/src/memory/v2/activation.ts +168 -97
  421. package/src/memory/v2/backfill-jobs.ts +15 -100
  422. package/src/memory/v2/consolidation-job.ts +14 -12
  423. package/src/memory/v2/edge-index.ts +191 -0
  424. package/src/memory/v2/injection.ts +182 -58
  425. package/src/memory/v2/migration.ts +57 -64
  426. package/src/memory/v2/now-text.ts +2 -3
  427. package/src/memory/v2/page-store.ts +168 -31
  428. package/src/memory/v2/prompts/consolidation.ts +118 -42
  429. package/src/memory/v2/prompts/sweep.ts +3 -3
  430. package/src/memory/v2/skill-store.ts +55 -7
  431. package/src/memory/v2/static-context.ts +62 -0
  432. package/src/memory/v2/types.ts +10 -20
  433. package/src/memory/validation.ts +0 -11
  434. package/src/messaging/draft-store.ts +0 -6
  435. package/src/messaging/provider-types.ts +8 -0
  436. package/src/messaging/provider.ts +7 -0
  437. package/src/messaging/providers/gmail/client.ts +1 -121
  438. package/src/messaging/providers/outlook/client.ts +0 -73
  439. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +226 -0
  440. package/src/messaging/providers/slack/adapter.ts +122 -21
  441. package/src/messaging/providers/slack/backfill.test.ts +95 -6
  442. package/src/messaging/providers/slack/backfill.ts +89 -11
  443. package/src/messaging/providers/slack/client.ts +10 -124
  444. package/src/messaging/providers/slack/message-metadata.ts +12 -2
  445. package/src/messaging/providers/slack/render-transcript.test.ts +56 -0
  446. package/src/messaging/providers/slack/render-transcript.ts +126 -25
  447. package/src/messaging/providers/slack/types.ts +1 -0
  448. package/src/oauth/connection-resolver.test.ts +8 -0
  449. package/src/oauth/connection-resolver.ts +8 -16
  450. package/src/oauth/credential-token-resolver.ts +97 -0
  451. package/src/oauth/manual-token-connection.ts +30 -34
  452. package/src/oauth/oauth-store.ts +6 -4
  453. package/src/outbound-proxy/certs.ts +0 -7
  454. package/src/outbound-proxy/config.ts +0 -74
  455. package/src/outbound-proxy/health.ts +0 -44
  456. package/src/outbound-proxy/index.ts +0 -22
  457. package/src/permissions/approval-provenance.test.ts +184 -0
  458. package/src/permissions/approval-provenance.ts +70 -0
  459. package/src/permissions/checker.ts +4 -1
  460. package/src/permissions/gateway-threshold-reader.ts +4 -1
  461. package/src/permissions/prompter.ts +9 -2
  462. package/src/permissions/secret-prompter.ts +21 -48
  463. package/src/permissions/types.ts +33 -0
  464. package/src/permissions/workspace-policy.ts +0 -5
  465. package/src/platform/sync-identity.ts +0 -8
  466. package/src/plugins/defaults/injectors.ts +69 -2
  467. package/src/plugins/defaults/overflow-reduce.ts +3 -2
  468. package/src/plugins/types.ts +8 -0
  469. package/src/prompts/system-prompt.ts +34 -70
  470. package/src/prompts/templates/BOOTSTRAP.md +52 -6
  471. package/src/prompts/update-bulletin-job.ts +2 -0
  472. package/src/providers/__tests__/retry-callsite.test.ts +138 -1
  473. package/src/providers/anthropic/client.ts +72 -33
  474. package/src/providers/call-site-routing.ts +42 -3
  475. package/src/providers/gemini/client.ts +18 -2
  476. package/src/providers/managed-proxy/context.ts +0 -5
  477. package/src/providers/model-catalog.ts +105 -19
  478. package/src/providers/openai/chat-completions-provider.ts +6 -0
  479. package/src/providers/openai/responses-provider.ts +7 -1
  480. package/src/providers/provider-send-message.ts +45 -2
  481. package/src/providers/ratelimit.ts +7 -2
  482. package/src/providers/registry.ts +14 -9
  483. package/src/providers/retry.ts +96 -8
  484. package/src/providers/types.ts +13 -0
  485. package/src/providers/usage-tracking.ts +96 -0
  486. package/src/runtime/AGENTS.md +10 -6
  487. package/src/runtime/__tests__/agent-wake.test.ts +89 -0
  488. package/src/runtime/agent-wake.ts +39 -2
  489. package/src/runtime/assistant-event-hub.ts +541 -45
  490. package/src/runtime/assistant-event.ts +1 -6
  491. package/src/runtime/auth/context.ts +0 -9
  492. package/src/runtime/auth/middleware.ts +1 -1
  493. package/src/runtime/auth/route-policy.ts +11 -9
  494. package/src/runtime/auth/token-service.ts +0 -11
  495. package/src/runtime/channel-approvals.ts +6 -2
  496. package/src/runtime/channel-verification-service.ts +3 -5
  497. package/src/runtime/http-errors.ts +0 -34
  498. package/src/runtime/http-router.ts +6 -3
  499. package/src/runtime/http-server.ts +22 -82
  500. package/src/runtime/http-types.ts +5 -0
  501. package/src/runtime/interactive-ui.ts +0 -1
  502. package/src/runtime/middleware/auth.ts +0 -20
  503. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +112 -0
  504. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +11 -4
  505. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +253 -0
  506. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +19 -6
  507. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +71 -27
  508. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +41 -2
  509. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +143 -79
  510. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +143 -23
  511. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +2 -2
  512. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +371 -0
  513. package/src/runtime/migrations/migration-transport.ts +46 -13
  514. package/src/runtime/migrations/migration-wizard.ts +2 -2
  515. package/src/runtime/migrations/origin-mode.ts +40 -0
  516. package/src/runtime/migrations/vbundle-builder.ts +133 -79
  517. package/src/runtime/migrations/vbundle-import-analyzer.ts +9 -7
  518. package/src/runtime/migrations/vbundle-importer.ts +7 -7
  519. package/src/runtime/migrations/vbundle-metadata-merge.ts +1 -1
  520. package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
  521. package/src/runtime/migrations/vbundle-streaming-validator.ts +48 -26
  522. package/src/runtime/migrations/vbundle-validator.ts +214 -41
  523. package/src/runtime/pending-interactions.ts +13 -4
  524. package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -1
  525. package/src/runtime/routes/__tests__/backup-routes.test.ts +28 -19
  526. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +235 -0
  527. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +58 -0
  528. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +54 -0
  529. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +19 -6
  530. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +7 -7
  531. package/src/runtime/routes/acp-routes.test.ts +0 -3
  532. package/src/runtime/routes/acp-routes.ts +3 -7
  533. package/src/runtime/routes/app-management-routes.ts +18 -9
  534. package/src/runtime/routes/approval-routes.ts +55 -14
  535. package/src/runtime/routes/avatar-routes.ts +3 -5
  536. package/src/runtime/routes/browser-routes.ts +1 -15
  537. package/src/runtime/routes/channel-guardian-routes.ts +1 -5
  538. package/src/runtime/routes/channel-readiness-routes.ts +3 -7
  539. package/src/runtime/routes/channel-route-shared.ts +2 -28
  540. package/src/runtime/routes/client-routes.ts +45 -12
  541. package/src/runtime/routes/consolidation-routes.ts +115 -0
  542. package/src/runtime/routes/conversation-list-routes.ts +12 -29
  543. package/src/runtime/routes/conversation-management-routes.ts +14 -51
  544. package/src/runtime/routes/conversation-query-routes.ts +120 -8
  545. package/src/runtime/routes/conversation-routes.ts +44 -528
  546. package/src/runtime/routes/conversation-starter-routes.ts +19 -40
  547. package/src/runtime/routes/documents-routes.ts +53 -18
  548. package/src/runtime/routes/events-routes.ts +59 -91
  549. package/src/runtime/routes/filing-routes.ts +18 -1
  550. package/src/runtime/routes/guardian-action-routes.ts +4 -9
  551. package/src/runtime/routes/host-bash-routes.ts +3 -2
  552. package/src/runtime/routes/host-browser-routes.ts +9 -33
  553. package/src/runtime/routes/host-cu-routes.ts +6 -1
  554. package/src/runtime/routes/host-file-routes.ts +3 -2
  555. package/src/runtime/routes/host-transfer-routes.ts +11 -15
  556. package/src/runtime/routes/identity-routes.ts +78 -6
  557. package/src/runtime/routes/inbound-message-handler.ts +580 -137
  558. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -88
  559. package/src/runtime/routes/inbound-stages/background-dispatch.ts +3 -0
  560. package/src/runtime/routes/index.ts +4 -0
  561. package/src/runtime/routes/integrations/slack/channel.ts +0 -24
  562. package/src/runtime/routes/llm-call-sites-routes.ts +22 -0
  563. package/src/runtime/routes/memory-v2-routes.ts +10 -15
  564. package/src/runtime/routes/migration-routes.ts +188 -31
  565. package/src/runtime/routes/playground/guard.ts +1 -1
  566. package/src/runtime/routes/playground/index.ts +0 -2
  567. package/src/runtime/routes/recording-routes.ts +4 -24
  568. package/src/runtime/routes/rename-conversation-routes.ts +2 -6
  569. package/src/runtime/routes/schedule-routes.ts +3 -6
  570. package/src/runtime/routes/secret-routes.ts +87 -18
  571. package/src/runtime/routes/settings-routes.ts +29 -28
  572. package/src/runtime/routes/skills-routes.ts +12 -31
  573. package/src/runtime/routes/suggest-trust-rule-routes.ts +32 -1
  574. package/src/runtime/routes/task-routes.ts +6 -6
  575. package/src/runtime/routes/trust-rules-routes.ts +3 -94
  576. package/src/runtime/routes/types.ts +4 -4
  577. package/src/runtime/routes/upgrade-broadcast-routes.ts +3 -10
  578. package/src/runtime/routes/usage-routes.ts +87 -10
  579. package/src/runtime/routes/user-routes.ts +17 -31
  580. package/src/runtime/routes/work-items-routes.ts +1 -4
  581. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
  582. package/src/runtime/services/analyze-conversation.ts +7 -17
  583. package/src/runtime/services/conversation-serializer.ts +2 -4
  584. package/src/runtime/verification-outbound-actions.ts +1 -1
  585. package/src/runtime/verification-rate-limiter.ts +1 -1
  586. package/src/schedule/schedule-store.ts +0 -16
  587. package/src/security/secret-scanner.ts +14 -547
  588. package/src/security/secure-keys.ts +31 -11
  589. package/src/security/token-manager.ts +7 -3
  590. package/src/signals/cancel.ts +16 -25
  591. package/src/signals/conversation-undo.ts +2 -27
  592. package/src/signals/emit-event.ts +1 -2
  593. package/src/signals/user-message.ts +108 -22
  594. package/src/skills/catalog-install.ts +1 -0
  595. package/src/skills/clawhub.ts +2 -2
  596. package/src/skills/inline-command-runner.ts +1 -7
  597. package/src/subagent/manager.ts +67 -84
  598. package/src/tasks/task-store.ts +1 -28
  599. package/src/telemetry/types.ts +6 -0
  600. package/src/telemetry/usage-telemetry-reporter.test.ts +38 -15
  601. package/src/telemetry/usage-telemetry-reporter.ts +3 -5
  602. package/src/tools/acp/spawn.test.ts +1 -2
  603. package/src/tools/acp/steer.test.ts +1 -2
  604. package/src/tools/browser/__tests__/browser-status.test.ts +44 -127
  605. package/src/tools/browser/browser-execution.ts +31 -147
  606. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +92 -68
  607. package/src/tools/browser/cdp-client/factory.ts +48 -76
  608. package/src/tools/browser/cdp-client/index.ts +1 -14
  609. package/src/tools/executor.ts +44 -31
  610. package/src/tools/host-filesystem/edit.ts +3 -2
  611. package/src/tools/host-filesystem/read.ts +3 -2
  612. package/src/tools/host-filesystem/transfer.test.ts +45 -42
  613. package/src/tools/host-filesystem/transfer.ts +4 -3
  614. package/src/tools/host-filesystem/write.ts +3 -2
  615. package/src/tools/host-terminal/host-shell.ts +4 -3
  616. package/src/tools/network/script-proxy/index.ts +1 -10
  617. package/src/tools/permission-checker.ts +66 -1
  618. package/src/tools/skills/sandbox-runner.ts +1 -6
  619. package/src/tools/skills/skill-tool-factory.ts +32 -0
  620. package/src/tools/terminal/safe-env.ts +1 -0
  621. package/src/tools/terminal/shell.ts +2 -78
  622. package/src/tools/types.ts +12 -39
  623. package/src/tts/__tests__/provider-catalog.test.ts +2 -2
  624. package/src/tts/provider-catalog.ts +1 -1
  625. package/src/usage/actors.ts +2 -1
  626. package/src/usage/attribution.ts +185 -0
  627. package/src/usage/pricing.ts +166 -0
  628. package/src/usage/types.ts +14 -0
  629. package/src/util/json.ts +13 -0
  630. package/src/util/logger.ts +3 -3
  631. package/src/util/pricing.ts +50 -3
  632. package/src/work-items/work-item-runner.ts +15 -42
  633. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +4 -3
  634. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +3 -3
  635. package/src/workspace/migrations/060-memory-v2-init.ts +2 -18
  636. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +59 -0
  637. package/src/workspace/migrations/062-drop-memory-v2-edges-json.ts +27 -0
  638. package/src/workspace/migrations/063-release-notes-dynamic-model-context.ts +70 -0
  639. package/src/workspace/migrations/064-unwind-main-agent-opus-seed.ts +64 -0
  640. package/src/workspace/migrations/registry.ts +8 -0
  641. package/src/workspace/provider-commit-message-generator.ts +3 -3
  642. package/src/__tests__/sandbox-diagnostics.test.ts +0 -138
  643. package/src/__tests__/sandbox-host-parity.test.ts +0 -1024
  644. package/src/__tests__/secret-detection-handler.test.ts +0 -67
  645. package/src/__tests__/secret-scanner-executor.test.ts +0 -450
  646. package/src/__tests__/tcc-sandbox-deny.test.ts +0 -198
  647. package/src/__tests__/terminal-sandbox.test.ts +0 -374
  648. package/src/__tests__/tool-notification-listener.test.ts +0 -65
  649. package/src/context/__tests__/microcompact.test.ts +0 -805
  650. package/src/context/microcompact.ts +0 -443
  651. package/src/daemon/handlers/slack-channel-oauth-install.ts +0 -197
  652. package/src/events/tool-notification-listener.ts +0 -17
  653. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +0 -219
  654. package/src/memory/v2/__tests__/edges.test.ts +0 -435
  655. package/src/memory/v2/edges.ts +0 -217
  656. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +0 -197
  657. package/src/runtime/__tests__/chrome-extension-registry.test.ts +0 -518
  658. package/src/runtime/__tests__/client-registry.test.ts +0 -271
  659. package/src/runtime/chrome-extension-registry.ts +0 -368
  660. package/src/runtime/client-registry.ts +0 -254
  661. package/src/runtime/routes/inbound-stages/verification-intercept.ts +0 -329
  662. package/src/tools/secret-detection-handler.ts +0 -269
  663. package/src/tools/terminal/backends/native.ts +0 -327
  664. package/src/tools/terminal/backends/types.ts +0 -37
  665. package/src/tools/terminal/sandbox-diagnostics.ts +0 -87
  666. package/src/tools/terminal/sandbox.ts +0 -40
@@ -1,1024 +0,0 @@
1
- /**
2
- * Integration tests for host/sandbox parity.
3
- *
4
- * Both the sandbox (file_read, file_write, file_edit, bash) and host
5
- * (host_file_read, host_file_write, host_file_edit, host_bash) tool
6
- * families delegate to the same underlying engine:
7
- *
8
- * FileSystemOps — shared read/write/edit logic
9
- * applyEdit — pure match/replace engine
10
- * shell.ts — spawn + output formatting
11
- *
12
- * The only intentional difference is the PathPolicy:
13
- * - sandboxPolicy: paths must stay within a boundary directory
14
- * - hostPolicy: paths must be absolute (no boundary)
15
- *
16
- * These tests verify that for equivalent in-scope operations, both
17
- * code-paths produce identical results — same content, same error
18
- * codes, same diff shapes — and that the expected divergence on
19
- * out-of-scope operations is correct.
20
- */
21
-
22
- import {
23
- existsSync,
24
- mkdirSync,
25
- mkdtempSync,
26
- readFileSync,
27
- realpathSync,
28
- rmSync,
29
- writeFileSync,
30
- } from "node:fs";
31
- import { tmpdir } from "node:os";
32
- import { join } from "node:path";
33
- import { afterEach, describe, expect, mock, test } from "bun:test";
34
-
35
- // Mock the logger before any transitive imports that depend on pino
36
- mock.module("../util/logger.js", () => ({
37
- getLogger: () => ({
38
- error: () => {},
39
- warn: () => {},
40
- info: () => {},
41
- debug: () => {},
42
- }),
43
- }));
44
-
45
- import { applyEdit } from "../tools/shared/filesystem/edit-engine.js";
46
- import {
47
- FileSystemOps,
48
- type PathPolicy,
49
- } from "../tools/shared/filesystem/file-ops-service.js";
50
- import {
51
- hostPolicy,
52
- sandboxPolicy,
53
- } from "../tools/shared/filesystem/path-policy.js";
54
- import {
55
- formatShellOutput,
56
- MAX_OUTPUT_LENGTH,
57
- } from "../tools/shared/shell-output.js";
58
-
59
- // Dynamically import modules that depend on the mocked logger
60
- const { NativeBackend } = await import("../tools/terminal/backends/native.js");
61
- const { wrapCommand } = await import("../tools/terminal/sandbox.js");
62
- const { ToolError } = await import("../util/errors.js");
63
-
64
- // ---------------------------------------------------------------------------
65
- // Helpers
66
- // ---------------------------------------------------------------------------
67
-
68
- const testDirs: string[] = [];
69
-
70
- function makeTempDir(): string {
71
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "parity-test-")));
72
- testDirs.push(dir);
73
- return dir;
74
- }
75
-
76
- afterEach(() => {
77
- for (const dir of testDirs.splice(0)) {
78
- rmSync(dir, { recursive: true, force: true });
79
- }
80
- });
81
-
82
- /** Build a sandbox-bound PathPolicy that resolves relative to boundary. */
83
- function sandboxPolicyFor(boundary: string): PathPolicy {
84
- return (rawPath, options) => sandboxPolicy(rawPath, boundary, options);
85
- }
86
-
87
- /** Build a host PathPolicy (just requires absolute paths). */
88
- function hostPolicyFn(): PathPolicy {
89
- return hostPolicy;
90
- }
91
-
92
- /**
93
- * Run the same operation against both sandbox and host FileSystemOps
94
- * and return both results for comparison.
95
- */
96
- function dualOps(boundary: string): {
97
- sandbox: FileSystemOps;
98
- host: FileSystemOps;
99
- } {
100
- return {
101
- sandbox: new FileSystemOps(sandboxPolicyFor(boundary)),
102
- host: new FileSystemOps(hostPolicyFn()),
103
- };
104
- }
105
-
106
- // ===========================================================================
107
- // 1. File read parity
108
- // ===========================================================================
109
-
110
- describe("Read parity: sandbox vs host produce identical content", () => {
111
- test("simple file read returns same numbered content", () => {
112
- const dir = makeTempDir();
113
- const content = "line one\nline two\nline three\n";
114
- writeFileSync(join(dir, "data.txt"), content);
115
-
116
- const { sandbox, host } = dualOps(dir);
117
-
118
- const sandboxResult = sandbox.readFileSafe({ path: "data.txt" });
119
- const hostResult = host.readFileSafe({ path: join(dir, "data.txt") });
120
-
121
- expect(sandboxResult.ok).toBe(true);
122
- expect(hostResult.ok).toBe(true);
123
- if (!sandboxResult.ok || !hostResult.ok) return;
124
-
125
- // Both should produce identical line-numbered output
126
- expect(sandboxResult.value.content).toBe(hostResult.value.content);
127
- });
128
-
129
- test("read with offset and limit returns same slice", () => {
130
- const dir = makeTempDir();
131
- writeFileSync(join(dir, "lines.txt"), "a\nb\nc\nd\ne\nf\n");
132
-
133
- const { sandbox, host } = dualOps(dir);
134
-
135
- const sandboxResult = sandbox.readFileSafe({
136
- path: "lines.txt",
137
- offset: 2,
138
- limit: 3,
139
- });
140
- const hostResult = host.readFileSafe({
141
- path: join(dir, "lines.txt"),
142
- offset: 2,
143
- limit: 3,
144
- });
145
-
146
- expect(sandboxResult.ok).toBe(true);
147
- expect(hostResult.ok).toBe(true);
148
- if (!sandboxResult.ok || !hostResult.ok) return;
149
-
150
- expect(sandboxResult.value.content).toBe(hostResult.value.content);
151
- });
152
-
153
- test("reading a missing file returns NOT_FOUND from both", () => {
154
- const dir = makeTempDir();
155
-
156
- const { sandbox, host } = dualOps(dir);
157
-
158
- const sandboxResult = sandbox.readFileSafe({ path: "nonexistent.txt" });
159
- const hostResult = host.readFileSafe({
160
- path: join(dir, "nonexistent.txt"),
161
- });
162
-
163
- expect(sandboxResult.ok).toBe(false);
164
- expect(hostResult.ok).toBe(false);
165
- if (sandboxResult.ok || hostResult.ok) return;
166
-
167
- expect(sandboxResult.error.code).toBe("NOT_FOUND");
168
- expect(hostResult.error.code).toBe("NOT_FOUND");
169
- });
170
-
171
- test("reading a directory returns NOT_A_FILE from both", () => {
172
- const dir = makeTempDir();
173
- mkdirSync(join(dir, "subdir"));
174
-
175
- const { sandbox, host } = dualOps(dir);
176
-
177
- const sandboxResult = sandbox.readFileSafe({ path: "subdir" });
178
- const hostResult = host.readFileSafe({ path: join(dir, "subdir") });
179
-
180
- expect(sandboxResult.ok).toBe(false);
181
- expect(hostResult.ok).toBe(false);
182
- if (sandboxResult.ok || hostResult.ok) return;
183
-
184
- expect(sandboxResult.error.code).toBe("NOT_A_FILE");
185
- expect(hostResult.error.code).toBe("NOT_A_FILE");
186
- });
187
-
188
- test("empty file read returns same content from both", () => {
189
- const dir = makeTempDir();
190
- writeFileSync(join(dir, "empty.txt"), "");
191
-
192
- const { sandbox, host } = dualOps(dir);
193
-
194
- const sandboxResult = sandbox.readFileSafe({ path: "empty.txt" });
195
- const hostResult = host.readFileSafe({ path: join(dir, "empty.txt") });
196
-
197
- expect(sandboxResult.ok).toBe(true);
198
- expect(hostResult.ok).toBe(true);
199
- if (!sandboxResult.ok || !hostResult.ok) return;
200
-
201
- expect(sandboxResult.value.content).toBe(hostResult.value.content);
202
- });
203
-
204
- test("file with unicode content returns same from both", () => {
205
- const dir = makeTempDir();
206
- const unicode =
207
- "Hello\nEmoji: \u{1F600}\nCJK: \u4F60\u597D\nAccent: caf\u00E9\n";
208
- writeFileSync(join(dir, "unicode.txt"), unicode);
209
-
210
- const { sandbox, host } = dualOps(dir);
211
-
212
- const sandboxResult = sandbox.readFileSafe({ path: "unicode.txt" });
213
- const hostResult = host.readFileSafe({ path: join(dir, "unicode.txt") });
214
-
215
- expect(sandboxResult.ok).toBe(true);
216
- expect(hostResult.ok).toBe(true);
217
- if (!sandboxResult.ok || !hostResult.ok) return;
218
-
219
- expect(sandboxResult.value.content).toBe(hostResult.value.content);
220
- });
221
- });
222
-
223
- // ===========================================================================
224
- // 2. File write parity
225
- // ===========================================================================
226
-
227
- describe("Write parity: sandbox vs host produce identical results", () => {
228
- test("writing a new file returns same shape from both", () => {
229
- const dir = makeTempDir();
230
-
231
- const { sandbox, host } = dualOps(dir);
232
-
233
- const sandboxResult = sandbox.writeFileSafe({
234
- path: "new-s.txt",
235
- content: "hello",
236
- });
237
- const hostResult = host.writeFileSafe({
238
- path: join(dir, "new-h.txt"),
239
- content: "hello",
240
- });
241
-
242
- expect(sandboxResult.ok).toBe(true);
243
- expect(hostResult.ok).toBe(true);
244
- if (!sandboxResult.ok || !hostResult.ok) return;
245
-
246
- expect(sandboxResult.value.isNewFile).toBe(true);
247
- expect(hostResult.value.isNewFile).toBe(true);
248
- expect(sandboxResult.value.newContent).toBe(hostResult.value.newContent);
249
- expect(sandboxResult.value.oldContent).toBe(hostResult.value.oldContent);
250
- });
251
-
252
- test("overwriting an existing file returns old content from both", () => {
253
- const dir = makeTempDir();
254
- writeFileSync(join(dir, "existing-s.txt"), "old");
255
- writeFileSync(join(dir, "existing-h.txt"), "old");
256
-
257
- const { sandbox, host } = dualOps(dir);
258
-
259
- const sandboxResult = sandbox.writeFileSafe({
260
- path: "existing-s.txt",
261
- content: "new",
262
- });
263
- const hostResult = host.writeFileSafe({
264
- path: join(dir, "existing-h.txt"),
265
- content: "new",
266
- });
267
-
268
- expect(sandboxResult.ok).toBe(true);
269
- expect(hostResult.ok).toBe(true);
270
- if (!sandboxResult.ok || !hostResult.ok) return;
271
-
272
- expect(sandboxResult.value.isNewFile).toBe(false);
273
- expect(hostResult.value.isNewFile).toBe(false);
274
- expect(sandboxResult.value.oldContent).toBe("old");
275
- expect(hostResult.value.oldContent).toBe("old");
276
- expect(sandboxResult.value.newContent).toBe("new");
277
- expect(hostResult.value.newContent).toBe("new");
278
-
279
- // Verify actual files on disk
280
- expect(readFileSync(join(dir, "existing-s.txt"), "utf-8")).toBe("new");
281
- expect(readFileSync(join(dir, "existing-h.txt"), "utf-8")).toBe("new");
282
- });
283
-
284
- test("creating nested directories works from both", () => {
285
- const dir = makeTempDir();
286
-
287
- const { sandbox, host } = dualOps(dir);
288
-
289
- const sandboxResult = sandbox.writeFileSafe({
290
- path: "a/b/deep-s.txt",
291
- content: "deep",
292
- });
293
- const hostResult = host.writeFileSafe({
294
- path: join(dir, "c/d/deep-h.txt"),
295
- content: "deep",
296
- });
297
-
298
- expect(sandboxResult.ok).toBe(true);
299
- expect(hostResult.ok).toBe(true);
300
- if (!sandboxResult.ok || !hostResult.ok) return;
301
-
302
- expect(existsSync(join(dir, "a/b/deep-s.txt"))).toBe(true);
303
- expect(existsSync(join(dir, "c/d/deep-h.txt"))).toBe(true);
304
- expect(sandboxResult.value.isNewFile).toBe(true);
305
- expect(hostResult.value.isNewFile).toBe(true);
306
- });
307
- });
308
-
309
- // ===========================================================================
310
- // 3. File edit parity
311
- // ===========================================================================
312
-
313
- describe("Edit parity: sandbox vs host produce identical edits", () => {
314
- test("unique match edit produces same result from both", () => {
315
- const dir = makeTempDir();
316
- writeFileSync(join(dir, "edit-s.txt"), "one two three");
317
- writeFileSync(join(dir, "edit-h.txt"), "one two three");
318
-
319
- const { sandbox, host } = dualOps(dir);
320
-
321
- const sandboxResult = sandbox.editFileSafe({
322
- path: "edit-s.txt",
323
- oldString: "two",
324
- newString: "TWO",
325
- replaceAll: false,
326
- });
327
- const hostResult = host.editFileSafe({
328
- path: join(dir, "edit-h.txt"),
329
- oldString: "two",
330
- newString: "TWO",
331
- replaceAll: false,
332
- });
333
-
334
- expect(sandboxResult.ok).toBe(true);
335
- expect(hostResult.ok).toBe(true);
336
- if (!sandboxResult.ok || !hostResult.ok) return;
337
-
338
- expect(sandboxResult.value.matchCount).toBe(1);
339
- expect(hostResult.value.matchCount).toBe(1);
340
- expect(sandboxResult.value.newContent).toBe("one TWO three");
341
- expect(hostResult.value.newContent).toBe("one TWO three");
342
- expect(sandboxResult.value.matchMethod).toBe(hostResult.value.matchMethod);
343
- });
344
-
345
- test("replaceAll edit produces same result from both", () => {
346
- const dir = makeTempDir();
347
- const original = "foo bar foo baz foo";
348
- writeFileSync(join(dir, "ra-s.txt"), original);
349
- writeFileSync(join(dir, "ra-h.txt"), original);
350
-
351
- const { sandbox, host } = dualOps(dir);
352
-
353
- const sandboxResult = sandbox.editFileSafe({
354
- path: "ra-s.txt",
355
- oldString: "foo",
356
- newString: "qux",
357
- replaceAll: true,
358
- });
359
- const hostResult = host.editFileSafe({
360
- path: join(dir, "ra-h.txt"),
361
- oldString: "foo",
362
- newString: "qux",
363
- replaceAll: true,
364
- });
365
-
366
- expect(sandboxResult.ok).toBe(true);
367
- expect(hostResult.ok).toBe(true);
368
- if (!sandboxResult.ok || !hostResult.ok) return;
369
-
370
- expect(sandboxResult.value.matchCount).toBe(3);
371
- expect(hostResult.value.matchCount).toBe(3);
372
- expect(sandboxResult.value.newContent).toBe(hostResult.value.newContent);
373
- expect(sandboxResult.value.newContent).toBe("qux bar qux baz qux");
374
- });
375
-
376
- test("missing old_string returns MATCH_NOT_FOUND from both", () => {
377
- const dir = makeTempDir();
378
- writeFileSync(join(dir, "mnf-s.txt"), "hello world");
379
- writeFileSync(join(dir, "mnf-h.txt"), "hello world");
380
-
381
- const { sandbox, host } = dualOps(dir);
382
-
383
- const sandboxResult = sandbox.editFileSafe({
384
- path: "mnf-s.txt",
385
- oldString: "xyz",
386
- newString: "abc",
387
- replaceAll: false,
388
- });
389
- const hostResult = host.editFileSafe({
390
- path: join(dir, "mnf-h.txt"),
391
- oldString: "xyz",
392
- newString: "abc",
393
- replaceAll: false,
394
- });
395
-
396
- expect(sandboxResult.ok).toBe(false);
397
- expect(hostResult.ok).toBe(false);
398
- if (sandboxResult.ok || hostResult.ok) return;
399
-
400
- expect(sandboxResult.error.code).toBe("MATCH_NOT_FOUND");
401
- expect(hostResult.error.code).toBe("MATCH_NOT_FOUND");
402
- });
403
-
404
- test("ambiguous match returns MATCH_AMBIGUOUS from both", () => {
405
- const dir = makeTempDir();
406
- const content = "repeat\nrepeat\n";
407
- writeFileSync(join(dir, "amb-s.txt"), content);
408
- writeFileSync(join(dir, "amb-h.txt"), content);
409
-
410
- const { sandbox, host } = dualOps(dir);
411
-
412
- const sandboxResult = sandbox.editFileSafe({
413
- path: "amb-s.txt",
414
- oldString: "repeat",
415
- newString: "unique",
416
- replaceAll: false,
417
- });
418
- const hostResult = host.editFileSafe({
419
- path: join(dir, "amb-h.txt"),
420
- oldString: "repeat",
421
- newString: "unique",
422
- replaceAll: false,
423
- });
424
-
425
- expect(sandboxResult.ok).toBe(false);
426
- expect(hostResult.ok).toBe(false);
427
- if (sandboxResult.ok || hostResult.ok) return;
428
-
429
- expect(sandboxResult.error.code).toBe("MATCH_AMBIGUOUS");
430
- expect(hostResult.error.code).toBe("MATCH_AMBIGUOUS");
431
- });
432
-
433
- test("editing a nonexistent file returns NOT_FOUND from both", () => {
434
- const dir = makeTempDir();
435
-
436
- const { sandbox, host } = dualOps(dir);
437
-
438
- const sandboxResult = sandbox.editFileSafe({
439
- path: "nope.txt",
440
- oldString: "a",
441
- newString: "b",
442
- replaceAll: false,
443
- });
444
- const hostResult = host.editFileSafe({
445
- path: join(dir, "nope.txt"),
446
- oldString: "a",
447
- newString: "b",
448
- replaceAll: false,
449
- });
450
-
451
- expect(sandboxResult.ok).toBe(false);
452
- expect(hostResult.ok).toBe(false);
453
- if (sandboxResult.ok || hostResult.ok) return;
454
-
455
- expect(sandboxResult.error.code).toBe("NOT_FOUND");
456
- expect(hostResult.error.code).toBe("NOT_FOUND");
457
- });
458
- });
459
-
460
- // ===========================================================================
461
- // 4. Edit engine consistency (pure function)
462
- // ===========================================================================
463
-
464
- describe("applyEdit engine: deterministic across invocations", () => {
465
- test("exact single match is idempotent", () => {
466
- const content = "alpha beta gamma";
467
- const r1 = applyEdit(content, "beta", "BETA", false);
468
- const r2 = applyEdit(content, "beta", "BETA", false);
469
-
470
- expect(r1).toEqual(r2);
471
- expect(r1.ok).toBe(true);
472
- if (!r1.ok) return;
473
- expect(r1.updatedContent).toBe("alpha BETA gamma");
474
- });
475
-
476
- test("replaceAll is idempotent", () => {
477
- const content = "x y x z x";
478
- const r1 = applyEdit(content, "x", "X", true);
479
- const r2 = applyEdit(content, "x", "X", true);
480
-
481
- expect(r1).toEqual(r2);
482
- expect(r1.ok).toBe(true);
483
- if (!r1.ok) return;
484
- expect(r1.matchCount).toBe(3);
485
- });
486
-
487
- test("not-found is consistent", () => {
488
- const content = "hello";
489
- const r1 = applyEdit(content, "missing", "found", false);
490
- const r2 = applyEdit(content, "missing", "found", false);
491
-
492
- expect(r1).toEqual(r2);
493
- expect(r1.ok).toBe(false);
494
- if (r1.ok) return;
495
- expect(r1.reason).toBe("not_found");
496
- });
497
-
498
- test("ambiguous is consistent", () => {
499
- const content = "dup dup dup";
500
- const r1 = applyEdit(content, "dup", "uniq", false);
501
- const r2 = applyEdit(content, "dup", "uniq", false);
502
-
503
- expect(r1).toEqual(r2);
504
- expect(r1.ok).toBe(false);
505
- if (r1.ok) return;
506
- expect(r1.reason).toBe("ambiguous");
507
- if (r1.reason !== "ambiguous") return;
508
- expect(r1.matchCount).toBe(3);
509
- });
510
-
511
- test("multiline content is handled correctly", () => {
512
- const content = "line1\nline2\nline3\nline4\n";
513
- const result = applyEdit(content, "line2\nline3", "replaced", false);
514
-
515
- expect(result.ok).toBe(true);
516
- if (!result.ok) return;
517
- expect(result.updatedContent).toBe("line1\nreplaced\nline4\n");
518
- expect(result.matchCount).toBe(1);
519
- });
520
-
521
- test("replacing with empty string works", () => {
522
- const content = "keep remove keep";
523
- const result = applyEdit(content, " remove", "", false);
524
-
525
- expect(result.ok).toBe(true);
526
- if (!result.ok) return;
527
- expect(result.updatedContent).toBe("keep keep");
528
- });
529
-
530
- test("special regex characters in old_string are treated as literals", () => {
531
- const content = "price is $100.00 (USD)";
532
- const result = applyEdit(content, "$100.00", "$200.00", false);
533
-
534
- expect(result.ok).toBe(true);
535
- if (!result.ok) return;
536
- expect(result.updatedContent).toBe("price is $200.00 (USD)");
537
- });
538
- });
539
-
540
- // ===========================================================================
541
- // 5. Path policy divergence — expected differences
542
- // ===========================================================================
543
-
544
- describe("Path policy divergence: sandbox blocks escapes, host requires absolute", () => {
545
- test("sandbox blocks path traversal (../), host allows any absolute path", () => {
546
- const dir = makeTempDir();
547
- writeFileSync(join(dir, "inside.txt"), "safe content");
548
-
549
- const sandboxOps = new FileSystemOps(sandboxPolicyFor(dir));
550
- const hostOps = new FileSystemOps(hostPolicyFn());
551
-
552
- // Sandbox: path traversal should be rejected
553
- const sandboxResult = sandboxOps.readFileSafe({
554
- path: "../../../etc/hostname",
555
- });
556
- expect(sandboxResult.ok).toBe(false);
557
- if (!sandboxResult.ok) {
558
- expect(sandboxResult.error.code).toBe("PATH_OUT_OF_BOUNDS");
559
- }
560
-
561
- // Host: relative paths are rejected (requires absolute)
562
- const hostResult = hostOps.readFileSafe({ path: "relative.txt" });
563
- expect(hostResult.ok).toBe(false);
564
- if (!hostResult.ok) {
565
- expect(hostResult.error.code).toBe("PATH_NOT_ABSOLUTE");
566
- }
567
- });
568
-
569
- test("sandbox allows relative paths within boundary", () => {
570
- const dir = makeTempDir();
571
- writeFileSync(join(dir, "valid.txt"), "data");
572
-
573
- const sandboxOps = new FileSystemOps(sandboxPolicyFor(dir));
574
-
575
- const result = sandboxOps.readFileSafe({ path: "valid.txt" });
576
- expect(result.ok).toBe(true);
577
- });
578
-
579
- test("host requires absolute path even for simple filenames", () => {
580
- const hostOps = new FileSystemOps(hostPolicyFn());
581
-
582
- const result = hostOps.readFileSafe({ path: "just-a-name.txt" });
583
- expect(result.ok).toBe(false);
584
- if (!result.ok) {
585
- expect(result.error.code).toBe("PATH_NOT_ABSOLUTE");
586
- }
587
- });
588
-
589
- test("sandbox rejects absolute paths outside boundary", () => {
590
- const dir = makeTempDir();
591
- const sandboxOps = new FileSystemOps(sandboxPolicyFor(dir));
592
-
593
- const result = sandboxOps.writeFileSafe({
594
- path: "/tmp/somewhere-else.txt",
595
- content: "bad",
596
- });
597
- expect(result.ok).toBe(false);
598
- if (!result.ok) {
599
- expect(result.error.code).toBe("PATH_OUT_OF_BOUNDS");
600
- }
601
- });
602
- });
603
-
604
- // ===========================================================================
605
- // 6. Sandbox backend parity — NativeBackend & DockerBackend SandboxResult shape
606
- // ===========================================================================
607
-
608
- describe("SandboxResult shape consistency across backends", () => {
609
- test("NativeBackend.wrap returns required fields", () => {
610
- const native = new NativeBackend();
611
-
612
- // On macOS this will succeed; on other platforms it will throw ToolError
613
- try {
614
- const result = native.wrap("echo test", "/tmp");
615
- expect(typeof result.command).toBe("string");
616
- expect(Array.isArray(result.args)).toBe(true);
617
- expect(typeof result.sandboxed).toBe("boolean");
618
- expect(result.sandboxed).toBe(true);
619
-
620
- // All args must be strings
621
- for (const arg of result.args) {
622
- expect(typeof arg).toBe("string");
623
- }
624
- } catch (err) {
625
- // NativeBackend explicitly throws ToolError on unsupported platforms or
626
- // unsafe paths. Infrastructure calls (writeFileSync, mkdirSync) can
627
- // throw system errors (ErrnoException). Both are legitimate — but
628
- // programming errors like TypeError/ReferenceError should still fail.
629
- const isToolError = err instanceof ToolError;
630
- const isSystemError =
631
- err instanceof Error &&
632
- "syscall" in err &&
633
- typeof (err as NodeJS.ErrnoException).code === "string";
634
- expect(isToolError || isSystemError).toBe(true);
635
- }
636
- });
637
-
638
- test("wrapCommand disabled returns bash with sandboxed=false", () => {
639
- const result = wrapCommand("echo hi", "/tmp", { enabled: false });
640
-
641
- expect(result.command).toBe("bash");
642
- expect(result.args).toEqual(["-c", "--", "echo hi"]);
643
- expect(result.sandboxed).toBe(false);
644
- });
645
-
646
- test("wrapCommand disabled result has same shape as enabled result", () => {
647
- const disabled = wrapCommand("echo hi", "/tmp", { enabled: false });
648
-
649
- // Both must have: command (string), args (string[]), sandboxed (boolean)
650
- expect(typeof disabled.command).toBe("string");
651
- expect(Array.isArray(disabled.args)).toBe(true);
652
- expect(typeof disabled.sandboxed).toBe("boolean");
653
-
654
- for (const arg of disabled.args) {
655
- expect(typeof arg).toBe("string");
656
- }
657
- });
658
- });
659
-
660
- // ===========================================================================
661
- // 7. Terminal output format consistency
662
- // ===========================================================================
663
-
664
- describe("Terminal output format: formatShellOutput shared by sandbox and host", () => {
665
- test("successful command output has no XML status tags", () => {
666
- const result = formatShellOutput("hello world", "", 0, false, 120);
667
-
668
- expect(result.content).toBe("hello world");
669
- expect(result.content).not.toContain("<command_exit");
670
- expect(result.content).not.toContain("<command_completed");
671
- expect(result.isError).toBe(false);
672
- expect(result.status).toBeUndefined();
673
- });
674
-
675
- test("empty output on success produces <command_completed /> tag", () => {
676
- const result = formatShellOutput("", "", 0, false, 120);
677
-
678
- expect(result.content).toBe("<command_completed />");
679
- expect(result.isError).toBe(false);
680
- });
681
-
682
- test("non-zero exit code with empty output produces <command_exit /> tag and descriptive message", () => {
683
- const result = formatShellOutput("", "", 42, false, 120);
684
-
685
- expect(result.content).toContain('<command_exit code="42" />');
686
- expect(result.content).toContain("Command failed with exit code 42");
687
- expect(result.content).toContain("No stdout or stderr output was produced");
688
- expect(result.isError).toBe(true);
689
- expect(result.status).toContain('<command_exit code="42" />');
690
- });
691
-
692
- test("stderr is appended to stdout with a newline separator", () => {
693
- const result = formatShellOutput("out", "err", 0, false, 120);
694
-
695
- expect(result.content).toBe("out\nerr");
696
- });
697
-
698
- test("stderr-only output uses stderr as the output", () => {
699
- const result = formatShellOutput("", "error message", 0, false, 120);
700
-
701
- expect(result.content).toBe("error message");
702
- });
703
-
704
- test("output truncation uses the shared MAX_OUTPUT_LENGTH constant", () => {
705
- const longOutput = "x".repeat(MAX_OUTPUT_LENGTH + 100);
706
- const result = formatShellOutput(longOutput, "", 0, false, 120);
707
-
708
- expect(result.content).toContain('limit="20K"');
709
- expect(result.content).toContain('file="');
710
- // The <output_truncated tag starts right after MAX_OUTPUT_LENGTH chars + 1 newline
711
- const tagStart = result.content.indexOf("<output_truncated");
712
- expect(tagStart).toBe(MAX_OUTPUT_LENGTH + 1);
713
- });
714
-
715
- test("timed-out command appends timeout tag and sets isError", () => {
716
- const result = formatShellOutput("partial", "", 137, true, 30);
717
-
718
- expect(result.content).toContain("partial");
719
- expect(result.content).toContain('<command_timeout seconds="30" />');
720
- expect(result.isError).toBe(true);
721
- expect(result.status).toContain("<command_timeout");
722
- });
723
- });
724
-
725
- // ===========================================================================
726
- // 8. Regression tests for edge cases found during migration
727
- // ===========================================================================
728
-
729
- describe("Regression: edge cases in shared FileSystemOps", () => {
730
- test("writing empty content creates a file with empty content", () => {
731
- const dir = makeTempDir();
732
-
733
- const { sandbox, host } = dualOps(dir);
734
-
735
- const sandboxResult = sandbox.writeFileSafe({
736
- path: "empty-s.txt",
737
- content: "",
738
- });
739
- const hostResult = host.writeFileSafe({
740
- path: join(dir, "empty-h.txt"),
741
- content: "",
742
- });
743
-
744
- expect(sandboxResult.ok).toBe(true);
745
- expect(hostResult.ok).toBe(true);
746
-
747
- expect(readFileSync(join(dir, "empty-s.txt"), "utf-8")).toBe("");
748
- expect(readFileSync(join(dir, "empty-h.txt"), "utf-8")).toBe("");
749
- });
750
-
751
- test("editing a file with only whitespace works correctly", () => {
752
- const dir = makeTempDir();
753
- writeFileSync(join(dir, "ws-s.txt"), " \n \n ");
754
- writeFileSync(join(dir, "ws-h.txt"), " \n \n ");
755
-
756
- const { sandbox, host } = dualOps(dir);
757
-
758
- const sandboxResult = sandbox.editFileSafe({
759
- path: "ws-s.txt",
760
- oldString: " \n \n ",
761
- newString: "replaced",
762
- replaceAll: false,
763
- });
764
- const hostResult = host.editFileSafe({
765
- path: join(dir, "ws-h.txt"),
766
- oldString: " \n \n ",
767
- newString: "replaced",
768
- replaceAll: false,
769
- });
770
-
771
- expect(sandboxResult.ok).toBe(true);
772
- expect(hostResult.ok).toBe(true);
773
- if (!sandboxResult.ok || !hostResult.ok) return;
774
-
775
- expect(sandboxResult.value.newContent).toBe("replaced");
776
- expect(hostResult.value.newContent).toBe("replaced");
777
- });
778
-
779
- test("write then read roundtrip produces consistent content", () => {
780
- const dir = makeTempDir();
781
- const content = "line 1\nline 2\nline 3";
782
-
783
- const { sandbox, host } = dualOps(dir);
784
-
785
- // Write via sandbox, read via both
786
- sandbox.writeFileSafe({ path: "roundtrip.txt", content });
787
-
788
- const sandboxRead = sandbox.readFileSafe({ path: "roundtrip.txt" });
789
- const hostRead = host.readFileSafe({ path: join(dir, "roundtrip.txt") });
790
-
791
- expect(sandboxRead.ok).toBe(true);
792
- expect(hostRead.ok).toBe(true);
793
- if (!sandboxRead.ok || !hostRead.ok) return;
794
-
795
- expect(sandboxRead.value.content).toBe(hostRead.value.content);
796
- });
797
-
798
- test("write then edit then read roundtrip is consistent", () => {
799
- const dir = makeTempDir();
800
- const initial = "const x = 1;\nconst y = 2;\nconst z = 3;";
801
-
802
- const { sandbox, host } = dualOps(dir);
803
-
804
- sandbox.writeFileSafe({ path: "code.ts", content: initial });
805
-
806
- sandbox.editFileSafe({
807
- path: "code.ts",
808
- oldString: "const y = 2;",
809
- newString: "const y = 42;",
810
- replaceAll: false,
811
- });
812
-
813
- const sandboxRead = sandbox.readFileSafe({ path: "code.ts" });
814
- const hostRead = host.readFileSafe({ path: join(dir, "code.ts") });
815
-
816
- expect(sandboxRead.ok).toBe(true);
817
- expect(hostRead.ok).toBe(true);
818
- if (!sandboxRead.ok || !hostRead.ok) return;
819
-
820
- expect(sandboxRead.value.content).toBe(hostRead.value.content);
821
- expect(sandboxRead.value.content).toContain("const y = 42;");
822
- expect(sandboxRead.value.content).not.toContain("const y = 2;");
823
- });
824
-
825
- test("file with very long lines is handled identically", () => {
826
- const dir = makeTempDir();
827
- const longLine = "a".repeat(10_000);
828
- writeFileSync(join(dir, "long-s.txt"), longLine);
829
- writeFileSync(join(dir, "long-h.txt"), longLine);
830
-
831
- const { sandbox, host } = dualOps(dir);
832
-
833
- const sandboxResult = sandbox.readFileSafe({ path: "long-s.txt" });
834
- const hostResult = host.readFileSafe({ path: join(dir, "long-h.txt") });
835
-
836
- expect(sandboxResult.ok).toBe(true);
837
- expect(hostResult.ok).toBe(true);
838
- if (!sandboxResult.ok || !hostResult.ok) return;
839
-
840
- expect(sandboxResult.value.content).toBe(hostResult.value.content);
841
- });
842
-
843
- test("concurrent writes to different files in same dir are isolated", () => {
844
- const dir = makeTempDir();
845
-
846
- const { sandbox } = dualOps(dir);
847
-
848
- // Simulate two "concurrent" writes (sequential here, but tests isolation)
849
- const r1 = sandbox.writeFileSafe({ path: "a.txt", content: "content-a" });
850
- const r2 = sandbox.writeFileSafe({ path: "b.txt", content: "content-b" });
851
-
852
- expect(r1.ok).toBe(true);
853
- expect(r2.ok).toBe(true);
854
-
855
- expect(readFileSync(join(dir, "a.txt"), "utf-8")).toBe("content-a");
856
- expect(readFileSync(join(dir, "b.txt"), "utf-8")).toBe("content-b");
857
- });
858
-
859
- test("edit preserves file content exactly except for the replacement", () => {
860
- const dir = makeTempDir();
861
- // Content with trailing newline, tabs, and special chars
862
- const content = "\tfirst line\n\tsecond line\n\tthird line\n";
863
- writeFileSync(join(dir, "precise.txt"), content);
864
-
865
- const ops = new FileSystemOps(sandboxPolicyFor(dir));
866
- const result = ops.editFileSafe({
867
- path: "precise.txt",
868
- oldString: "\tsecond line",
869
- newString: "\treplaced line",
870
- replaceAll: false,
871
- });
872
-
873
- expect(result.ok).toBe(true);
874
- if (!result.ok) return;
875
-
876
- expect(result.value.newContent).toBe(
877
- "\tfirst line\n\treplaced line\n\tthird line\n",
878
- );
879
- expect(readFileSync(join(dir, "precise.txt"), "utf-8")).toBe(
880
- "\tfirst line\n\treplaced line\n\tthird line\n",
881
- );
882
- });
883
-
884
- test("read with offset=1 and no limit returns all lines", () => {
885
- const dir = makeTempDir();
886
- writeFileSync(join(dir, "full.txt"), "one\ntwo\nthree\n");
887
-
888
- const ops = new FileSystemOps(sandboxPolicyFor(dir));
889
- const result = ops.readFileSafe({ path: "full.txt", offset: 1 });
890
-
891
- expect(result.ok).toBe(true);
892
- if (!result.ok) return;
893
-
894
- expect(result.value.content).toContain("one");
895
- expect(result.value.content).toContain("two");
896
- expect(result.value.content).toContain("three");
897
- });
898
-
899
- test("symlink within sandbox boundary is readable", () => {
900
- const dir = makeTempDir();
901
- const targetFile = join(dir, "target.txt");
902
- writeFileSync(targetFile, "linked content");
903
-
904
- const linkPath = join(dir, "link.txt");
905
- try {
906
- // eslint-disable-next-line @typescript-eslint/no-require-imports
907
- require("node:fs").symlinkSync(targetFile, linkPath);
908
- } catch {
909
- // Symlink creation may fail on some systems — skip gracefully
910
- return;
911
- }
912
-
913
- const ops = new FileSystemOps(sandboxPolicyFor(dir));
914
- const result = ops.readFileSafe({ path: "link.txt" });
915
-
916
- expect(result.ok).toBe(true);
917
- if (!result.ok) return;
918
- expect(result.value.content).toContain("linked content");
919
- });
920
- });
921
-
922
- // ===========================================================================
923
- // 9. NativeBackend shape verification
924
- // ===========================================================================
925
-
926
- describe("NativeBackend: SandboxResult shape", () => {
927
- test("NativeBackend has a wrap method", () => {
928
- const native = new NativeBackend();
929
- expect(typeof native.wrap).toBe("function");
930
- });
931
-
932
- test("disabled sandbox returns consistent bash -c -- invocation", () => {
933
- // Various commands should all be wrapped consistently when disabled
934
- const commands = [
935
- "echo hello",
936
- "ls -la",
937
- "cat /etc/hosts",
938
- "true && false",
939
- ];
940
- for (const cmd of commands) {
941
- const result = wrapCommand(cmd, "/tmp", { enabled: false });
942
- expect(result.command).toBe("bash");
943
- expect(result.args[0]).toBe("-c");
944
- expect(result.args[1]).toBe("--");
945
- expect(result.args[2]).toBe(cmd);
946
- expect(result.sandboxed).toBe(false);
947
- }
948
- });
949
- });
950
-
951
- // ===========================================================================
952
- // 10. Error handling consistency
953
- // ===========================================================================
954
-
955
- describe("Error handling consistency across code paths", () => {
956
- test("FsError codes are consistent between sandbox and host for same conditions", () => {
957
- const dir = makeTempDir();
958
-
959
- const { sandbox, host } = dualOps(dir);
960
-
961
- // NOT_FOUND
962
- const sfNotFound = sandbox.readFileSafe({ path: "missing.txt" });
963
- const hfNotFound = host.readFileSafe({ path: join(dir, "missing.txt") });
964
- expect(sfNotFound.ok).toBe(false);
965
- expect(hfNotFound.ok).toBe(false);
966
- if (!sfNotFound.ok && !hfNotFound.ok) {
967
- expect(sfNotFound.error.code).toBe(hfNotFound.error.code);
968
- }
969
-
970
- // NOT_A_FILE
971
- mkdirSync(join(dir, "dirA"));
972
- const sfNotFile = sandbox.readFileSafe({ path: "dirA" });
973
- const hfNotFile = host.readFileSafe({ path: join(dir, "dirA") });
974
- expect(sfNotFile.ok).toBe(false);
975
- expect(hfNotFile.ok).toBe(false);
976
- if (!sfNotFile.ok && !hfNotFile.ok) {
977
- expect(sfNotFile.error.code).toBe(hfNotFile.error.code);
978
- }
979
- });
980
-
981
- test("write error codes match between sandbox and host for same conditions", () => {
982
- const dir = makeTempDir();
983
-
984
- const { sandbox, host } = dualOps(dir);
985
-
986
- // Both should succeed for valid operations
987
- const sfWrite = sandbox.writeFileSafe({ path: "ok-s.txt", content: "ok" });
988
- const hfWrite = host.writeFileSafe({
989
- path: join(dir, "ok-h.txt"),
990
- content: "ok",
991
- });
992
-
993
- expect(sfWrite.ok).toBe(true);
994
- expect(hfWrite.ok).toBe(true);
995
- });
996
-
997
- test("edit MATCH_NOT_FOUND vs MATCH_AMBIGUOUS error codes match between paths", () => {
998
- const dir = makeTempDir();
999
- writeFileSync(join(dir, "err-s.txt"), "unique text");
1000
- writeFileSync(join(dir, "err-h.txt"), "unique text");
1001
-
1002
- const { sandbox, host } = dualOps(dir);
1003
-
1004
- // MATCH_NOT_FOUND
1005
- const sfMnf = sandbox.editFileSafe({
1006
- path: "err-s.txt",
1007
- oldString: "nope",
1008
- newString: "x",
1009
- replaceAll: false,
1010
- });
1011
- const hfMnf = host.editFileSafe({
1012
- path: join(dir, "err-h.txt"),
1013
- oldString: "nope",
1014
- newString: "x",
1015
- replaceAll: false,
1016
- });
1017
- expect(sfMnf.ok).toBe(false);
1018
- expect(hfMnf.ok).toBe(false);
1019
- if (!sfMnf.ok && !hfMnf.ok) {
1020
- expect(sfMnf.error.code).toBe(hfMnf.error.code);
1021
- expect(sfMnf.error.code).toBe("MATCH_NOT_FOUND");
1022
- }
1023
- });
1024
- });