mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Regression: the JS DIAGNOSTIC matcher compares RAW BYTES like native
|
|
2
|
+
// (main.rs:1867-1875 exact, 1891-1899 ws byte-trim, 1936-1944 normalize ONLY
|
|
3
|
+
// when BOTH bodies are valid UTF-8). It must mirror two native guards in
|
|
4
|
+
// evaluate_fuzzy_candidate:
|
|
5
|
+
// (a) the per-line trailing-newline guard (main.rs:1500-1526 marker flip,
|
|
6
|
+
// 1867-1875 exact, 1891-1899 ws, 1936-1944 normalize, 1832-1835 delete
|
|
7
|
+
// reject) — a tier matches only when expected/actual newline state agree.
|
|
8
|
+
// (b) the byte-level UTF-8 validity gate (main.rs:1936-1944) — the normalize
|
|
9
|
+
// tier matches ONLY when BOTH sides are valid UTF-8. Invalid-UTF-8 source
|
|
10
|
+
// bytes are NOT pre-collapsed to U+FFFD, so an EXACT byte match of invalid
|
|
11
|
+
// bytes still succeeds, while normalize is refused on invalid bytes; a
|
|
12
|
+
// VALID literal U+FFFD present in both sides DOES normalize-match (the old
|
|
13
|
+
// U+FFFD heuristic is gone — real validity replaces it).
|
|
14
|
+
import { __patchTestHooks } from '../src/agent/orchestrator/tools/patch.mjs';
|
|
15
|
+
|
|
16
|
+
const { findFirstFailingUnifiedHunk, unifiedOldLinesMatchAt, splitBufferLinesForPatch } = __patchTestHooks;
|
|
17
|
+
|
|
18
|
+
function assert(cond, msg) {
|
|
19
|
+
if (!cond) throw new Error(`assertion failed: ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// (a) Old line expects a trailing newline (no "\ No newline" marker) but the
|
|
23
|
+
// source's matching line is the final line of a file WITHOUT a trailing newline
|
|
24
|
+
// -> newline state differs -> the delete line falls through to reject.
|
|
25
|
+
{
|
|
26
|
+
const entry = { oldFileName: 'a/x', hunks: [{ oldStart: 1, lines: [' a', '-b'] }] };
|
|
27
|
+
const source = ['a', 'b'];
|
|
28
|
+
source.hasFinalNewline = false; // EOF line 'b' has no trailing newline
|
|
29
|
+
const failing = findFirstFailingUnifiedHunk(entry, source, 2);
|
|
30
|
+
assert(failing === entry.hunks[0], 'newline mismatch (expect-newline vs EOF-no-newline) must fail');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// (a-control) Same hunk against a source whose final line DOES end in a newline
|
|
34
|
+
// -> newline state agrees -> exact match -> no failing hunk.
|
|
35
|
+
{
|
|
36
|
+
const entry = { oldFileName: 'a/x', hunks: [{ oldStart: 1, lines: [' a', '-b'] }] };
|
|
37
|
+
const source = ['a', 'b'];
|
|
38
|
+
source.hasFinalNewline = true;
|
|
39
|
+
const failing = findFirstFailingUnifiedHunk(entry, source, 2);
|
|
40
|
+
assert(failing === null, 'matching newline state must match (no failing hunk)');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// (b-1) Invalid-UTF-8 source bytes: an EXACT raw-byte match of the invalid bytes
|
|
44
|
+
// must SUCCEED (native compares bytes; the bytes are NOT collapsed to U+FFFD).
|
|
45
|
+
// 0x80 alone is an invalid UTF-8 continuation byte. The expected old line is
|
|
46
|
+
// injected as a raw Buffer carrying the SAME invalid bytes.
|
|
47
|
+
{
|
|
48
|
+
const srcBytes = Buffer.from([0x61, 0x80, 0x62]); // a <0x80> b (invalid UTF-8)
|
|
49
|
+
const source = [srcBytes];
|
|
50
|
+
source.hasFinalNewline = true;
|
|
51
|
+
const oldLines = [{ tag: '-', line: Buffer.from([0x61, 0x80, 0x62]), hasNewline: true }];
|
|
52
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 2, null);
|
|
53
|
+
assert(matched !== null, 'exact byte match of invalid-UTF-8 bytes must succeed');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// (b-2) Normalize tier REFUSED on invalid UTF-8: source and expected differ only
|
|
57
|
+
// by a typographic dash and would normalize-match IF valid, but the source line
|
|
58
|
+
// carries an invalid lone 0x80 byte -> from_utf8 gate fails -> no normalize
|
|
59
|
+
// match -> the hunk is reported as failing.
|
|
60
|
+
{
|
|
61
|
+
// source: a <0x80> <en-dash 0xE2 0x80 0x93> b -> invalid due to lone 0x80
|
|
62
|
+
const srcBytes = Buffer.from([0x61, 0x80, 0xe2, 0x80, 0x93, 0x62]);
|
|
63
|
+
const source = [srcBytes];
|
|
64
|
+
source.hasFinalNewline = true;
|
|
65
|
+
// expected: a <0x80> - b (ASCII dash, also invalid due to 0x80)
|
|
66
|
+
const oldLines = [{ tag: '-', line: Buffer.from([0x61, 0x80, 0x2d, 0x62]), hasNewline: true }];
|
|
67
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 2, null);
|
|
68
|
+
assert(matched === null, 'normalize tier must be refused when a side is invalid UTF-8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// (b-3) VALID literal U+FFFD in BOTH sides now normalize-MATCHES (native parity:
|
|
72
|
+
// the old U+FFFD heuristic is removed; real UTF-8 validity governs). Source has a
|
|
73
|
+
// genuine, properly-encoded U+FFFD (0xEF 0xBF 0xBD) plus an en-dash; expected has
|
|
74
|
+
// the same valid U+FFFD plus an ASCII dash -> both valid UTF-8 -> normalize tier
|
|
75
|
+
// matches -> no failing hunk.
|
|
76
|
+
{
|
|
77
|
+
const fffd = [0xef, 0xbf, 0xbd];
|
|
78
|
+
const enDash = [0xe2, 0x80, 0x93];
|
|
79
|
+
const srcBytes = Buffer.from([0x61, ...fffd, ...enDash, 0x62]); // a <U+FFFD> <en-dash> b
|
|
80
|
+
const source = [srcBytes];
|
|
81
|
+
source.hasFinalNewline = true;
|
|
82
|
+
const oldLines = [{ tag: '-', line: Buffer.from([0x61, ...fffd, 0x2d, 0x62]), hasNewline: true }]; // a <U+FFFD> - b
|
|
83
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 2, null);
|
|
84
|
+
assert(matched !== null && matched.normCount === 1, 'valid literal U+FFFD in both sides must normalize-match');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// (b-4) End-to-end via findFirstFailingUnifiedHunk with the byte source view:
|
|
88
|
+
// typographic-only drift between VALID-UTF-8 source/patch normalize-matches.
|
|
89
|
+
{
|
|
90
|
+
const buf = Buffer.from('a\u2013b\n', 'utf8'); // en-dash, valid UTF-8, trailing NL
|
|
91
|
+
const source = splitBufferLinesForPatch(buf);
|
|
92
|
+
const entry = { oldFileName: 'a/x', hunks: [{ oldStart: 1, lines: ['-a-b'] }] }; // ASCII dash
|
|
93
|
+
const failing = findFirstFailingUnifiedHunk(entry, source, 2);
|
|
94
|
+
assert(failing === null, 'valid typographic-only drift matches via normalize (byte view)');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// (c-1) CR-parity: the FINAL unterminated line keeps a bare trailing \r. Native
|
|
98
|
+
// strips \r ONLY when immediately before \n (main.rs:1967-1972); a final line
|
|
99
|
+
// with NO trailing \n keeps its \r in the compared body (main.rs:1981-1987,
|
|
100
|
+
// exact byte compare 1867-1875). Source ends in a bare \r (no \n) -> the split
|
|
101
|
+
// must retain 'abc\r' and hasFinalNewline=false, so comparing against old line
|
|
102
|
+
// 'abc' must NOT match (native would compare 'abc\r' != 'abc').
|
|
103
|
+
{
|
|
104
|
+
const buf = Buffer.from([0x61, 0x62, 0x63, 0x0d]); // 'abc\r', no trailing \n
|
|
105
|
+
const source = splitBufferLinesForPatch(buf);
|
|
106
|
+
assert(source.length === 1, 'bare-\\r source splits into one line');
|
|
107
|
+
assert(source[0].equals(Buffer.from('abc\r')), 'final unterminated line KEEPS its bare \\r');
|
|
108
|
+
assert(source.hasFinalNewline === false, 'bare-\\r source has no final newline');
|
|
109
|
+
const oldLines = [{ tag: '-', line: Buffer.from('abc'), hasNewline: false }];
|
|
110
|
+
// fuzz=0 isolates the exact byte-compare tier (main.rs:1867-1875), the path
|
|
111
|
+
// the CR-parity split fix targets: 'abc\r' != 'abc'.
|
|
112
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 0, null);
|
|
113
|
+
assert(matched === null, 'bare trailing \\r must NOT match patch old line abc (native compares abc\\r)');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// (c-2) CRLF control: a \r\n-terminated line still strips its \r (CRLF -> drop
|
|
117
|
+
// the \r before \n) and matches the patch old line 'abc'.
|
|
118
|
+
{
|
|
119
|
+
const buf = Buffer.from('abc\r\n', 'utf8'); // CRLF-terminated
|
|
120
|
+
const source = splitBufferLinesForPatch(buf);
|
|
121
|
+
assert(source[0].equals(Buffer.from('abc')), 'CRLF line strips its \\r');
|
|
122
|
+
assert(source.hasFinalNewline === true, 'CRLF-terminated source has a final newline');
|
|
123
|
+
const oldLines = [{ tag: '-', line: Buffer.from('abc'), hasNewline: true }];
|
|
124
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 2, null);
|
|
125
|
+
assert(matched !== null, 'CRLF line (\\r stripped) must match patch old line abc');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// (d-1) Rust-White_Space trim parity: U+0085 (NEL) is Unicode White_Space, so
|
|
129
|
+
// native str::trim() strips it. JS String.trim() does NOT — hence the custom
|
|
130
|
+
// rustTrim() inside normalizeTypographic(). Source carries U+0085 at BOTH ends
|
|
131
|
+
// plus an em-dash; expected is the ASCII-dash form. After rust-trim (U+0085
|
|
132
|
+
// removed) + dash map, both sides reduce to 'a-b' -> normalize tier MATCHES,
|
|
133
|
+
// exactly as native does.
|
|
134
|
+
{
|
|
135
|
+
const srcBytes = Buffer.from('\u0085a\u2014b\u0085', 'utf8'); // <NEL>a<em-dash>b<NEL>
|
|
136
|
+
const source = [srcBytes];
|
|
137
|
+
source.hasFinalNewline = true;
|
|
138
|
+
const oldLines = [{ tag: '-', line: Buffer.from('a-b', 'utf8'), hasNewline: true }];
|
|
139
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 2, null);
|
|
140
|
+
assert(matched !== null && matched.normCount === 1, 'U+0085 boundary must be trimmed (native White_Space) so normalize-matches');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// (d-2) U+FEFF (BOM/ZWNBSP) is NOT Unicode White_Space, so native str::trim()
|
|
144
|
+
// leaves it in place; our rustTrim() must likewise NOT strip it (diverging from
|
|
145
|
+
// JS String.trim(), which WOULD). Source carries U+FEFF at both ends plus an
|
|
146
|
+
// em-dash; expected is ASCII 'a-b'. The U+FEFF survives the trim, so the sides
|
|
147
|
+
// differ -> normalize tier does NOT match, exactly as native.
|
|
148
|
+
{
|
|
149
|
+
const srcBytes = Buffer.from('\uFEFFa\u2014b\uFEFF', 'utf8'); // <BOM>a<em-dash>b<BOM>
|
|
150
|
+
const source = [srcBytes];
|
|
151
|
+
source.hasFinalNewline = true;
|
|
152
|
+
const oldLines = [{ tag: '-', line: Buffer.from('a-b', 'utf8'), hasNewline: true }];
|
|
153
|
+
const matched = unifiedOldLinesMatchAt(source, oldLines, 0, 2, null);
|
|
154
|
+
assert(matched === null, 'U+FEFF must NOT be trimmed (not White_Space) so normalize must NOT match');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('patch-newline-utf8-smoke OK');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { performance } from 'perf_hooks';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const HOOKS_DIR = join(__dirname, '..', 'hooks');
|
|
8
|
+
|
|
9
|
+
const HOOKS = [
|
|
10
|
+
'pre-tool-subagent.cjs',
|
|
11
|
+
'pre-mcp-sandbox.cjs',
|
|
12
|
+
'post-tool-use.cjs',
|
|
13
|
+
];
|
|
14
|
+
const RUNTIMES = ['bun', 'node'];
|
|
15
|
+
const N = 5;
|
|
16
|
+
const THRESHOLDS = { node: 200, bun: 400 };
|
|
17
|
+
|
|
18
|
+
const PAYLOAD = JSON.stringify({
|
|
19
|
+
tool_name: 'Edit',
|
|
20
|
+
tool_input: { file_path: '/tmp/x.js' },
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function runOnce(runtime, hookPath) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const start = performance.now();
|
|
27
|
+
const child = spawn(runtime, [hookPath], { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
28
|
+
child.stdin.write(PAYLOAD);
|
|
29
|
+
child.stdin.end();
|
|
30
|
+
child.on('close', () => resolve(performance.now() - start));
|
|
31
|
+
child.on('error', () => resolve(performance.now() - start));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function median(runtime, hookPath) {
|
|
36
|
+
const times = [];
|
|
37
|
+
for (let i = 0; i < N; i++) times.push(await runOnce(runtime, hookPath));
|
|
38
|
+
times.sort((a, b) => a - b);
|
|
39
|
+
return times[Math.floor(N / 2)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const results = [];
|
|
43
|
+
for (const hook of HOOKS) {
|
|
44
|
+
const hookPath = join(HOOKS_DIR, hook);
|
|
45
|
+
for (const runtime of RUNTIMES) {
|
|
46
|
+
const med = await median(runtime, hookPath);
|
|
47
|
+
const threshold = THRESHOLDS[runtime];
|
|
48
|
+
results.push({ hook, runtime, med, threshold, pass: med <= threshold });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const col0 = Math.max(...results.map(r => r.hook.length));
|
|
53
|
+
const header = `${'hook'.padEnd(col0)} runtime median(ms) threshold pass`;
|
|
54
|
+
const sep = '-'.repeat(header.length);
|
|
55
|
+
console.log(sep);
|
|
56
|
+
console.log(header);
|
|
57
|
+
console.log(sep);
|
|
58
|
+
for (const r of results) {
|
|
59
|
+
const line = [
|
|
60
|
+
r.hook.padEnd(col0),
|
|
61
|
+
r.runtime.padEnd(7),
|
|
62
|
+
String(r.med.toFixed(1)).padStart(10),
|
|
63
|
+
String(r.threshold).padStart(9),
|
|
64
|
+
r.pass ? ' OK' : ' FAIL',
|
|
65
|
+
].join(' ');
|
|
66
|
+
console.log(line);
|
|
67
|
+
}
|
|
68
|
+
console.log(sep);
|
|
69
|
+
|
|
70
|
+
const allPass = results.every(r => r.pass);
|
|
71
|
+
process.exit(allPass ? 0 : 1);
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/permission-eval-smoke.mjs
|
|
4
|
+
* Unit-level smoke tests for hooks/lib/permission-evaluator.cjs.
|
|
5
|
+
*/
|
|
6
|
+
import assert from 'assert';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const _require = createRequire(import.meta.url);
|
|
14
|
+
const evalPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../hooks/lib/permission-evaluator.cjs');
|
|
15
|
+
const { evaluatePermission } = _require(evalPath);
|
|
16
|
+
const loaderPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../hooks/lib/settings-loader.cjs');
|
|
17
|
+
const { loadPermissions, clearSettingsCache } = _require(loaderPath);
|
|
18
|
+
|
|
19
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
let pass = 0, fail = 0;
|
|
22
|
+
|
|
23
|
+
function makeProject(settings = {}) {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'perm-smoke-'));
|
|
25
|
+
fs.mkdirSync(path.join(dir, '.claude'), { recursive: true });
|
|
26
|
+
if (Object.keys(settings).length) {
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(dir, '.claude', 'settings.json'),
|
|
29
|
+
JSON.stringify({ permissions: settings }),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function check(label, fn) {
|
|
36
|
+
try {
|
|
37
|
+
fn();
|
|
38
|
+
console.log(` PASS ${label}`);
|
|
39
|
+
pass++;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.log(` FAIL ${label}: ${e.message}`);
|
|
42
|
+
fail++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ep(opts) { return evaluatePermission(opts); }
|
|
47
|
+
|
|
48
|
+
// ── Hard-deny path scenarios ──────────────────────────────────────────────────
|
|
49
|
+
// UNC paths and Program Files / System32 are hard-denied in
|
|
50
|
+
// permission-evaluator.cjs (isHardDenyPath) — evaluated before mode and list
|
|
51
|
+
// rules, so even allow=["mcp__*"] cannot override.
|
|
52
|
+
{
|
|
53
|
+
const dir = makeProject();
|
|
54
|
+
|
|
55
|
+
check('UNC path + default mode → deny (hard-deny pattern)', () => {
|
|
56
|
+
// UNC paths match the hard-deny pattern in isHardDenyPath before mode logic runs.
|
|
57
|
+
const r = ep({ toolName: 'read', toolInput: { path: '\\\\srv\\share\\f' }, permissionMode: 'default', projectDir: dir, userCwd: dir });
|
|
58
|
+
assert.strictEqual(r.decision, 'deny', `Expected deny (hard-deny UNC), got ${r.decision}: ${r.reason}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
check('C:\\Windows\\System32\\foo + allow=["mcp__*"] → deny (hard-deny overrides list)', () => {
|
|
62
|
+
// System32 matches HARD_DENY_PATH_PATTERNS — bypass-proof; allow list cannot override.
|
|
63
|
+
const dir2 = makeProject({ allow: ['mcp__*'] });
|
|
64
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__read', toolInput: { path: 'C:\\Windows\\System32\\foo' }, permissionMode: 'default', projectDir: dir2, userCwd: dir2 });
|
|
65
|
+
assert.strictEqual(r.decision, 'deny', `Expected deny (hard-deny System32), got ${r.decision}: ${r.reason}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
check('file_path alias + hard-deny path → deny for read/write/edit', () => {
|
|
69
|
+
const dir2 = makeProject({ allow: ['mcp__*'] });
|
|
70
|
+
for (const shortName of ['read', 'write', 'edit']) {
|
|
71
|
+
const r = ep({
|
|
72
|
+
toolName: `mcp__plugin_mixdog_mixdog__${shortName}`,
|
|
73
|
+
toolInput: { file_path: 'C:\\Windows\\System32\\drivers\\etc\\hosts', content: 'x', old_string: 'x', new_string: 'y' },
|
|
74
|
+
permissionMode: 'default',
|
|
75
|
+
projectDir: dir2,
|
|
76
|
+
userCwd: dir2,
|
|
77
|
+
});
|
|
78
|
+
assert.strictEqual(r.decision, 'deny', `${shortName}: expected deny, got ${r.decision}: ${r.reason}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
check('nested file_path aliases in edits[]/writes[] + hard-deny path → deny', () => {
|
|
83
|
+
const dir2 = makeProject({ allow: ['mcp__*'] });
|
|
84
|
+
const edit = ep({
|
|
85
|
+
toolName: 'mcp__plugin_mixdog_mixdog__edit',
|
|
86
|
+
toolInput: { path: path.join(dir2, 'safe.txt'), edits: [{ file_path: 'C:\\Windows\\System32\\drivers\\etc\\hosts', old_string: 'x', new_string: 'y' }] },
|
|
87
|
+
permissionMode: 'default',
|
|
88
|
+
projectDir: dir2,
|
|
89
|
+
userCwd: dir2,
|
|
90
|
+
});
|
|
91
|
+
assert.strictEqual(edit.decision, 'deny', `edit: expected deny, got ${edit.decision}: ${edit.reason}`);
|
|
92
|
+
|
|
93
|
+
const write = ep({
|
|
94
|
+
toolName: 'mcp__plugin_mixdog_mixdog__write',
|
|
95
|
+
toolInput: { writes: [{ file_path: 'C:\\Windows\\System32\\drivers\\etc\\hosts', content: 'x' }] },
|
|
96
|
+
permissionMode: 'default',
|
|
97
|
+
projectDir: dir2,
|
|
98
|
+
userCwd: dir2,
|
|
99
|
+
});
|
|
100
|
+
assert.strictEqual(write.decision, 'deny', `write: expected deny, got ${write.decision}: ${write.reason}`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── List eval ─────────────────────────────────────────────────────────────────
|
|
105
|
+
{
|
|
106
|
+
check('allow=["mcp__*"], any mcp tool → allow', () => {
|
|
107
|
+
const dir = makeProject({ allow: ['mcp__*'] });
|
|
108
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__read', toolInput: {}, projectDir: dir, userCwd: dir });
|
|
109
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
check('deny=["mcp__plugin_mixdog_mixdog__bash"], bash → deny', () => {
|
|
113
|
+
const dir = makeProject({ deny: ['mcp__plugin_mixdog_mixdog__bash'] });
|
|
114
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__bash', toolInput: { cwd: dir }, projectDir: dir, userCwd: dir });
|
|
115
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
check('ask=["mcp__plugin_mixdog_mixdog__edit"], edit → ask', () => {
|
|
119
|
+
const dir = makeProject({ ask: ['mcp__plugin_mixdog_mixdog__edit'] });
|
|
120
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__edit', toolInput: {}, projectDir: dir, userCwd: dir });
|
|
121
|
+
assert.strictEqual(r.decision, 'ask', r.reason);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
check('Tool(specifier): Read(*.env) matches Read NOT ReadExtra', () => {
|
|
125
|
+
const dir = makeProject({ deny: ['Read(*.env)'] });
|
|
126
|
+
const r1 = ep({ toolName: 'Read', toolInput: { path: 'foo.env' }, projectDir: dir, userCwd: dir });
|
|
127
|
+
const r2 = ep({ toolName: 'ReadExtra', toolInput: { path: 'foo.env' }, projectDir: dir, userCwd: dir });
|
|
128
|
+
assert.strictEqual(r1.decision, 'deny', `Read(*.env) should deny, got ${r1.decision}`);
|
|
129
|
+
assert.notStrictEqual(r2.decision, 'deny', `ReadExtra should not be denied by Read(*.env)`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
check('Edit(secret.txt) matches when toolInput.file_path=secret.txt', () => {
|
|
133
|
+
const dir = makeProject({ deny: ['Edit(secret.txt)'] });
|
|
134
|
+
const r = ep({ toolName: 'Edit', toolInput: { file_path: 'secret.txt' }, projectDir: dir, userCwd: dir });
|
|
135
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
check('Read(secret.txt) matches when toolInput.file_path=secret.txt (file_path bypass guard)', () => {
|
|
139
|
+
const dir = makeProject({ deny: ['Read(secret.txt)'] });
|
|
140
|
+
const r = ep({ toolName: 'Read', toolInput: { file_path: 'secret.txt' }, projectDir: dir, userCwd: dir });
|
|
141
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
check('Write(secret.txt) matches when toolInput.file_path=secret.txt (file_path bypass guard)', () => {
|
|
145
|
+
const dir = makeProject({ deny: ['Write(secret.txt)'] });
|
|
146
|
+
const r = ep({ toolName: 'Write', toolInput: { file_path: 'secret.txt', content: 'x' }, projectDir: dir, userCwd: dir });
|
|
147
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
check('Edit per-item edits[].file_path is collected (deny applies even when top-level path is missing)', () => {
|
|
151
|
+
const dir = makeProject({ deny: ['Edit(secret.txt)'] });
|
|
152
|
+
const r = ep({ toolName: 'Edit', toolInput: { edits: [{ file_path: 'secret.txt', old_string: 'a', new_string: 'b' }] }, projectDir: dir, userCwd: dir });
|
|
153
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
check('Write per-item writes[].file_path is collected (defensive — runtime drops items missing path, but the rule still catches the intent)', () => {
|
|
157
|
+
const dir = makeProject({ deny: ['Write(secret.txt)'] });
|
|
158
|
+
const r = ep({ toolName: 'Write', toolInput: { writes: [{ file_path: 'secret.txt', content: 'x' }] }, projectDir: dir, userCwd: dir });
|
|
159
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Mode defaults ─────────────────────────────────────────────────────────────
|
|
164
|
+
{
|
|
165
|
+
const baseDir = makeProject();
|
|
166
|
+
const outside = os.tmpdir();
|
|
167
|
+
|
|
168
|
+
check('default + path inside cwd → allow', () => {
|
|
169
|
+
const r = ep({ toolName: 'read', toolInput: { path: path.join(baseDir, 'foo.txt') }, permissionMode: 'default', projectDir: baseDir, userCwd: baseDir });
|
|
170
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
check('default + path outside cwd → allow (outside-cwd fallback policy)', () => {
|
|
174
|
+
// outside-cwd in default mode falls through to bypassPermissions branch per
|
|
175
|
+
// current policy. Was 'ask' in earlier hardening; relaxed to 'allow' once
|
|
176
|
+
// `auto` was unified with `bypassPermissions`.
|
|
177
|
+
const r = ep({ toolName: 'read', toolInput: { path: path.join(outside, 'bar.txt') }, permissionMode: 'default', projectDir: baseDir, userCwd: baseDir });
|
|
178
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
check('acceptEdits + read tool → allow', () => {
|
|
182
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__read', toolInput: { path: path.join(outside, 'x') }, permissionMode: 'acceptEdits', projectDir: baseDir, userCwd: baseDir });
|
|
183
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
check('acceptEdits + trib-plugin MCP edit/read prefixes → allow', () => {
|
|
187
|
+
for (const shortName of ['edit', 'read']) {
|
|
188
|
+
const r = ep({
|
|
189
|
+
toolName: `mcp__plugin_mixdog_trib-plugin__${shortName}`,
|
|
190
|
+
toolInput: { path: path.join(baseDir, 'x.txt'), old_string: 'x', new_string: 'y' },
|
|
191
|
+
permissionMode: 'acceptEdits',
|
|
192
|
+
projectDir: baseDir,
|
|
193
|
+
userCwd: baseDir,
|
|
194
|
+
});
|
|
195
|
+
assert.strictEqual(r.decision, 'allow', `${shortName}: ${r.reason}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
check('acceptEdits + bash inside → allow', () => {
|
|
200
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__bash', toolInput: { cwd: baseDir }, permissionMode: 'acceptEdits', projectDir: baseDir, userCwd: baseDir });
|
|
201
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
check('acceptEdits + bash outside → allow (bypass-fallback policy)', () => {
|
|
205
|
+
// acceptEdits + outside-cwd inherits the bypass-fallback policy.
|
|
206
|
+
const r = ep({ toolName: 'bash', toolInput: { cwd: outside }, permissionMode: 'acceptEdits', projectDir: baseDir, userCwd: baseDir });
|
|
207
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
check('plan + read → allow', () => {
|
|
211
|
+
const r = ep({ toolName: 'read', toolInput: {}, permissionMode: 'plan', projectDir: baseDir, userCwd: baseDir });
|
|
212
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
check('plan + bash → allow (bypass-fallback policy)', () => {
|
|
216
|
+
const r = ep({ toolName: 'bash', toolInput: { cwd: baseDir }, permissionMode: 'plan', projectDir: baseDir, userCwd: baseDir });
|
|
217
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
check('dontAsk + any → allow (no list match: defers to bypass-fallback)', () => {
|
|
221
|
+
// dontAsk without a matching list entry defers to the same bypass-fallback
|
|
222
|
+
// path as acceptEdits / plan. List-driven deny / ask still works (see matrix).
|
|
223
|
+
const r = ep({ toolName: 'read', toolInput: {}, permissionMode: 'dontAsk', projectDir: baseDir, userCwd: baseDir });
|
|
224
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
check('bypassPermissions + outside → allow', () => {
|
|
228
|
+
const r = ep({ toolName: 'bash', toolInput: { cwd: outside }, permissionMode: 'bypassPermissions', projectDir: baseDir, userCwd: baseDir });
|
|
229
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
check('mode "auto" → bypass (user policy: treated same as bypassPermissions)', () => {
|
|
233
|
+
// auto is now treated as bypass per user policy decision
|
|
234
|
+
const r = ep({ toolName: 'bash', toolInput: { cwd: outside }, permissionMode: 'auto', projectDir: baseDir, userCwd: baseDir });
|
|
235
|
+
assert.strictEqual(r.decision, 'allow', `Expected allow (auto=bypass), got ${r.decision}: ${r.reason}`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Zero-path calls ───────────────────────────────────────────────────────────
|
|
240
|
+
{
|
|
241
|
+
// `bridge` is a zero-path tool (no path arg in the common case) but, unlike
|
|
242
|
+
// the old read-only session-list tool, it is NOT read-only — it spawns/controls
|
|
243
|
+
// workers. Permissions are passed explicitly so the cases are deterministic
|
|
244
|
+
// regardless of any user-home settings on the test host.
|
|
245
|
+
const emptyPerms = { allow: [], deny: [], ask: [], defaultMode: 'default' };
|
|
246
|
+
|
|
247
|
+
check('bridge + default mode → allow', () => {
|
|
248
|
+
const dir = makeProject();
|
|
249
|
+
const r = ep({ toolName: 'bridge', toolInput: {}, permissionMode: 'default', projectDir: dir, userCwd: dir, permissions: emptyPerms });
|
|
250
|
+
assert.strictEqual(r.decision, 'allow', r.reason);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
check('bridge + dontAsk + no list match → deny (not read-only)', () => {
|
|
254
|
+
const dir = makeProject();
|
|
255
|
+
// dontAsk denies anything not explicitly allowed; bridge is not read-only
|
|
256
|
+
// so there is no read-only exemption to rescue it.
|
|
257
|
+
const r = ep({ toolName: 'bridge', toolInput: {}, permissionMode: 'dontAsk', projectDir: dir, userCwd: dir, permissions: emptyPerms });
|
|
258
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
check('bridge + deny=["bridge"] → deny', () => {
|
|
262
|
+
const dir = makeProject();
|
|
263
|
+
const r = ep({ toolName: 'bridge', toolInput: {}, permissionMode: 'default', projectDir: dir, userCwd: dir, permissions: { ...emptyPerms, deny: ['bridge'] } });
|
|
264
|
+
assert.strictEqual(r.decision, 'deny', r.reason);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Settings merge ────────────────────────────────────────────────────────────
|
|
269
|
+
{
|
|
270
|
+
check('user allow=[A] + project allow=[B] + local allow=[C] → all present', () => {
|
|
271
|
+
const dir = makeProject({ allow: ['B'] });
|
|
272
|
+
// Write local settings
|
|
273
|
+
fs.writeFileSync(
|
|
274
|
+
path.join(dir, '.claude', 'settings.local.json'),
|
|
275
|
+
JSON.stringify({ permissions: { allow: ['C'] } }),
|
|
276
|
+
);
|
|
277
|
+
// We can't write ~/.claude/settings.json safely in a test; verify B + C merge
|
|
278
|
+
const loaderPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../hooks/lib/settings-loader.cjs');
|
|
279
|
+
const { loadPermissions } = _require(loaderPath);
|
|
280
|
+
const perms = loadPermissions(dir);
|
|
281
|
+
assert.ok(perms.allow.includes('B'), 'project allow B missing');
|
|
282
|
+
assert.ok(perms.allow.includes('C'), 'local allow C missing');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
check('defaultMode local=plan overrides project=acceptEdits', () => {
|
|
286
|
+
const dir = makeProject({ defaultMode: 'acceptEdits' });
|
|
287
|
+
fs.writeFileSync(
|
|
288
|
+
path.join(dir, '.claude', 'settings.local.json'),
|
|
289
|
+
JSON.stringify({ permissions: { defaultMode: 'plan' } }),
|
|
290
|
+
);
|
|
291
|
+
const loaderPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../hooks/lib/settings-loader.cjs');
|
|
292
|
+
const { loadPermissions } = _require(loaderPath);
|
|
293
|
+
const perms = loadPermissions(dir);
|
|
294
|
+
assert.strictEqual(perms.defaultMode, 'plan');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
check('defaultMode local=bogus → coerced to default', () => {
|
|
298
|
+
const dir = makeProject({ defaultMode: 'bogus' });
|
|
299
|
+
const loaderPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../hooks/lib/settings-loader.cjs');
|
|
300
|
+
const { loadPermissions } = _require(loaderPath);
|
|
301
|
+
const perms = loadPermissions(dir);
|
|
302
|
+
assert.strictEqual(perms.defaultMode, 'default');
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── memory tool regression ────────────────────────────────────────────────────
|
|
307
|
+
{
|
|
308
|
+
check('acceptEdits + memory tool + outside-cwd → allow (bypass-fallback)', () => {
|
|
309
|
+
const dir = makeProject();
|
|
310
|
+
const outside = os.tmpdir();
|
|
311
|
+
// Use bare 'memory' (no mcp__ prefix) to avoid global mcp__* allow interference
|
|
312
|
+
// memory is not in READ_ONLY_TOOLS or EDIT_WRITE_TOOLS → mode-default applies
|
|
313
|
+
const r = ep({ toolName: 'memory', toolInput: { path: path.join(outside, 'x') }, permissionMode: 'acceptEdits', projectDir: dir, userCwd: dir });
|
|
314
|
+
assert.strictEqual(r.decision, 'allow', `Expected allow (bypass-fallback), got ${r.decision}: ${r.reason}`);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── New scenarios (P5.3) ──────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
// P5.3-1: user-global allow propagates when project settings absent
|
|
321
|
+
check('P5.3-1: user-global allow propagates when no project settings file', () => {
|
|
322
|
+
clearSettingsCache();
|
|
323
|
+
// Create a project dir with NO .claude/settings.json
|
|
324
|
+
const dir = makeProject();
|
|
325
|
+
// Load permissions — user-global may have entries but project is absent
|
|
326
|
+
// Just verify the loader returns the expected structure without throwing
|
|
327
|
+
const perms = loadPermissions(dir);
|
|
328
|
+
assert.ok(Array.isArray(perms.allow), 'allow must be array');
|
|
329
|
+
assert.ok(Array.isArray(perms.deny), 'deny must be array');
|
|
330
|
+
assert.ok(Array.isArray(perms.ask), 'ask must be array');
|
|
331
|
+
assert.ok(typeof perms.defaultMode === 'string', 'defaultMode must be string');
|
|
332
|
+
clearSettingsCache();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// P5.3-2: pattern with ** matches across path segments
|
|
336
|
+
check('P5.3-2: deny pattern mcp__*__bash matches mcp__plugin_mixdog_mixdog__bash via evaluator', () => {
|
|
337
|
+
clearSettingsCache();
|
|
338
|
+
const dir = makeProject({ deny: ['mcp__*__bash'] });
|
|
339
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__bash', toolInput: { cwd: dir }, permissionMode: 'default', projectDir: dir, userCwd: dir });
|
|
340
|
+
assert.strictEqual(r.decision, 'deny', `Expected deny via wildcard pattern, got ${r.decision}: ${r.reason}`);
|
|
341
|
+
clearSettingsCache();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// P5.3-3: mixed-slash path resolves correctly (forward + backward slash mix)
|
|
345
|
+
check('P5.3-3: mixed-slash path inside cwd → allow under default mode', () => {
|
|
346
|
+
clearSettingsCache();
|
|
347
|
+
const dir = makeProject();
|
|
348
|
+
// Build a path mixing slashes — normalizePath should handle it
|
|
349
|
+
const mixedPath = dir.replace(/\//g, '\\') + (process.platform === 'win32' ? '\\sub\\file.txt' : '/sub/file.txt');
|
|
350
|
+
const r = ep({ toolName: 'read', toolInput: { path: mixedPath }, permissionMode: 'default', projectDir: dir, userCwd: dir });
|
|
351
|
+
// On POSIX, backslash is a valid filename char — just assert no crash + returns a decision
|
|
352
|
+
assert.ok(['allow', 'ask', 'deny'].includes(r.decision), `Expected a valid decision, got: ${r.decision}`);
|
|
353
|
+
clearSettingsCache();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// P5.3-4: default + outside-cwd resolves to allow (bypass-fallback); no
|
|
357
|
+
// updatedInput.cwd injection because the request is allowed as-is.
|
|
358
|
+
check('P5.3-4: default + outside-cwd → allow (no updatedInput injection)', () => {
|
|
359
|
+
clearSettingsCache();
|
|
360
|
+
const dir = makeProject();
|
|
361
|
+
const outside = os.tmpdir();
|
|
362
|
+
const r = ep({ toolName: 'read', toolInput: { path: path.join(outside, 'foo.txt') }, permissionMode: 'default', projectDir: dir, userCwd: dir });
|
|
363
|
+
assert.strictEqual(r.decision, 'allow', `Expected allow (bypass-fallback), got ${r.decision}`);
|
|
364
|
+
clearSettingsCache();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ── Cross-matrix scenarios (0.1.292) ─────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
// 1. auto mode + outside-cwd → allow (treated as bypass per user policy)
|
|
370
|
+
check('[matrix] auto × outside-cwd → allow', () => {
|
|
371
|
+
clearSettingsCache();
|
|
372
|
+
const dir = makeProject();
|
|
373
|
+
const outside = os.tmpdir();
|
|
374
|
+
const r = ep({ toolName: 'bash', toolInput: { cwd: outside }, permissionMode: 'auto', projectDir: dir, userCwd: dir });
|
|
375
|
+
assert.strictEqual(r.decision, 'allow', `Expected allow (auto=bypass), got ${r.decision}: ${r.reason}`);
|
|
376
|
+
clearSettingsCache();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// 2. auto mode + inside-cwd → allow
|
|
380
|
+
check('[matrix] auto × inside-cwd → allow', () => {
|
|
381
|
+
clearSettingsCache();
|
|
382
|
+
const dir = makeProject();
|
|
383
|
+
const r = ep({ toolName: 'bash', toolInput: { cwd: dir }, permissionMode: 'auto', projectDir: dir, userCwd: dir });
|
|
384
|
+
assert.strictEqual(r.decision, 'allow', `Expected allow (default fallback, inside cwd), got ${r.decision}: ${r.reason}`);
|
|
385
|
+
clearSettingsCache();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// 3. dontAsk + list allow=read → allow (list short-circuits mode)
|
|
389
|
+
check('[matrix] dontAsk × list-allow → allow', () => {
|
|
390
|
+
clearSettingsCache();
|
|
391
|
+
const dir = makeProject({ allow: ['mcp__plugin_mixdog_mixdog__read'] });
|
|
392
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__read', toolInput: { path: path.join(dir, 'f.txt') }, permissionMode: 'dontAsk', projectDir: dir, userCwd: dir });
|
|
393
|
+
assert.strictEqual(r.decision, 'allow', `Expected allow (list overrides dontAsk), got ${r.decision}: ${r.reason}`);
|
|
394
|
+
clearSettingsCache();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// 4. dontAsk + list ask=edit → ask (list short-circuits mode)
|
|
398
|
+
check('[matrix] dontAsk × list-ask → ask', () => {
|
|
399
|
+
clearSettingsCache();
|
|
400
|
+
const dir = makeProject({ ask: ['mcp__plugin_mixdog_mixdog__edit'] });
|
|
401
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__edit', toolInput: {}, permissionMode: 'dontAsk', projectDir: dir, userCwd: dir });
|
|
402
|
+
assert.strictEqual(r.decision, 'ask', `Expected ask (list overrides dontAsk), got ${r.decision}: ${r.reason}`);
|
|
403
|
+
clearSettingsCache();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// 5. bypassPermissions + list deny=bash → deny (deny list wins over bypass)
|
|
407
|
+
check('[matrix] bypassPermissions × list-deny → deny', () => {
|
|
408
|
+
clearSettingsCache();
|
|
409
|
+
const dir = makeProject({ deny: ['mcp__plugin_mixdog_mixdog__bash'] });
|
|
410
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__bash', toolInput: { cwd: dir }, permissionMode: 'bypassPermissions', projectDir: dir, userCwd: dir });
|
|
411
|
+
assert.strictEqual(r.decision, 'deny', `Expected deny (list overrides bypass), got ${r.decision}: ${r.reason}`);
|
|
412
|
+
clearSettingsCache();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// 6. acceptEdits + list deny=write → deny (deny list wins over mode)
|
|
416
|
+
check('[matrix] acceptEdits × list-deny → deny', () => {
|
|
417
|
+
clearSettingsCache();
|
|
418
|
+
const dir = makeProject({ deny: ['mcp__plugin_mixdog_mixdog__write'] });
|
|
419
|
+
const r = ep({ toolName: 'mcp__plugin_mixdog_mixdog__write', toolInput: { path: path.join(dir, 'out.txt') }, permissionMode: 'acceptEdits', projectDir: dir, userCwd: dir });
|
|
420
|
+
assert.strictEqual(r.decision, 'deny', `Expected deny (list overrides acceptEdits), got ${r.decision}: ${r.reason}`);
|
|
421
|
+
clearSettingsCache();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
425
|
+
console.log(`\npermission-eval-smoke: ${pass} passed, ${fail} failed`);
|
|
426
|
+
process.exit(fail > 0 ? 1 : 0);
|