@vellumai/assistant 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -21,13 +21,23 @@ export interface ApprovalContext {
21
21
  isSkillBundled?: boolean;
22
22
  /** Whether the tool has a manifest override (unregistered skill tool). */
23
23
  hasManifestOverride?: boolean;
24
+ /** Whether the command's registry entry has sandboxAutoApprove: true. */
25
+ hasSandboxAutoApprove?: boolean;
24
26
  /**
25
27
  * Resolved auto-approve threshold for this execution context.
26
28
  * - "none": prompt for everything (strictest)
27
29
  * - "low": auto-approve Low risk (default, matches existing behavior)
28
30
  * - "medium": auto-approve Low and Medium risk
31
+ * - "high": auto-approve everything unconditionally
29
32
  */
30
- autoApproveUpTo?: "none" | "low" | "medium";
33
+ autoApproveUpTo?: "none" | "low" | "medium" | "high";
34
+ /**
35
+ * When true, the auto-approve threshold was resolved from the gateway
36
+ * (permission-controls-v3). This enables threshold-based override of
37
+ * ask rules — the user's threshold setting takes precedence over
38
+ * default ask rules when the risk falls within the threshold.
39
+ */
40
+ isGatewayThreshold?: boolean;
31
41
  }
32
42
 
33
43
  // ── Threshold resolution ─────────────────────────────────────────────────────
@@ -53,7 +63,10 @@ export interface ApprovalContext {
53
63
  * `"low"` is therefore *less strict* than the headless default. This is
54
64
  * intentional: the user explicitly chose a uniform threshold.
55
65
  */
56
- const CONTEXT_DEFAULTS: Record<ExecutionContext, "none" | "low" | "medium"> = {
66
+ const CONTEXT_DEFAULTS: Record<
67
+ ExecutionContext,
68
+ "none" | "low" | "medium" | "high"
69
+ > = {
57
70
  conversation: "low",
58
71
  background: "medium",
59
72
  headless: "none",
@@ -62,7 +75,7 @@ const CONTEXT_DEFAULTS: Record<ExecutionContext, "none" | "low" | "medium"> = {
62
75
  export function resolveThreshold(
63
76
  configValue: PermissionsConfig["autoApproveUpTo"] | undefined,
64
77
  executionContext?: ExecutionContext,
65
- ): "none" | "low" | "medium" {
78
+ ): "none" | "low" | "medium" | "high" {
66
79
  if (configValue == null) {
67
80
  return CONTEXT_DEFAULTS[executionContext ?? "conversation"];
68
81
  }
@@ -82,8 +95,22 @@ const THRESHOLD_ORDINAL: Record<string, number> = {
82
95
  none: -1,
83
96
  low: 0,
84
97
  medium: 1,
98
+ high: 2,
85
99
  };
86
100
 
101
+ /**
102
+ * Check whether a risk level falls within the configured auto-approve threshold.
103
+ * Returns `true` when the risk is at or below the threshold (i.e. auto-approve).
104
+ */
105
+ function isRiskWithinThreshold(
106
+ riskLevel: string,
107
+ autoApproveUpTo: string | undefined,
108
+ ): boolean {
109
+ const risk = RISK_ORDINAL[riskLevel] ?? 2;
110
+ const threshold = THRESHOLD_ORDINAL[autoApproveUpTo ?? "low"] ?? 0;
111
+ return risk <= threshold;
112
+ }
113
+
87
114
  /** The outcome of an approval policy evaluation. */
88
115
  export interface ApprovalDecision {
89
116
  decision: "allow" | "prompt" | "deny";
@@ -105,14 +132,20 @@ export interface ApprovalPolicy {
105
132
  * The decision flow:
106
133
  *
107
134
  * 1. Deny rule → deny
108
- * 2. Ask rule → prompt
109
- * 3. Allow rule + non-High → allow
110
- * 4. Allow rule + High + containerized bash → allow (shouldAutoAllowHighRisk)
111
- * 5. Allow rule + High + no auto-allow prompt (fall through)
112
- * 6. No rule + third-party skill tool → prompt
113
- * 7. No rule + strict mode → prompt
135
+ * 2. Ask rule + risk > autoApproveUpTo → prompt
136
+ * Ask rule + risk ≤ autoApproveUpTo → allow (v3 only: threshold overrides ask)
137
+ * Exception: skill_load_dynamic ask rules always prompt (inline-command safety gate)
138
+ * 3. Sandbox auto-approve: workspace mode + bash + sandboxAutoApproveallow
139
+ * (Path resolution is baked into `hasSandboxAutoApprove` upstream: containerized
140
+ * environments skip path checks; non-containerized environments validate all
141
+ * path arguments against the workspace root.)
142
+ * 4. Allow rule + non-High → allow
143
+ * 5. Allow rule + High → fall through to risk-based
144
+ * 6. No rule + third-party skill tool + risk > autoApproveUpTo → prompt
145
+ * No rule + third-party skill tool + risk ≤ autoApproveUpTo → allow (v3 only)
146
+ * 7. No rule + strict mode + risk > autoApproveUpTo → prompt
147
+ * No rule + strict mode + risk ≤ autoApproveUpTo → allow (v3 only)
114
148
  * 8. No rule + workspace mode + Low + workspace-scoped → allow
115
- * (except non-containerized bash — never auto-allow)
116
149
  * 9. No rule + Low + bundled skill → allow
117
150
  * 10. Risk ≤ autoApproveUpTo threshold → allow
118
151
  * 11. Risk > autoApproveUpTo threshold → prompt
@@ -124,11 +157,11 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
124
157
  toolName,
125
158
  matchedRule,
126
159
  permissionsMode,
127
- isContainerized,
128
160
  isWorkspaceScoped,
129
161
  toolOrigin,
130
162
  isSkillBundled,
131
163
  hasManifestOverride,
164
+ hasSandboxAutoApprove,
132
165
  } = context;
133
166
 
134
167
  // ── 1. Deny rules apply at ALL risk levels ────────────────────────
@@ -140,8 +173,29 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
140
173
  };
141
174
  }
142
175
 
143
- // ── 2. Ask rules always prompt ────────────────────────────────────
176
+ // ── 2. Ask rules prompt — unless the gateway threshold covers the risk.
177
+ // When permission-controls-v3 is active (isGatewayThreshold), the user's
178
+ // threshold setting takes precedence over ask rules: if the risk falls
179
+ // within autoApproveUpTo, the ask rule is overridden and the tool
180
+ // auto-approves. Without v3, ask rules always prompt (preserving
181
+ // backward-compatible behavior for default ask rules on host tools, etc.).
182
+ // Exception: skill_load_dynamic ask rules always prompt — they gate
183
+ // inline-command skill loads that execute embedded commands and must
184
+ // never be silently auto-approved.
144
185
  if (matchedRule && matchedRule.decision === "ask") {
186
+ const isDynamicSkillAsk = matchedRule.pattern.startsWith(
187
+ "skill_load_dynamic:",
188
+ );
189
+ if (
190
+ !isDynamicSkillAsk &&
191
+ context.isGatewayThreshold &&
192
+ isRiskWithinThreshold(riskLevel, context.autoApproveUpTo)
193
+ ) {
194
+ return {
195
+ decision: "allow",
196
+ reason: `${riskLevel} risk: within auto-approve threshold (ask rule overridden)`,
197
+ };
198
+ }
145
199
  return {
146
200
  decision: "prompt",
147
201
  reason: `Matched ask rule: ${matchedRule.pattern}`,
@@ -149,9 +203,24 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
149
203
  };
150
204
  }
151
205
 
152
- // ── 3–5. Allow rule handling ──────────────────────────────────────
206
+ // ── 3. Sandbox auto-approve: bash + allowlisted → allow ──
207
+ // Only fires in workspace mode — strict mode always requires explicit rules.
208
+ // Path resolution is baked into `hasSandboxAutoApprove` upstream:
209
+ // containerized environments skip path checks (entire fs is workspace),
210
+ // non-containerized environments validate all path args against workspace root.
211
+ if (
212
+ permissionsMode === "workspace" &&
213
+ toolName === "bash" &&
214
+ hasSandboxAutoApprove === true
215
+ ) {
216
+ return {
217
+ decision: "allow",
218
+ reason: "Workspace filesystem operation (sandbox auto-approve)",
219
+ };
220
+ }
221
+
222
+ // ── 4–5. Allow rule handling ──────────────────────────────────────
153
223
  if (matchedRule) {
154
- // 3. Allow rule + non-High → allow
155
224
  if (riskLevel !== RiskLevel.High) {
156
225
  return {
157
226
  decision: "allow",
@@ -159,30 +228,24 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
159
228
  matchedRule,
160
229
  };
161
230
  }
162
-
163
- // 4. Allow rule + High + containerized bash → allow
164
- if (this.shouldAutoAllowHighRisk(toolName, isContainerized)) {
165
- return {
166
- decision: "allow",
167
- reason: `Matched trust rule in auto-allow-high-risk context: ${matchedRule.pattern}`,
168
- matchedRule,
169
- };
170
- }
171
-
172
- // 5. Allow rule + High (no auto-allow) → fall through to risk-based
173
- // Note: matchedRule is intentionally omitted from the risk-based
174
- // fallback return — the decision is driven by risk, not the rule.
231
+ // High risk: fall through to risk-based regardless of rule
175
232
  }
176
233
 
177
- // ── 6. No rule + third-party skill tool → prompt ──────────────────
234
+ // ── 6. No rule + third-party skill tool → prompt (unless v3 threshold covers it)
178
235
  if (!matchedRule) {
179
- if (toolOrigin === "skill" && !isSkillBundled) {
180
- return {
181
- decision: "prompt",
182
- reason: "Skill tool: requires approval by default",
183
- };
184
- }
185
- if (hasManifestOverride && !toolOrigin) {
236
+ const isThirdPartySkill =
237
+ (toolOrigin === "skill" && !isSkillBundled) ||
238
+ (hasManifestOverride && !toolOrigin);
239
+ if (isThirdPartySkill) {
240
+ if (
241
+ context.isGatewayThreshold &&
242
+ isRiskWithinThreshold(riskLevel, context.autoApproveUpTo)
243
+ ) {
244
+ return {
245
+ decision: "allow",
246
+ reason: `${riskLevel} risk: within auto-approve threshold (skill tool)`,
247
+ };
248
+ }
186
249
  return {
187
250
  decision: "prompt",
188
251
  reason: "Skill tool: requires approval by default",
@@ -190,8 +253,17 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
190
253
  }
191
254
  }
192
255
 
193
- // ── 7. No rule + strict mode → prompt ─────────────────────────────
256
+ // ── 7. No rule + strict mode → prompt (unless v3 threshold covers it)
194
257
  if (permissionsMode === "strict" && !matchedRule) {
258
+ if (
259
+ context.isGatewayThreshold &&
260
+ isRiskWithinThreshold(riskLevel, context.autoApproveUpTo)
261
+ ) {
262
+ return {
263
+ decision: "allow",
264
+ reason: `${riskLevel} risk: within auto-approve threshold (strict mode overridden)`,
265
+ };
266
+ }
195
267
  return {
196
268
  decision: "prompt",
197
269
  reason: "Strict mode: no matching rule, requires approval",
@@ -199,15 +271,12 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
199
271
  }
200
272
 
201
273
  // ── 8. No rule + workspace mode + Low + workspace-scoped → allow ──
202
- // Exception: non-containerized bash never auto-allows.
203
274
  if (
204
275
  permissionsMode === "workspace" &&
205
276
  !matchedRule &&
206
277
  riskLevel === RiskLevel.Low
207
278
  ) {
208
- if (toolName === "bash" && !isContainerized) {
209
- // Fall through to risk-based policy below
210
- } else if (isWorkspaceScoped) {
279
+ if (isWorkspaceScoped) {
211
280
  return {
212
281
  decision: "allow",
213
282
  reason: "Workspace mode: workspace-scoped operation auto-allowed",
@@ -226,10 +295,7 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
226
295
  }
227
296
 
228
297
  // ── 10–11. Risk-based fallback: compare risk against configured threshold ─
229
- const autoApproveUpTo = context.autoApproveUpTo ?? "low";
230
- const risk = RISK_ORDINAL[riskLevel] ?? 2;
231
- const threshold = THRESHOLD_ORDINAL[autoApproveUpTo] ?? 0;
232
- if (risk <= threshold) {
298
+ if (isRiskWithinThreshold(riskLevel, context.autoApproveUpTo)) {
233
299
  return {
234
300
  decision: "allow",
235
301
  reason: `${riskLevel} risk: within auto-approve threshold`,
@@ -240,18 +306,4 @@ export class DefaultApprovalPolicy implements ApprovalPolicy {
240
306
  reason: `${riskLevel} risk: above auto-approve threshold`,
241
307
  };
242
308
  }
243
-
244
- /**
245
- * Determines at runtime whether a high-risk operation should be auto-allowed.
246
- * Auto-allows high-risk operations when running in a containerized sandbox.
247
- *
248
- * Auto-allow cases:
249
- * - Containerized bash: all commands are sandboxed, so high-risk is safe.
250
- */
251
- private shouldAutoAllowHighRisk(
252
- toolName: string,
253
- isContainerized: boolean,
254
- ): boolean {
255
- return toolName === "bash" && isContainerized;
256
- }
257
309
  }
@@ -0,0 +1,161 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { parseArgs } from "./arg-parser.js";
4
+
5
+ describe("parseArgs", () => {
6
+ test("boolean flags", () => {
7
+ const result = parseArgs(["-v", "-f"], {});
8
+ expect(result.flags.get("-v")).toBe(true);
9
+ expect(result.flags.get("-f")).toBe(true);
10
+ expect(result.positionals).toEqual([]);
11
+ expect(result.pathArgs).toEqual([]);
12
+ expect(result.sawDoubleDash).toBe(false);
13
+ });
14
+
15
+ test("value-consuming flags", () => {
16
+ const result = parseArgs(["-o", "out.txt", "in.txt"], {
17
+ valueFlags: ["-o"],
18
+ });
19
+ expect(result.flags.get("-o")).toBe("out.txt");
20
+ expect(result.positionals).toEqual(["in.txt"]);
21
+ // Default positionals mode is "paths", so in.txt is a path.
22
+ expect(result.pathArgs).toEqual(["in.txt"]);
23
+ });
24
+
25
+ test("path flags", () => {
26
+ const result = parseArgs(["-t", "/tmp/dir", "file.txt"], {
27
+ valueFlags: ["-t"],
28
+ pathFlags: { "-t": true },
29
+ });
30
+ expect(result.flags.get("-t")).toBe("/tmp/dir");
31
+ expect(result.positionals).toEqual(["file.txt"]);
32
+ // /tmp/dir from pathFlag + file.txt from default positional "paths" mode.
33
+ expect(result.pathArgs).toEqual(["/tmp/dir", "file.txt"]);
34
+ });
35
+
36
+ test("-- terminator", () => {
37
+ const result = parseArgs(["--", "-notaflag"], {});
38
+ expect(result.sawDoubleDash).toBe(true);
39
+ expect(result.positionals).toEqual(["-notaflag"]);
40
+ expect(result.pathArgs).toEqual(["-notaflag"]);
41
+ expect(result.flags.size).toBe(0);
42
+ });
43
+
44
+ test("respectsDoubleDash: false", () => {
45
+ const result = parseArgs(["--", "-notaflag"], {
46
+ respectsDoubleDash: false,
47
+ });
48
+ // `--` is treated as a boolean flag, not a terminator.
49
+ expect(result.sawDoubleDash).toBe(false);
50
+ expect(result.flags.get("--")).toBe(true);
51
+ expect(result.flags.get("-notaflag")).toBe(true);
52
+ expect(result.positionals).toEqual([]);
53
+ });
54
+
55
+ test("positionals 'paths' (default) — all positionals in pathArgs", () => {
56
+ const result = parseArgs(["a.txt", "b.txt"], {});
57
+ expect(result.positionals).toEqual(["a.txt", "b.txt"]);
58
+ expect(result.pathArgs).toEqual(["a.txt", "b.txt"]);
59
+ });
60
+
61
+ test("positionals 'paths' (explicit) — all positionals in pathArgs", () => {
62
+ const result = parseArgs(["a.txt", "b.txt"], { positionals: "paths" });
63
+ expect(result.positionals).toEqual(["a.txt", "b.txt"]);
64
+ expect(result.pathArgs).toEqual(["a.txt", "b.txt"]);
65
+ });
66
+
67
+ test("positionals 'none' — no positionals in pathArgs", () => {
68
+ const result = parseArgs(["a.txt", "b.txt"], { positionals: "none" });
69
+ expect(result.positionals).toEqual(["a.txt", "b.txt"]);
70
+ expect(result.pathArgs).toEqual([]);
71
+ });
72
+
73
+ test("mixed positionals (array) with rest descriptor", () => {
74
+ const result = parseArgs(["pattern", "file1.txt", "file2.txt"], {
75
+ positionals: [{ role: "pattern" }, { role: "path", rest: true }],
76
+ });
77
+ expect(result.positionals).toEqual(["pattern", "file1.txt", "file2.txt"]);
78
+ // "pattern" has role "pattern" → not a path.
79
+ // "file1.txt" at index 1 has role "path" with rest → path.
80
+ // "file2.txt" at index 2 exceeds array but rest descriptor applies → path.
81
+ expect(result.pathArgs).toEqual(["file1.txt", "file2.txt"]);
82
+ });
83
+
84
+ test("positional array without rest — excess positionals default to path", () => {
85
+ const result = parseArgs(["script", "extra1", "extra2"], {
86
+ positionals: [{ role: "script" }],
87
+ });
88
+ expect(result.positionals).toEqual(["script", "extra1", "extra2"]);
89
+ // "script" → role "script" → not a path.
90
+ // "extra1", "extra2" → no descriptor, no rest → conservative default → path.
91
+ expect(result.pathArgs).toEqual(["extra1", "extra2"]);
92
+ });
93
+
94
+ test("empty args", () => {
95
+ const result = parseArgs([], {});
96
+ expect(result.flags.size).toBe(0);
97
+ expect(result.positionals).toEqual([]);
98
+ expect(result.pathArgs).toEqual([]);
99
+ expect(result.sawDoubleDash).toBe(false);
100
+ });
101
+
102
+ test("value-consuming flag at end of args with no next token — treated as boolean", () => {
103
+ const result = parseArgs(["-o"], { valueFlags: ["-o"] });
104
+ expect(result.flags.get("-o")).toBe(true);
105
+ expect(result.positionals).toEqual([]);
106
+ expect(result.pathArgs).toEqual([]);
107
+ });
108
+
109
+ test("positionals after -- are still classified by positional descriptors", () => {
110
+ const result = parseArgs(["--", "pattern", "file.txt"], {
111
+ positionals: [{ role: "pattern" }, { role: "path" }],
112
+ });
113
+ expect(result.sawDoubleDash).toBe(true);
114
+ expect(result.positionals).toEqual(["pattern", "file.txt"]);
115
+ // "pattern" at index 0 → role "pattern" → not a path.
116
+ // "file.txt" at index 1 → role "path" → path.
117
+ expect(result.pathArgs).toEqual(["file.txt"]);
118
+ });
119
+
120
+ test("value flag value is not added to pathArgs unless flag is in pathFlags", () => {
121
+ const result = parseArgs(["-o", "/some/path"], {
122
+ valueFlags: ["-o"],
123
+ positionals: "none",
124
+ });
125
+ expect(result.flags.get("-o")).toBe("/some/path");
126
+ // -o is not in pathFlags, so the value is not a path.
127
+ expect(result.pathArgs).toEqual([]);
128
+ });
129
+
130
+ test("multiple path flags accumulate in pathArgs", () => {
131
+ const result = parseArgs(["-I", "/include1", "-I", "/include2", "src.c"], {
132
+ valueFlags: ["-I"],
133
+ pathFlags: { "-I": true },
134
+ });
135
+ expect(result.flags.get("-I")).toBe("/include2"); // last value wins in Map
136
+ expect(result.positionals).toEqual(["src.c"]);
137
+ expect(result.pathArgs).toEqual(["/include1", "/include2", "src.c"]);
138
+ });
139
+
140
+ test("--flag=value syntax with path flag", () => {
141
+ const result = parseArgs(["--target-directory=/tmp/dir", "file.txt"], {
142
+ valueFlags: ["--target-directory"],
143
+ pathFlags: { "--target-directory": true },
144
+ });
145
+ expect(result.flags.get("--target-directory")).toBe("/tmp/dir");
146
+ expect(result.positionals).toEqual(["file.txt"]);
147
+ // /tmp/dir from pathFlag + file.txt from default positional "paths" mode.
148
+ expect(result.pathArgs).toEqual(["/tmp/dir", "file.txt"]);
149
+ });
150
+
151
+ test("--flag=value syntax with non-path value flag", () => {
152
+ const result = parseArgs(["--output=out.txt", "in.txt"], {
153
+ valueFlags: ["--output"],
154
+ });
155
+ expect(result.flags.get("--output")).toBe("out.txt");
156
+ expect(result.positionals).toEqual(["in.txt"]);
157
+ // --output is not in pathFlags, so out.txt is not a path arg.
158
+ // in.txt is a positional with default "paths" mode → path.
159
+ expect(result.pathArgs).toEqual(["in.txt"]);
160
+ });
161
+ });
@@ -0,0 +1,141 @@
1
+ import type { ArgSchema, ParsedArgs, PositionalDesc } from "./risk-types.js";
2
+
3
+ /**
4
+ * Parse a command's arguments according to an {@link ArgSchema}.
5
+ *
6
+ * Classifies each token as a flag, positional, or path argument. The
7
+ * resulting {@link ParsedArgs} is consumed by downstream path-resolution
8
+ * and sandbox-policy checks.
9
+ */
10
+ export function parseArgs(args: string[], schema: ArgSchema): ParsedArgs {
11
+ const valueFlagSet = new Set(schema.valueFlags);
12
+ const pathFlagSet = new Set(
13
+ schema.pathFlags ? Object.keys(schema.pathFlags) : [],
14
+ );
15
+
16
+ const flags = new Map<string, string | true>();
17
+ const positionals: string[] = [];
18
+ const pathArgs: string[] = [];
19
+ let sawDoubleDash = false;
20
+
21
+ let i = 0;
22
+ while (i < args.length) {
23
+ const token = args[i]!;
24
+
25
+ // After `--`, everything is positional.
26
+ if (sawDoubleDash) {
27
+ positionals.push(token);
28
+ addIfPath(token, positionals.length - 1, schema.positionals, pathArgs);
29
+ i++;
30
+ continue;
31
+ }
32
+
33
+ // Double-dash terminator.
34
+ if (token === "--" && schema.respectsDoubleDash !== false) {
35
+ sawDoubleDash = true;
36
+ i++;
37
+ continue;
38
+ }
39
+
40
+ // Value-consuming flag: consume next token as the flag's value.
41
+ if (token.startsWith("-") && valueFlagSet.has(token)) {
42
+ const nextIndex = i + 1;
43
+ if (nextIndex < args.length) {
44
+ const value = args[nextIndex]!;
45
+ flags.set(token, value);
46
+ if (pathFlagSet.has(token)) {
47
+ pathArgs.push(value);
48
+ }
49
+ i += 2;
50
+ } else {
51
+ // Flag at end of args with no next token — treat as boolean.
52
+ flags.set(token, true);
53
+ i++;
54
+ }
55
+ continue;
56
+ }
57
+
58
+ // --flag=value form: split on the first `=` and check if the flag
59
+ // name is a value-consuming flag. This handles e.g.
60
+ // `--target-directory=/tmp/` or `--output=out.txt`.
61
+ if (token.startsWith("-") && token.includes("=")) {
62
+ const eqIndex = token.indexOf("=");
63
+ const flagName = token.slice(0, eqIndex);
64
+ const flagValue = token.slice(eqIndex + 1);
65
+
66
+ if (valueFlagSet.has(flagName)) {
67
+ flags.set(flagName, flagValue);
68
+ if (pathFlagSet.has(flagName)) {
69
+ pathArgs.push(flagValue);
70
+ }
71
+ i++;
72
+ continue;
73
+ }
74
+ }
75
+
76
+ // Boolean flag (starts with `-` but not a value-consuming flag).
77
+ if (token.startsWith("-")) {
78
+ flags.set(token, true);
79
+ i++;
80
+ continue;
81
+ }
82
+
83
+ // Positional argument.
84
+ positionals.push(token);
85
+ addIfPath(token, positionals.length - 1, schema.positionals, pathArgs);
86
+ i++;
87
+ }
88
+
89
+ return { flags, positionals, pathArgs, sawDoubleDash };
90
+ }
91
+
92
+ /**
93
+ * Determine whether a positional at the given index is a path and, if so,
94
+ * add it to `pathArgs`.
95
+ */
96
+ function addIfPath(
97
+ token: string,
98
+ index: number,
99
+ positionalsDef: ArgSchema["positionals"],
100
+ pathArgs: string[],
101
+ ): void {
102
+ if (positionalsDef === undefined || positionalsDef === "paths") {
103
+ // Default: all positionals are paths.
104
+ pathArgs.push(token);
105
+ return;
106
+ }
107
+
108
+ if (positionalsDef === "none") {
109
+ // Explicitly not paths.
110
+ return;
111
+ }
112
+
113
+ // Array of PositionalDesc — look up by index.
114
+ const descs: PositionalDesc[] = positionalsDef;
115
+
116
+ // Find the applicable descriptor: either the one at this index, or a
117
+ // previous `rest: true` descriptor that covers all subsequent positions.
118
+ let desc: PositionalDesc | undefined;
119
+
120
+ if (index < descs.length) {
121
+ desc = descs[index];
122
+ } else {
123
+ // Look backwards for the last `rest: true` descriptor.
124
+ for (let j = descs.length - 1; j >= 0; j--) {
125
+ if (descs[j]!.rest) {
126
+ desc = descs[j];
127
+ break;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (desc) {
133
+ if (desc.role === "path") {
134
+ pathArgs.push(token);
135
+ }
136
+ // Other roles (pattern, script, value, command) → not a path.
137
+ } else {
138
+ // No descriptor and no rest — conservative default: treat as path.
139
+ pathArgs.push(token);
140
+ }
141
+ }