@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.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/README.md +13 -10
- package/dist/agent/agent-definition-registry.d.ts +3 -0
- package/dist/agent/agent-definition-registry.js +3 -1
- package/dist/agent/agent-definition.d.ts +15 -0
- package/dist/agent/agent-definition.js +27 -3
- package/dist/arena/context/context-tools.js +47 -4
- package/dist/arena/ledger.js +9 -1
- package/dist/arena/strategies/utils.d.ts +1 -4
- package/dist/arena/strategies/utils.js +7 -84
- package/dist/automation/cron-expr.d.ts +10 -0
- package/dist/automation/cron-expr.js +23 -5
- package/dist/automation/runner.d.ts +21 -4
- package/dist/automation/runner.js +48 -12
- package/dist/automation/scheduler.d.ts +38 -0
- package/dist/automation/scheduler.js +89 -12
- package/dist/automation/store.js +5 -2
- package/dist/capability-control/disabled-lists.d.ts +25 -0
- package/dist/capability-control/disabled-lists.js +57 -0
- package/dist/capability-control/overlay.d.ts +15 -0
- package/dist/capability-control/overlay.js +27 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
- package/dist/cc-orchestrator/agent-adapter.js +133 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
- package/dist/cc-orchestrator/cc-capability.js +53 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
- package/dist/cc-orchestrator/codex-session-history.js +187 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +214 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
- package/dist/cc-orchestrator/external-agent-driver.js +69 -0
- package/dist/cc-orchestrator/index.d.ts +8 -0
- package/dist/cc-orchestrator/index.js +8 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
- package/dist/cc-orchestrator/relevance-judge.js +29 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
- package/dist/cc-orchestrator/session-discovery.js +125 -0
- package/dist/cc-orchestrator/session-history.d.ts +19 -0
- package/dist/cc-orchestrator/session-history.js +67 -0
- package/dist/cli/agent-server-stdio.d.ts +15 -1
- package/dist/cli/agent-server-stdio.js +115 -17
- package/dist/cli/agent-server-tcp.js +40 -26
- package/dist/context/compaction.d.ts +56 -0
- package/dist/context/compaction.js +186 -0
- package/dist/context/manager.js +25 -4
- package/dist/cost-tracker.js +5 -61
- package/dist/credentials/cipher.d.ts +49 -0
- package/dist/credentials/cipher.js +45 -0
- package/dist/credentials/cookie-jar.d.ts +24 -0
- package/dist/credentials/cookie-jar.js +40 -0
- package/dist/credentials/index.d.ts +7 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/inject-credential-tool.d.ts +20 -0
- package/dist/credentials/inject-credential-tool.js +130 -0
- package/dist/credentials/store.d.ts +72 -0
- package/dist/credentials/store.js +184 -0
- package/dist/credentials/types.d.ts +56 -0
- package/dist/credentials/use-credential-tool.d.ts +29 -0
- package/dist/credentials/use-credential-tool.js +205 -0
- package/dist/credentials/use-gate.d.ts +56 -0
- package/dist/credentials/use-gate.js +52 -0
- package/dist/data/model-metadata.d.ts +77 -0
- package/dist/data/model-metadata.js +56 -0
- package/dist/data/model-metadata.json +216 -0
- package/dist/data/openrouter-models.d.ts +18 -7
- package/dist/data/openrouter-models.js +35 -8
- package/dist/engine/aux-key.d.ts +10 -0
- package/dist/engine/aux-key.js +11 -0
- package/dist/engine/dynamic-tool-defs.d.ts +19 -0
- package/dist/engine/dynamic-tool-defs.js +36 -0
- package/dist/engine/engine.d.ts +267 -138
- package/dist/engine/engine.js +957 -191
- package/dist/engine/friendly-error.d.ts +18 -0
- package/dist/engine/friendly-error.js +63 -0
- package/dist/engine/goal.d.ts +145 -0
- package/dist/engine/goal.js +144 -0
- package/dist/engine/image-policy.d.ts +13 -0
- package/dist/engine/image-policy.js +24 -0
- package/dist/engine/model-connections-pool.d.ts +17 -0
- package/dist/engine/model-connections-pool.js +66 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +25 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.js +2 -1
- package/dist/engine/sandbox-cache-key.d.ts +10 -0
- package/dist/engine/sandbox-cache-key.js +9 -0
- package/dist/engine/sandbox-config.d.ts +31 -0
- package/dist/engine/sandbox-config.js +38 -0
- package/dist/engine/session-usage.d.ts +19 -0
- package/dist/engine/session-usage.js +25 -0
- package/dist/engine/steer-queue.d.ts +32 -0
- package/dist/engine/steer-queue.js +26 -0
- package/dist/engine/streaming-tool-queue.d.ts +12 -0
- package/dist/engine/streaming-tool-queue.js +50 -10
- package/dist/engine/turn-loop.d.ts +67 -3
- package/dist/engine/turn-loop.js +292 -41
- package/dist/engine/types.d.ts +175 -0
- package/dist/engine/types.js +13 -0
- package/dist/external-agents/config.d.ts +2 -0
- package/dist/external-agents/config.js +15 -0
- package/dist/external-agents/types.d.ts +31 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/git/utils.js +11 -6
- package/dist/git/worktree.d.ts +48 -0
- package/dist/git/worktree.js +86 -10
- package/dist/hooks/goal-stop-hook.d.ts +28 -0
- package/dist/hooks/goal-stop-hook.js +186 -9
- package/dist/hooks/registry.d.ts +8 -0
- package/dist/hooks/registry.js +19 -0
- package/dist/hooks/shell-runner.js +10 -21
- package/dist/index.d.ts +49 -6
- package/dist/index.js +60 -4
- package/dist/llm/capabilities/param-specs.d.ts +14 -0
- package/dist/llm/capabilities/param-specs.js +62 -0
- package/dist/llm/capabilities/rules.js +4 -0
- package/dist/llm/capabilities/types.d.ts +10 -0
- package/dist/llm/client-base.d.ts +28 -1
- package/dist/llm/client-base.js +119 -13
- package/dist/llm/model-cache.js +4 -2
- package/dist/llm/model-pool.d.ts +14 -0
- package/dist/llm/model-pool.js +13 -0
- package/dist/llm/provider-auth.d.ts +41 -0
- package/dist/llm/provider-auth.js +76 -0
- package/dist/llm/provider-catalog.d.ts +4 -0
- package/dist/llm/providers/anthropic.js +64 -14
- package/dist/llm/providers/openai.d.ts +36 -0
- package/dist/llm/providers/openai.js +257 -51
- package/dist/llm/reasoning-setting.d.ts +3 -3
- package/dist/llm/reasoning-setting.js +9 -1
- package/dist/llm/stream-watchdog.js +5 -1
- package/dist/llm/token-counter.js +9 -2
- package/dist/llm/types.d.ts +4 -0
- package/dist/logging/sanitize-messages.js +22 -0
- package/dist/lsp/manager.d.ts +1 -1
- package/dist/lsp/manager.js +40 -10
- package/dist/model-catalog/builtin.d.ts +12 -0
- package/dist/model-catalog/builtin.js +407 -0
- package/dist/model-catalog/gen-connections.d.ts +20 -0
- package/dist/model-catalog/gen-connections.js +28 -0
- package/dist/model-catalog/index.d.ts +41 -0
- package/dist/model-catalog/index.js +90 -0
- package/dist/model-catalog/params.d.ts +20 -0
- package/dist/model-catalog/params.js +45 -0
- package/dist/model-catalog/resolve.d.ts +48 -0
- package/dist/model-catalog/resolve.js +33 -0
- package/dist/model-catalog/save-entry.d.ts +32 -0
- package/dist/model-catalog/save-entry.js +104 -0
- package/dist/model-catalog/types.d.ts +561 -0
- package/dist/model-catalog/types.js +93 -0
- package/dist/model-catalog/upsert.d.ts +9 -0
- package/dist/model-catalog/upsert.js +8 -0
- package/dist/onboarding.d.ts +12 -82
- package/dist/onboarding.js +61 -326
- package/dist/plugins/gitOps.d.ts +19 -0
- package/dist/plugins/gitOps.js +73 -4
- package/dist/plugins/installer/checkUpdate.d.ts +16 -0
- package/dist/plugins/installer/checkUpdate.js +32 -0
- package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
- package/dist/plugins/installer/codex/convertCommands.js +46 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
- package/dist/plugins/installer/codex/convertMcp.js +45 -5
- package/dist/plugins/installer/install.js +25 -1
- package/dist/plugins/installer/installFromArchive.d.ts +43 -0
- package/dist/plugins/installer/installFromArchive.js +134 -0
- package/dist/plugins/installer/installFromSource.js +8 -2
- package/dist/plugins/installer/loadPluginAgents.js +6 -2
- package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
- package/dist/plugins/installer/loadPluginMcp.js +35 -2
- package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
- package/dist/plugins/installer/pruneDisabled.js +73 -0
- package/dist/plugins/installer/types.d.ts +5 -2
- package/dist/plugins/installer/types.js +1 -0
- package/dist/plugins/installer/uninstall.js +4 -0
- package/dist/plugins/installer/unzip.d.ts +14 -0
- package/dist/plugins/installer/unzip.js +82 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +53 -21
- package/dist/plugins/loadPluginHooks.d.ts +47 -1
- package/dist/plugins/loadPluginHooks.js +73 -1
- package/dist/plugins/marketplaceManager.d.ts +7 -0
- package/dist/plugins/marketplaceManager.js +20 -0
- package/dist/plugins/pluginCommandHook.js +4 -18
- package/dist/plugins/pluginContent.d.ts +30 -0
- package/dist/plugins/pluginContent.js +83 -0
- package/dist/plugins/pluginInstaller.d.ts +13 -0
- package/dist/plugins/pluginInstaller.js +42 -8
- package/dist/plugins/schemas.js +1 -0
- package/dist/plugins/types.d.ts +6 -0
- package/dist/preset/index.d.ts +7 -1
- package/dist/preset/index.js +96 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +27 -0
- package/dist/prompt/composer.js +67 -21
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +1 -0
- package/dist/prompt/sections/browser.md +10 -0
- package/dist/prompt/sections/coding.md +4 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -2
- package/dist/protocol/chat-session-manager.js +36 -0
- package/dist/protocol/chat-session.d.ts +65 -0
- package/dist/protocol/chat-session.js +94 -2
- package/dist/protocol/client.d.ts +27 -0
- package/dist/protocol/client.js +38 -0
- package/dist/protocol/server.d.ts +112 -0
- package/dist/protocol/server.js +586 -16
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +46 -0
- package/dist/protocol/types.js +16 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +155 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/run/FileRunStore.js +8 -3
- package/dist/run/RunApprovalBackend.js +25 -5
- package/dist/run/RunManager.d.ts +12 -0
- package/dist/run/RunManager.js +35 -0
- package/dist/run/factory.d.ts +1 -1
- package/dist/runtime/background-shell.d.ts +139 -0
- package/dist/runtime/background-shell.js +495 -0
- package/dist/runtime/output-clean.d.ts +24 -0
- package/dist/runtime/output-clean.js +41 -0
- package/dist/runtime/ring-file.d.ts +64 -0
- package/dist/runtime/ring-file.js +174 -0
- package/dist/runtime/safe-spawn.js +15 -34
- package/dist/runtime/spawn-common.d.ts +148 -0
- package/dist/runtime/spawn-common.js +303 -0
- package/dist/runtime/truncate-output.d.ts +22 -0
- package/dist/runtime/truncate-output.js +49 -0
- package/dist/runtime/utf8-cut.d.ts +11 -0
- package/dist/runtime/utf8-cut.js +23 -0
- package/dist/services/auto-dream.d.ts +4 -0
- package/dist/services/auto-dream.js +26 -26
- package/dist/services/diagnostics.d.ts +1 -2
- package/dist/services/diagnostics.js +12 -7
- package/dist/services/extract-memories.d.ts +14 -1
- package/dist/services/extract-memories.js +45 -6
- package/dist/services/memory-orchestrator.d.ts +21 -0
- package/dist/services/memory-orchestrator.js +125 -50
- package/dist/services/session-memory.js +24 -12
- package/dist/session/file-history.d.ts +124 -1
- package/dist/session/file-history.js +222 -6
- package/dist/session/memory.d.ts +116 -2
- package/dist/session/memory.js +250 -28
- package/dist/session/session-manager.d.ts +40 -0
- package/dist/session/session-manager.js +113 -3
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +25 -1
- package/dist/session/transcript.js +35 -2
- package/dist/session/undo-target.d.ts +67 -0
- package/dist/session/undo-target.js +144 -0
- package/dist/settings/disk-defaults.d.ts +9 -2
- package/dist/settings/disk-defaults.js +11 -2
- package/dist/settings/feature-flags.d.ts +64 -0
- package/dist/settings/feature-flags.js +61 -0
- package/dist/settings/manager.d.ts +68 -1
- package/dist/settings/manager.js +266 -29
- package/dist/settings/migrate-config.d.ts +45 -0
- package/dist/settings/migrate-config.js +125 -0
- package/dist/settings/schema-export.d.ts +25 -0
- package/dist/settings/schema-export.js +38 -0
- package/dist/settings/schema.d.ts +1186 -771
- package/dist/settings/schema.js +268 -97
- package/dist/skills/scanner.d.ts +9 -0
- package/dist/skills/scanner.js +30 -2
- package/dist/stt/resolve-transcribe.d.ts +31 -0
- package/dist/stt/resolve-transcribe.js +108 -0
- package/dist/stt/transcribe.d.ts +51 -0
- package/dist/stt/transcribe.js +65 -0
- package/dist/tool-system/browser-bridge.d.ts +226 -0
- package/dist/tool-system/browser-bridge.js +163 -0
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
- package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
- package/dist/tool-system/builtin/agent-notifications.js +9 -3
- package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
- package/dist/tool-system/builtin/agent-output-file.js +72 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -0
- package/dist/tool-system/builtin/agent.d.ts +21 -1
- package/dist/tool-system/builtin/agent.js +489 -42
- package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
- package/dist/tool-system/builtin/apply-patch/index.js +0 -15
- package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
- package/dist/tool-system/builtin/background-jobs.js +124 -0
- package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
- package/dist/tool-system/builtin/background-shell-tools.js +108 -0
- package/dist/tool-system/builtin/background-work.d.ts +67 -0
- package/dist/tool-system/builtin/background-work.js +86 -0
- package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
- package/dist/tool-system/builtin/bash-output-style.js +40 -0
- package/dist/tool-system/builtin/bash.d.ts +5 -2
- package/dist/tool-system/builtin/bash.js +98 -62
- package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
- package/dist/tool-system/builtin/browser-tools.js +312 -0
- package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
- package/dist/tool-system/builtin/cancel-goal.js +64 -0
- package/dist/tool-system/builtin/check-quota.d.ts +15 -0
- package/dist/tool-system/builtin/check-quota.js +34 -0
- package/dist/tool-system/builtin/config.js +7 -0
- package/dist/tool-system/builtin/cron.d.ts +7 -0
- package/dist/tool-system/builtin/cron.js +55 -3
- package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
- package/dist/tool-system/builtin/drive-claude-code.js +157 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
- package/dist/tool-system/builtin/edit.js +20 -16
- package/dist/tool-system/builtin/eol.d.ts +29 -0
- package/dist/tool-system/builtin/eol.js +37 -0
- package/dist/tool-system/builtin/file-cache.d.ts +6 -0
- package/dist/tool-system/builtin/file-cache.js +8 -0
- package/dist/tool-system/builtin/generate-image.d.ts +35 -0
- package/dist/tool-system/builtin/generate-image.js +278 -50
- package/dist/tool-system/builtin/generate-video.d.ts +55 -0
- package/dist/tool-system/builtin/generate-video.js +361 -0
- package/dist/tool-system/builtin/glob.js +0 -7
- package/dist/tool-system/builtin/grep.js +6 -8
- package/dist/tool-system/builtin/image-providers.d.ts +86 -0
- package/dist/tool-system/builtin/image-providers.js +190 -0
- package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
- package/dist/tool-system/builtin/image-uploader.js +74 -0
- package/dist/tool-system/builtin/index.d.ts +7 -1
- package/dist/tool-system/builtin/index.js +249 -25
- package/dist/tool-system/builtin/mcp-tools.js +23 -3
- package/dist/tool-system/builtin/memory.js +45 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -7
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/read.js +10 -10
- package/dist/tool-system/builtin/repl.js +4 -1
- package/dist/tool-system/builtin/skill.js +9 -0
- package/dist/tool-system/builtin/sleep.js +3 -2
- package/dist/tool-system/builtin/tool-search.js +25 -7
- package/dist/tool-system/builtin/video-providers.d.ts +154 -0
- package/dist/tool-system/builtin/video-providers.js +235 -0
- package/dist/tool-system/builtin/web-fetch.js +12 -2
- package/dist/tool-system/builtin/web-search.js +21 -7
- package/dist/tool-system/builtin/worktree.d.ts +2 -1
- package/dist/tool-system/builtin/worktree.js +31 -4
- package/dist/tool-system/builtin/write.js +0 -6
- package/dist/tool-system/context.d.ts +154 -6
- package/dist/tool-system/executor.d.ts +3 -1
- package/dist/tool-system/executor.js +172 -88
- package/dist/tool-system/investigation-guard.js +1 -1
- package/dist/tool-system/mcp-manager.d.ts +42 -4
- package/dist/tool-system/mcp-manager.js +186 -20
- package/dist/tool-system/path-policy.d.ts +5 -0
- package/dist/tool-system/path-policy.js +307 -8
- package/dist/tool-system/permission.d.ts +36 -1
- package/dist/tool-system/permission.js +281 -69
- package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
- package/dist/tool-system/plan-mode-allowlist.js +24 -2
- package/dist/tool-system/registry.d.ts +1 -0
- package/dist/tool-system/registry.js +16 -2
- package/dist/tool-system/sandbox/index.d.ts +8 -0
- package/dist/tool-system/sandbox/index.js +7 -2
- package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
- package/dist/tool-system/validate-tool-metadata.js +63 -0
- package/dist/types.d.ts +217 -6
- package/dist/updater.js +20 -9
- package/dist/utils/envUtils.d.ts +0 -9
- package/dist/utils/envUtils.js +3 -28
- package/dist/utils/exec.d.ts +40 -0
- package/dist/utils/exec.js +144 -0
- package/dist/utils/json.d.ts +12 -0
- package/dist/utils/json.js +92 -0
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/toolDisplay.js +0 -1
- package/package.json +13 -7
- package/dist/agent/coordinator.d.ts +0 -49
- package/dist/agent/coordinator.js +0 -77
- package/dist/settings/manager.test.js +0 -73
- package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
- package/dist/tool-system/builtin/remote-trigger.js +0 -54
- package/dist/tool-system/builtin/send-message.d.ts +0 -6
- package/dist/tool-system/builtin/send-message.js +0 -47
- /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission system — classifier + approval backend.
|
|
3
3
|
*/
|
|
4
|
+
import { resolve as resolvePath, dirname } from "node:path";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, } from "node:fs";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
4
7
|
import { logger as rootPermLogger } from "../logging/logger.js";
|
|
8
|
+
import { READ_ONLY_TOOLS } from "./plan-mode-allowlist.js";
|
|
5
9
|
export class HeadlessApprovalBackend {
|
|
6
10
|
mode;
|
|
7
11
|
constructor(mode) {
|
|
@@ -14,8 +18,7 @@ export class HeadlessApprovalBackend {
|
|
|
14
18
|
case "deny-all":
|
|
15
19
|
return { approved: false, reason: "headless deny-all mode" };
|
|
16
20
|
case "approve-read-only": {
|
|
17
|
-
|
|
18
|
-
if (readOnlyTools.includes(req.toolName)) {
|
|
21
|
+
if (READ_ONLY_TOOLS.has(req.toolName)) {
|
|
19
22
|
return { approved: true };
|
|
20
23
|
}
|
|
21
24
|
return { approved: false, reason: "read-only mode: write operations denied" };
|
|
@@ -103,7 +106,13 @@ export class AutoApprovalBackend {
|
|
|
103
106
|
* the next time the user opens the project.
|
|
104
107
|
*/
|
|
105
108
|
export class InteractiveApprovalBackend {
|
|
106
|
-
|
|
109
|
+
// Session-scoped grants, keyed on the OPERATION (tool + narrowed argsPattern
|
|
110
|
+
// via buildProjectRule), NOT just the tool name. Approving `git status` for
|
|
111
|
+
// the session must not auto-allow `rm -rf /` (both are "Bash"). Allow and
|
|
112
|
+
// deny are tracked separately so a one-off deny of `curl evil` never blocks
|
|
113
|
+
// an unrelated `git status`. (TODO 5.1 会话级权限缓存.)
|
|
114
|
+
sessionAllowRules = [];
|
|
115
|
+
sessionDenyRules = [];
|
|
107
116
|
promptFn = null;
|
|
108
117
|
cwd = null;
|
|
109
118
|
// Project rules saved during this session. Kept in-memory (not re-read from
|
|
@@ -113,6 +122,13 @@ export class InteractiveApprovalBackend {
|
|
|
113
122
|
// the same session. We accumulate here and hand the full list to the callback.
|
|
114
123
|
savedProjectRules = [];
|
|
115
124
|
onProjectRules = null;
|
|
125
|
+
// Serialize prompts so a burst of parallel tool calls doesn't queue N
|
|
126
|
+
// identical cards: while one ask is outstanding, later requests wait here
|
|
127
|
+
// and RE-CHECK the session rules when their turn comes — the first
|
|
128
|
+
// "本会话一直允许" answer then silently absorbs the queued duplicates.
|
|
129
|
+
// (All callers were already serialized at the UI anyway — the renderer
|
|
130
|
+
// shows one approval card at a time.)
|
|
131
|
+
promptTurn = Promise.resolve();
|
|
116
132
|
setPromptFn(fn) {
|
|
117
133
|
this.promptFn = fn;
|
|
118
134
|
}
|
|
@@ -140,25 +156,75 @@ export class InteractiveApprovalBackend {
|
|
|
140
156
|
this.onProjectRules = fn;
|
|
141
157
|
}
|
|
142
158
|
async requestApproval(req) {
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
return
|
|
147
|
-
if (
|
|
159
|
+
// Fast path: the operation may already be covered by a session rule.
|
|
160
|
+
const cached = this.checkSessionRules(req);
|
|
161
|
+
if (cached)
|
|
162
|
+
return cached;
|
|
163
|
+
if (!this.promptFn) {
|
|
164
|
+
return { approved: false, reason: "interactive approval backend has no prompt function" };
|
|
165
|
+
}
|
|
166
|
+
// Serialize prompts and re-check rules when our turn comes (see promptTurn
|
|
167
|
+
// field doc): a burst of parallel tool calls all passes the fast path
|
|
168
|
+
// before the first decision lands; without the re-check the user had to
|
|
169
|
+
// answer one card per duplicate.
|
|
170
|
+
const prevTurn = this.promptTurn;
|
|
171
|
+
let release;
|
|
172
|
+
this.promptTurn = new Promise((r) => (release = r));
|
|
173
|
+
try {
|
|
174
|
+
await prevTurn;
|
|
175
|
+
const nowCached = this.checkSessionRules(req);
|
|
176
|
+
if (nowCached)
|
|
177
|
+
return nowCached;
|
|
178
|
+
return await this.promptAndRecord(req);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
release();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Session-rule lookup — operation-scoped (see sessionAllowRules doc). Deny
|
|
185
|
+
* wins over allow if both somehow match (conservative). Null = no rule. */
|
|
186
|
+
checkSessionRules(req) {
|
|
187
|
+
if (this.sessionDenyRules.some((r) => ruleMatches(r, req.toolName, req.args))) {
|
|
148
188
|
return { approved: false };
|
|
189
|
+
}
|
|
190
|
+
if (this.sessionAllowRules.some((r) => ruleMatches(r, req.toolName, req.args))) {
|
|
191
|
+
return { approved: true };
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/** The actual interactive ask + rule recording (runs inside the prompt turn). */
|
|
196
|
+
async promptAndRecord(req) {
|
|
149
197
|
if (!this.promptFn) {
|
|
150
198
|
return { approved: false, reason: "interactive approval backend has no prompt function" };
|
|
151
199
|
}
|
|
152
200
|
const result = await this.promptFn(req);
|
|
153
201
|
const scope = result.scope ?? (result.always ? "session" : "once");
|
|
202
|
+
// Path narrowing only rides on an APPROVE: a path-scoped deny is confusing
|
|
203
|
+
// (deny stays tool-wide). pathScope is ignored by buildProjectRule for
|
|
204
|
+
// non-file tools / when absent.
|
|
205
|
+
const ruleOpts = result.approved
|
|
206
|
+
? { pathScope: result.pathScope, cwd: this.cwd ?? undefined }
|
|
207
|
+
: undefined;
|
|
154
208
|
if (scope === "session" && result.always) {
|
|
155
|
-
|
|
209
|
+
// Narrow to the operation (Bash → head command, file tools → path scope)
|
|
210
|
+
// so the session grant is scoped, not tool-wide. A rule with no
|
|
211
|
+
// argsPattern keeps the prior tool-granularity behavior.
|
|
212
|
+
const rule = buildProjectRule(req.toolName, req.args, ruleOpts);
|
|
213
|
+
if (rule) {
|
|
214
|
+
const target = result.approved ? this.sessionAllowRules : this.sessionDenyRules;
|
|
215
|
+
const dup = target.some((r) => r.tool === rule.tool &&
|
|
216
|
+
JSON.stringify(r.argsPattern) === JSON.stringify(rule.argsPattern));
|
|
217
|
+
if (!dup) {
|
|
218
|
+
// buildProjectRule always returns decision:"allow"; flip for deny.
|
|
219
|
+
target.push(result.approved ? rule : { ...rule, decision: "deny" });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
156
222
|
}
|
|
157
223
|
else if (scope === "project" && result.approved) {
|
|
158
224
|
// Project-scope: persist a PermissionRule to settings.local.json.
|
|
159
225
|
// We only persist allow rules — denies stay session-only because a
|
|
160
226
|
// persisted deny is harder to recover from than a session deny.
|
|
161
|
-
const rule = buildProjectRule(req.toolName, req.args);
|
|
227
|
+
const rule = buildProjectRule(req.toolName, req.args, ruleOpts);
|
|
162
228
|
if (rule && this.cwd) {
|
|
163
229
|
try {
|
|
164
230
|
persistProjectRule(this.cwd, rule);
|
|
@@ -189,20 +255,54 @@ export class InteractiveApprovalBackend {
|
|
|
189
255
|
console.error("Failed to persist project permission rule:", err.message);
|
|
190
256
|
}
|
|
191
257
|
}
|
|
192
|
-
// Also seed session
|
|
193
|
-
// even if the classifier path doesn't pick
|
|
194
|
-
|
|
258
|
+
// Also seed the session allow list (operation-scoped) so the rest of
|
|
259
|
+
// this REPL session benefits even if the classifier path doesn't pick
|
|
260
|
+
// the rule up immediately.
|
|
261
|
+
if (rule) {
|
|
262
|
+
const dup = this.sessionAllowRules.some((r) => r.tool === rule.tool &&
|
|
263
|
+
JSON.stringify(r.argsPattern) === JSON.stringify(rule.argsPattern));
|
|
264
|
+
if (!dup)
|
|
265
|
+
this.sessionAllowRules.push(rule);
|
|
266
|
+
}
|
|
195
267
|
}
|
|
196
268
|
return result;
|
|
197
269
|
}
|
|
198
270
|
}
|
|
271
|
+
/** File tools whose approvals can be narrowed to a path prefix. */
|
|
272
|
+
const PATH_SCOPED_TOOLS = new Set(["Write", "Edit"]);
|
|
273
|
+
/**
|
|
274
|
+
* Build the `file_path` argsPattern that narrows a remembered file-tool grant
|
|
275
|
+
* to a path scope. Pure: takes an ALREADY-RESOLVED absolute path (the caller
|
|
276
|
+
* normalizes against cwd so `../` can't slip past the prefix) and the scope.
|
|
277
|
+
*
|
|
278
|
+
* "file" → `^<esc(abs)>$` only this exact file
|
|
279
|
+
* "dir" → `^<esc(dir(abs))>/` this file's directory + subdirectories
|
|
280
|
+
* "tool" → null no narrowing (tool-wide; legacy behavior)
|
|
281
|
+
*
|
|
282
|
+
* The dir form keeps a trailing slash so `/repo/src/` can't match a sibling
|
|
283
|
+
* like `/repo/src-secret/x.ts`. Regex metacharacters in the path are escaped.
|
|
284
|
+
*/
|
|
285
|
+
export function pathRuleArgsPattern(absPath, scope) {
|
|
286
|
+
if (scope === "tool")
|
|
287
|
+
return null;
|
|
288
|
+
if (scope === "file")
|
|
289
|
+
return { file_path: `^${escapeRegex(absPath)}$` };
|
|
290
|
+
// dir: strip the basename, ensure a single trailing slash, anchor as prefix.
|
|
291
|
+
const slash = absPath.lastIndexOf("/");
|
|
292
|
+
const dir = slash > 0 ? absPath.slice(0, slash) : absPath;
|
|
293
|
+
return { file_path: `^${escapeRegex(dir + "/")}` };
|
|
294
|
+
}
|
|
199
295
|
/**
|
|
200
296
|
* Build a PermissionRule from a single approval. Bash narrows to the head
|
|
201
297
|
* command (first whitespace token) so "git status" → allow all `git ...`.
|
|
202
|
-
*
|
|
203
|
-
*
|
|
298
|
+
*
|
|
299
|
+
* File tools (Write/Edit) narrow to a PATH scope when `opts.pathScope` is
|
|
300
|
+
* "file"/"dir": "allow Write src/foo.ts for this session" then means exactly
|
|
301
|
+
* that file (or its directory), NOT every path. Without a pathScope (or
|
|
302
|
+
* "tool"), they keep the legacy tool-wide grant. Paths are resolved against
|
|
303
|
+
* `opts.cwd` first so `../` can't widen the prefix. Other tools stay tool-wide.
|
|
204
304
|
*/
|
|
205
|
-
function buildProjectRule(toolName, args) {
|
|
305
|
+
function buildProjectRule(toolName, args, opts) {
|
|
206
306
|
if (toolName === "Bash") {
|
|
207
307
|
const cmd = String(args.command ?? "").trim();
|
|
208
308
|
const head = cmd.split(/\s+/)[0];
|
|
@@ -215,6 +315,23 @@ function buildProjectRule(toolName, args) {
|
|
|
215
315
|
reason: `Allowed via permission prompt: ${head}`,
|
|
216
316
|
};
|
|
217
317
|
}
|
|
318
|
+
if (PATH_SCOPED_TOOLS.has(toolName) && opts?.pathScope && opts.pathScope !== "tool") {
|
|
319
|
+
const raw = String(args.file_path ?? "");
|
|
320
|
+
if (raw) {
|
|
321
|
+
// Resolve against cwd so a relative or `../`-laden path becomes the real
|
|
322
|
+
// absolute path the prefix anchors to.
|
|
323
|
+
const abs = resolvePath(opts.cwd ?? process.cwd(), raw);
|
|
324
|
+
const argsPattern = pathRuleArgsPattern(abs, opts.pathScope);
|
|
325
|
+
if (argsPattern) {
|
|
326
|
+
return {
|
|
327
|
+
tool: toolName,
|
|
328
|
+
argsPattern,
|
|
329
|
+
decision: "allow",
|
|
330
|
+
reason: `Allowed via permission prompt: ${toolName} ${opts.pathScope === "dir" ? dirname(abs) + "/" : abs}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
218
335
|
return {
|
|
219
336
|
tool: toolName,
|
|
220
337
|
decision: "allow",
|
|
@@ -224,6 +341,66 @@ function buildProjectRule(toolName, args) {
|
|
|
224
341
|
function escapeRegex(s) {
|
|
225
342
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
226
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Does `rule` match this tool call? Shared by the {@link PermissionClassifier}
|
|
346
|
+
* (project/user rules) and {@link InteractiveApprovalBackend}'s session cache
|
|
347
|
+
* so the two never diverge on what "the same operation" means. Handles the
|
|
348
|
+
* JSON round-trip where a persisted RegExp comes back as a string.
|
|
349
|
+
*/
|
|
350
|
+
export function ruleMatches(rule, toolName, args) {
|
|
351
|
+
if (rule.tool !== toolName && rule.tool !== "*")
|
|
352
|
+
return false;
|
|
353
|
+
if (!rule.argsPattern)
|
|
354
|
+
return true;
|
|
355
|
+
// A Bash allow-rule is narrowed to a HEAD command (e.g. `^git(\s|$)`), but a
|
|
356
|
+
// raw regex test would let a chained command smuggle a dangerous tail past a
|
|
357
|
+
// grant for its head: `git status && rm -rf /` starts with `git `, so the
|
|
358
|
+
// grant for `git status` would otherwise auto-allow the whole thing. A
|
|
359
|
+
// head-narrowed grant must only cover a SINGLE, non-dangerous command — if the
|
|
360
|
+
// candidate chains (`&&`/`||`/`;`), pipes (`| sh`), or uses substitution/
|
|
361
|
+
// redirection, refuse the match so it re-prompts. (We only gate narrowed Bash
|
|
362
|
+
// grants; a tool-wide `*`/no-pattern rule, or an explicit user `argsPattern`,
|
|
363
|
+
// is the user's own choice and untouched.)
|
|
364
|
+
if (toolName === "Bash" && rule.tool === "Bash" && rule.argsPattern.command) {
|
|
365
|
+
const cmd = String(args.command ?? "");
|
|
366
|
+
const scan = scanShellCommand(cmd);
|
|
367
|
+
// scanShellCommand flags `;`/`&&`/`||`/substitution/redirection (multi-segment
|
|
368
|
+
// or dangerous), but a single pipe stays IN-segment (it's per-segment data
|
|
369
|
+
// flow, not a statement boundary) — so `git log | sh` is one non-dangerous
|
|
370
|
+
// segment. A head grant must not cover a pipe either: reject any segment
|
|
371
|
+
// carrying an (unquoted-enough) pipe so pipe-to-shell can't ride the grant.
|
|
372
|
+
if (scan.dangerous || scan.segments.length > 1 || scan.segments.some((s) => s.includes("|"))) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const [key, pattern] of Object.entries(rule.argsPattern)) {
|
|
377
|
+
const argVal = String(args[key] ?? "");
|
|
378
|
+
if (pattern instanceof RegExp) {
|
|
379
|
+
if (!pattern.test(argVal))
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
else if (typeof pattern === "string") {
|
|
383
|
+
const looksLikeRegex = /[\\^$.*+?()[\]|{}]/.test(pattern);
|
|
384
|
+
if (looksLikeRegex) {
|
|
385
|
+
let re;
|
|
386
|
+
try {
|
|
387
|
+
re = new RegExp(pattern);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
if (!argVal.includes(pattern))
|
|
391
|
+
return false;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (!re.test(argVal))
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
else if (!argVal.includes(pattern)) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
227
404
|
/**
|
|
228
405
|
* Append a rule to <cwd>/.code-shell/settings.local.json. Local file because
|
|
229
406
|
* permission grants are per-developer and shouldn't be checked into git.
|
|
@@ -232,16 +409,17 @@ function escapeRegex(s) {
|
|
|
232
409
|
function persistProjectRule(cwd, rule) {
|
|
233
410
|
const dir = `${cwd}/.code-shell`;
|
|
234
411
|
const file = `${dir}/settings.local.json`;
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
412
|
+
// Don't resurrect a deleted project root: a recursive mkdir of
|
|
413
|
+
// <cwd>/.code-shell recreates `cwd` itself as an empty shell when cwd is gone.
|
|
414
|
+
// Persistence here is best-effort, so skip when the root is missing.
|
|
415
|
+
if (!existsSync(cwd))
|
|
416
|
+
return;
|
|
417
|
+
if (!existsSync(dir))
|
|
418
|
+
mkdirSync(dir, { recursive: true });
|
|
241
419
|
let settings = {};
|
|
242
|
-
if (
|
|
420
|
+
if (existsSync(file)) {
|
|
243
421
|
try {
|
|
244
|
-
settings = JSON.parse(
|
|
422
|
+
settings = JSON.parse(readFileSync(file, "utf-8"));
|
|
245
423
|
}
|
|
246
424
|
catch {
|
|
247
425
|
/* start fresh on parse error */
|
|
@@ -260,9 +438,11 @@ function persistProjectRule(cwd, rule) {
|
|
|
260
438
|
settings.permissions.rules.push(rule);
|
|
261
439
|
// Atomic write: stage to .tmp, then rename, so a crash mid-write can't
|
|
262
440
|
// truncate settings.local.json and lose every saved permission grant.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
441
|
+
// randomUUID() (not pid+Date.now()) guards against temp-name collisions when
|
|
442
|
+
// two writers fire within the same millisecond.
|
|
443
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
444
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
445
|
+
renameSync(tmp, file);
|
|
266
446
|
}
|
|
267
447
|
// Singleton interactive backend for use by the UI
|
|
268
448
|
let _interactiveBackend = null;
|
|
@@ -300,9 +480,16 @@ const DANGEROUS_PATTERNS = [
|
|
|
300
480
|
];
|
|
301
481
|
const SAFE_READ_PATTERNS = [
|
|
302
482
|
/^(cat|head|tail|less|more|wc|file|stat|du|df)\s/,
|
|
303
|
-
/^(ls|tree|
|
|
483
|
+
/^(ls|tree|locate|which|whereis|type)\s/,
|
|
484
|
+
// `find` is read-only ONLY without an action that executes or mutates:
|
|
485
|
+
// -delete, -exec/-execdir/-ok/-okdir run commands; -fprint/-fprintf/-fls
|
|
486
|
+
// write files. Reject those so `find . -delete` / `find . -exec rm {} +`
|
|
487
|
+
// are NOT classified safe-read. Plain predicates (-name, -type, -print) stay.
|
|
488
|
+
/^find\s(?!.*\s-(delete|exec(dir)?|ok(dir)?|fprintf?|fls)\b)/,
|
|
304
489
|
/^(grep|rg|ag|ack|fgrep|egrep)\s/,
|
|
305
|
-
|
|
490
|
+
// Word-boundary the git read subcommands so `git difftool -x <cmd>` (which
|
|
491
|
+
// runs an arbitrary external command) does NOT match on the `diff` branch.
|
|
492
|
+
/^(git\s+(status|log|diff|branch|show|blame|remote|tag|stash\s+list|rev-parse|describe))\b/,
|
|
306
493
|
/^(node|python|ruby|go|rustc|java|javac)\s+--version/,
|
|
307
494
|
/^(npm|pnpm|yarn|cargo|pip|gem|brew)\s+(list|ls|info|show|view|outdated|audit)/,
|
|
308
495
|
/^pwd$/,
|
|
@@ -313,6 +500,46 @@ const SAFE_READ_PATTERNS = [
|
|
|
313
500
|
/^env$/,
|
|
314
501
|
/^printenv/,
|
|
315
502
|
];
|
|
503
|
+
// ─── Sensitive safe-read downgrade ────────────────────────────────
|
|
504
|
+
//
|
|
505
|
+
// `Read ~/.ssh/id_rsa` routes through path-policy (asks); the equivalent
|
|
506
|
+
// `cat ~/.ssh/id_rsa` was YOLO-classified safe-read and auto-allowed with the
|
|
507
|
+
// desktop's default sandbox=off — a zero-approval credential exfil channel.
|
|
508
|
+
// A segment that would otherwise be safe-read is downgraded to `unsafe` (→
|
|
509
|
+
// ask) when it dumps the process env or its arguments touch a sensitive path.
|
|
510
|
+
// Conservative-and-narrow: only known credential/secret shapes trip it, so
|
|
511
|
+
// ordinary reads (README.md, src/*.ts) stay safe-read.
|
|
512
|
+
// Whole-command env dumps that leak Credential.exposeAsEnv / top-level `env`
|
|
513
|
+
// API keys. `env FOO=bar cmd` (env used as a launcher) is intentionally NOT
|
|
514
|
+
// matched — only a bare dump.
|
|
515
|
+
const ENV_DUMP_RE = /^(env|printenv)(\s+[A-Za-z_][A-Za-z0-9_]*)*\s*$/;
|
|
516
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
517
|
+
/\.ssh\//, // ~/.ssh/id_rsa, authorized_keys, known_hosts
|
|
518
|
+
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)\b/,
|
|
519
|
+
/(^|\/|\s)\.env(\.[A-Za-z0-9]|\b)/, // .env, .env.local, .env.production
|
|
520
|
+
/\.aws\/credentials\b/,
|
|
521
|
+
/\.code-shell\/credentials\b/,
|
|
522
|
+
/credentials\.json\b/,
|
|
523
|
+
/\.npmrc\b/,
|
|
524
|
+
/\.netrc\b/,
|
|
525
|
+
/\.pgpass\b/,
|
|
526
|
+
/\.docker\/config\.json\b/,
|
|
527
|
+
/\.kube\/config\b/,
|
|
528
|
+
/\.config\/gh\/hosts\b/,
|
|
529
|
+
/\.gnupg\//,
|
|
530
|
+
/(^|\/)\.git-credentials\b/,
|
|
531
|
+
];
|
|
532
|
+
/**
|
|
533
|
+
* True when an otherwise-safe-read shell segment must be downgraded to `unsafe`
|
|
534
|
+
* because it dumps the environment or reads a sensitive credential/secret path.
|
|
535
|
+
* Exported for direct unit testing of the predicate.
|
|
536
|
+
*/
|
|
537
|
+
export function segmentIsSensitiveRead(segment) {
|
|
538
|
+
const s = segment.trim();
|
|
539
|
+
if (ENV_DUMP_RE.test(s))
|
|
540
|
+
return true;
|
|
541
|
+
return SENSITIVE_PATH_PATTERNS.some((re) => re.test(s));
|
|
542
|
+
}
|
|
316
543
|
const SAFE_WRITE_PATTERNS = [
|
|
317
544
|
/^(git\s+(add|commit|stash\s+(save|push)))/,
|
|
318
545
|
/^mkdir\s/,
|
|
@@ -433,6 +660,12 @@ function scanShellCommand(input) {
|
|
|
433
660
|
i++;
|
|
434
661
|
}
|
|
435
662
|
flush();
|
|
663
|
+
// An unclosed quote means we stopped recognizing separators/metacharacters
|
|
664
|
+
// partway through (pipes, `;`, redirection inside the dangling quote were
|
|
665
|
+
// swallowed). Treat malformed shell syntax as dangerous rather than risk
|
|
666
|
+
// misclassifying it as safe.
|
|
667
|
+
if (quote !== null)
|
|
668
|
+
dangerous = true;
|
|
436
669
|
return { segments, dangerous };
|
|
437
670
|
}
|
|
438
671
|
function classifySegment(segment) {
|
|
@@ -454,12 +687,17 @@ function classifySegment(segment) {
|
|
|
454
687
|
const parts = segment.split("|").map((p) => p.trim());
|
|
455
688
|
const partIsSafeRead = (p) => SAFE_READ_PATTERNS.some((re) => re.test(p) || re.test(`${p} `));
|
|
456
689
|
if (parts.every(partIsSafeRead)) {
|
|
690
|
+
// A sensitive read anywhere in the pipe (`cat ~/.ssh/id_rsa | base64`,
|
|
691
|
+
// `env | grep KEY`) downgrades the whole pipe.
|
|
692
|
+
if (parts.some(segmentIsSensitiveRead))
|
|
693
|
+
return "unsafe";
|
|
457
694
|
return "safe-read";
|
|
458
695
|
}
|
|
459
696
|
return "unsafe";
|
|
460
697
|
}
|
|
461
|
-
if (SAFE_READ_PATTERNS.some((p) => p.test(segment)))
|
|
462
|
-
return "safe-read";
|
|
698
|
+
if (SAFE_READ_PATTERNS.some((p) => p.test(segment))) {
|
|
699
|
+
return segmentIsSensitiveRead(segment) ? "unsafe" : "safe-read";
|
|
700
|
+
}
|
|
463
701
|
if (SAFE_WRITE_PATTERNS.some((p) => p.test(segment)))
|
|
464
702
|
return "safe-write";
|
|
465
703
|
return "unsafe";
|
|
@@ -696,43 +934,10 @@ export class PermissionClassifier {
|
|
|
696
934
|
return undefined;
|
|
697
935
|
}
|
|
698
936
|
matchesRule(rule, toolName, args) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const argVal = String(args[key] ?? "");
|
|
704
|
-
if (pattern instanceof RegExp) {
|
|
705
|
-
if (!pattern.test(argVal))
|
|
706
|
-
return false;
|
|
707
|
-
}
|
|
708
|
-
else if (typeof pattern === "string") {
|
|
709
|
-
// Rules persisted in settings.local.json lose their RegExp type
|
|
710
|
-
// through JSON round-trip — they come back as strings. buildProjectRule
|
|
711
|
-
// produces regex sources like `^cd(\\s|$)`; treat any string with
|
|
712
|
-
// regex metacharacters as a pattern and only fall back to substring
|
|
713
|
-
// match for plain literals (kept for backward compat with hand-edited
|
|
714
|
-
// settings files that used plain substrings).
|
|
715
|
-
const looksLikeRegex = /[\\^$.*+?()[\]|{}]/.test(pattern);
|
|
716
|
-
if (looksLikeRegex) {
|
|
717
|
-
let re;
|
|
718
|
-
try {
|
|
719
|
-
re = new RegExp(pattern);
|
|
720
|
-
}
|
|
721
|
-
catch {
|
|
722
|
-
if (!argVal.includes(pattern))
|
|
723
|
-
return false;
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
if (!re.test(argVal))
|
|
727
|
-
return false;
|
|
728
|
-
}
|
|
729
|
-
else if (!argVal.includes(pattern)) {
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
return true;
|
|
937
|
+
// Delegate to the shared matcher so the classifier and the session cache
|
|
938
|
+
// agree on rule semantics (the JSON-string-vs-RegExp handling, the head-
|
|
939
|
+
// command narrowing for Bash, etc.).
|
|
940
|
+
return ruleMatches(rule, toolName, args);
|
|
736
941
|
}
|
|
737
942
|
isDangerousCommand(args) {
|
|
738
943
|
const command = String(args.command ?? "");
|
|
@@ -745,7 +950,14 @@ export class PermissionClassifier {
|
|
|
745
950
|
return "medium";
|
|
746
951
|
if (toolName === "Bash")
|
|
747
952
|
return "medium";
|
|
748
|
-
|
|
953
|
+
// Only genuinely read-only built-ins are "low" (the sole tier
|
|
954
|
+
// AutoApprovalBackend approves with no delegate). Everything else —
|
|
955
|
+
// crucially every MCP tool, which can delete records, send messages, or
|
|
956
|
+
// deploy — defaults to "medium" so auto mode delegates it (UI prompt) or
|
|
957
|
+
// fails closed, instead of blind-approving an unknown side-effecting tool.
|
|
958
|
+
if (READ_ONLY_TOOLS.has(toolName))
|
|
959
|
+
return "low";
|
|
960
|
+
return "medium";
|
|
749
961
|
}
|
|
750
962
|
/**
|
|
751
963
|
* Short, human-friendly subtitle. The UI shows this under the tool title;
|
|
@@ -15,7 +15,18 @@
|
|
|
15
15
|
* Membership policy: read-only tools, planning/agent tools, and
|
|
16
16
|
* non-destructive task-tracking tools. Bash is included so the model sees it;
|
|
17
17
|
* the executor additionally gates Bash to read-only commands at call time
|
|
18
|
-
* (
|
|
19
|
-
* other mutating tools are
|
|
18
|
+
* (executor defers to classifyBashCommand, admitting only "safe-read").
|
|
19
|
+
* Write/Edit/ApplyPatch/NotebookEdit and other mutating tools are
|
|
20
|
+
* intentionally excluded.
|
|
20
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* The built-in tools that only READ — no file writes, no side effects, no
|
|
24
|
+
* network mutations. Single source of truth shared by:
|
|
25
|
+
* - investigation-guard.ts (which tools count as "just looking")
|
|
26
|
+
* - permission.ts HeadlessApprovalBackend (approve-read-only mode)
|
|
27
|
+
* - permission.ts assessRisk (these are genuinely low-risk; everything else,
|
|
28
|
+
* including MCP tools, is at least medium so it isn't auto-approved blind)
|
|
29
|
+
* These three lists were independent byte-for-byte copies that could drift.
|
|
30
|
+
*/
|
|
31
|
+
export declare const READ_ONLY_TOOLS: ReadonlySet<string>;
|
|
21
32
|
export declare const PLAN_MODE_ALLOWED_TOOLS: ReadonlySet<string>;
|
|
@@ -15,9 +15,27 @@
|
|
|
15
15
|
* Membership policy: read-only tools, planning/agent tools, and
|
|
16
16
|
* non-destructive task-tracking tools. Bash is included so the model sees it;
|
|
17
17
|
* the executor additionally gates Bash to read-only commands at call time
|
|
18
|
-
* (
|
|
19
|
-
* other mutating tools are
|
|
18
|
+
* (executor defers to classifyBashCommand, admitting only "safe-read").
|
|
19
|
+
* Write/Edit/ApplyPatch/NotebookEdit and other mutating tools are
|
|
20
|
+
* intentionally excluded.
|
|
20
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* The built-in tools that only READ — no file writes, no side effects, no
|
|
24
|
+
* network mutations. Single source of truth shared by:
|
|
25
|
+
* - investigation-guard.ts (which tools count as "just looking")
|
|
26
|
+
* - permission.ts HeadlessApprovalBackend (approve-read-only mode)
|
|
27
|
+
* - permission.ts assessRisk (these are genuinely low-risk; everything else,
|
|
28
|
+
* including MCP tools, is at least medium so it isn't auto-approved blind)
|
|
29
|
+
* These three lists were independent byte-for-byte copies that could drift.
|
|
30
|
+
*/
|
|
31
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
32
|
+
"Read",
|
|
33
|
+
"Glob",
|
|
34
|
+
"Grep",
|
|
35
|
+
"WebSearch",
|
|
36
|
+
"WebFetch",
|
|
37
|
+
"ToolSearch",
|
|
38
|
+
]);
|
|
21
39
|
export const PLAN_MODE_ALLOWED_TOOLS = new Set([
|
|
22
40
|
// Plan lifecycle
|
|
23
41
|
"EnterPlanMode",
|
|
@@ -32,6 +50,10 @@ export const PLAN_MODE_ALLOWED_TOOLS = new Set([
|
|
|
32
50
|
"AskUserQuestion",
|
|
33
51
|
"Agent",
|
|
34
52
|
"ToolSearch",
|
|
53
|
+
// Skill: loads a skill's guidance text into context — read-only, no file
|
|
54
|
+
// writes. Planning is exactly when methodology skills (brainstorming,
|
|
55
|
+
// writing-plans, …) are most useful, so it belongs in the allow-list.
|
|
56
|
+
"Skill",
|
|
35
57
|
// Task tracking (non-destructive) — both the TodoWrite and the Task* family
|
|
36
58
|
"TodoWrite",
|
|
37
59
|
"TaskCreate",
|
|
@@ -19,6 +19,7 @@ export declare class ToolRegistry {
|
|
|
19
19
|
constructor(options?: ToolRegistryOptions);
|
|
20
20
|
private registerBuiltins;
|
|
21
21
|
registerTool(tool: RegisteredTool, executor?: BuiltinToolFn): void;
|
|
22
|
+
unregisterTool(name: string): void;
|
|
22
23
|
getToolDefinitions(): ToolDefinition[];
|
|
23
24
|
getTool(name: string): RegisteredTool | undefined;
|
|
24
25
|
hasTool(name: string): boolean;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { ConfigError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError } from "../exceptions.js";
|
|
5
5
|
import { BUILTIN_TOOLS } from "./builtin/index.js";
|
|
6
|
+
import { validateToolMetadata } from "./validate-tool-metadata.js";
|
|
6
7
|
/**
|
|
7
8
|
* Default execution timeout for any tool that does not declare its own
|
|
8
9
|
* timeoutMs at registration time. Tools that need to run longer (Agent,
|
|
@@ -28,16 +29,24 @@ export class ToolRegistry {
|
|
|
28
29
|
if (selectedNames && !selectedNames.has(tool.definition.name)) {
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
32
|
+
// Fail loud on pathPolicy.arg drift rather than silently disabling path
|
|
33
|
+
// protection (assessment §4.1).
|
|
34
|
+
validateToolMetadata(tool.definition);
|
|
31
35
|
this.tools.set(tool.definition.name, tool.definition);
|
|
32
36
|
this.builtinExecutors.set(tool.definition.name, tool.execute);
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
registerTool(tool, executor) {
|
|
40
|
+
validateToolMetadata(tool);
|
|
36
41
|
this.tools.set(tool.name, tool);
|
|
37
42
|
if (executor) {
|
|
38
43
|
this.builtinExecutors.set(tool.name, executor);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
46
|
+
unregisterTool(name) {
|
|
47
|
+
this.tools.delete(name);
|
|
48
|
+
this.builtinExecutors.delete(name);
|
|
49
|
+
}
|
|
41
50
|
getToolDefinitions() {
|
|
42
51
|
return [...this.tools.values()].map((t) => ({
|
|
43
52
|
name: t.name,
|
|
@@ -107,11 +116,16 @@ export class ToolRegistry {
|
|
|
107
116
|
if (typeof result === "string") {
|
|
108
117
|
return { id, toolName: name, result };
|
|
109
118
|
}
|
|
119
|
+
// 结构化返回:图片块(view_image)或沙箱标记(Bash 等)。两者互斥,
|
|
120
|
+
// 各取所有,缺的字段为 undefined。
|
|
121
|
+
const contentBlocks = "contentBlocks" in result ? result.contentBlocks : undefined;
|
|
122
|
+
const sandbox = "sandbox" in result ? result.sandbox : undefined;
|
|
110
123
|
return {
|
|
111
124
|
id,
|
|
112
125
|
toolName: name,
|
|
113
|
-
result: result.result ?? "(image)",
|
|
114
|
-
contentBlocks
|
|
126
|
+
result: result.result ?? (contentBlocks ? "(image)" : ""),
|
|
127
|
+
contentBlocks,
|
|
128
|
+
sandbox,
|
|
115
129
|
};
|
|
116
130
|
}
|
|
117
131
|
catch (err) {
|
|
@@ -27,6 +27,14 @@ export interface SandboxConfig {
|
|
|
27
27
|
export interface SandboxBackend {
|
|
28
28
|
/** Resolved backend name — "off" means no sandboxing. */
|
|
29
29
|
name: "off" | "seatbelt" | "bwrap";
|
|
30
|
+
/**
|
|
31
|
+
* Network policy that applied for this run. Not part of the backend's
|
|
32
|
+
* isolation mechanism — the Engine stamps it onto the resolved backend
|
|
33
|
+
* (it owns the resolved SandboxConfig) so tools can surface "网络 deny"
|
|
34
|
+
* on their result without threading the whole config through ToolContext.
|
|
35
|
+
* Absent on "off" (no policy enforced).
|
|
36
|
+
*/
|
|
37
|
+
network?: SandboxNetworkPolicy;
|
|
30
38
|
/**
|
|
31
39
|
* Decide the binary + argv that should actually be spawned to run `command`
|
|
32
40
|
* under this sandbox. The shell still interprets `command`; we only swap
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* when no sandbox is available.
|
|
18
18
|
*/
|
|
19
19
|
import { accessSync, constants, realpathSync } from "node:fs";
|
|
20
|
-
import { homedir } from "node:os";
|
|
20
|
+
import { homedir, tmpdir } from "node:os";
|
|
21
21
|
import { SandboxUnavailableError } from "../../exceptions.js";
|
|
22
22
|
/**
|
|
23
23
|
* Detect which sandbox mechanisms are available on the current host. Used by
|
|
@@ -111,9 +111,14 @@ function warnAutoDowngrade() {
|
|
|
111
111
|
`Set sandbox.mode=off in settings.json to silence, or CODE_SHELL_SANDBOX_QUIET=1.\n`);
|
|
112
112
|
}
|
|
113
113
|
export function defaultSandboxConfig(mode = "auto") {
|
|
114
|
+
// Temp roots are writable so tool-result spillover / scratch files land
|
|
115
|
+
// somewhere. POSIX: the canonical /tmp family. Windows has no OS sandbox
|
|
116
|
+
// backend (auto → off), so writableRoots is effectively unused there, but
|
|
117
|
+
// keep it host-correct by using the platform temp dir instead of /tmp.
|
|
118
|
+
const tmpRoots = process.platform === "win32" ? [tmpdir()] : ["/tmp", "/private/tmp", "/var/tmp"];
|
|
114
119
|
return {
|
|
115
120
|
mode,
|
|
116
|
-
writableRoots: ["${workspace}",
|
|
121
|
+
writableRoots: ["${workspace}", ...tmpRoots],
|
|
117
122
|
deniedReads: [
|
|
118
123
|
"~/.ssh",
|
|
119
124
|
"~/.aws",
|