@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.
- package/AGENTS.md +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- 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<
|
|
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
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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 + sandboxAutoApprove → allow
|
|
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
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
+
}
|