muonroi-cli 1.7.2 → 1.8.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/LICENSE +21 -21
- package/README.md +122 -122
- 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 +32 -3
- 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 +12 -12
- 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 +56 -21
- 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/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 +55 -60
- 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 +2 -2
- package/dist/src/lsp/manager.test.js +2 -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/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/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- package/dist/src/ops/doctor.js +7 -7
- 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 +2 -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 +1 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -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 +364 -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 +21 -5
- package/dist/src/orchestrator/message-processor.js +219 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +8 -2
- package/dist/src/orchestrator/orchestrator.js +187 -34
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +145 -0
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +76 -60
- 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-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
- package/dist/src/orchestrator/subagent-compactor.js +83 -10
- package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
- 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 +151 -0
- package/dist/src/orchestrator/tool-engine.js +3079 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -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__/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 +99 -44
- 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.js +3 -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/done-gate.js +3 -3
- package/dist/src/product-loop/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +20 -20
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- 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/mcp-vision-bridge.js +48 -48
- 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/reporter/index.js +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/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +19 -19
- 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__/migrations.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/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.js +43 -43
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +1 -0
- package/dist/src/storage/transcript.js +55 -0
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- 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 +56 -0
- 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 +5 -0
- package/dist/src/tools/registry.js +185 -11
- package/dist/src/types/index.d.ts +7 -1
- 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/council-inspect.js +4 -4
- 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/clipboard-image.js +23 -23
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- 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 +27 -31
- package/dist/src/utils/settings.js +100 -109
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -13,6 +13,28 @@ import { lookupPricing } from "../providers/pricing.js";
|
|
|
13
13
|
export function estimateTokensFromChars(chars) {
|
|
14
14
|
return Math.ceil(chars / 4);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize an actual input-token count by comparing it to the estimated
|
|
18
|
+
* (chars/4) count. Some providers (SiliconFlow, possibly others) return
|
|
19
|
+
* implausibly low `prompt_tokens` (e.g. 10) regardless of actual prompt size,
|
|
20
|
+
* which under-reports cost and inflates apparent cache-hit ratios.
|
|
21
|
+
*
|
|
22
|
+
* Rules:
|
|
23
|
+
* - When `actual === undefined`: return `estimated` (no data = use estimate).
|
|
24
|
+
* - When `actual === 0`: return 0 (preserve mock / failed-call semantics).
|
|
25
|
+
* - When `estimated > 0` and `actual < estimated * 0.1`: the value is likely
|
|
26
|
+
* bogus — return `estimated` so cost projections stay accurate.
|
|
27
|
+
* - Otherwise: return `actual` as-is.
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeInputTokens(actual, estimated) {
|
|
30
|
+
if (actual === undefined)
|
|
31
|
+
return estimated;
|
|
32
|
+
if (actual === 0)
|
|
33
|
+
return 0;
|
|
34
|
+
if (estimated > 0 && actual < estimated * 0.1)
|
|
35
|
+
return estimated;
|
|
36
|
+
return actual;
|
|
37
|
+
}
|
|
16
38
|
/**
|
|
17
39
|
* Project the USD cost of a request given token estimates and the static pricing table.
|
|
18
40
|
* Returns 0 for unknown provider/model -- caller decides how to handle (ledger treats as 0 risk).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/__tests__/compaction-caps.test.ts
|
|
3
|
+
*
|
|
4
|
+
* G4 regression guard for the token-thrift compaction caps.
|
|
5
|
+
*
|
|
6
|
+
* The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
|
|
7
|
+
* cover the algorithm; the *-tui specs cover the live wiring). The hole this
|
|
8
|
+
* file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
|
|
9
|
+
* the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
|
|
10
|
+
* options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
|
|
11
|
+
* the real getters. So a regression that widens a default (e.g. reverting the
|
|
12
|
+
* Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
|
|
13
|
+
* past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
|
|
14
|
+
*
|
|
15
|
+
* Part 1 pins each getter's documented contract (default / clamp / honor).
|
|
16
|
+
* Part 2 feeds the REAL getter outputs into the compactor over a synthetic
|
|
17
|
+
* load sized BETWEEN the default threshold and a plausible widened drift, so a
|
|
18
|
+
* default-value regression fails behaviourally too — not just at the value
|
|
19
|
+
* assertion.
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/__tests__/compaction-caps.test.ts
|
|
3
|
+
*
|
|
4
|
+
* G4 regression guard for the token-thrift compaction caps.
|
|
5
|
+
*
|
|
6
|
+
* The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
|
|
7
|
+
* cover the algorithm; the *-tui specs cover the live wiring). The hole this
|
|
8
|
+
* file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
|
|
9
|
+
* the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
|
|
10
|
+
* options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
|
|
11
|
+
* the real getters. So a regression that widens a default (e.g. reverting the
|
|
12
|
+
* Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
|
|
13
|
+
* past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
|
|
14
|
+
*
|
|
15
|
+
* Part 1 pins each getter's documented contract (default / clamp / honor).
|
|
16
|
+
* Part 2 feeds the REAL getter outputs into the compactor over a synthetic
|
|
17
|
+
* load sized BETWEEN the default threshold and a plausible widened drift, so a
|
|
18
|
+
* default-value regression fails behaviourally too — not just at the value
|
|
19
|
+
* assertion.
|
|
20
|
+
*/
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
22
|
+
import { compactSubAgentMessages, cumulativeMessageChars } from "../../orchestrator/subagent-compactor.js";
|
|
23
|
+
import { getSubAgentBudgetChars, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, getTopLevelCompactKeepLast, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, loadUserSettings, } from "../settings.js";
|
|
24
|
+
const ENV_KEYS = [
|
|
25
|
+
"MUONROI_SUB_AGENT_BUDGET_CHARS",
|
|
26
|
+
"MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS",
|
|
27
|
+
"MUONROI_SUBAGENT_COMPACT_KEEP_LAST",
|
|
28
|
+
"MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS",
|
|
29
|
+
"MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST",
|
|
30
|
+
"MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS",
|
|
31
|
+
];
|
|
32
|
+
const saved = {};
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
for (const k of ENV_KEYS) {
|
|
35
|
+
saved[k] = process.env[k];
|
|
36
|
+
delete process.env[k];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
for (const k of ENV_KEYS) {
|
|
41
|
+
if (saved[k] === undefined)
|
|
42
|
+
delete process.env[k];
|
|
43
|
+
else
|
|
44
|
+
process.env[k] = saved[k];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
describe("compaction cap getters — production contract (G4 drift guard)", () => {
|
|
48
|
+
describe("compact thresholds & keepLast (Phase C5 defaults — no user-settings layer)", () => {
|
|
49
|
+
it("return the documented defaults when env is unset", () => {
|
|
50
|
+
expect(getSubAgentCompactThresholdChars()).toBe(40_000);
|
|
51
|
+
expect(getSubAgentCompactKeepLast()).toBe(3);
|
|
52
|
+
expect(getTopLevelCompactThresholdChars()).toBe(100_000);
|
|
53
|
+
expect(getTopLevelCompactKeepLast()).toBe(5);
|
|
54
|
+
});
|
|
55
|
+
it("clamp out-of-range env back to the default", () => {
|
|
56
|
+
process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "999999999"; // > 500_000
|
|
57
|
+
expect(getSubAgentCompactThresholdChars()).toBe(40_000);
|
|
58
|
+
process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "1000"; // < 20_000
|
|
59
|
+
expect(getSubAgentCompactThresholdChars()).toBe(40_000);
|
|
60
|
+
process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "0"; // < 1
|
|
61
|
+
expect(getSubAgentCompactKeepLast()).toBe(3);
|
|
62
|
+
process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "99"; // > 20
|
|
63
|
+
expect(getSubAgentCompactKeepLast()).toBe(3);
|
|
64
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "10000"; // < 50_000
|
|
65
|
+
expect(getTopLevelCompactThresholdChars()).toBe(100_000);
|
|
66
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "99"; // > 30
|
|
67
|
+
expect(getTopLevelCompactKeepLast()).toBe(5);
|
|
68
|
+
});
|
|
69
|
+
it("honor valid in-range env overrides", () => {
|
|
70
|
+
process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "60000";
|
|
71
|
+
expect(getSubAgentCompactThresholdChars()).toBe(60_000);
|
|
72
|
+
process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "5";
|
|
73
|
+
expect(getSubAgentCompactKeepLast()).toBe(5);
|
|
74
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "250000";
|
|
75
|
+
expect(getTopLevelCompactThresholdChars()).toBe(250_000);
|
|
76
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "8";
|
|
77
|
+
expect(getTopLevelCompactKeepLast()).toBe(8);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("budget getters (env override → user-settings → default)", () => {
|
|
81
|
+
it("getSubAgentBudgetChars: default 120_000 when env unset and no user override", () => {
|
|
82
|
+
// Deterministic on a clean checkout / CI (no user-settings.json). When a
|
|
83
|
+
// dev machine HAS an explicit override the getter must honor it instead —
|
|
84
|
+
// the default-drift case is what CI guards.
|
|
85
|
+
const override = loadUserSettings().subAgentBudgetChars;
|
|
86
|
+
if (typeof override === "number" && override >= 20_000 && override <= 600_000) {
|
|
87
|
+
expect(getSubAgentBudgetChars()).toBe(Math.floor(override));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
expect(getSubAgentBudgetChars()).toBe(120_000);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("getTopLevelToolBudgetChars: default 200_000 when env unset and no user override", () => {
|
|
94
|
+
const override = loadUserSettings().topLevelToolBudgetChars;
|
|
95
|
+
if (typeof override === "number" && override >= 50_000 && override <= 1_500_000) {
|
|
96
|
+
expect(getTopLevelToolBudgetChars()).toBe(Math.floor(override));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
expect(getTopLevelToolBudgetChars()).toBe(200_000);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
it("honor valid in-range env overrides (env short-circuits user-settings)", () => {
|
|
103
|
+
process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "300000";
|
|
104
|
+
expect(getSubAgentBudgetChars()).toBe(300_000);
|
|
105
|
+
process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "800000";
|
|
106
|
+
expect(getTopLevelToolBudgetChars()).toBe(800_000);
|
|
107
|
+
});
|
|
108
|
+
it("reject out-of-range env (result stays within the documented range)", () => {
|
|
109
|
+
process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "999999999"; // > 600_000
|
|
110
|
+
const sub = getSubAgentBudgetChars();
|
|
111
|
+
expect(sub).not.toBe(999_999_999);
|
|
112
|
+
expect(sub).toBeGreaterThanOrEqual(20_000);
|
|
113
|
+
expect(sub).toBeLessThanOrEqual(600_000);
|
|
114
|
+
process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "1000"; // < 50_000
|
|
115
|
+
const top = getTopLevelToolBudgetChars();
|
|
116
|
+
expect(top).not.toBe(1000);
|
|
117
|
+
expect(top).toBeGreaterThanOrEqual(50_000);
|
|
118
|
+
expect(top).toBeLessThanOrEqual(1_500_000);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* Build N tool turns (assistant tool-call + tool result), each result
|
|
124
|
+
* `resultChars` long, using a tool name that is NOT high-value (so the
|
|
125
|
+
* compactor is allowed to elide it). Mirrors the cost-leak-b3 message shape.
|
|
126
|
+
*/
|
|
127
|
+
function buildToolTurns(n, resultChars) {
|
|
128
|
+
const msgs = [
|
|
129
|
+
{ role: "system", content: "You are the Explore sub-agent." },
|
|
130
|
+
{ role: "user", content: "trace auth wiring" },
|
|
131
|
+
];
|
|
132
|
+
for (let i = 0; i < n; i++) {
|
|
133
|
+
const id = `c${i}`;
|
|
134
|
+
msgs.push({
|
|
135
|
+
role: "assistant",
|
|
136
|
+
content: [{ type: "tool-call", toolCallId: id, toolName: "fake_read", input: { path: `/tmp/${id}.txt` } }],
|
|
137
|
+
});
|
|
138
|
+
msgs.push({
|
|
139
|
+
role: "tool",
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "tool-result",
|
|
143
|
+
toolCallId: id,
|
|
144
|
+
toolName: "fake_read",
|
|
145
|
+
output: { type: "text", value: "x".repeat(resultChars) },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return msgs;
|
|
151
|
+
}
|
|
152
|
+
describe("compaction fires with the REAL default getter values (G4 behavioural guard)", () => {
|
|
153
|
+
// Load sized BETWEEN the default thresholds (40K sub / 100K top) and a
|
|
154
|
+
// plausible widened drift (200K). So: with the real defaults it compacts;
|
|
155
|
+
// if a default were reverted to 200_000 the load would fall below threshold
|
|
156
|
+
// and these assertions would fail — catching drift behaviourally.
|
|
157
|
+
it("sub-agent: default threshold (40K) elides older tool results on a ~120K load", () => {
|
|
158
|
+
const messages = buildToolTurns(6, 20_000); // ~120K cumulative, 6 tool turns
|
|
159
|
+
const cumulative = cumulativeMessageChars(messages);
|
|
160
|
+
expect(cumulative).toBeGreaterThan(40_000);
|
|
161
|
+
expect(cumulative).toBeLessThan(200_000);
|
|
162
|
+
const compacted = compactSubAgentMessages(messages, {
|
|
163
|
+
thresholdChars: getSubAgentCompactThresholdChars(),
|
|
164
|
+
keepLastTurns: getSubAgentCompactKeepLast(),
|
|
165
|
+
});
|
|
166
|
+
expect(compacted).not.toBe(messages); // identity differs → compaction happened
|
|
167
|
+
const before = JSON.stringify(messages).length;
|
|
168
|
+
const after = JSON.stringify(compacted).length;
|
|
169
|
+
expect(after).toBeLessThan(before);
|
|
170
|
+
expect(JSON.stringify(compacted)).toContain("elided by sub-agent compactor");
|
|
171
|
+
});
|
|
172
|
+
it("top-level: default threshold (100K) elides older tool results on a ~160K load", () => {
|
|
173
|
+
const messages = buildToolTurns(8, 20_000); // ~160K cumulative, 8 tool turns
|
|
174
|
+
const cumulative = cumulativeMessageChars(messages);
|
|
175
|
+
expect(cumulative).toBeGreaterThan(100_000);
|
|
176
|
+
expect(cumulative).toBeLessThan(200_000);
|
|
177
|
+
const compacted = compactSubAgentMessages(messages, {
|
|
178
|
+
thresholdChars: getTopLevelCompactThresholdChars(),
|
|
179
|
+
keepLastTurns: getTopLevelCompactKeepLast(),
|
|
180
|
+
label: "top-level",
|
|
181
|
+
});
|
|
182
|
+
expect(compacted).not.toBe(messages);
|
|
183
|
+
expect(JSON.stringify(compacted).length).toBeLessThan(JSON.stringify(messages).length);
|
|
184
|
+
expect(JSON.stringify(compacted)).toContain("elided by top-level compactor");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
//# sourceMappingURL=compaction-caps.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* withDeadlineRace abort-grace: a provider that ignores its abortSignal mid-call
|
|
3
|
+
* must not keep the caller blocked until the (minutes-long) wall-clock deadline.
|
|
4
|
+
* When the user-abort signal fires, the race rejects within a short grace window.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { withDeadlineRace } from "../llm-deadline.js";
|
|
8
|
+
describe("withDeadlineRace — abort grace", () => {
|
|
9
|
+
it("rejects shortly after abort even when fn() never settles", async () => {
|
|
10
|
+
const ac = new AbortController();
|
|
11
|
+
const neverSettles = () => new Promise(() => { }); // simulates a stalled provider
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
setTimeout(() => ac.abort(new Error("user pressed Esc")), 40);
|
|
14
|
+
await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 80)).rejects.toThrow(/aborted by user/);
|
|
15
|
+
// Must unblock far before the 60s deadline.
|
|
16
|
+
expect(Date.now() - start).toBeLessThan(2_000);
|
|
17
|
+
});
|
|
18
|
+
it("rejects immediately when the signal is already aborted", async () => {
|
|
19
|
+
const ac = new AbortController();
|
|
20
|
+
ac.abort(new Error("already cancelled"));
|
|
21
|
+
const neverSettles = () => new Promise(() => { });
|
|
22
|
+
await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 50)).rejects.toThrow(/aborted by user/);
|
|
23
|
+
});
|
|
24
|
+
it("returns the fn() result when it settles before any abort", async () => {
|
|
25
|
+
const ac = new AbortController();
|
|
26
|
+
await expect(withDeadlineRace(() => Promise.resolve("ok"), 60_000, "test", ac.signal, 80)).resolves.toBe("ok");
|
|
27
|
+
});
|
|
28
|
+
it("still enforces the wall-clock deadline when no signal is passed (back-compat)", async () => {
|
|
29
|
+
const neverSettles = () => new Promise(() => { });
|
|
30
|
+
await expect(withDeadlineRace(neverSettles, 60, "test")).rejects.toThrow(/deadline/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=llm-deadline-abort.test.js.map
|
|
@@ -25,24 +25,24 @@ function readWin32() {
|
|
|
25
25
|
try {
|
|
26
26
|
const escaped = tmpFile.replace(/\\/g, "\\\\");
|
|
27
27
|
// Run in STA thread with retry — clipboard can be locked by other processes
|
|
28
|
-
const ps = `
|
|
29
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
-
Add-Type -AssemblyName System.Drawing
|
|
31
|
-
$maxRetries = 3
|
|
32
|
-
$img = $null
|
|
33
|
-
for ($i = 0; $i -lt $maxRetries; $i++) {
|
|
34
|
-
try {
|
|
35
|
-
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
36
|
-
if ($img -ne $null) { break }
|
|
37
|
-
} catch {
|
|
38
|
-
# clipboard locked — wait and retry
|
|
39
|
-
}
|
|
40
|
-
Start-Sleep -Milliseconds 100
|
|
41
|
-
}
|
|
42
|
-
if ($img -ne $null) {
|
|
43
|
-
$img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
44
|
-
$img.Dispose()
|
|
45
|
-
}
|
|
28
|
+
const ps = `
|
|
29
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
+
Add-Type -AssemblyName System.Drawing
|
|
31
|
+
$maxRetries = 3
|
|
32
|
+
$img = $null
|
|
33
|
+
for ($i = 0; $i -lt $maxRetries; $i++) {
|
|
34
|
+
try {
|
|
35
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
36
|
+
if ($img -ne $null) { break }
|
|
37
|
+
} catch {
|
|
38
|
+
# clipboard locked — wait and retry
|
|
39
|
+
}
|
|
40
|
+
Start-Sleep -Milliseconds 100
|
|
41
|
+
}
|
|
42
|
+
if ($img -ne $null) {
|
|
43
|
+
$img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
44
|
+
$img.Dispose()
|
|
45
|
+
}
|
|
46
46
|
`;
|
|
47
47
|
const result = spawnSync("powershell", ["-NoProfile", "-STA", "-Command", ps], { timeout: 8000 });
|
|
48
48
|
if (result.error || result.status !== 0)
|
|
@@ -81,11 +81,11 @@ function readDarwin() {
|
|
|
81
81
|
const pngpaste = spawnSync("pngpaste", [tmpFile], { timeout: 5000 });
|
|
82
82
|
if (pngpaste.status !== 0) {
|
|
83
83
|
spawnSync("screencapture", ["-c", "-x"]);
|
|
84
|
-
const osascript = `
|
|
85
|
-
set imgData to the clipboard as «class PNGf»
|
|
86
|
-
set f to open for access POSIX file "${tmpFile}" with write permission
|
|
87
|
-
write imgData to f
|
|
88
|
-
close access f
|
|
84
|
+
const osascript = `
|
|
85
|
+
set imgData to the clipboard as «class PNGf»
|
|
86
|
+
set f to open for access POSIX file "${tmpFile}" with write permission
|
|
87
|
+
write imgData to f
|
|
88
|
+
close access f
|
|
89
89
|
`;
|
|
90
90
|
spawnSync("osascript", ["-e", osascript], { timeout: 5000 });
|
|
91
91
|
}
|
|
@@ -41,4 +41,12 @@ export declare function withTimeoutSignal(parent: AbortSignal | undefined, timeo
|
|
|
41
41
|
* still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
|
|
42
42
|
* the caller is never blocked past the deadline.
|
|
43
43
|
*/
|
|
44
|
-
export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string
|
|
44
|
+
export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string,
|
|
45
|
+
/**
|
|
46
|
+
* User-abort signal. When it fires, the race rejects within `abortGraceMs`
|
|
47
|
+
* even if `fn()` hasn't settled — so a provider that ignores its abortSignal
|
|
48
|
+
* mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
|
|
49
|
+
* (and, for council, the locked composer) blocked until the full `deadlineMs`.
|
|
50
|
+
* The short grace lets the normal fetch-level abort win first when it's quick.
|
|
51
|
+
*/
|
|
52
|
+
abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;
|
|
@@ -62,19 +62,48 @@ export function withTimeoutSignal(parent, timeoutMs) {
|
|
|
62
62
|
* still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
|
|
63
63
|
* the caller is never blocked past the deadline.
|
|
64
64
|
*/
|
|
65
|
-
export async function withDeadlineRace(fn, deadlineMs, label
|
|
65
|
+
export async function withDeadlineRace(fn, deadlineMs, label,
|
|
66
|
+
/**
|
|
67
|
+
* User-abort signal. When it fires, the race rejects within `abortGraceMs`
|
|
68
|
+
* even if `fn()` hasn't settled — so a provider that ignores its abortSignal
|
|
69
|
+
* mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
|
|
70
|
+
* (and, for council, the locked composer) blocked until the full `deadlineMs`.
|
|
71
|
+
* The short grace lets the normal fetch-level abort win first when it's quick.
|
|
72
|
+
*/
|
|
73
|
+
abortSignal, abortGraceMs = 1500) {
|
|
66
74
|
let timer = null;
|
|
75
|
+
let abortTimer = null;
|
|
76
|
+
let abortListener = null;
|
|
67
77
|
const deadline = new Promise((_, reject) => {
|
|
68
78
|
timer = setTimeout(() => {
|
|
69
79
|
reject(new Error(`${label} exceeded ${deadlineMs}ms deadline (timeout)`));
|
|
70
80
|
}, deadlineMs);
|
|
71
81
|
});
|
|
82
|
+
const racers = [fn(), deadline];
|
|
83
|
+
if (abortSignal) {
|
|
84
|
+
const abortRace = new Promise((_, reject) => {
|
|
85
|
+
const arm = () => {
|
|
86
|
+
abortTimer = setTimeout(() => reject(new Error(`${label} aborted by user`)), abortGraceMs);
|
|
87
|
+
};
|
|
88
|
+
if (abortSignal.aborted)
|
|
89
|
+
arm();
|
|
90
|
+
else {
|
|
91
|
+
abortListener = arm;
|
|
92
|
+
abortSignal.addEventListener("abort", arm, { once: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
racers.push(abortRace);
|
|
96
|
+
}
|
|
72
97
|
try {
|
|
73
|
-
return await Promise.race(
|
|
98
|
+
return await Promise.race(racers);
|
|
74
99
|
}
|
|
75
100
|
finally {
|
|
76
101
|
if (timer)
|
|
77
102
|
clearTimeout(timer);
|
|
103
|
+
if (abortTimer)
|
|
104
|
+
clearTimeout(abortTimer);
|
|
105
|
+
if (abortListener && abortSignal)
|
|
106
|
+
abortSignal.removeEventListener("abort", abortListener);
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
//# sourceMappingURL=llm-deadline.js.map
|
|
@@ -5,6 +5,39 @@ export type PermissionMode = "safe" | "auto-edit" | "yolo";
|
|
|
5
5
|
* without manual confirmation when the user has opted into auto-edit mode.
|
|
6
6
|
*/
|
|
7
7
|
export declare const AUTO_EDIT_ALLOWED: ReadonlySet<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Structured block result returned by safety checks.
|
|
10
|
+
* `null` means the command is safe to proceed.
|
|
11
|
+
*/
|
|
12
|
+
export interface SafetyBlockResult {
|
|
13
|
+
/** Machine-readable kind for routing to the right askcard layout. */
|
|
14
|
+
kind: "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
|
|
15
|
+
/** Human-readable reason explaining why it was blocked. */
|
|
16
|
+
reason: string;
|
|
17
|
+
/** The original command text that was blocked. */
|
|
18
|
+
command: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns a SafetyBlockResult if `command` matches a catastrophic
|
|
22
|
+
* pattern, or `null` if the command is safe to proceed.
|
|
23
|
+
*
|
|
24
|
+
* This is checked at the tool layer (BashTool.prepareCommand) and is
|
|
25
|
+
* independent of the UI-layer permission approval system — it applies even
|
|
26
|
+
* in yolo mode.
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkCatastrophicCommand(command: string): SafetyBlockResult | null;
|
|
29
|
+
/**
|
|
30
|
+
* Check a command against the "dangerous" patterns used by `toolNeedsApproval`.
|
|
31
|
+
* These are less severe than catastrophic but should still prompt for approval
|
|
32
|
+
* in safe / auto-edit modes.
|
|
33
|
+
*
|
|
34
|
+
* Returns a SafetyBlockResult or null if safe.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkDangerousCommand(command: string): SafetyBlockResult | null;
|
|
37
|
+
/**
|
|
38
|
+
* Check if a command is a safety-blocked git operation (push-on-red, broad staging).
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkGitSafetyCommand(command: string, gitSafetyKey: string): SafetyBlockResult | null;
|
|
8
41
|
/**
|
|
9
42
|
* Returns true if the given tool call requires manual user approval
|
|
10
43
|
* under the specified permission mode.
|
|
@@ -19,6 +19,159 @@ export const AUTO_EDIT_ALLOWED = new Set([
|
|
|
19
19
|
"grep",
|
|
20
20
|
"list_directory",
|
|
21
21
|
]);
|
|
22
|
+
/**
|
|
23
|
+
* Catastrophic commands: unrecoverable operations that are ALWAYS hard-blocked
|
|
24
|
+
* at the tool layer regardless of permission mode (safe/auto-edit/yolo).
|
|
25
|
+
*
|
|
26
|
+
* These are patterns where the potential for irreversible damage to the host
|
|
27
|
+
* system is high enough that no user approval dialog is sufficient — they must
|
|
28
|
+
* never execute. This list intentionally errs on the side of caution.
|
|
29
|
+
*
|
|
30
|
+
* Override: set MUONROI_ALLOW_CATASTROPHIC=1 to bypass (not recommended;
|
|
31
|
+
* intended only for explicit test harness scenarios).
|
|
32
|
+
*/
|
|
33
|
+
const CATASTROPHIC_PATTERNS = [
|
|
34
|
+
// Disk / filesystem destruction
|
|
35
|
+
{
|
|
36
|
+
pattern: /\bdd\b[^|&;\n]*\bof=\/dev\//i,
|
|
37
|
+
reason: "dd writing to a raw device can destroy disk contents irreversibly.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: /\bmkfs\b/i,
|
|
41
|
+
reason: "mkfs formats a filesystem, destroying all data on the target device.",
|
|
42
|
+
},
|
|
43
|
+
// Fork bomb — classic bash pattern: :(){ :|:& };:
|
|
44
|
+
// Match the defining signature: a function named `:` that calls itself recursively.
|
|
45
|
+
// This is tight enough to avoid false positives on URLs containing `:/`.
|
|
46
|
+
{
|
|
47
|
+
pattern: /:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:/,
|
|
48
|
+
reason: "Fork bomb pattern detected — would exhaust host process table.",
|
|
49
|
+
},
|
|
50
|
+
// Remote code execution via piped shell
|
|
51
|
+
{
|
|
52
|
+
pattern: /\bcurl\b[^|&;\n]*\|\s*(?:ba)?sh\b/i,
|
|
53
|
+
reason: "curl piped to a shell executes arbitrary remote code without inspection.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /\bwget\b[^|&;\n]*\|\s*(?:ba)?sh\b/i,
|
|
57
|
+
reason: "wget piped to a shell executes arbitrary remote code without inspection.",
|
|
58
|
+
},
|
|
59
|
+
// Privilege escalation
|
|
60
|
+
{
|
|
61
|
+
pattern: /\bsudo\s+/i,
|
|
62
|
+
reason: "sudo privilege escalation is not permitted from the agent shell.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pattern: /\bsu\s+-\s*/i,
|
|
66
|
+
reason: "su to root is not permitted from the agent shell.",
|
|
67
|
+
},
|
|
68
|
+
// Persistence mechanisms — block crontab edits/writes; allow only -l (list, read-only)
|
|
69
|
+
// crontab <file>, crontab -, crontab -e all create/modify the crontab.
|
|
70
|
+
{
|
|
71
|
+
pattern: /\bcrontab\b(?!\s+-l\b)/i,
|
|
72
|
+
reason: "crontab write/edit commands create persistent execution outside the session.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
pattern: /\/etc\/(?:rc\.d|init\.d|systemd|cron)/i,
|
|
76
|
+
reason: "Writing to system init or cron directories would create persistence.",
|
|
77
|
+
},
|
|
78
|
+
// Reverse shells
|
|
79
|
+
{
|
|
80
|
+
pattern: /\bnc\b[^|&;\n]*(?:-e|-c)\s+/i,
|
|
81
|
+
reason: "nc with -e/-c flag can open a reverse shell.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
pattern: /\bsocat\b[^|&;\n]*(?:EXEC|SYSTEM):/i,
|
|
85
|
+
reason: "socat with EXEC/SYSTEM can open a reverse shell.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
pattern: /\/dev\/tcp\//i,
|
|
89
|
+
reason: "Bash /dev/tcp redirect can open a covert reverse shell connection.",
|
|
90
|
+
},
|
|
91
|
+
// Exfiltration of credentials / secrets directories
|
|
92
|
+
{
|
|
93
|
+
pattern: /(?:tar|zip|gzip|rsync|scp|sftp|ftp)\b[^|&;\n]*\.(?:muonroi-cli|ssh|gnupg|aws|kube)/i,
|
|
94
|
+
reason: "Archiving or uploading credential directories is not permitted.",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* Returns a SafetyBlockResult if `command` matches a catastrophic
|
|
99
|
+
* pattern, or `null` if the command is safe to proceed.
|
|
100
|
+
*
|
|
101
|
+
* This is checked at the tool layer (BashTool.prepareCommand) and is
|
|
102
|
+
* independent of the UI-layer permission approval system — it applies even
|
|
103
|
+
* in yolo mode.
|
|
104
|
+
*/
|
|
105
|
+
export function checkCatastrophicCommand(command) {
|
|
106
|
+
if (process.env.MUONROI_ALLOW_CATASTROPHIC === "1")
|
|
107
|
+
return null;
|
|
108
|
+
for (const { pattern, reason } of CATASTROPHIC_PATTERNS) {
|
|
109
|
+
if (pattern.test(command)) {
|
|
110
|
+
return { kind: "catastrophic", reason, command };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check a command against the "dangerous" patterns used by `toolNeedsApproval`.
|
|
117
|
+
* These are less severe than catastrophic but should still prompt for approval
|
|
118
|
+
* in safe / auto-edit modes.
|
|
119
|
+
*
|
|
120
|
+
* Returns a SafetyBlockResult or null if safe.
|
|
121
|
+
*/
|
|
122
|
+
export function checkDangerousCommand(command) {
|
|
123
|
+
const dangerousPatterns = [
|
|
124
|
+
// Filesystem destruction
|
|
125
|
+
/rm\s+-rf?\s+\/(?!tmp|var\/tmp)/i,
|
|
126
|
+
// Unrestricted chmod
|
|
127
|
+
/chmod\s+(?:777|a\+[rwx])/i,
|
|
128
|
+
// Code injection
|
|
129
|
+
/eval\s*\(/i,
|
|
130
|
+
/exec\s*\(/i,
|
|
131
|
+
// External network fetches (non-local)
|
|
132
|
+
/curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
|
|
133
|
+
/wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
|
|
134
|
+
// Process substitution with external commands
|
|
135
|
+
/\$\([^)]*(?:curl|wget|nc|socat)[^)]*\)/i,
|
|
136
|
+
// Writing to /etc (besides allowed subdirs)
|
|
137
|
+
/\bchown\s+root/i,
|
|
138
|
+
];
|
|
139
|
+
for (const pattern of dangerousPatterns) {
|
|
140
|
+
if (pattern.test(command)) {
|
|
141
|
+
return {
|
|
142
|
+
kind: "dangerous",
|
|
143
|
+
reason: `Command matched dangerous pattern: ${pattern.source}`,
|
|
144
|
+
command,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Check if a command is a safety-blocked git operation (push-on-red, broad staging).
|
|
152
|
+
*/
|
|
153
|
+
export function checkGitSafetyCommand(command, gitSafetyKey) {
|
|
154
|
+
// Lazy import to avoid circular deps at module level
|
|
155
|
+
const { analyzeGitCommand, checkPushGate, checkSensitiveStaging } = require_inline_safety_deps();
|
|
156
|
+
const gt = analyzeGitCommand(command);
|
|
157
|
+
if (gt.isPush) {
|
|
158
|
+
const gate = checkPushGate(gitSafetyKey);
|
|
159
|
+
if (gate.blocked) {
|
|
160
|
+
return {
|
|
161
|
+
kind: "git-safety",
|
|
162
|
+
reason: `git push blocked: ${gate.failed.join(", ")}`,
|
|
163
|
+
command,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Inline require to avoid circular dependency at module load.
|
|
170
|
+
function require_inline_safety_deps() {
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
172
|
+
const mod = require("../tools/git-safety.js");
|
|
173
|
+
return mod;
|
|
174
|
+
}
|
|
22
175
|
/**
|
|
23
176
|
* Returns true if the given tool call requires manual user approval
|
|
24
177
|
* under the specified permission mode.
|
|
@@ -34,12 +187,20 @@ export function toolNeedsApproval(toolName, mode, context) {
|
|
|
34
187
|
if (!cmd)
|
|
35
188
|
return false;
|
|
36
189
|
const patterns = [
|
|
190
|
+
// Filesystem destruction
|
|
37
191
|
/rm\s+-rf?\s+\/(?!tmp|var\/tmp)/i,
|
|
38
|
-
|
|
39
|
-
/
|
|
40
|
-
|
|
192
|
+
// Unrestricted chmod
|
|
193
|
+
/chmod\s+(?:777|a\+[rwx])/i,
|
|
194
|
+
// Code injection
|
|
41
195
|
/eval\s*\(/i,
|
|
42
196
|
/exec\s*\(/i,
|
|
197
|
+
// External network fetches (non-local)
|
|
198
|
+
/curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
|
|
199
|
+
/wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
|
|
200
|
+
// Process substitution with external commands
|
|
201
|
+
/\$\([^)]*(?:curl|wget|nc|socat)[^)]*\)/i,
|
|
202
|
+
// Writing to /etc (besides allowed subdirs)
|
|
203
|
+
/\bchown\s+root/i,
|
|
43
204
|
];
|
|
44
205
|
return patterns.some((p) => p.test(cmd));
|
|
45
206
|
};
|