@vellumai/assistant 0.3.0
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/.dockerignore +27 -0
- package/.env.example +22 -0
- package/Dockerfile +99 -0
- package/Dockerfile.sandbox +5 -0
- package/README.md +248 -0
- package/bun.lock +1723 -0
- package/bunfig.toml +2 -0
- package/docs/skills.md +158 -0
- package/drizzle/0000_dizzy_maggott.sql +301 -0
- package/drizzle/meta/0000_snapshot.json +1999 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/eslint.config.mjs +17 -0
- package/hook-templates/debug-prompt-logger/hook.json +7 -0
- package/hook-templates/debug-prompt-logger/run.sh +68 -0
- package/knip.json +9 -0
- package/package.json +70 -0
- package/scripts/capture-x-graphql.ts +545 -0
- package/scripts/ipc/check-contract-inventory.ts +104 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +166 -0
- package/scripts/ipc/generate-swift.ts +492 -0
- package/scripts/test-filesystem-tools.sh +48 -0
- package/scripts/test.sh +127 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2485 -0
- package/src/__tests__/account-registry.test.ts +245 -0
- package/src/__tests__/active-skill-tools.test.ts +378 -0
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/agent-loop-thinking.test.ts +81 -0
- package/src/__tests__/agent-loop.test.ts +1135 -0
- package/src/__tests__/anthropic-provider.test.ts +778 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +290 -0
- package/src/__tests__/app-bundler.test.ts +292 -0
- package/src/__tests__/app-executors.test.ts +613 -0
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/app-open-proxy.test.ts +62 -0
- package/src/__tests__/asset-materialize-tool.test.ts +452 -0
- package/src/__tests__/asset-search-tool.test.ts +477 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +401 -0
- package/src/__tests__/assistant-attachments.test.ts +437 -0
- package/src/__tests__/assistant-event-hub.test.ts +226 -0
- package/src/__tests__/assistant-event.test.ts +123 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/attachments-store.test.ts +476 -0
- package/src/__tests__/attachments.test.ts +134 -0
- package/src/__tests__/audit-log-rotation.test.ts +154 -0
- package/src/__tests__/browser-fill-credential.test.ts +309 -0
- package/src/__tests__/browser-manager.test.ts +203 -0
- package/src/__tests__/browser-runtime-check.test.ts +55 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +68 -0
- package/src/__tests__/browser-skill-endstate.test.ts +195 -0
- package/src/__tests__/bundle-scanner.test.ts +313 -0
- package/src/__tests__/call-bridge.test.ts +517 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +625 -0
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +699 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-state.test.ts +174 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/channel-approval-routes.test.ts +2356 -0
- package/src/__tests__/channel-approval.test.ts +299 -0
- package/src/__tests__/channel-approvals.test.ts +521 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/channel-guardian.test.ts +1005 -0
- package/src/__tests__/checker.test.ts +3519 -0
- package/src/__tests__/clarification-resolver.test.ts +159 -0
- package/src/__tests__/classifier.test.ts +67 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +127 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +88 -0
- package/src/__tests__/cli-discover.test.ts +85 -0
- package/src/__tests__/cli.test.ts +26 -0
- package/src/__tests__/clipboard.test.ts +80 -0
- package/src/__tests__/commit-guarantee.test.ts +335 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-session-compaction.test.ts +132 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +293 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +117 -0
- package/src/__tests__/computer-use-skill-baseline.test.ts +74 -0
- package/src/__tests__/computer-use-skill-endstate.test.ts +89 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +217 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +107 -0
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +54 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +1462 -0
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +332 -0
- package/src/__tests__/connection-policy.test.ts +102 -0
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/context-memory-e2e.test.ts +434 -0
- package/src/__tests__/context-token-estimator.test.ts +135 -0
- package/src/__tests__/context-window-manager.test.ts +376 -0
- package/src/__tests__/contradiction-checker.test.ts +314 -0
- package/src/__tests__/conversation-store.test.ts +612 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +517 -0
- package/src/__tests__/credential-broker-server-use.test.ts +554 -0
- package/src/__tests__/credential-broker.test.ts +167 -0
- package/src/__tests__/credential-host-pattern-match.test.ts +104 -0
- package/src/__tests__/credential-metadata-store.test.ts +779 -0
- package/src/__tests__/credential-policy-validate.test.ts +121 -0
- package/src/__tests__/credential-resolve.test.ts +328 -0
- package/src/__tests__/credential-security-e2e.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +583 -0
- package/src/__tests__/credential-selection.test.ts +354 -0
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/credential-vault.test.ts +852 -0
- package/src/__tests__/daemon-assistant-events.test.ts +164 -0
- package/src/__tests__/daemon-server-session-init.test.ts +522 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +97 -0
- package/src/__tests__/diff.test.ts +121 -0
- package/src/__tests__/domain-normalize.test.ts +112 -0
- package/src/__tests__/domain-policy.test.ts +124 -0
- package/src/__tests__/doordash-client.test.ts +186 -0
- package/src/__tests__/doordash-session.test.ts +152 -0
- package/src/__tests__/dynamic-page-surface.test.ts +91 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +132 -0
- package/src/__tests__/edit-engine.test.ts +180 -0
- package/src/__tests__/elevenlabs-client.test.ts +271 -0
- package/src/__tests__/email-cli.test.ts +283 -0
- package/src/__tests__/encrypted-store.test.ts +332 -0
- package/src/__tests__/entity-extractor.test.ts +190 -0
- package/src/__tests__/ephemeral-permissions.test.ts +362 -0
- package/src/__tests__/evaluate-typescript-tool.test.ts +286 -0
- package/src/__tests__/event-bus.test.ts +222 -0
- package/src/__tests__/file-edit-tool.test.ts +122 -0
- package/src/__tests__/file-ops-service.test.ts +330 -0
- package/src/__tests__/file-read-tool.test.ts +75 -0
- package/src/__tests__/file-write-tool.test.ts +113 -0
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/fixtures/credential-security-fixtures.ts +181 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +126 -0
- package/src/__tests__/fixtures/mock-signup-server.ts +387 -0
- package/src/__tests__/fixtures/proxy-fixtures.ts +147 -0
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +71 -0
- package/src/__tests__/fuzzy-match-property.test.ts +216 -0
- package/src/__tests__/fuzzy-match.test.ts +138 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +631 -0
- package/src/__tests__/gemini-image-service.test.ts +261 -0
- package/src/__tests__/gemini-provider.test.ts +651 -0
- package/src/__tests__/get-weather.test.ts +318 -0
- package/src/__tests__/gmail-integration.test.ts +73 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +352 -0
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +191 -0
- package/src/__tests__/handlers-slack-config.test.ts +200 -0
- package/src/__tests__/handlers-task-submit-slash.test.ts +38 -0
- package/src/__tests__/handlers-telegram-config.test.ts +968 -0
- package/src/__tests__/handlers-twilio-config.test.ts +659 -0
- package/src/__tests__/handlers-twitter-config.test.ts +858 -0
- package/src/__tests__/headless-browser-interactions.test.ts +536 -0
- package/src/__tests__/headless-browser-navigate.test.ts +211 -0
- package/src/__tests__/headless-browser-read-tools.test.ts +261 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +185 -0
- package/src/__tests__/history-repair-observability.test.ts +56 -0
- package/src/__tests__/history-repair.test.ts +510 -0
- package/src/__tests__/home-base-bootstrap.test.ts +82 -0
- package/src/__tests__/hooks-blocking.test.ts +128 -0
- package/src/__tests__/hooks-cli.test.ts +144 -0
- package/src/__tests__/hooks-config.test.ts +93 -0
- package/src/__tests__/hooks-discovery.test.ts +199 -0
- package/src/__tests__/hooks-integration.test.ts +189 -0
- package/src/__tests__/hooks-manager.test.ts +187 -0
- package/src/__tests__/hooks-runner.test.ts +182 -0
- package/src/__tests__/hooks-settings.test.ts +154 -0
- package/src/__tests__/hooks-templates.test.ts +137 -0
- package/src/__tests__/hooks-ts-runner.test.ts +125 -0
- package/src/__tests__/hooks-watch.test.ts +100 -0
- package/src/__tests__/host-file-edit-tool.test.ts +228 -0
- package/src/__tests__/host-file-read-tool.test.ts +123 -0
- package/src/__tests__/host-file-write-tool.test.ts +136 -0
- package/src/__tests__/host-shell-tool.test.ts +562 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ingress-url-consistency.test.ts +214 -0
- package/src/__tests__/intent-routing.test.ts +259 -0
- package/src/__tests__/ipc-blob-store.test.ts +315 -0
- package/src/__tests__/ipc-contract-inventory.test.ts +54 -0
- package/src/__tests__/ipc-contract.test.ts +74 -0
- package/src/__tests__/ipc-protocol.test.ts +113 -0
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +1769 -0
- package/src/__tests__/ipc-validate.test.ts +407 -0
- package/src/__tests__/key-migration.test.ts +206 -0
- package/src/__tests__/keychain.test.ts +258 -0
- package/src/__tests__/llm-usage-store.test.ts +221 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +257 -0
- package/src/__tests__/managed-store.test.ts +608 -0
- package/src/__tests__/media-generate-image.test.ts +238 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +676 -0
- package/src/__tests__/media-visibility-policy.test.ts +141 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +235 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +481 -0
- package/src/__tests__/memory-query-builder.test.ts +59 -0
- package/src/__tests__/memory-recall-quality.test.ts +846 -0
- package/src/__tests__/memory-regressions.experimental.test.ts +538 -0
- package/src/__tests__/memory-regressions.test.ts +4435 -0
- package/src/__tests__/memory-retrieval-budget.test.ts +49 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/migration-cli-flows.test.ts +169 -0
- package/src/__tests__/migration-ordering.test.ts +249 -0
- package/src/__tests__/mock-signup-server.test.ts +528 -0
- package/src/__tests__/oauth-callback-registry.test.ts +92 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +285 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +176 -0
- package/src/__tests__/onboarding-template-contract.test.ts +58 -0
- package/src/__tests__/openai-provider.test.ts +753 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/parser.test.ts +472 -0
- package/src/__tests__/path-classifier.test.ts +73 -0
- package/src/__tests__/path-policy.test.ts +435 -0
- package/src/__tests__/platform-move-helper.test.ts +99 -0
- package/src/__tests__/platform-socket-path.test.ts +52 -0
- package/src/__tests__/platform-workspace-migration.test.ts +1000 -0
- package/src/__tests__/platform.test.ts +131 -0
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +340 -0
- package/src/__tests__/prebuilt-home-base-seed.test.ts +75 -0
- package/src/__tests__/pricing.test.ts +256 -0
- package/src/__tests__/profile-compiler.test.ts +374 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +342 -0
- package/src/__tests__/provider-registry-ollama.test.ts +16 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/proxy-approval-callback.test.ts +601 -0
- package/src/__tests__/public-ingress-urls.test.ts +256 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/ratelimit.test.ts +297 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +175 -0
- package/src/__tests__/recurrence-engine.test.ts +78 -0
- package/src/__tests__/recurrence-types.test.ts +79 -0
- package/src/__tests__/registry.test.ts +494 -0
- package/src/__tests__/relay-server.test.ts +688 -0
- package/src/__tests__/reminder-store.test.ts +223 -0
- package/src/__tests__/reminder.test.ts +229 -0
- package/src/__tests__/request-file-tool.test.ts +158 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +227 -0
- package/src/__tests__/run-orchestrator.test.ts +425 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +189 -0
- package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
- package/src/__tests__/runtime-events-sse.test.ts +162 -0
- package/src/__tests__/runtime-runs-http.test.ts +438 -0
- package/src/__tests__/runtime-runs.test.ts +260 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +408 -0
- package/src/__tests__/sandbox-host-parity.test.ts +950 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +253 -0
- package/src/__tests__/schedule-store.test.ts +484 -0
- package/src/__tests__/schedule-tools.test.ts +783 -0
- package/src/__tests__/scheduler-recurrence.test.ts +430 -0
- package/src/__tests__/script-proxy-certs.test.ts +90 -0
- package/src/__tests__/script-proxy-connect-tunnel.test.ts +177 -0
- package/src/__tests__/script-proxy-decision-trace.test.ts +156 -0
- package/src/__tests__/script-proxy-http-forwarder.test.ts +281 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +401 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +407 -0
- package/src/__tests__/script-proxy-policy-runtime.test.ts +287 -0
- package/src/__tests__/script-proxy-policy.test.ts +310 -0
- package/src/__tests__/script-proxy-rewrite-specificity.test.ts +135 -0
- package/src/__tests__/script-proxy-router.test.ts +180 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +382 -0
- package/src/__tests__/script-proxy-session-runtime.test.ts +113 -0
- package/src/__tests__/secret-allowlist.test.ts +230 -0
- package/src/__tests__/secret-ingress-handler.test.ts +110 -0
- package/src/__tests__/secret-onetime-send.test.ts +130 -0
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +106 -0
- package/src/__tests__/secret-response-routing.test.ts +93 -0
- package/src/__tests__/secret-scanner-executor.test.ts +348 -0
- package/src/__tests__/secret-scanner.test.ts +900 -0
- package/src/__tests__/secure-keys.test.ts +323 -0
- package/src/__tests__/server-history-render.test.ts +431 -0
- package/src/__tests__/session-abort-tool-results.test.ts +240 -0
- package/src/__tests__/session-conflict-gate.test.ts +1136 -0
- package/src/__tests__/session-error.test.ts +369 -0
- package/src/__tests__/session-evictor.test.ts +188 -0
- package/src/__tests__/session-init.benchmark.test.ts +465 -0
- package/src/__tests__/session-load-history-repair.test.ts +222 -0
- package/src/__tests__/session-pre-run-repair.test.ts +213 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-profile-injection.test.ts +444 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +306 -0
- package/src/__tests__/session-queue.test.ts +1535 -0
- package/src/__tests__/session-runtime-assembly.test.ts +476 -0
- package/src/__tests__/session-runtime-workspace.test.ts +183 -0
- package/src/__tests__/session-skill-tools.test.ts +2431 -0
- package/src/__tests__/session-slash-known.test.ts +368 -0
- package/src/__tests__/session-slash-queue.test.ts +288 -0
- package/src/__tests__/session-slash-unknown.test.ts +271 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +473 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +140 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +140 -0
- package/src/__tests__/session-undo.test.ts +75 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +246 -0
- package/src/__tests__/session-workspace-injection.test.ts +327 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +240 -0
- package/src/__tests__/shared-filesystem-errors.test.ts +78 -0
- package/src/__tests__/shell-credential-ref.test.ts +187 -0
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/shell-parser-fuzz.test.ts +544 -0
- package/src/__tests__/shell-parser-property.test.ts +433 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +272 -0
- package/src/__tests__/signup-e2e.test.ts +353 -0
- package/src/__tests__/size-guard.test.ts +117 -0
- package/src/__tests__/skill-include-graph.test.ts +303 -0
- package/src/__tests__/skill-load-tool.test.ts +409 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +338 -0
- package/src/__tests__/skill-script-runner-host.test.ts +489 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +349 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/skill-tool-factory.test.ts +252 -0
- package/src/__tests__/skill-tool-manifest.test.ts +658 -0
- package/src/__tests__/skill-version-hash.test.ts +182 -0
- package/src/__tests__/skills.test.ts +680 -0
- package/src/__tests__/slash-commands-catalog.test.ts +86 -0
- package/src/__tests__/slash-commands-parser.test.ts +119 -0
- package/src/__tests__/slash-commands-resolver.test.ts +193 -0
- package/src/__tests__/slash-commands-rewrite.test.ts +39 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/starter-bundle.test.ts +136 -0
- package/src/__tests__/starter-task-flow.test.ts +143 -0
- package/src/__tests__/subagent-manager-notify.test.ts +404 -0
- package/src/__tests__/subagent-tools.test.ts +801 -0
- package/src/__tests__/subagent-types.test.ts +78 -0
- package/src/__tests__/swarm-orchestrator.test.ts +428 -0
- package/src/__tests__/swarm-plan-validator.test.ts +330 -0
- package/src/__tests__/swarm-recursion.test.ts +165 -0
- package/src/__tests__/swarm-router-planner.test.ts +208 -0
- package/src/__tests__/swarm-session-integration.test.ts +274 -0
- package/src/__tests__/swarm-tool.test.ts +145 -0
- package/src/__tests__/swarm-worker-backend.test.ts +129 -0
- package/src/__tests__/swarm-worker-runner.test.ts +272 -0
- package/src/__tests__/system-prompt.test.ts +439 -0
- package/src/__tests__/task-compiler.test.ts +284 -0
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +216 -0
- package/src/__tests__/task-scheduler.test.ts +217 -0
- package/src/__tests__/task-tools.test.ts +595 -0
- package/src/__tests__/terminal-sandbox-docker.test.ts +1064 -0
- package/src/__tests__/terminal-sandbox.integration.test.ts +178 -0
- package/src/__tests__/terminal-sandbox.test.ts +202 -0
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +90 -0
- package/src/__tests__/test-support/computer-use-skill-harness.ts +45 -0
- package/src/__tests__/tool-audit-listener.test.ts +113 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +253 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +516 -0
- package/src/__tests__/tool-executor-redaction.test.ts +289 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +1989 -0
- package/src/__tests__/tool-metrics-listener.test.ts +225 -0
- package/src/__tests__/tool-notification-listener.test.ts +49 -0
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/tool-policy.test.ts +54 -0
- package/src/__tests__/tool-profiling-listener.test.ts +268 -0
- package/src/__tests__/tool-result-truncation.test.ts +217 -0
- package/src/__tests__/tool-trace-listener.test.ts +226 -0
- package/src/__tests__/top-level-renderer.test.ts +121 -0
- package/src/__tests__/top-level-scanner.test.ts +141 -0
- package/src/__tests__/trace-emitter.test.ts +173 -0
- package/src/__tests__/trust-store.test.ts +1605 -0
- package/src/__tests__/turn-commit.test.ts +554 -0
- package/src/__tests__/twilio-provider.test.ts +329 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
- package/src/__tests__/twilio-routes.test.ts +577 -0
- package/src/__tests__/twitter-auth-handler.test.ts +667 -0
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/url-safety.test.ts +418 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/weather-skill-regression.test.ts +225 -0
- package/src/__tests__/web-fetch.test.ts +869 -0
- package/src/__tests__/web-search.test.ts +584 -0
- package/src/__tests__/workspace-git-service.test.ts +1153 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +486 -0
- package/src/__tests__/workspace-lifecycle.test.ts +292 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/agent/attachments.ts +35 -0
- package/src/agent/loop.ts +500 -0
- package/src/agent/message-types.ts +17 -0
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/autonomy/autonomy-resolver.ts +60 -0
- package/src/autonomy/autonomy-store.ts +122 -0
- package/src/autonomy/disposition-mapper.ts +31 -0
- package/src/autonomy/index.ts +11 -0
- package/src/autonomy/types.ts +39 -0
- package/src/bundler/app-bundler.ts +295 -0
- package/src/bundler/bundle-scanner.ts +535 -0
- package/src/bundler/bundle-signer.ts +124 -0
- package/src/bundler/manifest.ts +21 -0
- package/src/bundler/signature-verifier.ts +184 -0
- package/src/calls/call-bridge.ts +168 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +430 -0
- package/src/calls/call-orchestrator.ts +498 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +87 -0
- package/src/calls/call-store.ts +422 -0
- package/src/calls/elevenlabs-client.ts +97 -0
- package/src/calls/elevenlabs-config.ts +31 -0
- package/src/calls/relay-server.ts +390 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +45 -0
- package/src/calls/twilio-provider.ts +263 -0
- package/src/calls/twilio-rest.ts +156 -0
- package/src/calls/twilio-routes.ts +311 -0
- package/src/calls/types.ts +39 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/calls/voice-quality.ts +114 -0
- package/src/cli/autonomy.ts +188 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/contacts.ts +149 -0
- package/src/cli/core-commands.ts +784 -0
- package/src/cli/doordash.ts +1055 -0
- package/src/cli/email-guardrails.ts +200 -0
- package/src/cli/email.ts +405 -0
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/main-screen.tsx +53 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +754 -0
- package/src/cli.ts +918 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/.gitkeep +0 -0
- package/src/config/bundled-skills/agentmail/SKILL.md +128 -0
- package/src/config/bundled-skills/agentmail/icon.svg +21 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1404 -0
- package/src/config/bundled-skills/app-builder/TOOLS.json +279 -0
- package/src/config/bundled-skills/app-builder/icon.svg +9 -0
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +15 -0
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +11 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +18 -0
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +11 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-query.ts +10 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +20 -0
- package/src/config/bundled-skills/browser/SKILL.md +28 -0
- package/src/config/bundled-skills/browser/TOOLS.json +234 -0
- package/src/config/bundled-skills/browser/tools/browser-click.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-close.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-type.ts +9 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +9 -0
- package/src/config/bundled-skills/claude-code/SKILL.md +50 -0
- package/src/config/bundled-skills/claude-code/TOOLS.json +40 -0
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +9 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +17 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +326 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +9 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +9 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +57 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +60 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +66 -0
- package/src/config/bundled-skills/document/SKILL.md +26 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +163 -0
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/icon.svg +24 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +51 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +108 -0
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +165 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +21 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +42 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +13 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +30 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +41 -0
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +18 -0
- package/src/config/bundled-skills/google-calendar/types.ts +97 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +32 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +42 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +115 -0
- package/src/config/bundled-skills/macos-automation/SKILL.md +66 -0
- package/src/config/bundled-skills/messaging/SKILL.md +153 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +357 -0
- package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +26 -0
- package/src/config/bundled-skills/messaging/tools/gmail-label.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +23 -0
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +84 -0
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +125 -0
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +16 -0
- package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +49 -0
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +21 -0
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +28 -0
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +32 -0
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +22 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +31 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +76 -0
- package/src/config/bundled-skills/messaging/tools/slack-add-reaction.ts +25 -0
- package/src/config/bundled-skills/messaging/tools/slack-leave-channel.ts +23 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +533 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +98 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +54 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +76 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +113 -0
- package/src/config/bundled-skills/public-ingress/SKILL.md +200 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/self-upgrade/SKILL.md +68 -0
- package/src/config/bundled-skills/start-the-day/SKILL.md +70 -0
- package/src/config/bundled-skills/start-the-day/icon.svg +13 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +281 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
- package/src/config/bundled-skills/twitter/SKILL.md +220 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/bundled-skills/weather/SKILL.md +37 -0
- package/src/config/bundled-skills/weather/TOOLS.json +32 -0
- package/src/config/bundled-skills/weather/icon.svg +24 -0
- package/src/config/bundled-skills/weather/tools/get-weather.ts +9 -0
- package/src/config/computer-use-prompt.ts +97 -0
- package/src/config/defaults.ts +263 -0
- package/src/config/loader.ts +339 -0
- package/src/config/schema.ts +1436 -0
- package/src/config/skill-state.ts +95 -0
- package/src/config/skills.ts +972 -0
- package/src/config/system-prompt.ts +675 -0
- package/src/config/templates/BOOTSTRAP.md +70 -0
- package/src/config/templates/IDENTITY.md +25 -0
- package/src/config/templates/LOOKS.md +25 -0
- package/src/config/templates/SOUL.md +37 -0
- package/src/config/templates/USER.md +19 -0
- package/src/config/types.ts +42 -0
- package/src/config/vellum-skills/chatgpt-import/SKILL.md +24 -0
- package/src/config/vellum-skills/chatgpt-import/TOOLS.json +23 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +284 -0
- package/src/config/vellum-skills/deploy-fullstack-vercel/SKILL.md +179 -0
- package/src/config/vellum-skills/document-writer/SKILL.md +195 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +199 -0
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +153 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +143 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +213 -0
- package/src/contacts/contact-store.ts +410 -0
- package/src/contacts/index.ts +11 -0
- package/src/contacts/types.ts +28 -0
- package/src/context/token-estimator.ts +108 -0
- package/src/context/tool-result-truncation.ts +128 -0
- package/src/context/window-manager.ts +531 -0
- package/src/daemon/assistant-attachments.ts +691 -0
- package/src/daemon/classifier.ts +110 -0
- package/src/daemon/computer-use-session.ts +903 -0
- package/src/daemon/connection-policy.ts +41 -0
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +530 -0
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +187 -0
- package/src/daemon/handlers/config.ts +1517 -0
- package/src/daemon/handlers/diagnostics.ts +338 -0
- package/src/daemon/handlers/documents.ts +173 -0
- package/src/daemon/handlers/home-base.ts +78 -0
- package/src/daemon/handlers/identity.ts +127 -0
- package/src/daemon/handlers/index.ts +129 -0
- package/src/daemon/handlers/misc.ts +331 -0
- package/src/daemon/handlers/open-bundle-handler.ts +80 -0
- package/src/daemon/handlers/publish.ts +187 -0
- package/src/daemon/handlers/sessions.ts +555 -0
- package/src/daemon/handlers/shared.ts +570 -0
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +486 -0
- package/src/daemon/handlers/subagents.ts +210 -0
- package/src/daemon/handlers/twitter-auth.ts +198 -0
- package/src/daemon/handlers/work-items.ts +632 -0
- package/src/daemon/handlers/workspace-files.ts +75 -0
- package/src/daemon/handlers.ts +17 -0
- package/src/daemon/history-repair.ts +214 -0
- package/src/daemon/ipc-blob-store.ts +231 -0
- package/src/daemon/ipc-contract-inventory.json +495 -0
- package/src/daemon/ipc-contract-inventory.ts +126 -0
- package/src/daemon/ipc-contract.ts +2551 -0
- package/src/daemon/ipc-protocol.ts +75 -0
- package/src/daemon/ipc-validate.ts +188 -0
- package/src/daemon/lifecycle.ts +582 -0
- package/src/daemon/main.ts +21 -0
- package/src/daemon/media-visibility-policy.ts +57 -0
- package/src/daemon/ride-shotgun-handler.ts +309 -0
- package/src/daemon/server.ts +1215 -0
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +196 -0
- package/src/daemon/session-conflict-gate.ts +184 -0
- package/src/daemon/session-dynamic-profile.ts +63 -0
- package/src/daemon/session-error.ts +290 -0
- package/src/daemon/session-evictor.ts +196 -0
- package/src/daemon/session-history.ts +437 -0
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-memory.ts +212 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +193 -0
- package/src/daemon/session-process.ts +323 -0
- package/src/daemon/session-queue-manager.ts +82 -0
- package/src/daemon/session-runtime-assembly.ts +447 -0
- package/src/daemon/session-skill-tools.ts +356 -0
- package/src/daemon/session-slash.ts +305 -0
- package/src/daemon/session-surfaces.ts +702 -0
- package/src/daemon/session-tool-setup.ts +523 -0
- package/src/daemon/session-usage.ts +72 -0
- package/src/daemon/session-workspace.ts +19 -0
- package/src/daemon/session.ts +400 -0
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/trace-emitter.ts +82 -0
- package/src/daemon/video-thumbnail.ts +62 -0
- package/src/daemon/watch-handler.ts +274 -0
- package/src/doordash/client.ts +999 -0
- package/src/doordash/queries.ts +1311 -0
- package/src/doordash/query-extractor.ts +93 -0
- package/src/doordash/session.ts +82 -0
- package/src/email/provider.ts +117 -0
- package/src/email/providers/agentmail.ts +317 -0
- package/src/email/providers/index.ts +58 -0
- package/src/email/service.ts +303 -0
- package/src/email/types.ts +126 -0
- package/src/events/bus.ts +157 -0
- package/src/events/domain-events.ts +83 -0
- package/src/events/index.ts +18 -0
- package/src/events/tool-audit-listener.ts +80 -0
- package/src/events/tool-domain-event-publisher.ts +111 -0
- package/src/events/tool-metrics-listener.ts +159 -0
- package/src/events/tool-notification-listener.ts +17 -0
- package/src/events/tool-profiling-listener.ts +158 -0
- package/src/events/tool-trace-listener.ts +75 -0
- package/src/export/formatter.ts +98 -0
- package/src/followups/followup-store.ts +168 -0
- package/src/followups/index.ts +10 -0
- package/src/followups/types.ts +29 -0
- package/src/gallery/default-gallery.ts +795 -0
- package/src/gallery/gallery-manifest.ts +24 -0
- package/src/home-base/app-link-store.ts +82 -0
- package/src/home-base/bootstrap.ts +68 -0
- package/src/home-base/prebuilt/index.html +662 -0
- package/src/home-base/prebuilt/seed-metadata.json +21 -0
- package/src/home-base/prebuilt/seed.ts +112 -0
- package/src/home-base/prebuilt-home-base-updater.ts +30 -0
- package/src/hooks/cli.ts +163 -0
- package/src/hooks/config.ts +88 -0
- package/src/hooks/discovery.ts +110 -0
- package/src/hooks/manager.ts +124 -0
- package/src/hooks/runner.ts +123 -0
- package/src/hooks/templates.ts +52 -0
- package/src/hooks/types.ts +72 -0
- package/src/inbound/public-ingress-urls.ts +123 -0
- package/src/index.ts +81 -0
- package/src/instrument.ts +60 -0
- package/src/logfire.ts +99 -0
- package/src/media/gemini-image-service.ts +136 -0
- package/src/memory/account-store.ts +108 -0
- package/src/memory/admin.ts +211 -0
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +577 -0
- package/src/memory/attachments-store.ts +397 -0
- package/src/memory/channel-delivery-store.ts +353 -0
- package/src/memory/channel-guardian-store.ts +669 -0
- package/src/memory/checkpoints.ts +52 -0
- package/src/memory/clarification-resolver.ts +298 -0
- package/src/memory/conflict-intent.ts +157 -0
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +350 -0
- package/src/memory/contradiction-checker.ts +358 -0
- package/src/memory/conversation-key-store.ts +122 -0
- package/src/memory/conversation-store.ts +470 -0
- package/src/memory/db.ts +1991 -0
- package/src/memory/embedding-backend.ts +229 -0
- package/src/memory/embedding-gemini.ts +52 -0
- package/src/memory/embedding-local.ts +65 -0
- package/src/memory/embedding-ollama.ts +55 -0
- package/src/memory/embedding-openai.ts +25 -0
- package/src/memory/entity-extractor.ts +474 -0
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/fingerprint.ts +20 -0
- package/src/memory/indexer.ts +156 -0
- package/src/memory/items-extractor.ts +461 -0
- package/src/memory/job-handlers/backfill.ts +139 -0
- package/src/memory/job-handlers/cleanup.ts +58 -0
- package/src/memory/job-handlers/conflict.ts +141 -0
- package/src/memory/job-handlers/embedding.ts +61 -0
- package/src/memory/job-handlers/extraction.ts +123 -0
- package/src/memory/job-handlers/index-maintenance.ts +54 -0
- package/src/memory/job-handlers/summarization.ts +286 -0
- package/src/memory/job-utils.ts +170 -0
- package/src/memory/jobs-store.ts +401 -0
- package/src/memory/jobs-worker.ts +313 -0
- package/src/memory/llm-request-log-store.ts +45 -0
- package/src/memory/llm-usage-store.ts +60 -0
- package/src/memory/message-content.ts +54 -0
- package/src/memory/profile-compiler.ts +160 -0
- package/src/memory/published-pages-store.ts +137 -0
- package/src/memory/qdrant-client.ts +366 -0
- package/src/memory/qdrant-manager.ts +242 -0
- package/src/memory/query-builder.ts +45 -0
- package/src/memory/retrieval-budget.ts +30 -0
- package/src/memory/retriever.ts +653 -0
- package/src/memory/runs-store.ts +305 -0
- package/src/memory/schema.ts +677 -0
- package/src/memory/search/entity.ts +298 -0
- package/src/memory/search/formatting.ts +207 -0
- package/src/memory/search/lexical.ts +227 -0
- package/src/memory/search/ranking.ts +401 -0
- package/src/memory/search/semantic.ts +121 -0
- package/src/memory/search/types.ts +137 -0
- package/src/memory/segmenter.ts +68 -0
- package/src/memory/shared-app-links-store.ts +138 -0
- package/src/memory/tool-usage-store.ts +62 -0
- package/src/messaging/activity-analyzer.ts +76 -0
- package/src/messaging/draft-store.ts +88 -0
- package/src/messaging/index.ts +3 -0
- package/src/messaging/provider-types.ts +80 -0
- package/src/messaging/provider.ts +52 -0
- package/src/messaging/providers/gmail/adapter.ts +193 -0
- package/src/messaging/providers/gmail/client.ts +204 -0
- package/src/messaging/providers/gmail/types.ts +90 -0
- package/src/messaging/providers/slack/adapter.ts +202 -0
- package/src/messaging/providers/slack/client.ts +198 -0
- package/src/messaging/providers/slack/types.ts +119 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +35 -0
- package/src/messaging/style-analyzer.ts +159 -0
- package/src/messaging/thread-summarizer.ts +306 -0
- package/src/messaging/triage-engine.ts +323 -0
- package/src/messaging/types.ts +55 -0
- package/src/permissions/checker.ts +640 -0
- package/src/permissions/defaults.ts +254 -0
- package/src/permissions/prompter.ts +98 -0
- package/src/permissions/secret-prompter.ts +114 -0
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +607 -0
- package/src/permissions/types.ts +43 -0
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/playbooks/index.ts +2 -0
- package/src/playbooks/playbook-compiler.ts +90 -0
- package/src/playbooks/types.ts +55 -0
- package/src/providers/anthropic/client.ts +751 -0
- package/src/providers/failover.ts +129 -0
- package/src/providers/fireworks/client.ts +20 -0
- package/src/providers/gemini/client.ts +285 -0
- package/src/providers/ollama/client.ts +30 -0
- package/src/providers/openai/client.ts +337 -0
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/ratelimit.ts +93 -0
- package/src/providers/registry.ts +146 -0
- package/src/providers/retry.ts +81 -0
- package/src/providers/stream-timeout.ts +38 -0
- package/src/providers/types.ts +109 -0
- package/src/runtime/assistant-event-hub.ts +157 -0
- package/src/runtime/assistant-event.ts +82 -0
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +73 -0
- package/src/runtime/channel-approvals.ts +206 -0
- package/src/runtime/channel-guardian-service.ts +212 -0
- package/src/runtime/gateway-client.ts +58 -0
- package/src/runtime/http-server.ts +1076 -0
- package/src/runtime/http-types.ts +66 -0
- package/src/runtime/routes/app-routes.ts +174 -0
- package/src/runtime/routes/attachment-routes.ts +133 -0
- package/src/runtime/routes/call-routes.ts +190 -0
- package/src/runtime/routes/channel-routes.ts +1404 -0
- package/src/runtime/routes/conversation-routes.ts +352 -0
- package/src/runtime/routes/events-routes.ts +148 -0
- package/src/runtime/routes/run-routes.ts +257 -0
- package/src/runtime/routes/secret-routes.ts +76 -0
- package/src/runtime/run-orchestrator.ts +330 -0
- package/src/schedule/recurrence-engine.ts +162 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +506 -0
- package/src/schedule/scheduler.ts +171 -0
- package/src/security/encrypted-store.ts +238 -0
- package/src/security/keychain.ts +252 -0
- package/src/security/oauth-callback-registry.ts +66 -0
- package/src/security/oauth2.ts +274 -0
- package/src/security/redaction.ts +89 -0
- package/src/security/secret-allowlist.ts +164 -0
- package/src/security/secret-ingress.ts +57 -0
- package/src/security/secret-scanner.ts +550 -0
- package/src/security/secure-keys.ts +180 -0
- package/src/security/token-manager.ts +141 -0
- package/src/services/published-app-updater.ts +69 -0
- package/src/services/vercel-deploy.ts +73 -0
- package/src/skills/active-skill-tools.ts +81 -0
- package/src/skills/clawhub.ts +414 -0
- package/src/skills/include-graph.ts +146 -0
- package/src/skills/managed-store.ts +233 -0
- package/src/skills/path-classifier.ts +128 -0
- package/src/skills/slash-commands.ts +174 -0
- package/src/skills/tool-manifest.ts +165 -0
- package/src/skills/version-hash.ts +110 -0
- package/src/slack/slack-webhook.ts +61 -0
- package/src/subagent/index.ts +19 -0
- package/src/subagent/manager.ts +511 -0
- package/src/subagent/types.ts +69 -0
- package/src/swarm/backend-claude-code.ts +145 -0
- package/src/swarm/index.ts +44 -0
- package/src/swarm/limits.ts +37 -0
- package/src/swarm/orchestrator.ts +279 -0
- package/src/swarm/plan-validator.ts +151 -0
- package/src/swarm/router-planner.ts +100 -0
- package/src/swarm/router-prompts.ts +36 -0
- package/src/swarm/synthesizer.ts +62 -0
- package/src/swarm/types.ts +62 -0
- package/src/swarm/worker-backend.ts +121 -0
- package/src/swarm/worker-prompts.ts +79 -0
- package/src/swarm/worker-runner.ts +164 -0
- package/src/tasks/SPEC.md +139 -0
- package/src/tasks/candidate-store.ts +86 -0
- package/src/tasks/ephemeral-permissions.ts +48 -0
- package/src/tasks/task-compiler.ts +199 -0
- package/src/tasks/task-runner.ts +90 -0
- package/src/tasks/task-scheduler.ts +21 -0
- package/src/tasks/task-store.ts +127 -0
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/apps/definitions.ts +59 -0
- package/src/tools/apps/executors.ts +313 -0
- package/src/tools/apps/open-proxy.ts +43 -0
- package/src/tools/apps/registry.ts +16 -0
- package/src/tools/assets/materialize.ts +218 -0
- package/src/tools/assets/search.ts +361 -0
- package/src/tools/browser/__tests__/auth-cache.test.ts +219 -0
- package/src/tools/browser/__tests__/auth-detector.test.ts +362 -0
- package/src/tools/browser/__tests__/jit-auth.test.ts +189 -0
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auth-cache.ts +149 -0
- package/src/tools/browser/auth-detector.ts +347 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +980 -0
- package/src/tools/browser/browser-handoff.ts +79 -0
- package/src/tools/browser/browser-manager.ts +715 -0
- package/src/tools/browser/browser-screencast.ts +217 -0
- package/src/tools/browser/headless-browser.ts +450 -0
- package/src/tools/browser/jit-auth.ts +51 -0
- package/src/tools/browser/network-recorder.ts +349 -0
- package/src/tools/browser/network-recording-types.ts +49 -0
- package/src/tools/browser/recording-store.ts +49 -0
- package/src/tools/browser/runtime-check.ts +43 -0
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +67 -0
- package/src/tools/calls/call-start.ts +81 -0
- package/src/tools/calls/call-status.ts +81 -0
- package/src/tools/claude-code/claude-code.ts +428 -0
- package/src/tools/computer-use/definitions.ts +443 -0
- package/src/tools/computer-use/registry.ts +22 -0
- package/src/tools/computer-use/request-computer-control.ts +53 -0
- package/src/tools/computer-use/skill-proxy-bridge.ts +28 -0
- package/src/tools/credentials/account-registry.ts +127 -0
- package/src/tools/credentials/broker-types.ts +107 -0
- package/src/tools/credentials/broker.ts +372 -0
- package/src/tools/credentials/domain-policy.ts +51 -0
- package/src/tools/credentials/host-pattern-match.ts +60 -0
- package/src/tools/credentials/metadata-store.ts +335 -0
- package/src/tools/credentials/policy-types.ts +52 -0
- package/src/tools/credentials/policy-validate.ts +80 -0
- package/src/tools/credentials/resolve.ts +122 -0
- package/src/tools/credentials/selection.ts +159 -0
- package/src/tools/credentials/tool-policy.ts +25 -0
- package/src/tools/credentials/vault.ts +657 -0
- package/src/tools/document/document-tool.ts +92 -0
- package/src/tools/document/editor-template.ts +237 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +815 -0
- package/src/tools/filesystem/edit.ts +127 -0
- package/src/tools/filesystem/fuzzy-match.ts +202 -0
- package/src/tools/filesystem/read.ts +71 -0
- package/src/tools/filesystem/view-image.ts +199 -0
- package/src/tools/filesystem/write.ts +79 -0
- package/src/tools/followups/followup_create.ts +76 -0
- package/src/tools/followups/followup_list.ts +60 -0
- package/src/tools/followups/followup_resolve.ts +56 -0
- package/src/tools/host-filesystem/edit.ts +125 -0
- package/src/tools/host-filesystem/read.ts +80 -0
- package/src/tools/host-filesystem/write.ts +76 -0
- package/src/tools/host-terminal/cli-discover.ts +180 -0
- package/src/tools/host-terminal/host-shell.ts +191 -0
- package/src/tools/memory/definitions.ts +69 -0
- package/src/tools/memory/handlers.ts +246 -0
- package/src/tools/memory/register.ts +66 -0
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/domain-normalize.ts +85 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/script-proxy/certs.ts +237 -0
- package/src/tools/network/script-proxy/connect-tunnel.ts +82 -0
- package/src/tools/network/script-proxy/http-forwarder.ts +151 -0
- package/src/tools/network/script-proxy/index.ts +28 -0
- package/src/tools/network/script-proxy/logging.ts +196 -0
- package/src/tools/network/script-proxy/mitm-handler.ts +269 -0
- package/src/tools/network/script-proxy/policy.ts +152 -0
- package/src/tools/network/script-proxy/router.ts +60 -0
- package/src/tools/network/script-proxy/server.ts +136 -0
- package/src/tools/network/script-proxy/session-manager.ts +534 -0
- package/src/tools/network/script-proxy/types.ts +125 -0
- package/src/tools/network/url-safety.ts +227 -0
- package/src/tools/network/web-fetch.ts +713 -0
- package/src/tools/network/web-search.ts +296 -0
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/registry.ts +295 -0
- package/src/tools/reminder/reminder-store.ts +148 -0
- package/src/tools/reminder/reminder.ts +80 -0
- package/src/tools/schedule/create.ts +81 -0
- package/src/tools/schedule/delete.ts +28 -0
- package/src/tools/schedule/list.ts +69 -0
- package/src/tools/schedule/update.ts +97 -0
- package/src/tools/shared/filesystem/edit-engine.ts +56 -0
- package/src/tools/shared/filesystem/errors.ts +85 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +215 -0
- package/src/tools/shared/filesystem/format-diff.ts +35 -0
- package/src/tools/shared/filesystem/path-policy.ts +125 -0
- package/src/tools/shared/filesystem/size-guard.ts +41 -0
- package/src/tools/shared/filesystem/types.ts +80 -0
- package/src/tools/shared/shell-output.ts +52 -0
- package/src/tools/skills/delete-managed.ts +60 -0
- package/src/tools/skills/load.ts +139 -0
- package/src/tools/skills/sandbox-runner.ts +279 -0
- package/src/tools/skills/scaffold-managed.ts +150 -0
- package/src/tools/skills/script-contract.ts +6 -0
- package/src/tools/skills/skill-script-runner.ts +86 -0
- package/src/tools/skills/skill-tool-factory.ts +64 -0
- package/src/tools/skills/vellum-catalog.ts +217 -0
- package/src/tools/subagent/abort.ts +33 -0
- package/src/tools/subagent/message.ts +39 -0
- package/src/tools/subagent/read.ts +67 -0
- package/src/tools/subagent/spawn.ts +46 -0
- package/src/tools/subagent/status.ts +45 -0
- package/src/tools/swarm/delegate.ts +183 -0
- package/src/tools/system/request-permission.ts +98 -0
- package/src/tools/system/version.ts +43 -0
- package/src/tools/tasks/index.ts +27 -0
- package/src/tools/tasks/task-delete.ts +82 -0
- package/src/tools/tasks/task-list.ts +44 -0
- package/src/tools/tasks/task-run.ts +97 -0
- package/src/tools/tasks/task-save.ts +47 -0
- package/src/tools/tasks/work-item-enqueue.ts +234 -0
- package/src/tools/tasks/work-item-list.ts +55 -0
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-run.ts +78 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/docker.ts +372 -0
- package/src/tools/terminal/backends/native.ts +190 -0
- package/src/tools/terminal/backends/types.ts +26 -0
- package/src/tools/terminal/evaluate-typescript.ts +275 -0
- package/src/tools/terminal/parser.ts +413 -0
- package/src/tools/terminal/safe-env.ts +37 -0
- package/src/tools/terminal/sandbox-diagnostics.ts +149 -0
- package/src/tools/terminal/sandbox.ts +44 -0
- package/src/tools/terminal/shell.ts +257 -0
- package/src/tools/tool-manifest.ts +198 -0
- package/src/tools/types.ts +176 -0
- package/src/tools/ui-surface/definitions.ts +244 -0
- package/src/tools/ui-surface/registry.ts +14 -0
- package/src/tools/watch/screen-watch.ts +130 -0
- package/src/tools/watch/watch-state.ts +119 -0
- package/src/tools/watcher/create.ts +64 -0
- package/src/tools/watcher/delete.ts +27 -0
- package/src/tools/watcher/digest.ts +50 -0
- package/src/tools/watcher/list.ts +60 -0
- package/src/tools/watcher/update.ts +56 -0
- package/src/tools/weather/service.ts +551 -0
- package/src/twitter/client.ts +690 -0
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/actors.ts +24 -0
- package/src/usage/types.ts +37 -0
- package/src/util/clipboard.ts +33 -0
- package/src/util/content-id.ts +16 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/diff.ts +181 -0
- package/src/util/errors.ts +129 -0
- package/src/util/logger.ts +243 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +632 -0
- package/src/util/pricing.ts +150 -0
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/spinner.ts +51 -0
- package/src/util/time.ts +16 -0
- package/src/util/truncate.ts +6 -0
- package/src/util/xml.ts +4 -0
- package/src/version.ts +3 -0
- package/src/watcher/constants.ts +11 -0
- package/src/watcher/engine.ts +199 -0
- package/src/watcher/provider-registry.ts +15 -0
- package/src/watcher/provider-types.ts +48 -0
- package/src/watcher/providers/gmail.ts +198 -0
- package/src/watcher/providers/google-calendar.ts +228 -0
- package/src/watcher/providers/slack.ts +129 -0
- package/src/watcher/watcher-store.ts +419 -0
- package/src/work-items/work-item-runner.ts +171 -0
- package/src/work-items/work-item-store.ts +325 -0
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +857 -0
- package/src/workspace/heartbeat-service.ts +345 -0
- package/src/workspace/provider-commit-message-generator.ts +285 -0
- package/src/workspace/top-level-renderer.ts +19 -0
- package/src/workspace/top-level-scanner.ts +41 -0
- package/src/workspace/turn-commit.ts +175 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,1605 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, mock, spyOn } from 'bun:test';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// Create a temp directory for the trust file
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), 'trust-store-test-'));
|
|
9
|
+
|
|
10
|
+
// Mock platform module so trust-store writes to temp dir instead of ~/.vellum
|
|
11
|
+
mock.module('../util/platform.js', () => ({
|
|
12
|
+
getRootDir: () => testDir,
|
|
13
|
+
getDataDir: () => testDir,
|
|
14
|
+
isMacOS: () => process.platform === 'darwin',
|
|
15
|
+
isLinux: () => process.platform === 'linux',
|
|
16
|
+
isWindows: () => process.platform === 'win32',
|
|
17
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
18
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
19
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
20
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
21
|
+
ensureDataDir: () => {},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Mock logger to suppress output during tests
|
|
25
|
+
mock.module('../util/logger.js', () => ({
|
|
26
|
+
getLogger: () => ({
|
|
27
|
+
info: () => {},
|
|
28
|
+
warn: () => {},
|
|
29
|
+
error: () => {},
|
|
30
|
+
debug: () => {},
|
|
31
|
+
trace: () => {},
|
|
32
|
+
fatal: () => {},
|
|
33
|
+
child: () => ({
|
|
34
|
+
info: () => {},
|
|
35
|
+
warn: () => {},
|
|
36
|
+
error: () => {},
|
|
37
|
+
debug: () => {},
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
import { addRule, removeRule, updateRule, findMatchingRule, findDenyRule, findHighestPriorityRule, getAllRules, clearAllRules, clearCache } from '../permissions/trust-store.js';
|
|
43
|
+
import { getDefaultRuleTemplates } from '../permissions/defaults.js';
|
|
44
|
+
|
|
45
|
+
const trustPath = join(testDir, 'protected', 'trust.json');
|
|
46
|
+
const DEFAULT_TEMPLATES = getDefaultRuleTemplates();
|
|
47
|
+
const NUM_DEFAULTS = DEFAULT_TEMPLATES.length;
|
|
48
|
+
const DEFAULT_PRIORITY_BY_ID = new Map(DEFAULT_TEMPLATES.map((t) => [t.id, t.priority]));
|
|
49
|
+
|
|
50
|
+
describe('Trust Store', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
// Clear cached rules and remove the trust file between tests
|
|
53
|
+
clearCache();
|
|
54
|
+
try { rmSync(trustPath); } catch { /* may not exist */ }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Intentionally do not remove `testDir` in afterAll.
|
|
58
|
+
// A late async log flush can still attempt to open `test.log` under this dir,
|
|
59
|
+
// which intermittently causes an unhandled ENOENT in CI if the dir is removed.
|
|
60
|
+
// ── addRule ─────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe('addRule', () => {
|
|
63
|
+
test('adds a rule and returns it', () => {
|
|
64
|
+
const rule = addRule('bash', 'git *', '/home/user/project');
|
|
65
|
+
expect(rule.id).toBeDefined();
|
|
66
|
+
expect(rule.tool).toBe('bash');
|
|
67
|
+
expect(rule.pattern).toBe('git *');
|
|
68
|
+
expect(rule.scope).toBe('/home/user/project');
|
|
69
|
+
expect(rule.decision).toBe('allow');
|
|
70
|
+
expect(rule.priority).toBe(100);
|
|
71
|
+
expect(rule.createdAt).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('assigns unique IDs to each rule', () => {
|
|
75
|
+
const rule1 = addRule('bash', 'npm *', '/tmp');
|
|
76
|
+
const rule2 = addRule('bash', 'bun *', '/tmp');
|
|
77
|
+
expect(rule1.id).not.toBe(rule2.id);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('persists rule to disk', () => {
|
|
81
|
+
addRule('bash', 'git push', '/home/user');
|
|
82
|
+
const raw = readFileSync(trustPath, 'utf-8');
|
|
83
|
+
const data = JSON.parse(raw);
|
|
84
|
+
expect(data.version).toBe(3);
|
|
85
|
+
expect(data.rules).toHaveLength(1 + NUM_DEFAULTS);
|
|
86
|
+
const userRule = data.rules.find((r: { pattern: string }) => r.pattern === 'git push');
|
|
87
|
+
expect(userRule).toBeDefined();
|
|
88
|
+
expect(userRule.priority).toBe(100);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('multiple rules accumulate', () => {
|
|
92
|
+
addRule('bash', 'git *', '/tmp');
|
|
93
|
+
addRule('file_write', '/tmp/*', '/tmp');
|
|
94
|
+
addRule('bash', 'npm *', '/tmp');
|
|
95
|
+
expect(getAllRules()).toHaveLength(3 + NUM_DEFAULTS);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('default priority is 100', () => {
|
|
99
|
+
const rule = addRule('bash', 'git *', '/tmp');
|
|
100
|
+
expect(rule.priority).toBe(100);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('custom priority is respected', () => {
|
|
104
|
+
const rule = addRule('bash', 'git *', '/tmp', 'allow', 5);
|
|
105
|
+
expect(rule.priority).toBe(5);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('rules are sorted by priority descending in getAllRules', () => {
|
|
109
|
+
addRule('bash', 'low *', '/tmp', 'allow', 0);
|
|
110
|
+
addRule('bash', 'high *', '/tmp', 'allow', 2);
|
|
111
|
+
addRule('bash', 'med *', '/tmp', 'allow', 1);
|
|
112
|
+
const rules = getAllRules();
|
|
113
|
+
// Default ask rules have higher priority than user rules
|
|
114
|
+
const maxDefaultPriority = Math.max(...DEFAULT_TEMPLATES.map((t) => t.priority));
|
|
115
|
+
expect(rules[0].priority).toBe(maxDefaultPriority);
|
|
116
|
+
const userRules = rules.filter((r) => !r.id.startsWith('default:'));
|
|
117
|
+
expect(userRules[0].priority).toBe(2);
|
|
118
|
+
expect(userRules[1].priority).toBe(1);
|
|
119
|
+
expect(userRules[2].priority).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('accepts allowHighRisk option and persists it', () => {
|
|
123
|
+
const rule = addRule('bash', 'sudo *', 'everywhere', 'allow', 100, { allowHighRisk: true });
|
|
124
|
+
expect(rule.allowHighRisk).toBe(true);
|
|
125
|
+
// Verify it persists to disk
|
|
126
|
+
clearCache();
|
|
127
|
+
const rules = getAllRules();
|
|
128
|
+
const found = rules.find((r) => r.id === rule.id);
|
|
129
|
+
expect(found).toBeDefined();
|
|
130
|
+
expect(found!.allowHighRisk).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('addRule without allowHighRisk option does not set the field', () => {
|
|
134
|
+
const rule = addRule('bash', 'git *', '/tmp');
|
|
135
|
+
expect(rule.allowHighRisk).toBeUndefined();
|
|
136
|
+
// Verify on disk
|
|
137
|
+
const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
138
|
+
const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
|
|
139
|
+
expect(diskRule).toBeDefined();
|
|
140
|
+
expect(diskRule).not.toHaveProperty('allowHighRisk');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('at same priority deny rules sort before allow rules', () => {
|
|
144
|
+
addRule('bash', 'allow *', '/tmp', 'allow', 100);
|
|
145
|
+
addRule('bash', 'deny *', '/tmp', 'deny', 100);
|
|
146
|
+
const userRules = getAllRules().filter((r) => !r.id.startsWith('default:'));
|
|
147
|
+
expect(userRules[0].decision).toBe('deny');
|
|
148
|
+
expect(userRules[1].decision).toBe('allow');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('accepts executionTarget option and persists it', () => {
|
|
152
|
+
const rule = addRule('skill_tool', 'skill_tool:*', '/tmp', 'allow', 100, {
|
|
153
|
+
executionTarget: 'sandbox',
|
|
154
|
+
});
|
|
155
|
+
expect(rule.executionTarget).toBe('sandbox');
|
|
156
|
+
|
|
157
|
+
// Verify persistence to disk
|
|
158
|
+
clearCache();
|
|
159
|
+
const rules = getAllRules();
|
|
160
|
+
const found = rules.find((r) => r.id === rule.id);
|
|
161
|
+
expect(found).toBeDefined();
|
|
162
|
+
expect(found!.executionTarget).toBe('sandbox');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('accepts all contextual options together (target, allowHighRisk)', () => {
|
|
166
|
+
const rule = addRule('risky_tool', 'risky_tool:*', 'everywhere', 'allow', 100, {
|
|
167
|
+
allowHighRisk: true,
|
|
168
|
+
executionTarget: 'host',
|
|
169
|
+
});
|
|
170
|
+
expect(rule.allowHighRisk).toBe(true);
|
|
171
|
+
expect(rule.executionTarget).toBe('host');
|
|
172
|
+
|
|
173
|
+
// Verify on disk
|
|
174
|
+
const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
175
|
+
const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
|
|
176
|
+
expect(diskRule).toBeDefined();
|
|
177
|
+
expect(diskRule.allowHighRisk).toBe(true);
|
|
178
|
+
expect(diskRule.executionTarget).toBe('host');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('addRule without options does not set optional fields', () => {
|
|
182
|
+
const rule = addRule('bash', 'echo *', '/tmp');
|
|
183
|
+
expect(rule.executionTarget).toBeUndefined();
|
|
184
|
+
|
|
185
|
+
// Verify on disk
|
|
186
|
+
const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
187
|
+
const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
|
|
188
|
+
expect(diskRule).toBeDefined();
|
|
189
|
+
expect(diskRule).not.toHaveProperty('executionTarget');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── removeRule ──────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
describe('removeRule', () => {
|
|
196
|
+
test('removes an existing rule', () => {
|
|
197
|
+
const rule = addRule('bash', 'git *', '/tmp');
|
|
198
|
+
expect(removeRule(rule.id)).toBe(true);
|
|
199
|
+
expect(getAllRules()).toHaveLength(NUM_DEFAULTS);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('returns false for non-existent ID', () => {
|
|
203
|
+
expect(removeRule('non-existent-id')).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('persists removal to disk', () => {
|
|
207
|
+
const rule = addRule('bash', 'npm *', '/tmp');
|
|
208
|
+
removeRule(rule.id);
|
|
209
|
+
// Reload from disk to verify
|
|
210
|
+
clearCache();
|
|
211
|
+
expect(getAllRules()).toHaveLength(NUM_DEFAULTS);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('only removes the targeted rule', () => {
|
|
215
|
+
const rule1 = addRule('bash', 'git *', '/tmp');
|
|
216
|
+
const rule2 = addRule('bash', 'npm *', '/tmp');
|
|
217
|
+
removeRule(rule1.id);
|
|
218
|
+
const remaining = getAllRules();
|
|
219
|
+
expect(remaining).toHaveLength(1 + NUM_DEFAULTS);
|
|
220
|
+
expect(remaining.find((r) => r.id === rule2.id)).toBeDefined();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── updateRule ─────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe('updateRule', () => {
|
|
227
|
+
test('updates pattern on an existing rule', () => {
|
|
228
|
+
const rule = addRule('bash', 'git *', '/tmp');
|
|
229
|
+
const updated = updateRule(rule.id, { pattern: 'git push *' });
|
|
230
|
+
expect(updated.pattern).toBe('git push *');
|
|
231
|
+
expect(updated.id).toBe(rule.id);
|
|
232
|
+
expect(updated.tool).toBe('bash');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('updates multiple fields at once', () => {
|
|
236
|
+
const rule = addRule('bash', 'npm *', '/tmp');
|
|
237
|
+
const updated = updateRule(rule.id, { tool: 'file_write', scope: '/home', decision: 'deny', priority: 50 });
|
|
238
|
+
expect(updated.tool).toBe('file_write');
|
|
239
|
+
expect(updated.scope).toBe('/home');
|
|
240
|
+
expect(updated.decision).toBe('deny');
|
|
241
|
+
expect(updated.priority).toBe(50);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('throws for non-existent rule ID', () => {
|
|
245
|
+
expect(() => updateRule('non-existent-id', { pattern: 'test' })).toThrow('Trust rule not found: non-existent-id');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('persists update to disk', () => {
|
|
249
|
+
const rule = addRule('bash', 'git *', '/tmp');
|
|
250
|
+
updateRule(rule.id, { pattern: 'git status' });
|
|
251
|
+
clearCache();
|
|
252
|
+
const rules = getAllRules();
|
|
253
|
+
const found = rules.find((r) => r.id === rule.id);
|
|
254
|
+
expect(found).toBeDefined();
|
|
255
|
+
expect(found!.pattern).toBe('git status');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('re-sorts rules after priority change', () => {
|
|
259
|
+
const rule1 = addRule('bash', 'low *', '/tmp', 'allow', 10);
|
|
260
|
+
const rule2 = addRule('bash', 'high *', '/tmp', 'allow', 200);
|
|
261
|
+
// rule2 should be first (higher priority)
|
|
262
|
+
let userRules = getAllRules().filter((r) => !r.id.startsWith('default:'));
|
|
263
|
+
expect(userRules[0].id).toBe(rule2.id);
|
|
264
|
+
// Update rule1 to have higher priority
|
|
265
|
+
updateRule(rule1.id, { priority: 300 });
|
|
266
|
+
userRules = getAllRules().filter((r) => !r.id.startsWith('default:'));
|
|
267
|
+
expect(userRules[0].id).toBe(rule1.id);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('leaves unchanged fields intact', () => {
|
|
271
|
+
const rule = addRule('bash', 'git *', '/home/user', 'allow', 100);
|
|
272
|
+
updateRule(rule.id, { pattern: 'git push *' });
|
|
273
|
+
const updated = getAllRules().find((r) => r.id === rule.id)!;
|
|
274
|
+
expect(updated.tool).toBe('bash');
|
|
275
|
+
expect(updated.scope).toBe('/home/user');
|
|
276
|
+
expect(updated.decision).toBe('allow');
|
|
277
|
+
expect(updated.priority).toBe(100);
|
|
278
|
+
expect(updated.createdAt).toBe(rule.createdAt);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ── findMatchingRule ────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe('findMatchingRule', () => {
|
|
285
|
+
test('finds exact match', () => {
|
|
286
|
+
addRule('bash', 'git push', '/tmp');
|
|
287
|
+
const match = findMatchingRule('bash', 'git push', '/tmp');
|
|
288
|
+
expect(match).not.toBeNull();
|
|
289
|
+
expect(match!.pattern).toBe('git push');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('finds glob wildcard match', () => {
|
|
293
|
+
addRule('bash', 'git *', '/tmp');
|
|
294
|
+
const match = findMatchingRule('bash', 'git push origin main', '/tmp');
|
|
295
|
+
expect(match).not.toBeNull();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('returns null when tool does not match', () => {
|
|
299
|
+
addRule('file_write', 'git *', '/tmp');
|
|
300
|
+
// host_bash default is 'ask' so findMatchingRule (allow-only) won't find it
|
|
301
|
+
const match = findMatchingRule('host_bash', 'git push', '/tmp');
|
|
302
|
+
expect(match).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('returns null when pattern does not match', () => {
|
|
306
|
+
addRule('host_bash', 'git *', '/tmp');
|
|
307
|
+
const match = findMatchingRule('host_bash', 'npm install', '/tmp');
|
|
308
|
+
expect(match).toBeNull();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Scope matching
|
|
312
|
+
describe('scope matching', () => {
|
|
313
|
+
test('matches when scope equals rule scope', () => {
|
|
314
|
+
addRule('bash', 'npm *', '/home/user/project');
|
|
315
|
+
const match = findMatchingRule('bash', 'npm install', '/home/user/project');
|
|
316
|
+
expect(match).not.toBeNull();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('matches when scope is under rule scope (prefix)', () => {
|
|
320
|
+
addRule('bash', 'npm *', '/home/user');
|
|
321
|
+
const match = findMatchingRule('bash', 'npm install', '/home/user/project/sub');
|
|
322
|
+
expect(match).not.toBeNull();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('does not match when scope is outside rule scope', () => {
|
|
326
|
+
addRule('host_bash', 'npm *', '/home/user/project');
|
|
327
|
+
const match = findMatchingRule('host_bash', 'npm install', '/home/other');
|
|
328
|
+
expect(match).toBeNull();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('everywhere scope matches any directory', () => {
|
|
332
|
+
addRule('bash', 'git *', 'everywhere');
|
|
333
|
+
const match = findMatchingRule('bash', 'git status', '/any/random/path');
|
|
334
|
+
expect(match).not.toBeNull();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('everywhere scope matches root', () => {
|
|
338
|
+
addRule('bash', 'ls', 'everywhere');
|
|
339
|
+
const match = findMatchingRule('bash', 'ls', '/');
|
|
340
|
+
expect(match).not.toBeNull();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('does not match sibling path with shared prefix', () => {
|
|
344
|
+
addRule('host_bash', 'npm *', '/home/user/project');
|
|
345
|
+
const match = findMatchingRule('host_bash', 'npm install', '/home/user/project-evil');
|
|
346
|
+
expect(match).toBeNull();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('matches exact scope with trailing slash on working dir', () => {
|
|
350
|
+
addRule('bash', 'npm *', '/home/user/project');
|
|
351
|
+
const match = findMatchingRule('bash', 'npm install', '/home/user/project/');
|
|
352
|
+
expect(match).not.toBeNull();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('matches when rule scope has trailing slash', () => {
|
|
356
|
+
addRule('bash', 'npm *', '/home/user/project/');
|
|
357
|
+
const match = findMatchingRule('bash', 'npm install', '/home/user/project');
|
|
358
|
+
expect(match).not.toBeNull();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('does not match sibling with glob-suffixed scope', () => {
|
|
362
|
+
addRule('host_bash', 'npm *', '/home/user/project*');
|
|
363
|
+
const match = findMatchingRule('host_bash', 'npm install', '/home/user/project-evil');
|
|
364
|
+
expect(match).toBeNull();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Pattern matching with minimatch
|
|
369
|
+
describe('pattern matching', () => {
|
|
370
|
+
test('matches * wildcard', () => {
|
|
371
|
+
addRule('bash', 'npm *', '/tmp');
|
|
372
|
+
expect(findMatchingRule('bash', 'npm install', '/tmp')).not.toBeNull();
|
|
373
|
+
expect(findMatchingRule('bash', 'npm test', '/tmp')).not.toBeNull();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('matches exact string', () => {
|
|
377
|
+
addRule('host_bash', 'git status', '/tmp');
|
|
378
|
+
expect(findMatchingRule('host_bash', 'git status', '/tmp')).not.toBeNull();
|
|
379
|
+
expect(findMatchingRule('host_bash', 'git push', '/tmp')).toBeNull();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('matches file path pattern', () => {
|
|
383
|
+
addRule('file_write', '/tmp/*', '/tmp');
|
|
384
|
+
expect(findMatchingRule('file_write', '/tmp/file.txt', '/tmp')).not.toBeNull();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('star pattern matches single-segment strings', () => {
|
|
388
|
+
addRule('file_write', '*', '/tmp');
|
|
389
|
+
// minimatch '*' matches strings without path separators
|
|
390
|
+
expect(findMatchingRule('file_write', 'file.txt', '/tmp')).not.toBeNull();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('star pattern does not match paths with slashes', () => {
|
|
394
|
+
addRule('file_write', '*', '/tmp');
|
|
395
|
+
// minimatch '*' does not cross '/' boundaries
|
|
396
|
+
expect(findMatchingRule('file_write', '/any/path/file.txt', '/tmp')).toBeNull();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ── findHighestPriorityRule ──────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe('findHighestPriorityRule', () => {
|
|
404
|
+
test('returns highest priority matching rule', () => {
|
|
405
|
+
addRule('bash', 'rm *', '/tmp', 'allow', 0);
|
|
406
|
+
addRule('bash', 'rm *', '/tmp', 'deny', 100);
|
|
407
|
+
const match = findHighestPriorityRule('bash', ['rm file.txt'], '/tmp');
|
|
408
|
+
expect(match).not.toBeNull();
|
|
409
|
+
expect(match!.decision).toBe('deny');
|
|
410
|
+
expect(match!.priority).toBe(100);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('higher priority allow beats lower priority deny', () => {
|
|
414
|
+
addRule('bash', 'rm *', '/tmp', 'deny', 0);
|
|
415
|
+
addRule('bash', 'rm *', '/tmp', 'allow', 100);
|
|
416
|
+
const match = findHighestPriorityRule('bash', ['rm file.txt'], '/tmp');
|
|
417
|
+
expect(match).not.toBeNull();
|
|
418
|
+
expect(match!.decision).toBe('allow');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('same priority: deny beats allow', () => {
|
|
422
|
+
addRule('bash', 'rm *', '/tmp', 'allow', 100);
|
|
423
|
+
addRule('bash', 'rm *', '/tmp', 'deny', 100);
|
|
424
|
+
const match = findHighestPriorityRule('bash', ['rm file.txt'], '/tmp');
|
|
425
|
+
expect(match).not.toBeNull();
|
|
426
|
+
expect(match!.decision).toBe('deny');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('checks multiple command candidates', () => {
|
|
430
|
+
addRule('web_fetch', 'web_fetch:https://example.com/*', '/tmp', 'allow');
|
|
431
|
+
const match = findHighestPriorityRule(
|
|
432
|
+
'web_fetch',
|
|
433
|
+
['web_fetch:https://example.com/page', 'web_fetch:https://example.com/*'],
|
|
434
|
+
'/tmp',
|
|
435
|
+
);
|
|
436
|
+
expect(match).not.toBeNull();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('returns null when no rule matches', () => {
|
|
440
|
+
// Use file_read with a non-workspace path — file_read defaults only
|
|
441
|
+
// cover specific workspace files, so /tmp paths won't match any default.
|
|
442
|
+
addRule('file_read', 'file_read:/specific/*', '/tmp', 'allow');
|
|
443
|
+
const match = findHighestPriorityRule('file_read', ['file_read:/other/path'], '/tmp');
|
|
444
|
+
expect(match).toBeNull();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('respects scope matching', () => {
|
|
448
|
+
// Use file_read — bash has a global default allow rule that matches everywhere.
|
|
449
|
+
addRule('file_read', 'file_read:/home/user/project/*', '/home/user/project', 'deny');
|
|
450
|
+
expect(findHighestPriorityRule('file_read', ['file_read:/home/user/project/file.txt'], '/home/user/project/sub')).not.toBeNull();
|
|
451
|
+
expect(findHighestPriorityRule('file_read', ['file_read:/home/user/project/file.txt'], '/home/other')).toBeNull();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('everywhere scope matches any directory', () => {
|
|
455
|
+
addRule('bash', 'git *', 'everywhere', 'allow');
|
|
456
|
+
const match = findHighestPriorityRule('bash', ['git status'], '/any/random/path');
|
|
457
|
+
expect(match).not.toBeNull();
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ── getAllRules ─────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
describe('getAllRules', () => {
|
|
464
|
+
test('returns default rules when no user rules exist', () => {
|
|
465
|
+
const rules = getAllRules();
|
|
466
|
+
expect(rules).toHaveLength(NUM_DEFAULTS);
|
|
467
|
+
expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('returns a copy (not the internal array)', () => {
|
|
471
|
+
addRule('bash', 'git *', '/tmp');
|
|
472
|
+
const rules1 = getAllRules();
|
|
473
|
+
const rules2 = getAllRules();
|
|
474
|
+
expect(rules1).toEqual(rules2);
|
|
475
|
+
expect(rules1).not.toBe(rules2); // different references
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ── clearCache ─────────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
describe('clearCache', () => {
|
|
482
|
+
test('forces reload from disk on next access', () => {
|
|
483
|
+
addRule('bash', 'git *', '/tmp');
|
|
484
|
+
expect(getAllRules()).toHaveLength(1 + NUM_DEFAULTS);
|
|
485
|
+
clearCache();
|
|
486
|
+
// After clearing cache, rules are reloaded from disk
|
|
487
|
+
expect(getAllRules()).toHaveLength(1 + NUM_DEFAULTS);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ── persistence ─────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
describe('persistence', () => {
|
|
494
|
+
test('rules survive cache clear (loaded from disk)', () => {
|
|
495
|
+
const rule = addRule('bash', 'npm *', '/tmp');
|
|
496
|
+
clearCache();
|
|
497
|
+
const rules = getAllRules();
|
|
498
|
+
expect(rules).toHaveLength(1 + NUM_DEFAULTS);
|
|
499
|
+
expect(rules.find((r) => r.id === rule.id)).toBeDefined();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('trust file has correct structure', () => {
|
|
503
|
+
addRule('bash', 'git *', '/tmp');
|
|
504
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
505
|
+
expect(data).toHaveProperty('version', 3);
|
|
506
|
+
expect(data).toHaveProperty('rules');
|
|
507
|
+
expect(Array.isArray(data.rules)).toBe(true);
|
|
508
|
+
const userRule = data.rules.find((r: { pattern: string }) => r.pattern === 'git *');
|
|
509
|
+
expect(userRule).toHaveProperty('priority', 100);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ── deny rules ─────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
describe('deny rules', () => {
|
|
516
|
+
test('addRule with deny decision creates a deny rule', () => {
|
|
517
|
+
const rule = addRule('bash', 'rm -rf *', '/tmp', 'deny');
|
|
518
|
+
expect(rule.decision).toBe('deny');
|
|
519
|
+
expect(rule.tool).toBe('bash');
|
|
520
|
+
expect(rule.pattern).toBe('rm -rf *');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('deny rule persists to disk', () => {
|
|
524
|
+
addRule('bash', 'rm *', '/tmp', 'deny');
|
|
525
|
+
clearCache();
|
|
526
|
+
const rules = getAllRules();
|
|
527
|
+
expect(rules).toHaveLength(1 + NUM_DEFAULTS);
|
|
528
|
+
const userRule = rules.find((r) => r.pattern === 'rm *');
|
|
529
|
+
expect(userRule).toBeDefined();
|
|
530
|
+
expect(userRule!.decision).toBe('deny');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('findDenyRule finds deny rules', () => {
|
|
534
|
+
addRule('bash', 'rm *', '/tmp', 'deny');
|
|
535
|
+
const match = findDenyRule('bash', 'rm file.txt', '/tmp');
|
|
536
|
+
expect(match).not.toBeNull();
|
|
537
|
+
expect(match!.decision).toBe('deny');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('findDenyRule ignores allow rules', () => {
|
|
541
|
+
addRule('bash', 'rm *', '/tmp', 'allow');
|
|
542
|
+
const match = findDenyRule('bash', 'rm file.txt', '/tmp');
|
|
543
|
+
expect(match).toBeNull();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('findMatchingRule ignores deny rules', () => {
|
|
547
|
+
// Use host_bash — bash has a default allow rule that would match.
|
|
548
|
+
addRule('host_bash', 'rm *', '/tmp', 'deny');
|
|
549
|
+
const match = findMatchingRule('host_bash', 'rm file.txt', '/tmp');
|
|
550
|
+
expect(match).toBeNull();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test('deny and allow rules coexist', () => {
|
|
554
|
+
addRule('bash', 'git *', '/tmp', 'allow');
|
|
555
|
+
addRule('bash', 'git push --force *', '/tmp', 'deny');
|
|
556
|
+
expect(findMatchingRule('bash', 'git status', '/tmp')).not.toBeNull();
|
|
557
|
+
expect(findDenyRule('bash', 'git push --force origin', '/tmp')).not.toBeNull();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('deny rule with scope matching', () => {
|
|
561
|
+
addRule('bash', 'rm *', '/home/user/project', 'deny');
|
|
562
|
+
expect(findDenyRule('bash', 'rm file.txt', '/home/user/project/sub')).not.toBeNull();
|
|
563
|
+
expect(findDenyRule('bash', 'rm file.txt', '/home/other')).toBeNull();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('deny rule with everywhere scope', () => {
|
|
567
|
+
addRule('bash', 'rm -rf *', 'everywhere', 'deny');
|
|
568
|
+
expect(findDenyRule('bash', 'rm -rf /', '/any/path')).not.toBeNull();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('removeRule works for deny rules', () => {
|
|
572
|
+
const rule = addRule('bash', 'rm *', '/tmp', 'deny');
|
|
573
|
+
expect(removeRule(rule.id)).toBe(true);
|
|
574
|
+
expect(findDenyRule('bash', 'rm file.txt', '/tmp')).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ── v1 migration ───────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
describe('v1 migration', () => {
|
|
581
|
+
test('v1 rules get priority 100 on load', () => {
|
|
582
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
583
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
584
|
+
version: 1,
|
|
585
|
+
rules: [{
|
|
586
|
+
id: 'test-v1-id',
|
|
587
|
+
tool: 'bash',
|
|
588
|
+
pattern: 'git *',
|
|
589
|
+
scope: '/tmp',
|
|
590
|
+
decision: 'allow',
|
|
591
|
+
createdAt: 1000,
|
|
592
|
+
}],
|
|
593
|
+
}));
|
|
594
|
+
clearCache();
|
|
595
|
+
const rules = getAllRules();
|
|
596
|
+
expect(rules).toHaveLength(1 + NUM_DEFAULTS);
|
|
597
|
+
const migratedRule = rules.find((r) => r.id === 'test-v1-id');
|
|
598
|
+
expect(migratedRule).toBeDefined();
|
|
599
|
+
expect(migratedRule!.priority).toBe(100);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('v1 file is upgraded to v3 on disk', () => {
|
|
603
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
604
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
605
|
+
version: 1,
|
|
606
|
+
rules: [{
|
|
607
|
+
id: 'migrate-me',
|
|
608
|
+
tool: 'bash',
|
|
609
|
+
pattern: 'npm *',
|
|
610
|
+
scope: 'everywhere',
|
|
611
|
+
decision: 'allow',
|
|
612
|
+
createdAt: 2000,
|
|
613
|
+
}],
|
|
614
|
+
}));
|
|
615
|
+
clearCache();
|
|
616
|
+
getAllRules(); // triggers load + migration
|
|
617
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
618
|
+
expect(data.version).toBe(3);
|
|
619
|
+
const migratedRule = data.rules.find((r: { id: string }) => r.id === 'migrate-me');
|
|
620
|
+
expect(migratedRule.priority).toBe(100);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ── loadFromDisk resilience ─────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
describe('loadFromDisk resilience', () => {
|
|
627
|
+
test('returns in-memory rules when saveToDisk fails during migration', () => {
|
|
628
|
+
// Write a v1 trust file that triggers needsSave on load
|
|
629
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
630
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
631
|
+
version: 1,
|
|
632
|
+
rules: [{
|
|
633
|
+
id: 'v1-readonly',
|
|
634
|
+
tool: 'bash',
|
|
635
|
+
pattern: 'git *',
|
|
636
|
+
scope: '/tmp',
|
|
637
|
+
decision: 'allow' as const,
|
|
638
|
+
createdAt: 1000,
|
|
639
|
+
}],
|
|
640
|
+
}));
|
|
641
|
+
|
|
642
|
+
// Spy on writeFileSync to throw when saveToDisk is called during migration.
|
|
643
|
+
// This is deterministic regardless of user privileges (unlike chmod 0o555).
|
|
644
|
+
const spy = spyOn(fs, 'writeFileSync').mockImplementation(() => {
|
|
645
|
+
throw new Error('Simulated write failure');
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
clearCache();
|
|
650
|
+
const rules = getAllRules();
|
|
651
|
+
// Should still return the migrated rules + defaults in-memory
|
|
652
|
+
expect(rules).toHaveLength(1 + NUM_DEFAULTS);
|
|
653
|
+
const migratedRule = rules.find((r) => r.id === 'v1-readonly');
|
|
654
|
+
expect(migratedRule).toBeDefined();
|
|
655
|
+
expect(migratedRule!.priority).toBe(100);
|
|
656
|
+
// Verify that saveToDisk was attempted (writeFileSync was called)
|
|
657
|
+
expect(spy).toHaveBeenCalled();
|
|
658
|
+
} finally {
|
|
659
|
+
spy.mockRestore();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// ── default rules ─────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
describe('default rules', () => {
|
|
667
|
+
test('backfills default rules on first load', () => {
|
|
668
|
+
const rules = getAllRules();
|
|
669
|
+
const defaults = rules.filter((r) => r.id.startsWith('default:'));
|
|
670
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
671
|
+
for (const rule of defaults) {
|
|
672
|
+
expect(rule.priority).toBe(DEFAULT_PRIORITY_BY_ID.get(rule.id)!);
|
|
673
|
+
if (rule.id === 'default:allow-bash-rm-bootstrap') {
|
|
674
|
+
expect(rule.scope).toBe(join(testDir, 'workspace'));
|
|
675
|
+
} else {
|
|
676
|
+
expect(rule.scope).toBe('everywhere');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test('default rules cover file, host file, host shell, and workspace prompt tools', () => {
|
|
683
|
+
const rules = getAllRules();
|
|
684
|
+
const defaultTools = [...new Set(
|
|
685
|
+
rules
|
|
686
|
+
.filter((r) => r.id.startsWith('default:'))
|
|
687
|
+
.map((r) => r.tool),
|
|
688
|
+
)].sort();
|
|
689
|
+
expect(defaultTools).toEqual([
|
|
690
|
+
'bash',
|
|
691
|
+
'browser_click',
|
|
692
|
+
'browser_close',
|
|
693
|
+
'browser_extract',
|
|
694
|
+
'browser_fill_credential',
|
|
695
|
+
'browser_navigate',
|
|
696
|
+
'browser_press_key',
|
|
697
|
+
'browser_screenshot',
|
|
698
|
+
'browser_snapshot',
|
|
699
|
+
'browser_type',
|
|
700
|
+
'browser_wait_for',
|
|
701
|
+
'computer_use_click',
|
|
702
|
+
'computer_use_double_click',
|
|
703
|
+
'computer_use_drag',
|
|
704
|
+
'computer_use_key',
|
|
705
|
+
'computer_use_open_app',
|
|
706
|
+
'computer_use_request_control',
|
|
707
|
+
'computer_use_right_click',
|
|
708
|
+
'computer_use_run_applescript',
|
|
709
|
+
'computer_use_scroll',
|
|
710
|
+
'computer_use_type_text',
|
|
711
|
+
'computer_use_wait',
|
|
712
|
+
'delete_managed_skill',
|
|
713
|
+
'file_edit',
|
|
714
|
+
'file_read',
|
|
715
|
+
'file_write',
|
|
716
|
+
'host_bash',
|
|
717
|
+
'host_file_edit',
|
|
718
|
+
'host_file_read',
|
|
719
|
+
'host_file_write',
|
|
720
|
+
'memory_search',
|
|
721
|
+
'scaffold_managed_skill',
|
|
722
|
+
'skill_load',
|
|
723
|
+
'ui_dismiss',
|
|
724
|
+
'ui_update',
|
|
725
|
+
'view_image',
|
|
726
|
+
]);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('default rules are not duplicated on reload', () => {
|
|
730
|
+
getAllRules(); // first load
|
|
731
|
+
clearCache();
|
|
732
|
+
const rules = getAllRules(); // second load
|
|
733
|
+
const defaults = rules.filter((r) => r.id.startsWith('default:'));
|
|
734
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test('default rules persist to disk', () => {
|
|
738
|
+
getAllRules(); // triggers backfill + save
|
|
739
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
740
|
+
const defaults = data.rules.filter((r: { id: string }) => r.id.startsWith('default:'));
|
|
741
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test('default rules are backfilled alongside v1 migration', () => {
|
|
745
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
746
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
747
|
+
version: 1,
|
|
748
|
+
rules: [{
|
|
749
|
+
id: 'v1-user-rule',
|
|
750
|
+
tool: 'bash',
|
|
751
|
+
pattern: 'git *',
|
|
752
|
+
scope: '/tmp',
|
|
753
|
+
decision: 'allow',
|
|
754
|
+
createdAt: 1000,
|
|
755
|
+
}],
|
|
756
|
+
}));
|
|
757
|
+
clearCache();
|
|
758
|
+
const rules = getAllRules();
|
|
759
|
+
expect(rules).toHaveLength(1 + NUM_DEFAULTS);
|
|
760
|
+
expect(rules.find((r) => r.id === 'v1-user-rule')!.priority).toBe(100);
|
|
761
|
+
const defaults = rules.filter((r) => r.id.startsWith('default:'));
|
|
762
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
763
|
+
expect(defaults.every((r) => r.priority === DEFAULT_PRIORITY_BY_ID.get(r.id))).toBe(true);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('removed default rule is re-backfilled on next load', () => {
|
|
767
|
+
// First load backfills defaults
|
|
768
|
+
getAllRules();
|
|
769
|
+
// Remove one default rule by editing trust.json directly on disk
|
|
770
|
+
// (removeRule() throws for default rules, so we simulate external editing)
|
|
771
|
+
const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
772
|
+
raw.rules = raw.rules.filter((r: { id: string }) => r.id !== 'default:ask-host_file_read-global');
|
|
773
|
+
writeFileSync(trustPath, JSON.stringify(raw, null, 2));
|
|
774
|
+
// After reload, the rule is re-backfilled (defaults are always present)
|
|
775
|
+
clearCache();
|
|
776
|
+
const rules = getAllRules();
|
|
777
|
+
expect(rules.find((r) => r.id === 'default:ask-host_file_read-global')).toBeDefined();
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test('findHighestPriorityRule matches default ask for host_file_read', () => {
|
|
781
|
+
const match = findHighestPriorityRule('host_file_read', ['host_file_read:/etc/hosts'], '/tmp');
|
|
782
|
+
expect(match).not.toBeNull();
|
|
783
|
+
expect(match!.id).toBe('default:ask-host_file_read-global');
|
|
784
|
+
expect(match!.decision).toBe('ask');
|
|
785
|
+
expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_read-global')!);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test('findHighestPriorityRule matches default ask for host_file_write', () => {
|
|
789
|
+
const match = findHighestPriorityRule('host_file_write', ['host_file_write:/etc/hosts'], '/tmp');
|
|
790
|
+
expect(match).not.toBeNull();
|
|
791
|
+
expect(match!.id).toBe('default:ask-host_file_write-global');
|
|
792
|
+
expect(match!.decision).toBe('ask');
|
|
793
|
+
expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_write-global')!);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test('findHighestPriorityRule matches default ask for host_file_edit', () => {
|
|
797
|
+
const match = findHighestPriorityRule('host_file_edit', ['host_file_edit:/etc/hosts'], '/tmp');
|
|
798
|
+
expect(match).not.toBeNull();
|
|
799
|
+
expect(match!.id).toBe('default:ask-host_file_edit-global');
|
|
800
|
+
expect(match!.decision).toBe('ask');
|
|
801
|
+
expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_edit-global')!);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test('findHighestPriorityRule matches default ask for host_bash', () => {
|
|
805
|
+
const match = findHighestPriorityRule('host_bash', ['ls'], '/tmp');
|
|
806
|
+
expect(match).not.toBeNull();
|
|
807
|
+
expect(match!.id).toBe('default:ask-host_bash-global');
|
|
808
|
+
expect(match!.decision).toBe('ask');
|
|
809
|
+
expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_bash-global')!);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test('findHighestPriorityRule matches default ask for computer_use_click', () => {
|
|
813
|
+
const match = findHighestPriorityRule('computer_use_click', ['computer_use_click:'], '/tmp');
|
|
814
|
+
expect(match).not.toBeNull();
|
|
815
|
+
expect(match!.id).toBe('default:ask-computer_use_click-global');
|
|
816
|
+
expect(match!.decision).toBe('ask');
|
|
817
|
+
expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-computer_use_click-global')!);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('findHighestPriorityRule matches default ask for computer_use_request_control', () => {
|
|
821
|
+
const match = findHighestPriorityRule('computer_use_request_control', ['computer_use_request_control:'], '/tmp');
|
|
822
|
+
expect(match).not.toBeNull();
|
|
823
|
+
expect(match!.id).toBe('default:ask-computer_use_request_control-global');
|
|
824
|
+
expect(match!.decision).toBe('ask');
|
|
825
|
+
expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-computer_use_request_control-global')!);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test('bootstrap delete rule matches only when workingDir is the workspace dir', () => {
|
|
829
|
+
const workspaceDir = join(testDir, 'workspace');
|
|
830
|
+
// Should match when workingDir is the workspace directory — the bootstrap
|
|
831
|
+
// rule (priority 100) outranks the global default allow (priority 50).
|
|
832
|
+
const match = findHighestPriorityRule('bash', ['rm BOOTSTRAP.md'], workspaceDir);
|
|
833
|
+
expect(match).not.toBeNull();
|
|
834
|
+
expect(match!.id).toBe('default:allow-bash-rm-bootstrap');
|
|
835
|
+
expect(match!.decision).toBe('allow');
|
|
836
|
+
// Outside workspace, the bootstrap rule doesn't match — the global
|
|
837
|
+
// default:allow-bash-global rule matches instead (not the bootstrap rule).
|
|
838
|
+
const other = findHighestPriorityRule('bash', ['rm BOOTSTRAP.md'], '/tmp/other-project');
|
|
839
|
+
expect(other).not.toBeNull();
|
|
840
|
+
expect(other!.id).not.toBe('default:allow-bash-rm-bootstrap');
|
|
841
|
+
expect(other!.id).toBe('default:allow-bash-global');
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test('default ask does not affect files outside protected directory', () => {
|
|
845
|
+
const safePath = join(testDir, 'data', 'assistant.db');
|
|
846
|
+
const match = findHighestPriorityRule('file_read', [`file_read:${safePath}`], '/tmp');
|
|
847
|
+
// Should not match a default deny rule
|
|
848
|
+
expect(match === null || !match.id.startsWith('default:')).toBe(true);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test('default rules are backfilled after malformed JSON in trust file', () => {
|
|
852
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
853
|
+
writeFileSync(trustPath, 'NOT VALID JSON {{{');
|
|
854
|
+
clearCache();
|
|
855
|
+
const rules = getAllRules();
|
|
856
|
+
const defaults = rules.filter((r) => r.id.startsWith('default:'));
|
|
857
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test('default rules are backfilled in-memory after unknown file version without overwriting disk', () => {
|
|
861
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
862
|
+
const originalContent = JSON.stringify({ version: 9999, rules: [{ id: 'future-rule', tool: 'bash', pattern: 'future *', scope: 'everywhere', decision: 'allow', priority: 50, createdAt: 1000 }] });
|
|
863
|
+
writeFileSync(trustPath, originalContent);
|
|
864
|
+
clearCache();
|
|
865
|
+
const rules = getAllRules();
|
|
866
|
+
// Defaults should be present in-memory
|
|
867
|
+
const defaults = rules.filter((r) => r.id.startsWith('default:'));
|
|
868
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
869
|
+
// The on-disk file must NOT be overwritten — it preserves the unknown format
|
|
870
|
+
const diskContent = readFileSync(trustPath, 'utf-8');
|
|
871
|
+
expect(diskContent).toBe(originalContent);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test('clearAllRules preserves default rules', () => {
|
|
875
|
+
addRule('bash', 'git *', '/tmp');
|
|
876
|
+
clearAllRules();
|
|
877
|
+
const rules = getAllRules();
|
|
878
|
+
// User rules should be gone, but defaults should remain
|
|
879
|
+
expect(rules.filter((r) => !r.id.startsWith('default:'))).toHaveLength(0);
|
|
880
|
+
const defaults = rules.filter((r) => r.id.startsWith('default:'));
|
|
881
|
+
expect(defaults).toHaveLength(NUM_DEFAULTS);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// ── skill source mutation rules ────────────────────────────────
|
|
885
|
+
|
|
886
|
+
test('default rules include ask rules for file_write on skill source paths', () => {
|
|
887
|
+
const rules = getAllRules();
|
|
888
|
+
const managed = rules.find((r) => r.id === 'default:ask-file_write-managed-skills');
|
|
889
|
+
expect(managed).toBeDefined();
|
|
890
|
+
expect(managed!.tool).toBe('file_write');
|
|
891
|
+
expect(managed!.decision).toBe('ask');
|
|
892
|
+
expect(managed!.priority).toBe(50);
|
|
893
|
+
expect(managed!.pattern).toContain('workspace/skills/**');
|
|
894
|
+
|
|
895
|
+
const bundled = rules.find((r) => r.id === 'default:ask-file_write-bundled-skills');
|
|
896
|
+
expect(bundled).toBeDefined();
|
|
897
|
+
expect(bundled!.tool).toBe('file_write');
|
|
898
|
+
expect(bundled!.decision).toBe('ask');
|
|
899
|
+
expect(bundled!.priority).toBe(50);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test('default rules include ask rules for file_edit on skill source paths', () => {
|
|
903
|
+
const rules = getAllRules();
|
|
904
|
+
const managed = rules.find((r) => r.id === 'default:ask-file_edit-managed-skills');
|
|
905
|
+
expect(managed).toBeDefined();
|
|
906
|
+
expect(managed!.tool).toBe('file_edit');
|
|
907
|
+
expect(managed!.decision).toBe('ask');
|
|
908
|
+
expect(managed!.priority).toBe(50);
|
|
909
|
+
expect(managed!.pattern).toContain('workspace/skills/**');
|
|
910
|
+
|
|
911
|
+
const bundled = rules.find((r) => r.id === 'default:ask-file_edit-bundled-skills');
|
|
912
|
+
expect(bundled).toBeDefined();
|
|
913
|
+
expect(bundled!.tool).toBe('file_edit');
|
|
914
|
+
expect(bundled!.decision).toBe('ask');
|
|
915
|
+
expect(bundled!.priority).toBe(50);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// ── default allow: skill_load ────────────────────────────────
|
|
919
|
+
|
|
920
|
+
test('skill_load default allow rule exists in templates', () => {
|
|
921
|
+
const templates = getDefaultRuleTemplates();
|
|
922
|
+
const skillLoadRule = templates.find(t => t.id === 'default:allow-skill_load-global');
|
|
923
|
+
expect(skillLoadRule).toBeDefined();
|
|
924
|
+
expect(skillLoadRule!.tool).toBe('skill_load');
|
|
925
|
+
expect(skillLoadRule!.pattern).toBe('skill_load:*');
|
|
926
|
+
expect(skillLoadRule!.decision).toBe('allow');
|
|
927
|
+
expect(skillLoadRule!.scope).toBe('everywhere');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test('findHighestPriorityRule matches default allow for skill_load', () => {
|
|
931
|
+
const match = findHighestPriorityRule('skill_load', ['skill_load:browser'], '/tmp');
|
|
932
|
+
expect(match).not.toBeNull();
|
|
933
|
+
expect(match!.id).toBe('default:allow-skill_load-global');
|
|
934
|
+
expect(match!.decision).toBe('allow');
|
|
935
|
+
expect(match!.priority).toBe(100);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test('findHighestPriorityRule matches default allow for skill_load with any skill name', () => {
|
|
939
|
+
const match = findHighestPriorityRule('skill_load', ['skill_load:some-random-skill'], '/tmp');
|
|
940
|
+
expect(match).not.toBeNull();
|
|
941
|
+
expect(match!.id).toBe('default:allow-skill_load-global');
|
|
942
|
+
expect(match!.decision).toBe('allow');
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// ── default allow: browser tools ────────────────────────────
|
|
946
|
+
|
|
947
|
+
test('all 10 browser tools have default allow rules', () => {
|
|
948
|
+
const templates = getDefaultRuleTemplates();
|
|
949
|
+
const browserTools = [
|
|
950
|
+
'browser_navigate', 'browser_snapshot', 'browser_screenshot', 'browser_close',
|
|
951
|
+
'browser_click', 'browser_type', 'browser_press_key', 'browser_wait_for',
|
|
952
|
+
'browser_extract', 'browser_fill_credential',
|
|
953
|
+
];
|
|
954
|
+
|
|
955
|
+
for (const tool of browserTools) {
|
|
956
|
+
const rule = templates.find(t => t.id === `default:allow-${tool}-global`);
|
|
957
|
+
expect(rule).toBeDefined();
|
|
958
|
+
expect(rule!.tool).toBe(tool);
|
|
959
|
+
// browser_navigate uses standalone "**" because its candidates
|
|
960
|
+
// contain URLs with "/" that single "*" cannot match.
|
|
961
|
+
const expectedPattern = tool === 'browser_navigate' ? '**' : `${tool}:*`;
|
|
962
|
+
expect(rule!.pattern).toBe(expectedPattern);
|
|
963
|
+
expect(rule!.decision).toBe('allow');
|
|
964
|
+
expect(rule!.scope).toBe('everywhere');
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test('browser tool default rules match via findHighestPriorityRule', () => {
|
|
969
|
+
// Use a candidate without slashes so the `browser_snapshot:*` pattern
|
|
970
|
+
// matches (minimatch `*` does not cross `/` boundaries).
|
|
971
|
+
const result = findHighestPriorityRule('browser_snapshot', ['browser_snapshot:'], '/tmp');
|
|
972
|
+
expect(result).toBeDefined();
|
|
973
|
+
expect(result!.decision).toBe('allow');
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
test('no default ask rules exist for file_read on skill source paths', () => {
|
|
977
|
+
const rules = getAllRules();
|
|
978
|
+
// There should be no default rules with IDs matching file_read for skill sources
|
|
979
|
+
const readManagedSkill = rules.find((r) => r.id === 'default:ask-file_read-managed-skills');
|
|
980
|
+
const readBundledSkill = rules.find((r) => r.id === 'default:ask-file_read-bundled-skills');
|
|
981
|
+
expect(readManagedSkill).toBeUndefined();
|
|
982
|
+
expect(readBundledSkill).toBeUndefined();
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test('findHighestPriorityRule matches default ask for file_write on managed skill path', () => {
|
|
986
|
+
const skillFile = join(testDir, 'workspace', 'skills', 'my-skill', 'SKILL.md');
|
|
987
|
+
const match = findHighestPriorityRule('file_write', [`file_write:${skillFile}`], '/tmp');
|
|
988
|
+
expect(match).not.toBeNull();
|
|
989
|
+
expect(match!.id).toBe('default:ask-file_write-managed-skills');
|
|
990
|
+
expect(match!.decision).toBe('ask');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
test('findHighestPriorityRule matches default ask for file_edit on managed skill path', () => {
|
|
994
|
+
const skillFile = join(testDir, 'workspace', 'skills', 'my-skill', 'tools.ts');
|
|
995
|
+
const match = findHighestPriorityRule('file_edit', [`file_edit:${skillFile}`], '/tmp');
|
|
996
|
+
expect(match).not.toBeNull();
|
|
997
|
+
expect(match!.id).toBe('default:ask-file_edit-managed-skills');
|
|
998
|
+
expect(match!.decision).toBe('ask');
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// ── trust rule schema v3 (PR 14) ──────────────────────────────
|
|
1003
|
+
|
|
1004
|
+
describe('trust rule schema v3 (PR 14)', () => {
|
|
1005
|
+
test('new rules can include v3 optional fields', () => {
|
|
1006
|
+
const rule = addRule('bash', 'git *', '/tmp');
|
|
1007
|
+
// Manually set v3 optional fields on the rule and persist
|
|
1008
|
+
rule.executionTarget = '/usr/local/bin/node';
|
|
1009
|
+
rule.allowHighRisk = true;
|
|
1010
|
+
// Re-persist the updated rules
|
|
1011
|
+
const rules = getAllRules().map((r) =>
|
|
1012
|
+
r.id === rule.id ? rule : r,
|
|
1013
|
+
);
|
|
1014
|
+
// Write directly to verify round-trip
|
|
1015
|
+
const trustData = { version: 3, rules };
|
|
1016
|
+
writeFileSync(trustPath, JSON.stringify(trustData, null, 2));
|
|
1017
|
+
clearCache();
|
|
1018
|
+
const reloaded = getAllRules();
|
|
1019
|
+
const found = reloaded.find((r) => r.id === rule.id);
|
|
1020
|
+
expect(found).toBeDefined();
|
|
1021
|
+
expect(found!.executionTarget).toBe('/usr/local/bin/node');
|
|
1022
|
+
expect(found!.allowHighRisk).toBe(true);
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test('v2 file is upgraded to v3 on disk', () => {
|
|
1026
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1027
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1028
|
+
version: 2,
|
|
1029
|
+
rules: [{
|
|
1030
|
+
id: 'v2-rule',
|
|
1031
|
+
tool: 'bash',
|
|
1032
|
+
pattern: 'npm *',
|
|
1033
|
+
scope: 'everywhere',
|
|
1034
|
+
decision: 'allow',
|
|
1035
|
+
priority: 100,
|
|
1036
|
+
createdAt: 3000,
|
|
1037
|
+
}],
|
|
1038
|
+
}));
|
|
1039
|
+
clearCache();
|
|
1040
|
+
getAllRules(); // triggers load + migration
|
|
1041
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
1042
|
+
expect(data.version).toBe(3);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test('v2 rules survive v3 migration with no v3-only fields', () => {
|
|
1046
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1047
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1048
|
+
version: 2,
|
|
1049
|
+
rules: [
|
|
1050
|
+
{
|
|
1051
|
+
id: 'user-v2-a',
|
|
1052
|
+
tool: 'bash',
|
|
1053
|
+
pattern: 'git *',
|
|
1054
|
+
scope: '/tmp',
|
|
1055
|
+
decision: 'allow',
|
|
1056
|
+
priority: 100,
|
|
1057
|
+
createdAt: 4000,
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
id: 'user-v2-b',
|
|
1061
|
+
tool: 'file_write',
|
|
1062
|
+
pattern: '/tmp/*',
|
|
1063
|
+
scope: '/tmp',
|
|
1064
|
+
decision: 'deny',
|
|
1065
|
+
priority: 50,
|
|
1066
|
+
createdAt: 4001,
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
}));
|
|
1070
|
+
clearCache();
|
|
1071
|
+
const rules = getAllRules();
|
|
1072
|
+
const ruleA = rules.find((r) => r.id === 'user-v2-a');
|
|
1073
|
+
const ruleB = rules.find((r) => r.id === 'user-v2-b');
|
|
1074
|
+
expect(ruleA).toBeDefined();
|
|
1075
|
+
expect(ruleB).toBeDefined();
|
|
1076
|
+
expect(ruleA!.pattern).toBe('git *');
|
|
1077
|
+
expect(ruleB!.decision).toBe('deny');
|
|
1078
|
+
// No v3-only fields should be present
|
|
1079
|
+
expect(ruleA).not.toHaveProperty('executionTarget');
|
|
1080
|
+
expect(ruleA).not.toHaveProperty('allowHighRisk');
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test('trust file persists with version 3', () => {
|
|
1084
|
+
addRule('bash', 'echo *', '/tmp');
|
|
1085
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
1086
|
+
expect(data.version).toBe(3);
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// ── v2 → v3 migration hardening (PR 15) ────────────────────────
|
|
1091
|
+
|
|
1092
|
+
describe('v2 → v3 migration hardening (PR 15)', () => {
|
|
1093
|
+
test('v2 rules with extra unknown fields survive migration cleanly', () => {
|
|
1094
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1095
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1096
|
+
version: 2,
|
|
1097
|
+
rules: [{
|
|
1098
|
+
id: 'v2-extra-fields',
|
|
1099
|
+
tool: 'bash',
|
|
1100
|
+
pattern: 'git *',
|
|
1101
|
+
scope: '/tmp',
|
|
1102
|
+
decision: 'allow',
|
|
1103
|
+
priority: 100,
|
|
1104
|
+
createdAt: 5000,
|
|
1105
|
+
customField: 'should-survive',
|
|
1106
|
+
nested: { deep: true },
|
|
1107
|
+
}],
|
|
1108
|
+
}));
|
|
1109
|
+
clearCache();
|
|
1110
|
+
const rules = getAllRules();
|
|
1111
|
+
const rule = rules.find((r) => r.id === 'v2-extra-fields');
|
|
1112
|
+
expect(rule).toBeDefined();
|
|
1113
|
+
expect(rule!.tool).toBe('bash');
|
|
1114
|
+
expect(rule!.pattern).toBe('git *');
|
|
1115
|
+
// Extra fields pass through because the migration does not strip them
|
|
1116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- asserting extra fields pass through migration
|
|
1117
|
+
expect((rule as any).customField).toBe('should-survive');
|
|
1118
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- asserting extra fields pass through migration
|
|
1119
|
+
expect((rule as any).nested).toEqual({ deep: true });
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
test('v2 file with empty rules array migrates correctly', () => {
|
|
1123
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1124
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1125
|
+
version: 2,
|
|
1126
|
+
rules: [],
|
|
1127
|
+
}));
|
|
1128
|
+
clearCache();
|
|
1129
|
+
const rules = getAllRules();
|
|
1130
|
+
// Should only have default rules, no user rules
|
|
1131
|
+
expect(rules).toHaveLength(NUM_DEFAULTS);
|
|
1132
|
+
expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
|
|
1133
|
+
// File should be upgraded to v3 on disk
|
|
1134
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
1135
|
+
expect(data.version).toBe(3);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test('v2 file with no rules field at all migrates correctly', () => {
|
|
1139
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1140
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1141
|
+
version: 2,
|
|
1142
|
+
}));
|
|
1143
|
+
clearCache();
|
|
1144
|
+
const rules = getAllRules();
|
|
1145
|
+
// rules defaults to [] so only defaults should appear
|
|
1146
|
+
expect(rules).toHaveLength(NUM_DEFAULTS);
|
|
1147
|
+
expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
|
|
1148
|
+
// File should be upgraded to v3 on disk
|
|
1149
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
1150
|
+
expect(data.version).toBe(3);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test('malformed v2 file (rules is a string instead of array) is handled gracefully', () => {
|
|
1154
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1155
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1156
|
+
version: 2,
|
|
1157
|
+
rules: 'not-an-array',
|
|
1158
|
+
}));
|
|
1159
|
+
clearCache();
|
|
1160
|
+
const rules = getAllRules();
|
|
1161
|
+
// Should fall back to empty rules and backfill defaults
|
|
1162
|
+
expect(rules).toHaveLength(NUM_DEFAULTS);
|
|
1163
|
+
expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test('malformed v2 file (rules is an object instead of array) is handled gracefully', () => {
|
|
1167
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1168
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1169
|
+
version: 2,
|
|
1170
|
+
rules: { notAnArray: true },
|
|
1171
|
+
}));
|
|
1172
|
+
clearCache();
|
|
1173
|
+
const rules = getAllRules();
|
|
1174
|
+
expect(rules).toHaveLength(NUM_DEFAULTS);
|
|
1175
|
+
expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
test('malformed file (valid JSON but null) is handled gracefully', () => {
|
|
1179
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1180
|
+
writeFileSync(trustPath, 'null');
|
|
1181
|
+
clearCache();
|
|
1182
|
+
const rules = getAllRules();
|
|
1183
|
+
// Accessing null.version throws TypeError, caught by try/catch,
|
|
1184
|
+
// falls through to backfill defaults
|
|
1185
|
+
expect(rules).toHaveLength(NUM_DEFAULTS);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
test('concurrent v2 → v3 migration (loading twice in sequence) is idempotent', () => {
|
|
1189
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1190
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1191
|
+
version: 2,
|
|
1192
|
+
rules: [{
|
|
1193
|
+
id: 'idempotent-rule',
|
|
1194
|
+
tool: 'bash',
|
|
1195
|
+
pattern: 'npm *',
|
|
1196
|
+
scope: 'everywhere',
|
|
1197
|
+
decision: 'allow',
|
|
1198
|
+
priority: 100,
|
|
1199
|
+
createdAt: 6000,
|
|
1200
|
+
}],
|
|
1201
|
+
}));
|
|
1202
|
+
// First load — triggers v2 → v3 migration
|
|
1203
|
+
clearCache();
|
|
1204
|
+
const rules1 = getAllRules();
|
|
1205
|
+
const rule1 = rules1.find((r) => r.id === 'idempotent-rule');
|
|
1206
|
+
expect(rule1).toBeDefined();
|
|
1207
|
+
expect(rule1!.pattern).toBe('npm *');
|
|
1208
|
+
|
|
1209
|
+
// Second load — should load the already-migrated v3 file without re-migrating
|
|
1210
|
+
clearCache();
|
|
1211
|
+
const rules2 = getAllRules();
|
|
1212
|
+
const rule2 = rules2.find((r) => r.id === 'idempotent-rule');
|
|
1213
|
+
expect(rule2).toBeDefined();
|
|
1214
|
+
expect(rule2!.pattern).toBe('npm *');
|
|
1215
|
+
expect(rule2!.priority).toBe(100);
|
|
1216
|
+
|
|
1217
|
+
// Verify file is still v3 and rule count is stable
|
|
1218
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
1219
|
+
expect(data.version).toBe(3);
|
|
1220
|
+
const userRules = data.rules.filter((r: { id: string }) => !r.id.startsWith('default:'));
|
|
1221
|
+
expect(userRules).toHaveLength(1);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
test('v3 file with optional fields is loaded correctly without re-migration', () => {
|
|
1225
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1226
|
+
const v3Rules = [
|
|
1227
|
+
{
|
|
1228
|
+
id: 'v3-with-options',
|
|
1229
|
+
tool: 'bash',
|
|
1230
|
+
pattern: 'skill-cmd *',
|
|
1231
|
+
scope: '/tmp',
|
|
1232
|
+
decision: 'allow',
|
|
1233
|
+
priority: 100,
|
|
1234
|
+
createdAt: 7000,
|
|
1235
|
+
executionTarget: '/usr/bin/node',
|
|
1236
|
+
allowHighRisk: false,
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
id: 'v3-without-options',
|
|
1240
|
+
tool: 'bash',
|
|
1241
|
+
pattern: 'git *',
|
|
1242
|
+
scope: '/tmp',
|
|
1243
|
+
decision: 'allow',
|
|
1244
|
+
priority: 100,
|
|
1245
|
+
createdAt: 7001,
|
|
1246
|
+
},
|
|
1247
|
+
];
|
|
1248
|
+
writeFileSync(trustPath, JSON.stringify({ version: 3, rules: v3Rules }));
|
|
1249
|
+
clearCache();
|
|
1250
|
+
const rules = getAllRules();
|
|
1251
|
+
|
|
1252
|
+
// Rule with optional fields should have them preserved
|
|
1253
|
+
const withOptions = rules.find((r) => r.id === 'v3-with-options');
|
|
1254
|
+
expect(withOptions).toBeDefined();
|
|
1255
|
+
expect(withOptions!.executionTarget).toBe('/usr/bin/node');
|
|
1256
|
+
expect(withOptions!.allowHighRisk).toBe(false);
|
|
1257
|
+
|
|
1258
|
+
// Rule without optional fields should remain without them
|
|
1259
|
+
const withoutOptions = rules.find((r) => r.id === 'v3-without-options');
|
|
1260
|
+
expect(withoutOptions).toBeDefined();
|
|
1261
|
+
expect(withoutOptions).not.toHaveProperty('executionTarget');
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
test('v2 migration preserves rule meaning exactly — no extra fields added', () => {
|
|
1265
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1266
|
+
const originalRules = [
|
|
1267
|
+
{
|
|
1268
|
+
id: 'preserve-a',
|
|
1269
|
+
tool: 'bash',
|
|
1270
|
+
pattern: 'git *',
|
|
1271
|
+
scope: '/home/user',
|
|
1272
|
+
decision: 'allow' as const,
|
|
1273
|
+
priority: 100,
|
|
1274
|
+
createdAt: 8000,
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
id: 'preserve-b',
|
|
1278
|
+
tool: 'file_write',
|
|
1279
|
+
pattern: '/tmp/**',
|
|
1280
|
+
scope: 'everywhere',
|
|
1281
|
+
decision: 'deny' as const,
|
|
1282
|
+
priority: 50,
|
|
1283
|
+
createdAt: 8001,
|
|
1284
|
+
},
|
|
1285
|
+
];
|
|
1286
|
+
writeFileSync(trustPath, JSON.stringify({ version: 2, rules: originalRules }));
|
|
1287
|
+
clearCache();
|
|
1288
|
+
const rules = getAllRules();
|
|
1289
|
+
|
|
1290
|
+
for (const original of originalRules) {
|
|
1291
|
+
const migrated = rules.find((r) => r.id === original.id);
|
|
1292
|
+
expect(migrated).toBeDefined();
|
|
1293
|
+
// Every original field is preserved exactly
|
|
1294
|
+
expect(migrated!.tool).toBe(original.tool);
|
|
1295
|
+
expect(migrated!.pattern).toBe(original.pattern);
|
|
1296
|
+
expect(migrated!.scope).toBe(original.scope);
|
|
1297
|
+
expect(migrated!.decision).toBe(original.decision);
|
|
1298
|
+
expect(migrated!.priority).toBe(original.priority);
|
|
1299
|
+
expect(migrated!.createdAt).toBe(original.createdAt);
|
|
1300
|
+
// No extra fields were injected by migration
|
|
1301
|
+
expect(migrated).not.toHaveProperty('executionTarget');
|
|
1302
|
+
expect(migrated).not.toHaveProperty('allowHighRisk');
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
test('v1 → v3 full migration preserves rules and adds priority', () => {
|
|
1307
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1308
|
+
writeFileSync(trustPath, JSON.stringify({
|
|
1309
|
+
version: 1,
|
|
1310
|
+
rules: [{
|
|
1311
|
+
id: 'v1-full-migration',
|
|
1312
|
+
tool: 'bash',
|
|
1313
|
+
pattern: 'docker *',
|
|
1314
|
+
scope: '/srv',
|
|
1315
|
+
decision: 'allow',
|
|
1316
|
+
createdAt: 9000,
|
|
1317
|
+
}],
|
|
1318
|
+
}));
|
|
1319
|
+
clearCache();
|
|
1320
|
+
const rules = getAllRules();
|
|
1321
|
+
const rule = rules.find((r) => r.id === 'v1-full-migration');
|
|
1322
|
+
expect(rule).toBeDefined();
|
|
1323
|
+
// v1 → v2 adds priority 100
|
|
1324
|
+
expect(rule!.priority).toBe(100);
|
|
1325
|
+
// File should be v3 on disk
|
|
1326
|
+
const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
|
|
1327
|
+
expect(data.version).toBe(3);
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// ── executionTarget-aware rule matching ──────────────────────
|
|
1332
|
+
|
|
1333
|
+
describe('executionTarget-aware rule matching', () => {
|
|
1334
|
+
/**
|
|
1335
|
+
* Helper: write a v3 trust file with the given rules directly to disk,
|
|
1336
|
+
* then clear the cache so the next getRules() call picks them up.
|
|
1337
|
+
*/
|
|
1338
|
+
function seedRules(rules: Array<Record<string, unknown>>): void {
|
|
1339
|
+
mkdirSync(dirname(trustPath), { recursive: true });
|
|
1340
|
+
writeFileSync(trustPath, JSON.stringify({ version: 3, rules }));
|
|
1341
|
+
clearCache();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// ── wildcard semantics (no executionTarget on rule) ──────────
|
|
1345
|
+
|
|
1346
|
+
describe('wildcard semantics — rules without executionTarget', () => {
|
|
1347
|
+
test('rule with no executionTarget matches when no context is provided', () => {
|
|
1348
|
+
addRule('bash', 'git *', '/tmp', 'allow', 200);
|
|
1349
|
+
const match = findHighestPriorityRule('bash', ['git status'], '/tmp');
|
|
1350
|
+
expect(match).not.toBeNull();
|
|
1351
|
+
expect(match!.decision).toBe('allow');
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
test('rule with no executionTarget matches any execution target', () => {
|
|
1355
|
+
addRule('bash', 'git *', '/tmp', 'allow', 200);
|
|
1356
|
+
const match = findHighestPriorityRule('bash', ['git status'], '/tmp', {
|
|
1357
|
+
executionTarget: '/usr/bin/node',
|
|
1358
|
+
});
|
|
1359
|
+
expect(match).not.toBeNull();
|
|
1360
|
+
expect(match!.decision).toBe('allow');
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
// ── executionTarget matching ──────────────────────────────────
|
|
1365
|
+
|
|
1366
|
+
describe('executionTarget matching', () => {
|
|
1367
|
+
test('rule with executionTarget matches exact target', () => {
|
|
1368
|
+
seedRules([{
|
|
1369
|
+
id: 'et-exact',
|
|
1370
|
+
tool: 'bash',
|
|
1371
|
+
pattern: 'run *',
|
|
1372
|
+
scope: 'everywhere',
|
|
1373
|
+
decision: 'allow',
|
|
1374
|
+
priority: 200,
|
|
1375
|
+
createdAt: Date.now(),
|
|
1376
|
+
executionTarget: '/usr/local/bin/node',
|
|
1377
|
+
}]);
|
|
1378
|
+
const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {
|
|
1379
|
+
executionTarget: '/usr/local/bin/node',
|
|
1380
|
+
});
|
|
1381
|
+
expect(match).not.toBeNull();
|
|
1382
|
+
expect(match!.id).toBe('et-exact');
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
test('rule with executionTarget does NOT match different target', () => {
|
|
1386
|
+
seedRules([{
|
|
1387
|
+
id: 'et-diff',
|
|
1388
|
+
tool: 'bash',
|
|
1389
|
+
pattern: 'run *',
|
|
1390
|
+
scope: 'everywhere',
|
|
1391
|
+
decision: 'allow',
|
|
1392
|
+
priority: 200,
|
|
1393
|
+
createdAt: Date.now(),
|
|
1394
|
+
executionTarget: '/usr/local/bin/node',
|
|
1395
|
+
}]);
|
|
1396
|
+
const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {
|
|
1397
|
+
executionTarget: '/usr/local/bin/bun',
|
|
1398
|
+
});
|
|
1399
|
+
expect(match === null || match.id !== 'et-diff').toBe(true);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
test('rule with executionTarget does NOT match when no target in context', () => {
|
|
1403
|
+
seedRules([{
|
|
1404
|
+
id: 'et-no-ctx',
|
|
1405
|
+
tool: 'bash',
|
|
1406
|
+
pattern: 'run *',
|
|
1407
|
+
scope: 'everywhere',
|
|
1408
|
+
decision: 'allow',
|
|
1409
|
+
priority: 200,
|
|
1410
|
+
createdAt: Date.now(),
|
|
1411
|
+
executionTarget: '/usr/local/bin/node',
|
|
1412
|
+
}]);
|
|
1413
|
+
const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {});
|
|
1414
|
+
expect(match === null || match.id !== 'et-no-ctx').toBe(true);
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
test('rule WITHOUT executionTarget matches any target (wildcard)', () => {
|
|
1418
|
+
addRule('bash', 'run *', '/tmp', 'allow', 200);
|
|
1419
|
+
const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {
|
|
1420
|
+
executionTarget: '/any/path/to/runtime',
|
|
1421
|
+
});
|
|
1422
|
+
expect(match).not.toBeNull();
|
|
1423
|
+
expect(match!.pattern).toBe('run *');
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
// ── backward compatibility ────────────────────────────────────
|
|
1428
|
+
|
|
1429
|
+
describe('backward compatibility', () => {
|
|
1430
|
+
test('existing callers without ctx parameter still work', () => {
|
|
1431
|
+
addRule('bash', 'git *', '/tmp', 'allow', 200);
|
|
1432
|
+
// Calling without the 4th argument — must still match
|
|
1433
|
+
const match = findHighestPriorityRule('bash', ['git status'], '/tmp');
|
|
1434
|
+
expect(match).not.toBeNull();
|
|
1435
|
+
expect(match!.pattern).toBe('git *');
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
test('empty PolicyContext object behaves the same as no context', () => {
|
|
1439
|
+
addRule('bash', 'ls *', '/tmp', 'allow', 200);
|
|
1440
|
+
const matchNoCtx = findHighestPriorityRule('bash', ['ls -la'], '/tmp');
|
|
1441
|
+
const matchEmptyCtx = findHighestPriorityRule('bash', ['ls -la'], '/tmp', {});
|
|
1442
|
+
expect(matchNoCtx).not.toBeNull();
|
|
1443
|
+
expect(matchEmptyCtx).not.toBeNull();
|
|
1444
|
+
expect(matchNoCtx!.id).toBe(matchEmptyCtx!.id);
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// ── network_request trust rule matching ────────────────────────
|
|
1450
|
+
|
|
1451
|
+
describe('network_request trust rules', () => {
|
|
1452
|
+
test('exact origin rule matches network_request candidates', () => {
|
|
1453
|
+
addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere');
|
|
1454
|
+
const rule = findHighestPriorityRule(
|
|
1455
|
+
'network_request',
|
|
1456
|
+
['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
|
|
1457
|
+
'/tmp',
|
|
1458
|
+
);
|
|
1459
|
+
expect(rule).not.toBeNull();
|
|
1460
|
+
expect(rule!.decision).toBe('allow');
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
test('exact url rule matches only that url candidate', () => {
|
|
1464
|
+
addRule('network_request', 'network_request:https://api.example.com/v1/data', 'everywhere');
|
|
1465
|
+
const match = findHighestPriorityRule(
|
|
1466
|
+
'network_request',
|
|
1467
|
+
['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
|
|
1468
|
+
'/tmp',
|
|
1469
|
+
);
|
|
1470
|
+
expect(match).not.toBeNull();
|
|
1471
|
+
|
|
1472
|
+
const noMatch = findHighestPriorityRule(
|
|
1473
|
+
'network_request',
|
|
1474
|
+
['network_request:https://api.example.com/v2/other'],
|
|
1475
|
+
'/tmp',
|
|
1476
|
+
);
|
|
1477
|
+
expect(noMatch).toBeNull();
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
test('globstar rule matches any network_request candidate', () => {
|
|
1481
|
+
// minimatch treats standalone "**" as globstar (matching "/"), but
|
|
1482
|
+
// "network_request:*" uses single "*" which doesn't cross slashes.
|
|
1483
|
+
// The tool field is already filtered by findHighestPriorityRule, so
|
|
1484
|
+
// "**" is the correct catch-all pattern.
|
|
1485
|
+
addRule('network_request', '**', 'everywhere');
|
|
1486
|
+
const rule = findHighestPriorityRule(
|
|
1487
|
+
'network_request',
|
|
1488
|
+
['network_request:https://any-host.example.org/path'],
|
|
1489
|
+
'/tmp',
|
|
1490
|
+
);
|
|
1491
|
+
expect(rule).not.toBeNull();
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
test('single-star wildcard matches flat candidates only', () => {
|
|
1495
|
+
// "network_request:*" won't match URLs with slashes — consistent
|
|
1496
|
+
// with the behavior of web_fetch:* and browser_navigate:* patterns.
|
|
1497
|
+
addRule('network_request', 'network_request:*', 'everywhere');
|
|
1498
|
+
const noSlashMatch = findHighestPriorityRule(
|
|
1499
|
+
'network_request',
|
|
1500
|
+
['network_request:flat-target'],
|
|
1501
|
+
'/tmp',
|
|
1502
|
+
);
|
|
1503
|
+
expect(noSlashMatch).not.toBeNull();
|
|
1504
|
+
|
|
1505
|
+
const slashNoMatch = findHighestPriorityRule(
|
|
1506
|
+
'network_request',
|
|
1507
|
+
['network_request:https://example.com/path'],
|
|
1508
|
+
'/tmp',
|
|
1509
|
+
);
|
|
1510
|
+
// Single "*" does not match "/" so this URL candidate won't match.
|
|
1511
|
+
expect(slashNoMatch).toBeNull();
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test('network_request rule does not match web_fetch tool', () => {
|
|
1515
|
+
addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere');
|
|
1516
|
+
const rule = findHighestPriorityRule(
|
|
1517
|
+
'web_fetch',
|
|
1518
|
+
['web_fetch:https://api.example.com/v1/data', 'web_fetch:https://api.example.com/*'],
|
|
1519
|
+
'/tmp',
|
|
1520
|
+
);
|
|
1521
|
+
expect(rule).toBeNull();
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
test('web_fetch rule does not match network_request tool', () => {
|
|
1525
|
+
addRule('web_fetch', 'web_fetch:https://api.example.com/*', 'everywhere');
|
|
1526
|
+
const rule = findHighestPriorityRule(
|
|
1527
|
+
'network_request',
|
|
1528
|
+
['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
|
|
1529
|
+
'/tmp',
|
|
1530
|
+
);
|
|
1531
|
+
expect(rule).toBeNull();
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
test('deny rule takes precedence over allow at same priority', () => {
|
|
1535
|
+
addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'allow', 100);
|
|
1536
|
+
addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'deny', 100);
|
|
1537
|
+
const rule = findHighestPriorityRule(
|
|
1538
|
+
'network_request',
|
|
1539
|
+
['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
|
|
1540
|
+
'/tmp',
|
|
1541
|
+
);
|
|
1542
|
+
expect(rule).not.toBeNull();
|
|
1543
|
+
expect(rule!.decision).toBe('deny');
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
test('higher-priority allow overrides lower-priority deny', () => {
|
|
1547
|
+
addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'deny', 50);
|
|
1548
|
+
addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'allow', 100);
|
|
1549
|
+
const rule = findHighestPriorityRule(
|
|
1550
|
+
'network_request',
|
|
1551
|
+
['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
|
|
1552
|
+
'/tmp',
|
|
1553
|
+
);
|
|
1554
|
+
expect(rule).not.toBeNull();
|
|
1555
|
+
expect(rule!.decision).toBe('allow');
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
test('scope restricts network_request rule matching', () => {
|
|
1559
|
+
addRule('network_request', 'network_request:https://api.example.com/*', '/home/user/project');
|
|
1560
|
+
const inScope = findHighestPriorityRule(
|
|
1561
|
+
'network_request',
|
|
1562
|
+
['network_request:https://api.example.com/*'],
|
|
1563
|
+
'/home/user/project',
|
|
1564
|
+
);
|
|
1565
|
+
expect(inScope).not.toBeNull();
|
|
1566
|
+
|
|
1567
|
+
const outOfScope = findHighestPriorityRule(
|
|
1568
|
+
'network_request',
|
|
1569
|
+
['network_request:https://api.example.com/*'],
|
|
1570
|
+
'/tmp/other',
|
|
1571
|
+
);
|
|
1572
|
+
expect(outOfScope).toBeNull();
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
describe('computer-use tool trust rule matching', () => {
|
|
1578
|
+
test('actionable CU tools have default ask trust rules', () => {
|
|
1579
|
+
// Actionable CU tools (those that perform screen interactions) should
|
|
1580
|
+
// have default "ask" rules so strict mode prompts before use.
|
|
1581
|
+
const actionableCuTools = [
|
|
1582
|
+
'computer_use_click',
|
|
1583
|
+
'computer_use_type_text',
|
|
1584
|
+
'computer_use_request_control',
|
|
1585
|
+
];
|
|
1586
|
+
|
|
1587
|
+
for (const name of actionableCuTools) {
|
|
1588
|
+
const rule = findHighestPriorityRule(name, [name], '/tmp/test');
|
|
1589
|
+
expect(rule).not.toBeNull();
|
|
1590
|
+
expect(rule!.decision).toBe('ask');
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
test('terminal CU tools (done/respond) have no default trust rules', () => {
|
|
1595
|
+
// computer_use_done and computer_use_respond are terminal signal tools
|
|
1596
|
+
// with RiskLevel.Low — they should not have ask rules since they don't
|
|
1597
|
+
// perform any screen action.
|
|
1598
|
+
const terminalCuTools = ['computer_use_done', 'computer_use_respond'];
|
|
1599
|
+
|
|
1600
|
+
for (const name of terminalCuTools) {
|
|
1601
|
+
const defaultRule = DEFAULT_TEMPLATES.find((t) => t.tool === name);
|
|
1602
|
+
expect(defaultRule).toBeUndefined();
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
});
|