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,411 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* permission-evaluator.cjs
|
|
4
|
+
* Reusable permission evaluation extracted from pre-mcp-sandbox.cjs.
|
|
5
|
+
*
|
|
6
|
+
* Permission priority: deny > ask > allow > mode-default
|
|
7
|
+
*
|
|
8
|
+
* Exported function:
|
|
9
|
+
* evaluatePermission({ toolName, toolInput, permissionMode, projectDir, userCwd, permissions })
|
|
10
|
+
* → { decision: 'allow'|'deny'|'ask', reason: string }
|
|
11
|
+
*
|
|
12
|
+
* `permissions` (optional): pre-loaded `{ allow, deny, ask, defaultMode }` from
|
|
13
|
+
* settings-loader. When provided, evaluator skips its own loadPermissions call.
|
|
14
|
+
* The PreToolUse hook / hook-pipe-server already loads settings to gate the
|
|
15
|
+
* bypass fast-path; passing it through avoids a second 3-tier merge + stat round.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const { loadPermissions } = require('./settings-loader.cjs');
|
|
23
|
+
const { evaluateRules, isReadOnlyTool } = require('./permission-rules.cjs');
|
|
24
|
+
|
|
25
|
+
// ── constants ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const MCP_PREFIXES = [
|
|
28
|
+
'mcp__plugin_mixdog_mixdog__',
|
|
29
|
+
'mcp__plugin_mixdog_trib-plugin__',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// edit/write-class tools allowed under acceptEdits mode
|
|
33
|
+
const EDIT_WRITE_TOOLS = new Set([
|
|
34
|
+
'edit', 'write', 'apply_patch',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// ── hard-deny patterns (bypass-proof) ────────────────────────────────────────
|
|
38
|
+
// These patterns are evaluated BEFORE mode checks, including bypassPermissions.
|
|
39
|
+
// They cover UNC paths and dangerous absolute system locations.
|
|
40
|
+
|
|
41
|
+
const HARD_DENY_PATH_PATTERNS = [
|
|
42
|
+
// UNC network paths (\\server\share)
|
|
43
|
+
/^\\\\/,
|
|
44
|
+
// Unix system sensitive dirs
|
|
45
|
+
/^\/etc\//i,
|
|
46
|
+
/^\/etc$/i,
|
|
47
|
+
/^\/proc\//i,
|
|
48
|
+
/^\/proc$/i,
|
|
49
|
+
/^\/sys\//i,
|
|
50
|
+
/^\/sys$/i,
|
|
51
|
+
/^\/boot\//i,
|
|
52
|
+
/^\/boot$/i,
|
|
53
|
+
/^\/dev\//i,
|
|
54
|
+
/^\/dev$/i,
|
|
55
|
+
// Windows system dirs (various drive letters)
|
|
56
|
+
/^[a-z]:[/\\]windows[/\\]/i,
|
|
57
|
+
/^[a-z]:[/\\]windows$/i,
|
|
58
|
+
/^[a-z]:[/\\]program files[/\\]/i,
|
|
59
|
+
/^[a-z]:[/\\]program files$/i,
|
|
60
|
+
/^[a-z]:[/\\]program files \(x86\)[/\\]/i,
|
|
61
|
+
/^[a-z]:[/\\]program files \(x86\)$/i,
|
|
62
|
+
/^[a-z]:[/\\]system32/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if any extracted path matches a hard-deny pattern.
|
|
67
|
+
* Called before mode checks — bypass-proof.
|
|
68
|
+
*
|
|
69
|
+
* Symlink/junction-resilient: also probes the realpath of each candidate so
|
|
70
|
+
* a symlink (or NTFS junction) pointing into a forbidden directory cannot
|
|
71
|
+
* smuggle access. Without this, `read({ path: <tmp>/safe })` where
|
|
72
|
+
* <tmp>/safe is a junction to C:/Windows would slip past the literal
|
|
73
|
+
* pattern match against the surface path and access protected content at
|
|
74
|
+
* runtime via fs.readFile's transparent link resolution.
|
|
75
|
+
*/
|
|
76
|
+
// NTFS strips trailing dots and spaces from every path component at the
|
|
77
|
+
// filesystem layer, so `C:\Windows \foo` and `C:\Windows.\foo` actually
|
|
78
|
+
// resolve to `C:\Windows\foo` even though the surface string keeps the
|
|
79
|
+
// trailing chars. Without this normalisation a deny rule on /Windows/
|
|
80
|
+
// could be bypassed by appending a stray space/dot before the next
|
|
81
|
+
// separator. Applies to each `\` and `/` separated segment; safe on
|
|
82
|
+
// POSIX too (the segments simply don't contain trailing spaces and the
|
|
83
|
+
// transform is a no-op).
|
|
84
|
+
function _stripNtfsTrailingChars(p) {
|
|
85
|
+
return String(p).replace(/[. ]+(?=[\\/]|$)/g, '');
|
|
86
|
+
}
|
|
87
|
+
function isHardDenyPath(rawPaths, opts) {
|
|
88
|
+
const trustedRootsRaw = (opts && Array.isArray(opts.trustedRoots)) ? opts.trustedRoots : [];
|
|
89
|
+
const baseCwd = (opts && typeof opts.cwd === 'string' && opts.cwd) ? opts.cwd : process.cwd();
|
|
90
|
+
// Pre-normalize trusted roots once per call (windows: case-insensitive).
|
|
91
|
+
const trustedRoots = [];
|
|
92
|
+
for (const r of trustedRootsRaw) {
|
|
93
|
+
if (!r || typeof r !== 'string') continue;
|
|
94
|
+
let n;
|
|
95
|
+
try { n = path.resolve(r); } catch { n = r; }
|
|
96
|
+
n = n.replace(/[\\/]+$/, '');
|
|
97
|
+
if (path.sep === '\\') n = n.toLowerCase();
|
|
98
|
+
if (n) trustedRoots.push(n);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const p of rawPaths) {
|
|
102
|
+
if (!p || typeof p !== 'string') continue;
|
|
103
|
+
// UNC check on raw value (before normalization strips leading slashes)
|
|
104
|
+
if (/^\\\\/.test(p)) return true;
|
|
105
|
+
// Normalize for platform-independent matching
|
|
106
|
+
let norm;
|
|
107
|
+
try { norm = path.resolve(p); } catch { norm = p; }
|
|
108
|
+
norm = norm.replace(/\\/g, '/');
|
|
109
|
+
for (const re of HARD_DENY_PATH_PATTERNS) {
|
|
110
|
+
if (re.test(p) || re.test(norm)) return true;
|
|
111
|
+
}
|
|
112
|
+
// NTFS trailing-char strip: `C:\Windows \foo` resolves to `C:\Windows\foo`
|
|
113
|
+
// at the filesystem layer. Re-test both the surface and resolved forms
|
|
114
|
+
// with trailing dots/spaces stripped from every segment.
|
|
115
|
+
const stripped = _stripNtfsTrailingChars(p);
|
|
116
|
+
const strippedNorm = _stripNtfsTrailingChars(norm);
|
|
117
|
+
if (stripped !== p || strippedNorm !== norm) {
|
|
118
|
+
for (const re of HARD_DENY_PATH_PATTERNS) {
|
|
119
|
+
if (re.test(stripped) || re.test(strippedNorm)) return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Fast-path: skip realpath when the resolved path lives under a trusted
|
|
123
|
+
// root (cwd / plugin root / project dir). Junction-to-system-dir attacks
|
|
124
|
+
// inside user-owned trees are not part of this threat model — the surface
|
|
125
|
+
// pattern checks above still catch any path that literally names a
|
|
126
|
+
// protected system dir, and realpath() on every read is the dominant
|
|
127
|
+
// syscall cost on the hot path. Paths outside the trusted set (writes
|
|
128
|
+
// against absolute system paths, untrusted external roots) still fall
|
|
129
|
+
// through to the realpath check below.
|
|
130
|
+
if (trustedRoots.length > 0) {
|
|
131
|
+
let resolved;
|
|
132
|
+
try { resolved = path.isAbsolute(p) ? path.normalize(p) : path.resolve(baseCwd, p); }
|
|
133
|
+
catch { resolved = null; }
|
|
134
|
+
if (resolved) {
|
|
135
|
+
let cmp = resolved.replace(/[\\/]+$/, '');
|
|
136
|
+
if (path.sep === '\\') cmp = cmp.toLowerCase();
|
|
137
|
+
let inTrusted = false;
|
|
138
|
+
for (const root of trustedRoots) {
|
|
139
|
+
if (cmp === root || cmp.startsWith(root + '\\') || cmp.startsWith(root + '/')) {
|
|
140
|
+
inTrusted = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (inTrusted) continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Resolve symlinks / junctions and re-test. realpathSync throws ENOENT
|
|
148
|
+
// for paths that don't exist yet (legitimate for write/create flows),
|
|
149
|
+
// so missing paths fall through with no extra check — only the literal
|
|
150
|
+
// surface form is enforced for those.
|
|
151
|
+
let real;
|
|
152
|
+
try { real = fs.realpathSync(p); }
|
|
153
|
+
catch { real = null; }
|
|
154
|
+
if (real && real !== p && real !== norm.replace(/\//g, path.sep)) {
|
|
155
|
+
const realNorm = real.replace(/\\/g, '/');
|
|
156
|
+
for (const re of HARD_DENY_PATH_PATTERNS) {
|
|
157
|
+
if (re.test(real) || re.test(realNorm)) return true;
|
|
158
|
+
}
|
|
159
|
+
// Re-test UNC after symlink resolve too: a local-looking path could
|
|
160
|
+
// realpath to a network share and would otherwise dodge the surface
|
|
161
|
+
// UNC test above.
|
|
162
|
+
if (/^\\\\/.test(real)) return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports._isHardDenyPath = isHardDenyPath; // exported for tests
|
|
169
|
+
|
|
170
|
+
// ── path helpers ──────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function normalizePath(p) {
|
|
173
|
+
if (!p || typeof p !== 'string') return null;
|
|
174
|
+
if (/^\\\\/.test(p)) return p;
|
|
175
|
+
|
|
176
|
+
if (path.sep === '\\') {
|
|
177
|
+
const posixDriveMatch = p.match(/^\/([a-zA-Z])(\/.*)?$/);
|
|
178
|
+
if (posixDriveMatch) {
|
|
179
|
+
const drive = posixDriveMatch[1].toUpperCase();
|
|
180
|
+
const rest = (posixDriveMatch[2] || '').replace(/\//g, '\\');
|
|
181
|
+
p = drive + ':' + (rest || '\\');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
return path.isAbsolute(p) ? path.normalize(p) : null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveCandidate(p, baseCwd) {
|
|
193
|
+
if (!p || typeof p !== 'string') return null;
|
|
194
|
+
if (/^\\\\/.test(p)) return p;
|
|
195
|
+
try {
|
|
196
|
+
const normalized = normalizePath(p);
|
|
197
|
+
if (normalized !== null && path.isAbsolute(normalized)) return normalized;
|
|
198
|
+
return path.resolve(baseCwd, p);
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isInside(child, parent) {
|
|
205
|
+
const norm = p => p.replace(/[/\\]+$/, '');
|
|
206
|
+
let c = norm(child);
|
|
207
|
+
let p2 = norm(parent);
|
|
208
|
+
if (path.sep === '\\') { c = c.toLowerCase(); p2 = p2.toLowerCase(); }
|
|
209
|
+
return c === p2 || c.startsWith(p2 + '\\') || c.startsWith(p2 + '/');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function deepestExistingAncestor(p) {
|
|
213
|
+
let cur = p;
|
|
214
|
+
while (cur) {
|
|
215
|
+
try { if (fs.existsSync(cur) && fs.statSync(cur).isDirectory()) return cur; } catch { /* walk */ }
|
|
216
|
+
const parent = path.dirname(cur);
|
|
217
|
+
if (parent === cur) break;
|
|
218
|
+
cur = parent;
|
|
219
|
+
}
|
|
220
|
+
return path.dirname(p);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function extractPaths(toolName, toolInput) {
|
|
224
|
+
if (!toolInput || typeof toolInput !== 'object') return [];
|
|
225
|
+
|
|
226
|
+
const prefix = MCP_PREFIXES.find(p => toolName.startsWith(p));
|
|
227
|
+
const tool = prefix ? toolName.slice(prefix.length) : toolName;
|
|
228
|
+
const candidates = [];
|
|
229
|
+
|
|
230
|
+
const push = (...vals) => {
|
|
231
|
+
for (const v of vals) { if (v && typeof v === 'string') candidates.push(v); }
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
switch (tool) {
|
|
235
|
+
case 'bash':
|
|
236
|
+
case 'bash_session':
|
|
237
|
+
push(toolInput.cwd);
|
|
238
|
+
break;
|
|
239
|
+
case 'bridge':
|
|
240
|
+
push(toolInput.cwd);
|
|
241
|
+
push(toolInput.file);
|
|
242
|
+
break;
|
|
243
|
+
case 'apply_patch': {
|
|
244
|
+
push(toolInput.base_path);
|
|
245
|
+
const patch = toolInput.patch;
|
|
246
|
+
if (typeof patch === 'string') {
|
|
247
|
+
for (const m of patch.matchAll(/^\+\+\+\s+b\/(.+)$/gm)) push(m[1]);
|
|
248
|
+
for (const m of patch.matchAll(/^---\s+a\/(.+)$/gm)) push(m[1]);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'read': {
|
|
253
|
+
if (toolInput.path && typeof toolInput.path === 'string') push(toolInput.path);
|
|
254
|
+
// CC `file_path` alias: builtin.mjs:3835 maps args.file_path → args.path
|
|
255
|
+
// before execution, so the permission gate must inspect it too or a
|
|
256
|
+
// deny rule on /Windows can be bypassed by sending file_path:.../Windows/...
|
|
257
|
+
push(toolInput.file_path);
|
|
258
|
+
push(toolInput.cwd);
|
|
259
|
+
if (Array.isArray(toolInput.reads)) toolInput.reads.forEach(r => push(r?.path));
|
|
260
|
+
if (Array.isArray(toolInput.path)) toolInput.path.forEach(p => {
|
|
261
|
+
if (p && typeof p === 'string') push(p);
|
|
262
|
+
else if (p && typeof p === 'object' && p.path) push(p.path);
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
default:
|
|
267
|
+
if (toolInput.path && typeof toolInput.path === 'string') push(toolInput.path);
|
|
268
|
+
// CC `file_path` alias: builtin.mjs:4282/4439 map args.file_path →
|
|
269
|
+
// args.path before execution. Without it edit/write deny rules on a
|
|
270
|
+
// forbidden directory can be bypassed by sending file_path:.../forbidden/...
|
|
271
|
+
push(toolInput.file, toolInput.file_path, toolInput.cwd, toolInput.base_path);
|
|
272
|
+
if (Array.isArray(toolInput.path)) toolInput.path.forEach(p => push(p));
|
|
273
|
+
if (Array.isArray(toolInput.reads)) toolInput.reads.forEach(r => push(r?.path));
|
|
274
|
+
// Per-edit / per-write file_path alias mirrors builtin.mjs:4440 — each
|
|
275
|
+
// item in edits[]/writes[] can carry file_path that overrides the top-level
|
|
276
|
+
// path; collect both forms so deny rules apply uniformly.
|
|
277
|
+
if (Array.isArray(toolInput.edits)) toolInput.edits.forEach(e => push(e?.path, e?.file_path));
|
|
278
|
+
if (Array.isArray(toolInput.writes)) toolInput.writes.forEach(w => push(w?.path, w?.file_path));
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [...new Set(candidates)];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── main evaluator ────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Evaluate whether a tool call should be allowed, denied, or asked.
|
|
289
|
+
*
|
|
290
|
+
* @param {object} opts
|
|
291
|
+
* @param {string} opts.toolName — full tool name (mcp__ prefix expected for mixdog tools)
|
|
292
|
+
* @param {object} opts.toolInput — tool arguments object
|
|
293
|
+
* @param {string} [opts.permissionMode] — override mode ('bypassPermissions', 'acceptEdits',
|
|
294
|
+
* 'plan', 'dontAsk', 'default'). Falls back to
|
|
295
|
+
* settings defaultMode.
|
|
296
|
+
* @param {string} [opts.projectDir] — project root for settings lookup
|
|
297
|
+
* @param {string} [opts.userCwd] — user working directory for path resolution
|
|
298
|
+
* @returns {{ decision: 'allow'|'deny'|'ask', reason: string, updatedInput?: object }}
|
|
299
|
+
*/
|
|
300
|
+
function evaluatePermission({ toolName, toolInput, permissionMode, projectDir, userCwd, permissions }) {
|
|
301
|
+
const name = typeof toolName === 'string' ? toolName : '';
|
|
302
|
+
const input = (toolInput && typeof toolInput === 'object') ? toolInput : {};
|
|
303
|
+
const cwd = (typeof userCwd === 'string' && userCwd) ? userCwd : process.cwd();
|
|
304
|
+
|
|
305
|
+
// Single extractPaths call — reused for hard-deny, sandbox, and plugin-root checks.
|
|
306
|
+
const rawPaths = extractPaths(name, input);
|
|
307
|
+
|
|
308
|
+
// Trusted-root set powers the hard-deny realpath fast-path.
|
|
309
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
|
|
310
|
+
const trustedRoots = [cwd];
|
|
311
|
+
if (pluginRoot) trustedRoots.push(pluginRoot);
|
|
312
|
+
if (projectDir && projectDir !== cwd) trustedRoots.push(projectDir);
|
|
313
|
+
|
|
314
|
+
// 0. Hard-deny: bypass-proof path check (UNC, dangerous system paths).
|
|
315
|
+
// Evaluated before any mode check — even bypassPermissions cannot override.
|
|
316
|
+
if (isHardDenyPath(rawPaths, { trustedRoots, cwd })) {
|
|
317
|
+
return { decision: 'deny', reason: `Tool '${name}' targets a protected system path.` };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Plugin source tree: read-only exemption.
|
|
321
|
+
// Paths inside CLAUDE_PLUGIN_ROOT are always allowed for read-class tools.
|
|
322
|
+
if (pluginRoot && isReadOnlyTool(name) && rawPaths.length > 0 &&
|
|
323
|
+
rawPaths.every(p => { const r = resolveCandidate(p, cwd); return r && isInside(r, pluginRoot); })) {
|
|
324
|
+
return { decision: 'allow', reason: 'Plugin source tree read-only access allowed.' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 1. Resolve paths; find first outside-cwd hit
|
|
328
|
+
let firstOutsidePath = null;
|
|
329
|
+
let firstOutsideResolved = null;
|
|
330
|
+
|
|
331
|
+
for (const raw of rawPaths) {
|
|
332
|
+
const resolved = resolveCandidate(raw, cwd);
|
|
333
|
+
if (!resolved) continue;
|
|
334
|
+
if (!isInside(resolved, cwd) && firstOutsidePath === null) {
|
|
335
|
+
firstOutsidePath = raw;
|
|
336
|
+
firstOutsideResolved = resolved;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 2. Use caller-supplied settings when available; otherwise load.
|
|
341
|
+
const { allow, deny, ask, defaultMode } = (permissions && typeof permissions === 'object')
|
|
342
|
+
? permissions
|
|
343
|
+
: loadPermissions(projectDir || cwd);
|
|
344
|
+
|
|
345
|
+
// 4. Permission-list evaluation (deny > ask > allow)
|
|
346
|
+
const listResult = evaluateRules(name, input, allow, deny, ask);
|
|
347
|
+
|
|
348
|
+
if (listResult === 'deny') {
|
|
349
|
+
return { decision: 'deny', reason: `Tool '${name}' blocked by deny rule.` };
|
|
350
|
+
}
|
|
351
|
+
if (listResult === 'ask') {
|
|
352
|
+
const outsideReason = firstOutsidePath
|
|
353
|
+
? `Path '${firstOutsidePath}' is outside project sandbox (${cwd}).`
|
|
354
|
+
: `Tool '${name}' requires explicit approval.`;
|
|
355
|
+
const updatedInput = firstOutsideResolved
|
|
356
|
+
? { cwd: deepestExistingAncestor(firstOutsideResolved) }
|
|
357
|
+
: undefined;
|
|
358
|
+
return { decision: 'ask', reason: outsideReason, ...(updatedInput ? { updatedInput } : {}) };
|
|
359
|
+
}
|
|
360
|
+
if (listResult === 'allow') {
|
|
361
|
+
return { decision: 'allow', reason: 'Matched allow rule.' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 5. Mode default (no list matched)
|
|
365
|
+
// Settings-derived auto-approval modes take priority over a payload
|
|
366
|
+
// 'default' so that a user-level bypassPermissions is never shadowed.
|
|
367
|
+
const AUTO_MODES = new Set(['bypassPermissions', 'auto']);
|
|
368
|
+
const mode = (AUTO_MODES.has(defaultMode) && !AUTO_MODES.has(permissionMode))
|
|
369
|
+
? defaultMode
|
|
370
|
+
: (permissionMode || defaultMode || 'default');
|
|
371
|
+
|
|
372
|
+
if (AUTO_MODES.has(mode)) {
|
|
373
|
+
return { decision: 'allow', reason: 'bypassPermissions mode.' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (mode === 'acceptEdits') {
|
|
377
|
+
const prefix = MCP_PREFIXES.find(p => name.startsWith(p));
|
|
378
|
+
const shortTool = prefix ? name.slice(prefix.length) : name;
|
|
379
|
+
if (isReadOnlyTool(name) || EDIT_WRITE_TOOLS.has(shortTool)) {
|
|
380
|
+
return { decision: 'allow', reason: 'acceptEdits mode: read-only or edit/write tool.' };
|
|
381
|
+
}
|
|
382
|
+
if (firstOutsideResolved !== null) {
|
|
383
|
+
return { decision: 'ask', reason: `Tool '${name}' is outside project sandbox in acceptEdits mode.` };
|
|
384
|
+
}
|
|
385
|
+
return { decision: 'allow', reason: 'acceptEdits mode: tool is inside project sandbox.' };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (mode === 'plan') {
|
|
389
|
+
if (isReadOnlyTool(name)) {
|
|
390
|
+
return { decision: 'allow', reason: 'plan mode: read-only tool.' };
|
|
391
|
+
}
|
|
392
|
+
return { decision: 'ask', reason: `Tool '${name}' is not allowed in plan mode.` };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (mode === 'dontAsk') {
|
|
396
|
+
return { decision: 'deny', reason: `Tool '${name}' not matched by any allow rule (dontAsk mode).` };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// default / unknown mode
|
|
400
|
+
if (firstOutsideResolved !== null) {
|
|
401
|
+
return {
|
|
402
|
+
decision: 'ask',
|
|
403
|
+
reason: `Path '${firstOutsidePath}' is outside project sandbox (${cwd}). Approve to grant mcp access.`,
|
|
404
|
+
updatedInput: { cwd: deepestExistingAncestor(firstOutsideResolved) },
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { decision: 'allow', reason: 'default mode: tool is inside project sandbox.' };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
module.exports.evaluatePermission = evaluatePermission;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Shared helper for permission-request hooks to decide whether a prompt can
|
|
4
|
+
* actually be routed to Discord (owner terminal is up + HTTP server live)
|
|
5
|
+
* or must fall through to the built-in terminal prompt.
|
|
6
|
+
*
|
|
7
|
+
* Used by:
|
|
8
|
+
* - hooks/pre-tool-subagent.cjs (sub-agent protected-path requests)
|
|
9
|
+
*
|
|
10
|
+
* Routing criteria (all must hold for `discord`):
|
|
11
|
+
* 1. `active-instance.json` exists and has a non-empty `instanceId`
|
|
12
|
+
* 2. Active owner PID is alive (`ownerLeadPid` → `terminalLeadPid` →
|
|
13
|
+
* legacy `supervisor_pid` → `instanceId`)
|
|
14
|
+
* 3. Either `active.httpPort` is set, or `bridge-state.json.active === true`
|
|
15
|
+
*
|
|
16
|
+
* Returns: `{ route: 'discord' | 'terminal', httpPort?: number }`
|
|
17
|
+
*
|
|
18
|
+
* Not exported: the Discord API wiring itself — each hook keeps its own
|
|
19
|
+
* flow. This helper only answers the routing question.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const {
|
|
26
|
+
getActiveOwnerPid,
|
|
27
|
+
getPermissionInstanceId,
|
|
28
|
+
getPermissionInstanceIds,
|
|
29
|
+
isPidAlive,
|
|
30
|
+
readActiveInstance,
|
|
31
|
+
} = require('./active-instance.cjs');
|
|
32
|
+
|
|
33
|
+
const RUNTIME_ROOT = path.join(os.tmpdir(), 'mixdog');
|
|
34
|
+
const BRIDGE_STATE_FILE = path.join(RUNTIME_ROOT, 'bridge-state.json');
|
|
35
|
+
|
|
36
|
+
function readJson(file) {
|
|
37
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
38
|
+
catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shouldRoutePermissionToDiscord() {
|
|
42
|
+
const active = readActiveInstance();
|
|
43
|
+
if (!active || !active.instanceId) return { route: 'terminal', reason: 'no-active-instance' };
|
|
44
|
+
const ownerPid = getActiveOwnerPid(active);
|
|
45
|
+
if (!isPidAlive(ownerPid)) return { route: 'terminal', reason: 'owner-dead' };
|
|
46
|
+
|
|
47
|
+
const hasHttpPort = typeof active.httpPort === 'number' && active.httpPort > 0;
|
|
48
|
+
const bridgeState = readJson(BRIDGE_STATE_FILE);
|
|
49
|
+
const bridgeActive = bridgeState && bridgeState.active === true;
|
|
50
|
+
|
|
51
|
+
if (!hasHttpPort && !bridgeActive) return { route: 'terminal', reason: 'bridge-inactive' };
|
|
52
|
+
const common = {
|
|
53
|
+
active,
|
|
54
|
+
ownerPid,
|
|
55
|
+
permissionInstanceId: getPermissionInstanceId(active),
|
|
56
|
+
permissionInstanceIds: getPermissionInstanceIds(active),
|
|
57
|
+
};
|
|
58
|
+
return hasHttpPort
|
|
59
|
+
? { route: 'discord', httpPort: active.httpPort, ...common }
|
|
60
|
+
: { route: 'discord', ...common };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { shouldRoutePermissionToDiscord };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* permission-rules.cjs
|
|
4
|
+
* Pattern matcher and priority evaluator. Priority order and Tool(content)
|
|
5
|
+
* parsing mirror Claude Code native logic; specifier matching uses a local
|
|
6
|
+
* path/command glob (native delegates per-tool content matching to each tool).
|
|
7
|
+
*
|
|
8
|
+
* Priority (highest → lowest): deny > ask > allow > mode default
|
|
9
|
+
*
|
|
10
|
+
* Pattern forms:
|
|
11
|
+
* mcp__* → toolName starts with "mcp__"
|
|
12
|
+
* ToolName → exact toolName match (no parens)
|
|
13
|
+
* ToolName(*) → toolName matches AND specifier glob matches any
|
|
14
|
+
* ToolName(specifier) → toolName matches AND specifier glob matches
|
|
15
|
+
* toolInput.path / .command / .file
|
|
16
|
+
* Glob chars: * (any chars within segment), ** (any segments)
|
|
17
|
+
*
|
|
18
|
+
* No external deps; no eval/Function.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Read-only tool set (mixdog tool names).
|
|
22
|
+
const READ_ONLY_TOOLS = new Set([
|
|
23
|
+
'read', 'list', 'glob', 'grep', 'find_symbol', 'find_references', 'find_callers', 'explore',
|
|
24
|
+
'recall', 'search', 'fetch', 'web_fetch',
|
|
25
|
+
'schedule_status', 'list_models', 'job_wait', 'download_attachment',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the index of the first/last UNescaped occurrence of a char.
|
|
30
|
+
* A char is escaped when preceded by an odd number of backslashes.
|
|
31
|
+
* Mirrors native permissionRuleParser.findFirst/LastUnescapedChar.
|
|
32
|
+
*/
|
|
33
|
+
function findFirstUnescapedChar(str, char) {
|
|
34
|
+
for (let i = 0; i < str.length; i++) {
|
|
35
|
+
if (str[i] !== char) continue;
|
|
36
|
+
let bs = 0, j = i - 1;
|
|
37
|
+
while (j >= 0 && str[j] === '\\') { bs++; j--; }
|
|
38
|
+
if (bs % 2 === 0) return i;
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findLastUnescapedChar(str, char) {
|
|
44
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
45
|
+
if (str[i] !== char) continue;
|
|
46
|
+
let bs = 0, j = i - 1;
|
|
47
|
+
while (j >= 0 && str[j] === '\\') { bs++; j--; }
|
|
48
|
+
if (bs % 2 === 0) return i;
|
|
49
|
+
}
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reverse escapeRuleContent: \( -> (, \) -> ), \\ -> \ (order matters). */
|
|
54
|
+
function unescapeRuleContent(content) {
|
|
55
|
+
return content
|
|
56
|
+
.replace(/\\\(/g, '(')
|
|
57
|
+
.replace(/\\\)/g, ')')
|
|
58
|
+
.replace(/\\\\/g, '\\');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert a glob pattern string to a RegExp.
|
|
63
|
+
* Supports: * (non-slash wildcard), ** (any), ? (single char).
|
|
64
|
+
* Safe: no eval.
|
|
65
|
+
*/
|
|
66
|
+
function globToRegex(glob) {
|
|
67
|
+
let src = '';
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < glob.length) {
|
|
70
|
+
const ch = glob[i];
|
|
71
|
+
if (ch === '*' && glob[i + 1] === '*') {
|
|
72
|
+
src += '.*';
|
|
73
|
+
i += 2;
|
|
74
|
+
if (glob[i] === '/') i++; // consume trailing slash
|
|
75
|
+
} else if (ch === '*') {
|
|
76
|
+
src += '[^/\\\\]*';
|
|
77
|
+
i++;
|
|
78
|
+
} else if (ch === '?') {
|
|
79
|
+
src += '[^/\\\\]';
|
|
80
|
+
i++;
|
|
81
|
+
} else {
|
|
82
|
+
// Escape regex metacharacters
|
|
83
|
+
src += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return new RegExp('^' + src + '$', 'i');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Match a single pattern entry against (toolName, toolInput).
|
|
92
|
+
* Returns true if the pattern applies.
|
|
93
|
+
*/
|
|
94
|
+
function matchesPattern(pattern, toolName, toolInput) {
|
|
95
|
+
if (typeof pattern !== 'string') return false;
|
|
96
|
+
|
|
97
|
+
// Detect Tool(specifier) form. Escape-aware, mirroring native
|
|
98
|
+
// permissionRuleValueFromString: only UNescaped parens delimit the
|
|
99
|
+
// specifier, and the close paren must be the final char.
|
|
100
|
+
const parenIdx = findFirstUnescapedChar(pattern, '(');
|
|
101
|
+
const closeIdx = findLastUnescapedChar(pattern, ')');
|
|
102
|
+
if (parenIdx > 0 && closeIdx === pattern.length - 1 && closeIdx > parenIdx) {
|
|
103
|
+
const namePart = pattern.slice(0, parenIdx);
|
|
104
|
+
const rawSpecifier = pattern.slice(parenIdx + 1, closeIdx);
|
|
105
|
+
|
|
106
|
+
if (toolName !== namePart) return false;
|
|
107
|
+
|
|
108
|
+
// Empty "()" or wildcard "(*)" specifier → tool-wide rule, match any
|
|
109
|
+
if (rawSpecifier === '' || rawSpecifier === '*') return true;
|
|
110
|
+
|
|
111
|
+
// Unescape \( \) before glob matching
|
|
112
|
+
const specifier = unescapeRuleContent(rawSpecifier);
|
|
113
|
+
// Match specifier against known path-bearing fields
|
|
114
|
+
const specRe = globToRegex(specifier);
|
|
115
|
+
const inp = toolInput || {};
|
|
116
|
+
const candidates = [...new Set([
|
|
117
|
+
inp.path, inp.command, inp.file, inp.file_path, inp.base_path, inp.cwd,
|
|
118
|
+
...(Array.isArray(inp.path) ? inp.path.map(p => (p && typeof p === 'object' ? p.path : p)) : []),
|
|
119
|
+
...(Array.isArray(inp.reads) ? inp.reads.map(r => r?.path) : []),
|
|
120
|
+
...(Array.isArray(inp.edits) ? inp.edits.flatMap(e => [e?.path, e?.file_path]) : []),
|
|
121
|
+
...(Array.isArray(inp.writes) ? inp.writes.flatMap(w => [w?.path, w?.file_path]) : []),
|
|
122
|
+
].filter(v => typeof v === 'string'))];
|
|
123
|
+
|
|
124
|
+
return candidates.some(c => specRe.test(c));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Prefix wildcard: e.g. "mcp__*"
|
|
128
|
+
if (pattern.endsWith('*')) {
|
|
129
|
+
const prefix = pattern.slice(0, -1);
|
|
130
|
+
return toolName.startsWith(prefix);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Glob on toolName itself (may contain * or **)
|
|
134
|
+
if (pattern.includes('*') || pattern.includes('?')) {
|
|
135
|
+
return globToRegex(pattern).test(toolName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Exact match
|
|
139
|
+
return toolName === pattern;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Evaluate priority rules against a tool call.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} toolName
|
|
146
|
+
* @param {object} toolInput
|
|
147
|
+
* @param {string[]} allowList
|
|
148
|
+
* @param {string[]} denyList
|
|
149
|
+
* @param {string[]} askList
|
|
150
|
+
* @returns {'deny'|'ask'|'allow'|null} null = no list matched (use mode default)
|
|
151
|
+
*/
|
|
152
|
+
function evaluateRules(toolName, toolInput, allowList, denyList, askList) {
|
|
153
|
+
if (denyList.some(p => matchesPattern(p, toolName, toolInput))) return 'deny';
|
|
154
|
+
if (askList.some(p => matchesPattern(p, toolName, toolInput))) return 'ask';
|
|
155
|
+
if (allowList.some(p => matchesPattern(p, toolName, toolInput))) return 'allow';
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Determine whether a tool is considered read-only.
|
|
161
|
+
* For mcp__ tools: strip the mcp__ prefix chain and check the short tool name.
|
|
162
|
+
*/
|
|
163
|
+
function isReadOnlyTool(toolName) {
|
|
164
|
+
// Strip mcp__plugin_...__ prefix (one or more double-underscore segments)
|
|
165
|
+
const parts = toolName.split('__');
|
|
166
|
+
const shortName = parts[parts.length - 1];
|
|
167
|
+
return READ_ONLY_TOOLS.has(shortName);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { matchesPattern, evaluateRules, isReadOnlyTool, READ_ONLY_TOOLS };
|