mixdog 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- 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/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { stdout, stderr } from 'node:process';
|
|
2
|
+
import { createStandaloneAgent } from './standalone/agent-tool.mjs';
|
|
3
|
+
import * as cfgMod from './runtime/agent/orchestrator/config.mjs';
|
|
4
|
+
import * as reg from './runtime/agent/orchestrator/providers/registry.mjs';
|
|
5
|
+
import * as mgr from './runtime/agent/orchestrator/session/manager.mjs';
|
|
6
|
+
|
|
7
|
+
const TERMINAL_STATUS_RE = /^status:\s*(completed|failed|error|cancelled|canceled)\b/im;
|
|
8
|
+
const FAILURE_STATUS_RE = /^status:\s*(failed|error|cancelled|canceled)\b/im;
|
|
9
|
+
|
|
10
|
+
function clean(value) {
|
|
11
|
+
return String(value ?? '').trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sleep(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function taskIdFromOutput(text) {
|
|
19
|
+
return clean(text).match(/agent task:\s*(\S+)/i)?.[1] || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeTag(role) {
|
|
23
|
+
return `headless-${role}-${process.pid}-${Date.now()}`
|
|
24
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '-')
|
|
25
|
+
.replace(/^-+|-+$/g, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildHeadlessSpawnArgs({ role, tag, cwd, message, provider, model } = {}) {
|
|
29
|
+
const spawnArgs = {
|
|
30
|
+
type: 'spawn',
|
|
31
|
+
role: clean(role),
|
|
32
|
+
tag,
|
|
33
|
+
cwd,
|
|
34
|
+
prompt: clean(message),
|
|
35
|
+
};
|
|
36
|
+
if (clean(provider)) spawnArgs.provider = clean(provider);
|
|
37
|
+
if (clean(model)) spawnArgs.model = clean(model);
|
|
38
|
+
return spawnArgs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildAgentRunner(cwd) {
|
|
42
|
+
return createStandaloneAgent({
|
|
43
|
+
cfgMod: {
|
|
44
|
+
loadConfig: cfgMod.loadConfig,
|
|
45
|
+
resolveRuntimeSpec: cfgMod.resolveRuntimeSpec,
|
|
46
|
+
},
|
|
47
|
+
reg,
|
|
48
|
+
mgr,
|
|
49
|
+
dataDir: cfgMod.getPluginData(),
|
|
50
|
+
cwd,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runHeadlessRole({
|
|
55
|
+
role,
|
|
56
|
+
message,
|
|
57
|
+
provider,
|
|
58
|
+
model,
|
|
59
|
+
cwd = process.cwd(),
|
|
60
|
+
write = (text) => stdout.write(text),
|
|
61
|
+
writeErr = (text) => stderr.write(text),
|
|
62
|
+
} = {}) {
|
|
63
|
+
const cleanRole = clean(role);
|
|
64
|
+
const cleanMessage = clean(message);
|
|
65
|
+
if (!cleanRole) {
|
|
66
|
+
writeErr('mixdog: role is required\n');
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
if (!cleanMessage) {
|
|
70
|
+
writeErr('mixdog: message is required\n');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const agent = buildAgentRunner(cwd);
|
|
75
|
+
const tag = makeTag(cleanRole);
|
|
76
|
+
const context = {
|
|
77
|
+
invocationSource: 'headless',
|
|
78
|
+
cwd,
|
|
79
|
+
callerCwd: cwd,
|
|
80
|
+
clientHostPid: process.pid,
|
|
81
|
+
};
|
|
82
|
+
const spawnArgs = buildHeadlessSpawnArgs({
|
|
83
|
+
role: cleanRole,
|
|
84
|
+
tag,
|
|
85
|
+
cwd,
|
|
86
|
+
message: cleanMessage,
|
|
87
|
+
provider,
|
|
88
|
+
model,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
let taskId = null;
|
|
92
|
+
let lastOutput = '';
|
|
93
|
+
try {
|
|
94
|
+
const started = await agent.execute(spawnArgs, context);
|
|
95
|
+
lastOutput = clean(started);
|
|
96
|
+
taskId = taskIdFromOutput(started);
|
|
97
|
+
if (!taskId) {
|
|
98
|
+
write(`${lastOutput}\n`);
|
|
99
|
+
return /^Error\b/i.test(lastOutput) ? 1 : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (;;) {
|
|
103
|
+
lastOutput = clean(await agent.execute({ type: 'read', task_id: taskId }, context));
|
|
104
|
+
if (TERMINAL_STATUS_RE.test(lastOutput)) break;
|
|
105
|
+
await sleep(500);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
write(`${lastOutput}\n`);
|
|
109
|
+
return FAILURE_STATUS_RE.test(lastOutput) ? 1 : 0;
|
|
110
|
+
} finally {
|
|
111
|
+
try {
|
|
112
|
+
await agent.execute({ type: 'close', tag }, context);
|
|
113
|
+
} catch {
|
|
114
|
+
// Best-effort cleanup only; the command result above is the useful signal.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/help.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { stdout } from 'node:process';
|
|
2
|
+
|
|
3
|
+
/** Help text shared by `--help` and the `/help` slash command. */
|
|
4
|
+
export const HELP_LINES = [
|
|
5
|
+
'mixdog — standalone mixdog CLI/TUI coding agent.',
|
|
6
|
+
'',
|
|
7
|
+
'Usage:',
|
|
8
|
+
' mixdog [--provider <name>] [--model <name>] [--readonly]',
|
|
9
|
+
' mixdog <role> <message...>',
|
|
10
|
+
' mixdog --help',
|
|
11
|
+
'',
|
|
12
|
+
'Slash commands (inside mixdog):',
|
|
13
|
+
' /help show this help',
|
|
14
|
+
' /clear reset the conversation and clear the screen',
|
|
15
|
+
' /compact compact older conversation context',
|
|
16
|
+
' /model <name> switch model/preset for subsequent turns',
|
|
17
|
+
' /OutputStyle [name] show or switch Lead output style',
|
|
18
|
+
' /providers manage provider auth and local endpoints',
|
|
19
|
+
' /agents show available workflow agents',
|
|
20
|
+
' /agent manage active agents and async tasks',
|
|
21
|
+
' /mode <name> switch tool surface: full | readonly',
|
|
22
|
+
' /exit quit',
|
|
23
|
+
'',
|
|
24
|
+
'History: use ↑ / ↓ to recall previous inputs.',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Print the help text without importing the REPL/runtime stack. */
|
|
28
|
+
export function printHelp(write = (s) => stdout.write(s)) {
|
|
29
|
+
write(`${HELP_LINES.join('\n')}\n`);
|
|
30
|
+
}
|
|
@@ -1,488 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/**
|
|
3
3
|
* permission-evaluator.cjs
|
|
4
|
-
* Reusable permission evaluation extracted from pre-mcp-sandbox.cjs.
|
|
5
4
|
*
|
|
6
|
-
* Permission
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the user can ask the agent to install/update tokens explicitly.
|
|
13
|
-
* - Explicit deny rules from settings always enforced.
|
|
14
|
-
* - Explicit ask rules from settings are treated as no-match (no prompts).
|
|
15
|
-
* - Default mode is trust/allow — no cwd-sandbox prompts.
|
|
16
|
-
* - Permission modes and ask rules are config-compatible no-ops for the
|
|
17
|
-
* runtime gate; the evaluator now returns only allow/deny. The 'ask'
|
|
18
|
-
* return type remains for external API compatibility.
|
|
5
|
+
* Permission enforcement has been removed by user request: every tool call is
|
|
6
|
+
* trusted. This module retains only the `evaluatePermission` entry point for
|
|
7
|
+
* API compatibility with callers (PreToolUse hook / hook-pipe-server,
|
|
8
|
+
* session manager, agent loop). It unconditionally returns an `allow`
|
|
9
|
+
* decision — no path-based hard-deny, no secret-read deny, no settings.json
|
|
10
|
+
* allow/deny rules, no permission-mode sandbox.
|
|
19
11
|
*
|
|
20
12
|
* Exported function:
|
|
21
|
-
* evaluatePermission({ toolName
|
|
22
|
-
* → { decision: 'allow'|'deny'|'ask', reason: string }
|
|
13
|
+
* evaluatePermission({ toolName }) → { decision: 'allow', reason: string }
|
|
23
14
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* The PreToolUse hook / hook-pipe-server already loads settings to gate the
|
|
27
|
-
* bypass fast-path; passing it through avoids a second 3-tier merge + stat round.
|
|
15
|
+
* The 'deny'/'ask' decision types are no longer produced; the return shape is
|
|
16
|
+
* kept as `{ decision, reason }` so existing consumers keep working.
|
|
28
17
|
*/
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
const path = require('path');
|
|
32
|
-
const os = require('os');
|
|
33
|
-
|
|
34
|
-
const { loadPermissions } = require('./settings-loader.cjs');
|
|
35
|
-
const { evaluateRules } = require('./permission-rules.cjs');
|
|
36
|
-
const { mixdogRoot } = require('../../lib/plugin-paths.cjs');
|
|
37
|
-
|
|
38
|
-
// ── constants ─────────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
const MCP_PREFIXES = [
|
|
41
|
-
'mcp__plugin_mixdog_mixdog__',
|
|
42
|
-
'mcp__plugin_mixdog_trib-plugin__',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
// ── hard-deny patterns (bypass-proof) ────────────────────────────────────────
|
|
46
|
-
// These patterns are evaluated BEFORE mode checks, including bypassPermissions.
|
|
47
|
-
// They cover UNC paths and dangerous OS locations. This is intentionally not a
|
|
48
|
-
// full sandbox; it is the small hard-stop layer that remains in the pi-like
|
|
49
|
-
// trust model.
|
|
50
|
-
|
|
51
|
-
const HARD_DENY_PATH_PATTERNS = [
|
|
52
|
-
// UNC network paths (\\server\share)
|
|
53
|
-
/^\\\\/,
|
|
54
|
-
// Unix system sensitive dirs
|
|
55
|
-
/^\/etc\//i,
|
|
56
|
-
/^\/etc$/i,
|
|
57
|
-
/^\/proc\//i,
|
|
58
|
-
/^\/proc$/i,
|
|
59
|
-
/^\/sys\//i,
|
|
60
|
-
/^\/sys$/i,
|
|
61
|
-
/^\/boot\//i,
|
|
62
|
-
/^\/boot$/i,
|
|
63
|
-
/^\/dev\//i,
|
|
64
|
-
/^\/dev$/i,
|
|
65
|
-
/^\/root\//i,
|
|
66
|
-
/^\/root$/i,
|
|
67
|
-
/^\/run\//i,
|
|
68
|
-
/^\/run$/i,
|
|
69
|
-
/^\/var\/run\//i,
|
|
70
|
-
/^\/var\/run$/i,
|
|
71
|
-
/^\/var\/lib\//i,
|
|
72
|
-
/^\/var\/lib$/i,
|
|
73
|
-
/^\/var\/db\//i,
|
|
74
|
-
/^\/var\/db$/i,
|
|
75
|
-
/^\/bin\//i,
|
|
76
|
-
/^\/bin$/i,
|
|
77
|
-
/^\/sbin\//i,
|
|
78
|
-
/^\/sbin$/i,
|
|
79
|
-
/^\/usr\/bin\//i,
|
|
80
|
-
/^\/usr\/bin$/i,
|
|
81
|
-
/^\/usr\/sbin\//i,
|
|
82
|
-
/^\/usr\/sbin$/i,
|
|
83
|
-
/^\/usr\/lib\//i,
|
|
84
|
-
/^\/usr\/lib$/i,
|
|
85
|
-
/^\/usr\/lib64\//i,
|
|
86
|
-
/^\/usr\/lib64$/i,
|
|
87
|
-
// macOS system locations
|
|
88
|
-
/^\/system\//i,
|
|
89
|
-
/^\/system$/i,
|
|
90
|
-
/^\/library\//i,
|
|
91
|
-
/^\/library$/i,
|
|
92
|
-
/^\/private\/etc\//i,
|
|
93
|
-
/^\/private\/etc$/i,
|
|
94
|
-
/^\/private\/var\/db\//i,
|
|
95
|
-
/^\/private\/var\/db$/i,
|
|
96
|
-
/^\/private\/var\/run\//i,
|
|
97
|
-
/^\/private\/var\/run$/i,
|
|
98
|
-
/^\/volumes\//i,
|
|
99
|
-
/^\/volumes$/i,
|
|
100
|
-
// Windows system dirs (various drive letters)
|
|
101
|
-
/^[a-z]:[/\\]windows[/\\]/i,
|
|
102
|
-
/^[a-z]:[/\\]windows$/i,
|
|
103
|
-
/^[a-z]:[/\\]program files[/\\]/i,
|
|
104
|
-
/^[a-z]:[/\\]program files$/i,
|
|
105
|
-
/^[a-z]:[/\\]program files \(x86\)[/\\]/i,
|
|
106
|
-
/^[a-z]:[/\\]program files \(x86\)$/i,
|
|
107
|
-
/^[a-z]:[/\\]system32/i,
|
|
108
|
-
/^[a-z]:[/\\]programdata[/\\]/i,
|
|
109
|
-
/^[a-z]:[/\\]programdata$/i,
|
|
110
|
-
/^[a-z]:[/\\]programdata[/\\]microsoft[/\\]/i,
|
|
111
|
-
/^[a-z]:[/\\]programdata[/\\]microsoft$/i,
|
|
112
|
-
/^[a-z]:[/\\]programdata[/\\]ssh[/\\]/i,
|
|
113
|
-
/^[a-z]:[/\\]programdata[/\\]ssh$/i,
|
|
114
|
-
/^[a-z]:[/\\]programdata[/\\]docker[/\\]/i,
|
|
115
|
-
/^[a-z]:[/\\]programdata[/\\]docker$/i,
|
|
116
|
-
/^[a-z]:[/\\]recovery[/\\]/i,
|
|
117
|
-
/^[a-z]:[/\\]recovery$/i,
|
|
118
|
-
/^[a-z]:[/\\]boot[/\\]/i,
|
|
119
|
-
/^[a-z]:[/\\]boot$/i,
|
|
120
|
-
/^[a-z]:[/\\]efi[/\\]/i,
|
|
121
|
-
/^[a-z]:[/\\]efi$/i,
|
|
122
|
-
/^[a-z]:[/\\]system volume information[/\\]/i,
|
|
123
|
-
/^[a-z]:[/\\]system volume information$/i,
|
|
124
|
-
/^[a-z]:[/\\]\$recycle\.bin[/\\]/i,
|
|
125
|
-
/^[a-z]:[/\\]\$recycle\.bin$/i,
|
|
126
|
-
/^[a-z]:[/\\](pagefile|hiberfil|swapfile)\.sys$/i,
|
|
127
|
-
/^[a-z]:[/\\]documents and settings[/\\]/i,
|
|
128
|
-
/^[a-z]:[/\\]documents and settings$/i,
|
|
129
|
-
/^[a-z]:[/\\]users[/\\]all users[/\\]/i,
|
|
130
|
-
/^[a-z]:[/\\]users[/\\]all users$/i,
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
// Credential/token locations are different from OS system paths: reading,
|
|
134
|
-
// searching, or delegating them through free-form prompts is a common
|
|
135
|
-
// exfiltration footgun, but writing them is a normal
|
|
136
|
-
// user-requested setup task. Keep them out of HARD_DENY so `write`/`edit` can
|
|
137
|
-
// still install tokens when the user explicitly provides the value.
|
|
138
|
-
const SECRET_READ_DENY_PATH_PATTERNS = [
|
|
139
|
-
// Cross-platform user credential stores and token files
|
|
140
|
-
/^\/home\/[^/\\]+[/\\]\.(ssh|gnupg|aws|azure|docker|kube)([/\\]|$)/i,
|
|
141
|
-
/^\/home\/[^/\\]+[/\\]\.config[/\\]gcloud([/\\]|$)/i,
|
|
142
|
-
/^\/home\/[^/\\]+[/\\]\.(npmrc|netrc|pypirc|git-credentials)$/i,
|
|
143
|
-
/^\/users\/[^/\\]+[/\\]\.(ssh|gnupg|aws|azure|docker|kube)([/\\]|$)/i,
|
|
144
|
-
/^\/users\/[^/\\]+[/\\]\.config[/\\]gcloud([/\\]|$)/i,
|
|
145
|
-
/^\/users\/[^/\\]+[/\\]\.(npmrc|netrc|pypirc|git-credentials)$/i,
|
|
146
|
-
/^\/users\/[^/\\]+[/\\]library[/\\]keychains([/\\]|$)/i,
|
|
147
|
-
/^\/users\/[^/\\]+[/\\]library[/\\]application support[/\\](google[/\\]chrome|microsoft edge|bravesoftware|firefox|mozilla)([/\\]|$)/i,
|
|
148
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]\.(ssh|gnupg|aws|azure|docker|kube)([/\\]|$)/i,
|
|
149
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]\.config[/\\]gcloud([/\\]|$)/i,
|
|
150
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]\.(npmrc|netrc|pypirc|git-credentials)$/i,
|
|
151
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]appdata[/\\](roaming|local)[/\\]microsoft[/\\](credentials|protect)([/\\]|$)/i,
|
|
152
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]appdata[/\\]roaming[/\\]gnupg([/\\]|$)/i,
|
|
153
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]appdata[/\\]local[/\\](google[/\\]chrome|microsoft[/\\]edge|bravesoftware|mozilla|firefox)([/\\]|$)/i,
|
|
154
|
-
/^[a-z]:[/\\]users[/\\][^/\\]+[/\\]appdata[/\\]roaming[/\\](mozilla[/\\]firefox|mozilla|firefox)([/\\]|$)/i,
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
const SECRET_WRITE_TOOLS = new Set([
|
|
158
|
-
'write', 'edit', 'apply_patch', 'multi_edit', 'multiedit',
|
|
159
|
-
]);
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Returns true if any extracted path matches a hard-deny pattern.
|
|
163
|
-
* Called before mode checks — bypass-proof.
|
|
164
|
-
*
|
|
165
|
-
* Symlink/junction-resilient: also probes the realpath of each candidate so
|
|
166
|
-
* a symlink (or NTFS junction) pointing into a forbidden directory cannot
|
|
167
|
-
* smuggle access. Without this, `read({ path: <tmp>/safe })` where
|
|
168
|
-
* <tmp>/safe is a junction to C:/Windows would slip past the literal
|
|
169
|
-
* pattern match against the surface path and access protected content at
|
|
170
|
-
* runtime via fs.readFile's transparent link resolution.
|
|
171
|
-
*/
|
|
172
|
-
// NTFS strips trailing dots and spaces from every path component at the
|
|
173
|
-
// filesystem layer, so `C:\Windows \foo` and `C:\Windows.\foo` actually
|
|
174
|
-
// resolve to `C:\Windows\foo` even though the surface string keeps the
|
|
175
|
-
// trailing chars. Without this normalisation a deny rule on /Windows/
|
|
176
|
-
// could be bypassed by appending a stray space/dot before the next
|
|
177
|
-
// separator. Applies to each `\` and `/` separated segment; safe on
|
|
178
|
-
// POSIX too (the segments simply don't contain trailing spaces and the
|
|
179
|
-
// transform is a no-op).
|
|
180
|
-
function _stripNtfsTrailingChars(p) {
|
|
181
|
-
return String(p).replace(/[. ]+(?=[\\/]|$)/g, '');
|
|
182
|
-
}
|
|
183
|
-
function shortToolName(toolName) {
|
|
19
|
+
function evaluatePermission({ toolName } = {}) {
|
|
184
20
|
const name = typeof toolName === 'string' ? toolName : '';
|
|
185
|
-
|
|
186
|
-
if (prefix) return name.slice(prefix.length);
|
|
187
|
-
if (name.startsWith('mcp__')) return name.split('__').pop() || name;
|
|
188
|
-
return name;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isSecretWriteTool(toolName) {
|
|
192
|
-
return SECRET_WRITE_TOOLS.has(shortToolName(toolName).toLowerCase());
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function extractPathLiteralsFromText(text) {
|
|
196
|
-
if (typeof text !== 'string' || !text) return [];
|
|
197
|
-
const out = [];
|
|
198
|
-
const addMatches = (re, group = 0) => {
|
|
199
|
-
for (const m of text.matchAll(re)) {
|
|
200
|
-
const value = (m[group] || '').trim().replace(/[),.;:]+$/g, '');
|
|
201
|
-
if (value) out.push(value);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
// Capture path-looking spans until punctuation/quotes, or a whitespace that
|
|
205
|
-
// is not followed by another path segment. This keeps paths with spaces such
|
|
206
|
-
// as `Google/Chrome/User Data`, but stops before prose like `.npmrc please`.
|
|
207
|
-
addMatches(/[a-zA-Z]:[/\\](?:[^\s"'`<>|),;:]+|\s+(?=[^\s"'`<>|),;:]+[/\\][^\s"'`<>|),;:]*))*/g);
|
|
208
|
-
addMatches(/\\\\(?:[^\s"'`<>|),;:]+|\s+(?=[^\s"'`<>|),;:]+[/\\][^\s"'`<>|),;:]*))*/g);
|
|
209
|
-
addMatches(/~[/\\][^\s"'`<>|]*/g);
|
|
210
|
-
addMatches(/\/(?:etc|proc|sys|boot|dev|root|run|var|bin|sbin|usr|System|Library|private|Volumes|home|Users|users)(?:[^\s"'`<>|),;:]+|\s+(?=[^\s"'`<>|),;:]+[/\\][^\s"'`<>|),;:]*))*/g);
|
|
211
|
-
return out;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function matchesProtectedPath(rawPaths, opts, patterns) {
|
|
215
|
-
const trustedRootsRaw = (opts && Array.isArray(opts.trustedRoots)) ? opts.trustedRoots : [];
|
|
216
|
-
const baseCwd = (opts && typeof opts.cwd === 'string' && opts.cwd) ? opts.cwd : process.cwd();
|
|
217
|
-
// Pre-normalize trusted roots once per call (windows: case-insensitive).
|
|
218
|
-
const trustedRoots = [];
|
|
219
|
-
for (const r of trustedRootsRaw) {
|
|
220
|
-
if (!r || typeof r !== 'string') continue;
|
|
221
|
-
let n;
|
|
222
|
-
try { n = path.resolve(r); } catch { n = r; }
|
|
223
|
-
n = n.replace(/[\\/]+$/, '');
|
|
224
|
-
if (path.sep === '\\') n = n.toLowerCase();
|
|
225
|
-
if (n) trustedRoots.push(n);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const p of rawPaths) {
|
|
229
|
-
if (!p || typeof p !== 'string') continue;
|
|
230
|
-
// UNC check on raw value (before normalization strips leading slashes)
|
|
231
|
-
if (/^\\\\/.test(p)) return true;
|
|
232
|
-
// Normalize for platform-independent matching. Relative paths must resolve
|
|
233
|
-
// against the user cwd supplied by the hook payload, not the hook server's
|
|
234
|
-
// process.cwd(), or `..\\Windows\\System32` can miss the hard-deny check.
|
|
235
|
-
let resolved;
|
|
236
|
-
try { resolved = resolveCandidate(p, baseCwd); } catch { resolved = null; }
|
|
237
|
-
let norm = (resolved || p).replace(/\\/g, '/');
|
|
238
|
-
for (const re of patterns) {
|
|
239
|
-
if (re.test(p) || re.test(norm)) return true;
|
|
240
|
-
}
|
|
241
|
-
// NTFS trailing-char strip: `C:\Windows \foo` resolves to `C:\Windows\foo`
|
|
242
|
-
// at the filesystem layer. Re-test both the surface and resolved forms
|
|
243
|
-
// with trailing dots/spaces stripped from every segment.
|
|
244
|
-
const stripped = _stripNtfsTrailingChars(p);
|
|
245
|
-
const strippedNorm = _stripNtfsTrailingChars(norm);
|
|
246
|
-
if (stripped !== p || strippedNorm !== norm) {
|
|
247
|
-
for (const re of patterns) {
|
|
248
|
-
if (re.test(stripped) || re.test(strippedNorm)) return true;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
// Fast-path: skip realpath when the resolved path lives under a trusted
|
|
252
|
-
// root (cwd / plugin root / project dir). Junction-to-system-dir attacks
|
|
253
|
-
// inside user-owned trees are not part of this threat model — the surface
|
|
254
|
-
// pattern checks above still catch any path that literally names a
|
|
255
|
-
// protected system dir, and realpath() on every read is the dominant
|
|
256
|
-
// syscall cost on the hot path. Paths outside the trusted set (writes
|
|
257
|
-
// against absolute system paths, untrusted external roots) still fall
|
|
258
|
-
// through to the realpath check below.
|
|
259
|
-
if (trustedRoots.length > 0) {
|
|
260
|
-
if (resolved) {
|
|
261
|
-
let cmp = resolved.replace(/[\\/]+$/, '');
|
|
262
|
-
if (path.sep === '\\') cmp = cmp.toLowerCase();
|
|
263
|
-
let inTrusted = false;
|
|
264
|
-
for (const root of trustedRoots) {
|
|
265
|
-
if (cmp === root || cmp.startsWith(root + '\\') || cmp.startsWith(root + '/')) {
|
|
266
|
-
inTrusted = true;
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (inTrusted) continue;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// Resolve symlinks / junctions and re-test. realpathSync throws ENOENT
|
|
274
|
-
// for paths that don't exist yet (legitimate for write/create flows),
|
|
275
|
-
// so missing paths fall through with no extra check — only the literal
|
|
276
|
-
// surface form is enforced for those.
|
|
277
|
-
let real;
|
|
278
|
-
try { real = fs.realpathSync(resolved || p); }
|
|
279
|
-
catch { real = null; }
|
|
280
|
-
if (real && real !== p && real !== norm.replace(/\//g, path.sep)) {
|
|
281
|
-
const realNorm = real.replace(/\\/g, '/');
|
|
282
|
-
for (const re of patterns) {
|
|
283
|
-
if (re.test(real) || re.test(realNorm)) return true;
|
|
284
|
-
}
|
|
285
|
-
// Re-test UNC after symlink resolve too: a local-looking path could
|
|
286
|
-
// realpath to a network share and would otherwise dodge the surface
|
|
287
|
-
// UNC test above.
|
|
288
|
-
if (/^\\\\/.test(real)) return true;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function isHardDenyPath(rawPaths, opts) {
|
|
295
|
-
return matchesProtectedPath(rawPaths, opts, HARD_DENY_PATH_PATTERNS);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function isSecretReadDenyPath(rawPaths, opts) {
|
|
299
|
-
return matchesProtectedPath(rawPaths, opts, SECRET_READ_DENY_PATH_PATTERNS);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
module.exports._isHardDenyPath = isHardDenyPath; // exported for tests
|
|
303
|
-
|
|
304
|
-
// ── path helpers ──────────────────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
function normalizePath(p) {
|
|
307
|
-
if (!p || typeof p !== 'string') return null;
|
|
308
|
-
if (/^\\\\/.test(p)) return p;
|
|
309
|
-
|
|
310
|
-
// Shell-style home shorthand is common in user prompts/settings. Expand it
|
|
311
|
-
// before absolute/relative resolution so `~/.npmrc` is treated as the real
|
|
312
|
-
// home token file, not as a harmless repo-relative path named `~`.
|
|
313
|
-
if (p === '~') p = os.homedir();
|
|
314
|
-
else if (/^~[/\\]/.test(p)) p = path.join(os.homedir(), p.slice(2));
|
|
315
|
-
|
|
316
|
-
if (path.sep === '\\') {
|
|
317
|
-
const posixDriveMatch = p.match(/^\/([a-zA-Z])(\/.*)?$/);
|
|
318
|
-
if (posixDriveMatch) {
|
|
319
|
-
const drive = posixDriveMatch[1].toUpperCase();
|
|
320
|
-
const rest = (posixDriveMatch[2] || '').replace(/\//g, '\\');
|
|
321
|
-
p = drive + ':' + (rest || '\\');
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
return path.isAbsolute(p) ? path.normalize(p) : null;
|
|
327
|
-
} catch {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function resolveCandidate(p, baseCwd) {
|
|
333
|
-
if (!p || typeof p !== 'string') return null;
|
|
334
|
-
if (/^\\\\/.test(p)) return p;
|
|
335
|
-
try {
|
|
336
|
-
const normalized = normalizePath(p);
|
|
337
|
-
if (normalized !== null && path.isAbsolute(normalized)) return normalized;
|
|
338
|
-
return path.resolve(baseCwd, p);
|
|
339
|
-
} catch {
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function extractPaths(toolName, toolInput) {
|
|
345
|
-
if (!toolInput || typeof toolInput !== 'object') return [];
|
|
346
|
-
|
|
347
|
-
const prefix = MCP_PREFIXES.find(p => toolName.startsWith(p));
|
|
348
|
-
const tool = prefix ? toolName.slice(prefix.length) : toolName;
|
|
349
|
-
const candidates = [];
|
|
350
|
-
|
|
351
|
-
const push = (...vals) => {
|
|
352
|
-
for (const v of vals) { if (v && typeof v === 'string') candidates.push(v); }
|
|
353
|
-
};
|
|
354
|
-
const pushTextPaths = (...vals) => {
|
|
355
|
-
for (const v of vals) push(...extractPathLiteralsFromText(v));
|
|
356
|
-
};
|
|
357
|
-
const pushPatchTarget = (target) => {
|
|
358
|
-
if (!target || typeof target !== 'string') return;
|
|
359
|
-
const cleaned = target.trim();
|
|
360
|
-
if (!cleaned) return;
|
|
361
|
-
push(cleaned);
|
|
362
|
-
if (toolInput.base_path && typeof toolInput.base_path === 'string') {
|
|
363
|
-
try {
|
|
364
|
-
const normalized = normalizePath(cleaned);
|
|
365
|
-
if (!(normalized !== null && path.isAbsolute(normalized))) {
|
|
366
|
-
push(path.join(toolInput.base_path, cleaned));
|
|
367
|
-
}
|
|
368
|
-
} catch {}
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
switch (tool) {
|
|
373
|
-
case 'bash':
|
|
374
|
-
case 'bash_session':
|
|
375
|
-
push(toolInput.cwd);
|
|
376
|
-
pushTextPaths(toolInput.command);
|
|
377
|
-
break;
|
|
378
|
-
case 'bridge':
|
|
379
|
-
push(toolInput.cwd);
|
|
380
|
-
push(toolInput.file);
|
|
381
|
-
pushTextPaths(toolInput.prompt, toolInput.message, toolInput.context);
|
|
382
|
-
break;
|
|
383
|
-
case 'apply_patch': {
|
|
384
|
-
push(toolInput.base_path);
|
|
385
|
-
const patch = toolInput.patch;
|
|
386
|
-
if (typeof patch === 'string') {
|
|
387
|
-
for (const m of patch.matchAll(/^\+\+\+\s+b\/(.+)$/gm)) pushPatchTarget(m[1]);
|
|
388
|
-
for (const m of patch.matchAll(/^---\s+a\/(.+)$/gm)) pushPatchTarget(m[1]);
|
|
389
|
-
for (const m of patch.matchAll(/^\*\*\*\s+(?:Add|Update|Delete) File:\s+(.+)$/gm)) pushPatchTarget(m[1]);
|
|
390
|
-
}
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
case 'read': {
|
|
394
|
-
if (toolInput.path && typeof toolInput.path === 'string') push(toolInput.path);
|
|
395
|
-
// CC `file_path` alias: builtin.mjs:3835 maps args.file_path → args.path
|
|
396
|
-
// before execution, so the permission gate must inspect it too or a
|
|
397
|
-
// deny rule on /Windows can be bypassed by sending file_path:.../Windows/...
|
|
398
|
-
push(toolInput.file_path);
|
|
399
|
-
push(toolInput.cwd);
|
|
400
|
-
if (Array.isArray(toolInput.reads)) toolInput.reads.forEach(r => push(r?.path));
|
|
401
|
-
if (Array.isArray(toolInput.path)) toolInput.path.forEach(p => {
|
|
402
|
-
if (p && typeof p === 'string') push(p);
|
|
403
|
-
else if (p && typeof p === 'object' && p.path) push(p.path);
|
|
404
|
-
});
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
default:
|
|
408
|
-
if (toolInput.path && typeof toolInput.path === 'string') push(toolInput.path);
|
|
409
|
-
// CC `file_path` alias: builtin.mjs:4282/4439 map args.file_path →
|
|
410
|
-
// args.path before execution. Without it edit/write deny rules on a
|
|
411
|
-
// forbidden directory can be bypassed by sending file_path:.../forbidden/...
|
|
412
|
-
push(toolInput.file, toolInput.file_path, toolInput.cwd, toolInput.base_path);
|
|
413
|
-
if (Array.isArray(toolInput.path)) toolInput.path.forEach(p => push(p));
|
|
414
|
-
if (Array.isArray(toolInput.reads)) toolInput.reads.forEach(r => push(r?.path));
|
|
415
|
-
// Per-edit / per-write file_path alias mirrors builtin.mjs:4440 — each
|
|
416
|
-
// item in edits[]/writes[] can carry file_path that overrides the top-level
|
|
417
|
-
// path; collect both forms so deny rules apply uniformly.
|
|
418
|
-
if (Array.isArray(toolInput.edits)) toolInput.edits.forEach(e => push(e?.path, e?.file_path));
|
|
419
|
-
if (Array.isArray(toolInput.writes)) toolInput.writes.forEach(w => push(w?.path, w?.file_path));
|
|
420
|
-
pushTextPaths(toolInput.command, toolInput.query, toolInput.prompt, toolInput.message, toolInput.context);
|
|
421
|
-
break;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return [...new Set(candidates)];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// ── main evaluator ────────────────────────────────────────────────────────────
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Evaluate whether a tool call should be allowed, denied, or asked.
|
|
431
|
-
*
|
|
432
|
-
* @param {object} opts
|
|
433
|
-
* @param {string} opts.toolName — full tool name (mcp__ prefix expected for mixdog tools)
|
|
434
|
-
* @param {object} opts.toolInput — tool arguments object
|
|
435
|
-
* @param {string} [opts.permissionMode] — override mode ('bypassPermissions', 'acceptEdits',
|
|
436
|
-
* 'plan', 'dontAsk', 'default'). Falls back to
|
|
437
|
-
* settings defaultMode.
|
|
438
|
-
* @param {string} [opts.projectDir] — project root for settings lookup
|
|
439
|
-
* @param {string} [opts.userCwd] — user working directory for path resolution
|
|
440
|
-
* @returns {{ decision: 'allow'|'deny'|'ask', reason: string, updatedInput?: object }}
|
|
441
|
-
*/
|
|
442
|
-
function evaluatePermission({ toolName, toolInput, permissionMode, projectDir, userCwd, permissions }) {
|
|
443
|
-
const name = typeof toolName === 'string' ? toolName : '';
|
|
444
|
-
const input = (toolInput && typeof toolInput === 'object') ? toolInput : {};
|
|
445
|
-
const cwd = (typeof userCwd === 'string' && userCwd) ? userCwd : process.cwd();
|
|
446
|
-
|
|
447
|
-
// Single extractPaths call — reused for hard-deny, secret-read, and explicit deny checks.
|
|
448
|
-
const rawPaths = extractPaths(name, input);
|
|
449
|
-
|
|
450
|
-
// Trusted-root set powers the hard-deny realpath fast-path.
|
|
451
|
-
const root = mixdogRoot();
|
|
452
|
-
const trustedRoots = [cwd];
|
|
453
|
-
if (root) trustedRoots.push(root);
|
|
454
|
-
if (projectDir && projectDir !== cwd) trustedRoots.push(projectDir);
|
|
455
|
-
|
|
456
|
-
// 0. Hard-deny: bypass-proof path check (UNC, dangerous system paths).
|
|
457
|
-
// Evaluated before any mode check — even bypassPermissions cannot override.
|
|
458
|
-
if (isHardDenyPath(rawPaths, { trustedRoots, cwd })) {
|
|
459
|
-
return { decision: 'deny', reason: `Tool '${name}' targets a protected system path.` };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (isSecretReadDenyPath(rawPaths, { trustedRoots, cwd }) && !isSecretWriteTool(name)) {
|
|
463
|
-
return { decision: 'deny', reason: `Tool '${name}' targets a protected credential path without a write-class tool.` };
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 1. Use caller-supplied settings when available; otherwise load.
|
|
467
|
-
const { allow, deny, defaultMode } = (permissions && typeof permissions === 'object')
|
|
468
|
-
? permissions
|
|
469
|
-
: loadPermissions(projectDir || cwd);
|
|
470
|
-
|
|
471
|
-
// 2. Explicit rules. Pi-like practical keeps deny as the only user rule that
|
|
472
|
-
// blocks at runtime. Ask rules are ignored by passing an empty ask list.
|
|
473
|
-
const listResult = evaluateRules(name, input, allow, deny, []);
|
|
474
|
-
|
|
475
|
-
if (listResult === 'deny') {
|
|
476
|
-
return { decision: 'deny', reason: `Tool '${name}' blocked by deny rule.` };
|
|
477
|
-
}
|
|
478
|
-
if (listResult === 'allow') {
|
|
479
|
-
return { decision: 'allow', reason: 'Matched allow rule.' };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// 3. Permission modes are accepted for compatibility only. They no longer
|
|
483
|
-
// implement a sandbox policy; runtime safety is hard-deny + secret-read deny + explicit deny.
|
|
484
|
-
const mode = permissionMode || defaultMode || 'default';
|
|
485
|
-
return { decision: 'allow', reason: `${mode} mode: trust/allow (pi-like).` };
|
|
21
|
+
return { decision: 'allow', reason: `permissions disabled: trust/allow (${name})` };
|
|
486
22
|
}
|
|
487
23
|
|
|
488
24
|
module.exports.evaluatePermission = evaluatePermission;
|
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -306,6 +306,14 @@ function getSecret(account) {
|
|
|
306
306
|
return value;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
function hasSecret(account) {
|
|
310
|
+
const cached = _cacheGet(account);
|
|
311
|
+
if (cached !== undefined) return cached != null;
|
|
312
|
+
if (platform() === 'win32') return fs.existsSync(dpApiFile(account));
|
|
313
|
+
try { return getSecret(account) != null; }
|
|
314
|
+
catch { return false; }
|
|
315
|
+
}
|
|
316
|
+
|
|
309
317
|
function setSecret(account, value) {
|
|
310
318
|
switch (platform()) {
|
|
311
319
|
case 'darwin': darwinSet(account, value); break;
|
|
@@ -329,4 +337,4 @@ function deleteSecret(account) {
|
|
|
329
337
|
}
|
|
330
338
|
}
|
|
331
339
|
|
|
332
|
-
module.exports = { getSecret, setSecret, deleteSecret, SERVICE };
|
|
340
|
+
module.exports = { getSecret, setSecret, deleteSecret, hasSecret, SERVICE };
|