@vellumai/assistant 0.8.12 → 0.9.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -0,0 +1,339 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ createWorkflowSandbox,
5
+ WorkflowDeterminismError,
6
+ WorkflowResourceError,
7
+ WorkflowScriptError,
8
+ } from "./sandbox.js";
9
+
10
+ // Adversarial containment suite. Each test attempts to break OUT of the sandbox
11
+ // and asserts the attempt fails (throws / returns undefined / is interrupted).
12
+ // A green run here IS the security claim: the sandbox is the boundary between
13
+ // hostile script content and the daemon.
14
+
15
+ const sandbox = () => createWorkflowSandbox({ hostFunctions: {} });
16
+
17
+ describe("sandbox containment — ambient capabilities absent", () => {
18
+ test.each([
19
+ "fetch",
20
+ "XMLHttpRequest",
21
+ "WebSocket",
22
+ "process",
23
+ "Bun",
24
+ "require",
25
+ "setTimeout",
26
+ "setInterval",
27
+ "setImmediate",
28
+ "queueMicrotask",
29
+ "navigator",
30
+ ])("global '%s' is undefined", async (cap) => {
31
+ const result = await sandbox().run(
32
+ `return typeof globalThis[${JSON.stringify(cap)}];`,
33
+ null,
34
+ );
35
+ expect(result).toBe("undefined");
36
+ });
37
+
38
+ test("globalThis.process is undefined (no env / argv reachable)", async () => {
39
+ const result = await sandbox().run(
40
+ `return typeof globalThis.process;`,
41
+ null,
42
+ );
43
+ expect(result).toBe("undefined");
44
+ });
45
+
46
+ test("calling an absent capability as a function throws (it is undefined)", async () => {
47
+ let caught: unknown;
48
+ try {
49
+ await sandbox().run(`return fetch("https://example.com");`, null);
50
+ } catch (e) {
51
+ caught = e;
52
+ }
53
+ // `fetch` resolves to `undefined`, so invoking it is a TypeError — the
54
+ // network call can never be made.
55
+ expect(caught).toBeInstanceOf(WorkflowScriptError);
56
+ expect((caught as WorkflowScriptError).message).toMatch(
57
+ /not a function|undefined/i,
58
+ );
59
+ });
60
+ });
61
+
62
+ describe("sandbox containment — module system unreachable", () => {
63
+ test("dynamic import() cannot synchronously resolve any module", async () => {
64
+ // No module loader is configured, so `import()` yields a promise that the
65
+ // synchronous runner can never settle — and which rejects if pumped (verified
66
+ // separately). Critically, it never synchronously returns a loaded module, so
67
+ // a script can never reach `fs`, `child_process`, etc.
68
+ const result = await sandbox().run(
69
+ `const p = import("node:fs");
70
+ return p instanceof Promise && typeof p.then === "function"
71
+ ? "promise-only"
72
+ : "MODULE-LEAKED";`,
73
+ null,
74
+ );
75
+ expect(result).toBe("promise-only");
76
+ });
77
+
78
+ test("import() rejects with no-loader error when its job is pumped", async () => {
79
+ // Capture the rejection via a host callback so we prove non-resolution
80
+ // rather than mere non-settlement. The host call (asyncify) lets the VM
81
+ // run the rejection microtask before the synchronous runner returns.
82
+ let importOutcome: string | undefined;
83
+ const sb = createWorkflowSandbox({
84
+ hostFunctions: {
85
+ report: (outcome: unknown) => {
86
+ importOutcome = String(outcome);
87
+ },
88
+ },
89
+ });
90
+ await sb.run(
91
+ `import("node:fs").then(
92
+ () => report("LOADED"),
93
+ (e) => report("rejected:" + (e && e.message)),
94
+ );
95
+ // Yield to the host (asyncify) so the rejection microtask can run.
96
+ report("scheduled");`,
97
+ null,
98
+ );
99
+ // The script ran; the import never loaded a module.
100
+ expect(importOutcome).not.toBe("LOADED");
101
+ });
102
+
103
+ test("require is not callable", async () => {
104
+ let caught: unknown;
105
+ try {
106
+ await sandbox().run(`return require("node:fs");`, null);
107
+ } catch (e) {
108
+ caught = e;
109
+ }
110
+ expect(caught).toBeInstanceOf(WorkflowScriptError);
111
+ });
112
+ });
113
+
114
+ describe("sandbox containment — realm-escape attempts", () => {
115
+ test("Function-constructor escape cannot reach a host global", async () => {
116
+ // ({}).constructor.constructor is Function; the constructed function runs
117
+ // in the SAME VM realm, so `process` is still undefined there.
118
+ const result = await sandbox().run(
119
+ `const f = ({}).constructor.constructor("return typeof process");
120
+ return f();`,
121
+ null,
122
+ );
123
+ expect(result).toBe("undefined");
124
+ });
125
+
126
+ test("Function('return this')() yields the VM global, not a Node global", async () => {
127
+ const result = await sandbox().run(
128
+ `const g = Function("return this")();
129
+ return [typeof g.process, typeof g.require, typeof g.Bun].join(",");`,
130
+ null,
131
+ );
132
+ expect(result).toBe("undefined,undefined,undefined");
133
+ });
134
+
135
+ test("prototype pollution does not surface on the host side", async () => {
136
+ // Pollute Object.prototype inside the VM; assert the host's Object is clean.
137
+ await sandbox().run(`Object.prototype.__pwned = "yes"; return 1;`, null);
138
+ expect(({} as Record<string, unknown>).__pwned).toBeUndefined();
139
+ });
140
+
141
+ test("a second run does not see the first run's prototype pollution", async () => {
142
+ const sb = createWorkflowSandbox({ hostFunctions: {} });
143
+ await sb.run(`Object.prototype.__pwned = "yes"; return 1;`, null);
144
+ const result = await sb.run(
145
+ `return ({}).__pwned === undefined ? "clean" : "polluted";`,
146
+ null,
147
+ );
148
+ expect(result).toBe("clean");
149
+ });
150
+ });
151
+
152
+ describe("sandbox containment — determinism bans", () => {
153
+ test("Date.now() throws WorkflowDeterminismError", async () => {
154
+ let caught: unknown;
155
+ try {
156
+ await sandbox().run(`return Date.now();`, null);
157
+ } catch (e) {
158
+ caught = e;
159
+ }
160
+ expect(caught).toBeInstanceOf(WorkflowDeterminismError);
161
+ expect((caught as Error).message).toMatch(/args/i);
162
+ });
163
+
164
+ test("Math.random() throws WorkflowDeterminismError", async () => {
165
+ let caught: unknown;
166
+ try {
167
+ await sandbox().run(`return Math.random();`, null);
168
+ } catch (e) {
169
+ caught = e;
170
+ }
171
+ expect(caught).toBeInstanceOf(WorkflowDeterminismError);
172
+ });
173
+
174
+ test("argless new Date() throws WorkflowDeterminismError", async () => {
175
+ let caught: unknown;
176
+ try {
177
+ await sandbox().run(`return new Date().toISOString();`, null);
178
+ } catch (e) {
179
+ caught = e;
180
+ }
181
+ expect(caught).toBeInstanceOf(WorkflowDeterminismError);
182
+ });
183
+
184
+ test("new Date(timestamp) from args is still allowed (deterministic)", async () => {
185
+ const result = await sandbox().run(
186
+ `return new Date(args.ts).toISOString();`,
187
+ { ts: 0 },
188
+ );
189
+ expect(result).toBe("1970-01-01T00:00:00.000Z");
190
+ });
191
+ });
192
+
193
+ describe("sandbox containment — resource guards", () => {
194
+ test("cumulative host-call latency beyond the deadline does NOT abort the run", async () => {
195
+ // Regression guard for a production-killing bug: the interrupt deadline used
196
+ // to bound TOTAL run wall-clock, which includes time the VM is suspended in
197
+ // asyncify awaiting host promises. Real workflows make many `agent()` host
198
+ // calls each costing seconds of LLM latency, so after a couple of leaves the
199
+ // run tripped the deadline and aborted on latency alone.
200
+ //
201
+ // Here three host calls each sleep ~250ms (cumulative ~750ms) under a 400ms
202
+ // deadline. Because the deadline measures CONTIGUOUS script CPU between host
203
+ // calls — reset around each host-call boundary — the suspended latency must
204
+ // NOT count, and the run must complete successfully.
205
+ const sb = createWorkflowSandbox({
206
+ hostFunctions: {
207
+ slow: async (n: unknown) => {
208
+ await new Promise((r) => setTimeout(r, 250));
209
+ return (n as number) + 1;
210
+ },
211
+ },
212
+ interruptDeadlineMs: 400,
213
+ });
214
+
215
+ const result = await sb.run(
216
+ `let n = 0;
217
+ for (let i = 0; i < 3; i++) {
218
+ n = slow(n); // each call sleeps ~250ms; cumulative ~750ms > 400ms deadline
219
+ }
220
+ return n;`,
221
+ null,
222
+ );
223
+ // slow(n) = n + 1, applied three times starting from 0 -> 3.
224
+ expect(result).toBe(3);
225
+ }, 20_000);
226
+
227
+ test("a tight CPU loop with no host calls is still interrupted (spin guard)", async () => {
228
+ // Companion to the latency test above: a genuine spin that never yields to a
229
+ // host call must STILL trip the deadline. Short deadline keeps the test fast.
230
+ const sb = createWorkflowSandbox({
231
+ hostFunctions: {},
232
+ interruptDeadlineMs: 200,
233
+ });
234
+ const start = Date.now();
235
+ let caught: unknown;
236
+ try {
237
+ await sb.run(`while (true) {}`, null);
238
+ } catch (e) {
239
+ caught = e;
240
+ }
241
+ const elapsed = Date.now() - start;
242
+ expect(caught).toBeInstanceOf(WorkflowResourceError);
243
+ // 200ms deadline; allow generous slack but assert it is bounded.
244
+ expect(elapsed).toBeLessThan(10_000);
245
+ }, 20_000);
246
+
247
+ test("an infinite loop is interrupted within a bounded time", async () => {
248
+ const start = Date.now();
249
+ let caught: unknown;
250
+ try {
251
+ await sandbox().run(`while (true) {}`, null);
252
+ } catch (e) {
253
+ caught = e;
254
+ }
255
+ const elapsed = Date.now() - start;
256
+ expect(caught).toBeInstanceOf(WorkflowResourceError);
257
+ // INTERRUPT_DEADLINE_MS is 5s; allow generous slack but assert it is bounded.
258
+ expect(elapsed).toBeLessThan(15_000);
259
+ }, 20_000);
260
+
261
+ test("AbortSignal interrupts a running script", async () => {
262
+ const controller = new AbortController();
263
+ const sb = createWorkflowSandbox({
264
+ hostFunctions: {},
265
+ signal: controller.signal,
266
+ });
267
+ // Abort almost immediately so the interrupt handler trips.
268
+ setTimeout(() => controller.abort(), 20);
269
+ let caught: unknown;
270
+ try {
271
+ await sb.run(`while (true) {}`, null);
272
+ } catch (e) {
273
+ caught = e;
274
+ }
275
+ expect(caught).toBeInstanceOf(WorkflowResourceError);
276
+ }, 20_000);
277
+
278
+ test("a large allocation hits the memory limit and throws rather than growing RSS", async () => {
279
+ const sb = createWorkflowSandbox({
280
+ hostFunctions: {},
281
+ memoryLimitBytes: 8 * 1024 * 1024, // 8 MiB ceiling
282
+ });
283
+ let caught: unknown;
284
+ try {
285
+ await sb.run(
286
+ `const acc = [];
287
+ // Grow well past the 8 MiB ceiling.
288
+ for (let i = 0; i < 1e9; i++) { acc.push(new Array(1000).fill(i)); }
289
+ return acc.length;`,
290
+ null,
291
+ );
292
+ } catch (e) {
293
+ caught = e;
294
+ }
295
+ // Out-of-memory surfaces as a script error or resource error — never a
296
+ // successful return, and never an unhandled daemon crash.
297
+ expect(caught).toBeDefined();
298
+ expect(
299
+ caught instanceof WorkflowScriptError ||
300
+ caught instanceof WorkflowResourceError,
301
+ ).toBe(true);
302
+ }, 20_000);
303
+ });
304
+
305
+ describe("sandbox containment — host failures are contained", () => {
306
+ test("a throwing host function surfaces as a catchable script error, not a daemon crash", async () => {
307
+ const sb = createWorkflowSandbox({
308
+ hostFunctions: {
309
+ explode: () => {
310
+ throw new Error("boom from host");
311
+ },
312
+ },
313
+ });
314
+ let caught: unknown;
315
+ try {
316
+ await sb.run(`return explode();`, null);
317
+ } catch (e) {
318
+ caught = e;
319
+ }
320
+ expect(caught).toBeInstanceOf(WorkflowScriptError);
321
+ expect((caught as Error).message).toContain("boom from host");
322
+ });
323
+
324
+ test("a script can catch a throwing host function itself", async () => {
325
+ const sb = createWorkflowSandbox({
326
+ hostFunctions: {
327
+ explode: () => {
328
+ throw new Error("boom");
329
+ },
330
+ },
331
+ });
332
+ const result = await sb.run(
333
+ `try { explode(); return "unreached"; }
334
+ catch (e) { return "caught:" + e.message; }`,
335
+ null,
336
+ );
337
+ expect(result).toBe("caught:boom");
338
+ });
339
+ });
@@ -0,0 +1,251 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ createWorkflowSandbox,
5
+ WorkflowResourceError,
6
+ WorkflowScriptError,
7
+ } from "./sandbox.js";
8
+
9
+ // Workflow scripts are SYNCHRONOUS: host functions block the script via
10
+ // asyncify (the VM stack unwinds while the host promise settles, then resumes),
11
+ // so authors call `agent(...)` and get the value back directly — no `await`.
12
+ describe("createWorkflowSandbox — functional", () => {
13
+ test("async host function round-trips through the VM (asyncify suspends)", async () => {
14
+ const calls: unknown[][] = [];
15
+ const sandbox = createWorkflowSandbox({
16
+ hostFunctions: {
17
+ agent: async (...args: unknown[]) => {
18
+ calls.push(args);
19
+ await new Promise((r) => setTimeout(r, 5));
20
+ return { reply: `handled:${String(args[0])}` };
21
+ },
22
+ },
23
+ });
24
+
25
+ const result = await sandbox.run(
26
+ `const r = agent("hello", 42);
27
+ return r.reply + "!";`,
28
+ null,
29
+ );
30
+
31
+ expect(result).toBe("handled:hello!");
32
+ expect(calls).toEqual([["hello", 42]]);
33
+ });
34
+
35
+ test("multiple sequential async host calls preserve ordering", async () => {
36
+ const sandbox = createWorkflowSandbox({
37
+ hostFunctions: {
38
+ step: async (n: unknown) => {
39
+ await new Promise((r) => setTimeout(r, 1));
40
+ return (n as number) * 2;
41
+ },
42
+ },
43
+ });
44
+
45
+ const result = await sandbox.run(
46
+ `let total = 0;
47
+ for (let i = 1; i <= 3; i++) { total += step(i); }
48
+ return total;`,
49
+ null,
50
+ );
51
+
52
+ expect(result).toBe(12); // (1+2+3)*2
53
+ });
54
+
55
+ test("synchronous host function works", async () => {
56
+ const sandbox = createWorkflowSandbox({
57
+ hostFunctions: { double: (n: unknown) => (n as number) * 2 },
58
+ });
59
+ const result = await sandbox.run(`return double(21);`, null);
60
+ expect(result).toBe(42);
61
+ });
62
+
63
+ test("TypeScript source is transpiled and runs", async () => {
64
+ const sandbox = createWorkflowSandbox({ hostFunctions: {} });
65
+ const result = await sandbox.run(
66
+ `interface Point { x: number; y: number }
67
+ const p: Point = { x: 3, y: 4 };
68
+ const dist: number = Math.sqrt(p.x ** 2 + p.y ** 2);
69
+ return dist;`,
70
+ null,
71
+ );
72
+ expect(result).toBe(5);
73
+ });
74
+
75
+ test("args arrive intact (nested object)", async () => {
76
+ const sandbox = createWorkflowSandbox({ hostFunctions: {} });
77
+ const result = await sandbox.run(
78
+ `return { sum: args.values.reduce((a, b) => a + b, 0), name: args.name };`,
79
+ { values: [1, 2, 3, 4], name: "Alice" },
80
+ );
81
+ expect(result).toEqual({ sum: 10, name: "Alice" });
82
+ });
83
+
84
+ test("script exceptions propagate as WorkflowScriptError with QuickJS stack", async () => {
85
+ const sandbox = createWorkflowSandbox({ hostFunctions: {} });
86
+ let caught: unknown;
87
+ try {
88
+ await sandbox.run(
89
+ `function boom() { throw new Error("kaboom"); }
90
+ boom();`,
91
+ null,
92
+ );
93
+ } catch (e) {
94
+ caught = e;
95
+ }
96
+ expect(caught).toBeInstanceOf(WorkflowScriptError);
97
+ const err = caught as WorkflowScriptError;
98
+ expect(err.message).toContain("kaboom");
99
+ expect(err.scriptStack).toBeDefined();
100
+ expect(err.scriptStack).toContain("boom");
101
+ });
102
+
103
+ test("host function args marshal as JSON (nested values intact)", async () => {
104
+ let received: unknown[] | undefined;
105
+ const sandbox = createWorkflowSandbox({
106
+ hostFunctions: {
107
+ capture: (...args: unknown[]) => {
108
+ received = args;
109
+ return "ok";
110
+ },
111
+ },
112
+ });
113
+ await sandbox.run(
114
+ `capture({ nested: [1, { a: true }] }, "str", 99);`,
115
+ null,
116
+ );
117
+ expect(received).toEqual([{ nested: [1, { a: true }] }, "str", 99]);
118
+ });
119
+
120
+ test("host function returning undefined yields undefined in the script", async () => {
121
+ const sandbox = createWorkflowSandbox({
122
+ hostFunctions: { noop: () => undefined },
123
+ });
124
+ const result = await sandbox.run(
125
+ `return noop() === undefined ? "undef" : "defined";`,
126
+ null,
127
+ );
128
+ expect(result).toBe("undef");
129
+ });
130
+
131
+ test("onLog receives console output", async () => {
132
+ const logs: string[] = [];
133
+ const sandbox = createWorkflowSandbox({
134
+ hostFunctions: {},
135
+ onLog: (m) => logs.push(m),
136
+ });
137
+ await sandbox.run(`console.log("hi", { a: 1 });`, null);
138
+ expect(logs).toEqual(['hi {"a":1}']);
139
+ });
140
+
141
+ test("the same sandbox can run multiple scripts (fresh isolation each run)", async () => {
142
+ const sandbox = createWorkflowSandbox({ hostFunctions: {} });
143
+ await sandbox.run(`globalThis.leaked = "first";`, null);
144
+ const result = await sandbox.run(`return typeof globalThis.leaked;`, null);
145
+ expect(result).toBe("undefined");
146
+ });
147
+ });
148
+
149
+ // Workflow scripts must be deterministic so journaled resume replays identically
150
+ // between the original run and a resume. Wall-clock and entropy are banned.
151
+ describe("createWorkflowSandbox — determinism bans", () => {
152
+ const banned = async (script: string): Promise<void> => {
153
+ const sandbox = createWorkflowSandbox({ hostFunctions: {} });
154
+ await expect(sandbox.run(script, null)).rejects.toThrow(
155
+ /non-deterministic|new Date/,
156
+ );
157
+ };
158
+
159
+ test("Date.now() is banned", () => banned(`return Date.now();`));
160
+ test("new Date() is banned", () => banned(`return new Date().getTime();`));
161
+ test("Math.random() is banned", () => banned(`return Math.random();`));
162
+
163
+ test("new Date() cannot be reached via Date.prototype.constructor", () =>
164
+ // The wrapper shares RealDate.prototype; if its `constructor` still pointed
165
+ // at the original Date, this would bypass the ban and read wall-clock time.
166
+ banned(`return new (Date.prototype.constructor)().getTime();`));
167
+
168
+ test("an instance's .constructor is the banned wrapper (no escape)", () =>
169
+ // `new Date(args)` returns a RealDate whose prototype is the shared object,
170
+ // so `inst.constructor` must resolve to the banned wrapper, not the original.
171
+ banned(
172
+ `const d = new Date(1577836800000); return new (d.constructor)().getTime();`,
173
+ ));
174
+
175
+ test("new Date(timestamp) still works and stays instanceof Date", async () => {
176
+ const sandbox = createWorkflowSandbox({ hostFunctions: {} });
177
+ const result = await sandbox.run(
178
+ `const d = new Date(1577836800000);
179
+ return [d instanceof Date, d.getUTCFullYear()];`,
180
+ null,
181
+ );
182
+ // 1577836800000 = 2020-01-01T00:00:00Z. The explicit-arg path is allowed
183
+ // (deterministic); only the zero-arg wall-clock read is banned.
184
+ expect(result).toEqual([true, 2020]);
185
+ });
186
+ });
187
+
188
+ // The interrupt deadline must bound script CPU even when the script loops over
189
+ // host calls — a cheap host call must NOT reset the deadline, or the guard is
190
+ // trivially evaded.
191
+ describe("createWorkflowSandbox — CPU guard", () => {
192
+ test("a tight loop over a cheap host fn trips the interrupt deadline", async () => {
193
+ // `cheap()` returns instantly, so it must not reset the CPU deadline —
194
+ // otherwise this loop would refresh the guard every iteration and pin the
195
+ // run forever. With a short deadline it trips promptly.
196
+ const sandbox = createWorkflowSandbox({
197
+ hostFunctions: { cheap: () => 1 },
198
+ interruptDeadlineMs: 100,
199
+ });
200
+ await expect(
201
+ sandbox.run(`while (true) { cheap(); }`, null),
202
+ ).rejects.toThrow(WorkflowResourceError);
203
+ });
204
+
205
+ test("a genuinely long host call does not trip the deadline (latency excused)", async () => {
206
+ // A single host call whose await (1000ms) far exceeds the deadline (200ms)
207
+ // must NOT trip: the reset excuses real leaf/agent latency. With the reset
208
+ // gated on a 50ms threshold, this 1000ms call still resets.
209
+ const sandbox = createWorkflowSandbox({
210
+ hostFunctions: {
211
+ slow: async () => {
212
+ await new Promise((r) => setTimeout(r, 1000));
213
+ return "ok";
214
+ },
215
+ },
216
+ interruptDeadlineMs: 200,
217
+ });
218
+ const result = await sandbox.run(`return slow();`, null);
219
+ expect(result).toBe("ok");
220
+ });
221
+
222
+ test("a host call that REJECTS after a long await still resets the deadline (catch survives)", async () => {
223
+ // A leaf that fails after a multi-second round-trip must not leave the
224
+ // script's catch block resuming against an already-expired CPU deadline.
225
+ // The reject-path reset (finally) excuses the awaited latency on failure
226
+ // too, so the catch — which does enough work to hit an interrupt check —
227
+ // runs instead of being killed as a WorkflowResourceError. Without the
228
+ // reset, this run rejects.
229
+ const sandbox = createWorkflowSandbox({
230
+ hostFunctions: {
231
+ slowFail: async () => {
232
+ await new Promise((r) => setTimeout(r, 1000));
233
+ throw new Error("leaf failed");
234
+ },
235
+ },
236
+ interruptDeadlineMs: 300,
237
+ });
238
+ const result = await sandbox.run(
239
+ `try {
240
+ slowFail();
241
+ return "no-throw";
242
+ } catch (e) {
243
+ let s = 0;
244
+ for (let i = 0; i < 100000; i++) s += i;
245
+ return "caught:" + s;
246
+ }`,
247
+ null,
248
+ );
249
+ expect(String(result).startsWith("caught:")).toBe(true);
250
+ });
251
+ });