one-code-extension 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auto-mode transcript's `{"meta":{"resolvedPaths":…}}` line (pure apart
|
|
3
|
+
* from filesystem reads): where a path the action names really lands, when a
|
|
4
|
+
* symlink takes it out of the working directories.
|
|
5
|
+
*
|
|
6
|
+
* The classifier judges the call's text. `Set-Content ' leading.txt' x` reads
|
|
7
|
+
* as an in-project write even when ` leading.txt` is a symlink to
|
|
8
|
+
* `~/.ssh/authorized_keys`, and Claude Code's ruleset allows "local file
|
|
9
|
+
* operations within project scope". So the harness resolves every constant
|
|
10
|
+
* word of the action that names a path inside the working directories, and
|
|
11
|
+
* when one resolves outside them it puts the fact directly above the action,
|
|
12
|
+
* as it does the `gitStatus` line (git-status-meta.ts). An action whose paths
|
|
13
|
+
* all stay inside gets no line, so the payload of ordinary calls is Claude
|
|
14
|
+
* Code's own. The fact is measured by the harness, never read from tool input.
|
|
15
|
+
*
|
|
16
|
+
* Only words the call spells are judged: a link inside a listed directory, or
|
|
17
|
+
* a path computed at run time, is not seen (the pre-gates refuse those shapes
|
|
18
|
+
* on their own; decisions/auto-mode.md "The resolvedPaths line").
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { comparablePath } from "../lib/paths.ts";
|
|
22
|
+
import type { PowerShellParse } from "../lib/powershell-parser.ts";
|
|
23
|
+
import { isPathSubjectTool } from "../permissions/matcher.ts";
|
|
24
|
+
import { powershellSeparators, powershellUnescaped } from "../permissions/powershell-paths.ts";
|
|
25
|
+
import { isWithin, resolveForContainment, resolveThroughLinks, toAbsolute, toAbsoluteBash } from "./paths.ts";
|
|
26
|
+
import { parseCommand } from "./shell-parse.ts";
|
|
27
|
+
|
|
28
|
+
/** One path the action names that resolves outside every working directory. */
|
|
29
|
+
export interface ResolvedPathFact {
|
|
30
|
+
/** The word as the call spells it (quotes and escapes resolved). */
|
|
31
|
+
path: string;
|
|
32
|
+
/** Where it resolves through symlinks, in the filesystem's own spelling. */
|
|
33
|
+
resolvesTo: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The line's own explanation, since the ruleset has no sentence for it. */
|
|
37
|
+
export const RESOLVED_PATHS_NOTE =
|
|
38
|
+
"Measured by the harness, not written by the agent: each path below is named by the action under review as if inside the working directory, but resolves through a symbolic link to resolvesTo, outside every working directory. Judge the action as reading or writing resolvesTo.";
|
|
39
|
+
|
|
40
|
+
/** The most words one action has resolved: a huge command must not stall the gate. */
|
|
41
|
+
const MAX_CANDIDATES = 256;
|
|
42
|
+
/** A word longer than this is not a path worth resolving; it also bounds the line, whose words the agent wrote. */
|
|
43
|
+
const MAX_WORD = 1_024;
|
|
44
|
+
/** The most paths the line reports: enough to show the action escapes, bounded so it cannot swell the classifier's prompt. */
|
|
45
|
+
const MAX_FACTS = 16;
|
|
46
|
+
|
|
47
|
+
/** The constant words of a bash command line: every known token value and redirect target, and the value of `--opt=value`. */
|
|
48
|
+
export function bashPathCandidates(command: string): string[] {
|
|
49
|
+
const words: string[] = [];
|
|
50
|
+
for (const segment of parseCommand(command).segments) {
|
|
51
|
+
for (const token of segment.tokens) {
|
|
52
|
+
if (token.dynamic) continue;
|
|
53
|
+
words.push(token.value);
|
|
54
|
+
const eq = token.value.indexOf("=");
|
|
55
|
+
if (token.value.startsWith("-") && eq > 0) words.push(token.value.slice(eq + 1));
|
|
56
|
+
}
|
|
57
|
+
words.push(...segment.redirects);
|
|
58
|
+
}
|
|
59
|
+
return words;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The constant strings of a PowerShell parse (every node PowerShell gave a
|
|
64
|
+
* value), with the backtick escapes `-Path` drops removed as a second
|
|
65
|
+
* spelling: the gate does not know which parameter took the value.
|
|
66
|
+
*/
|
|
67
|
+
export function powershellPathCandidates(parse: PowerShellParse): string[] {
|
|
68
|
+
const words: string[] = [];
|
|
69
|
+
for (const node of parse.nodes) {
|
|
70
|
+
if (typeof node.value !== "string") continue;
|
|
71
|
+
words.push(node.value);
|
|
72
|
+
const unescaped = powershellUnescaped(node.value);
|
|
73
|
+
if (unescaped !== node.value) words.push(unescaped);
|
|
74
|
+
}
|
|
75
|
+
return words;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ResolvedPathsOptions {
|
|
79
|
+
/** The call's working directory. */
|
|
80
|
+
cwd: string;
|
|
81
|
+
home: string;
|
|
82
|
+
/** Every working directory (the cwd and any added workspace directories). */
|
|
83
|
+
roots: readonly string[];
|
|
84
|
+
/** How the words spell a path: `bash` (Git Bash's `/c/…` on Windows), `powershell` (`\` separates on macOS and Linux too), else the platform's own. */
|
|
85
|
+
spelling?: "bash" | "powershell";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The words that name a path inside a working directory but resolve outside
|
|
90
|
+
* all of them, or undefined when none does. A word that is outside as spelled
|
|
91
|
+
* (`/etc/hosts`, `../x`) is left to the classifier's own reading of the text.
|
|
92
|
+
*/
|
|
93
|
+
export function resolvedPathFacts(words: readonly string[], opts: ResolvedPathsOptions): ResolvedPathFact[] | undefined {
|
|
94
|
+
const roots = opts.roots.map((root) => resolveForContainment(root) ?? root);
|
|
95
|
+
// A word may spell a root through the alias it was given as (`/var/…` for
|
|
96
|
+
// `/private/var/…`): it names a path inside when it sits under either spelling.
|
|
97
|
+
const spelledRoots = [...opts.roots, ...roots];
|
|
98
|
+
const { cwd } = opts;
|
|
99
|
+
const facts: ResolvedPathFact[] = [];
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
for (const word of words) {
|
|
102
|
+
if (seen.size >= MAX_CANDIDATES || facts.length >= MAX_FACTS) break;
|
|
103
|
+
if (!word || word.length > MAX_WORD || word.includes("\0") || seen.has(word)) continue;
|
|
104
|
+
seen.add(word);
|
|
105
|
+
const lexical =
|
|
106
|
+
opts.spelling === "bash"
|
|
107
|
+
? toAbsoluteBash(cwd, word, opts.home)
|
|
108
|
+
: toAbsolute(cwd, opts.spelling === "powershell" ? powershellSeparators(word) : word, opts.home);
|
|
109
|
+
if (!spelledRoots.some((root) => isWithin(root, lexical))) continue;
|
|
110
|
+
const resolvesTo = resolveThroughLinks(lexical);
|
|
111
|
+
if (resolvesTo === undefined || roots.some((root) => isWithin(root, comparablePath(resolvesTo)))) continue;
|
|
112
|
+
facts.push({ path: word, resolvesTo });
|
|
113
|
+
}
|
|
114
|
+
return facts.length > 0 ? facts : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The facts for one action under review, or undefined: a shell command's
|
|
119
|
+
* constant words (`bash` and `monitor` from the bash parse, `powershell` from
|
|
120
|
+
* PowerShell's own parse, none without one), or a path tool's path argument.
|
|
121
|
+
* `subject` is the command line or the path, as the permission gate extracts it.
|
|
122
|
+
*/
|
|
123
|
+
export function actionResolvedPaths(
|
|
124
|
+
tool: string,
|
|
125
|
+
subject: string,
|
|
126
|
+
powershellParse: PowerShellParse | undefined,
|
|
127
|
+
opts: Omit<ResolvedPathsOptions, "spelling">,
|
|
128
|
+
): ResolvedPathFact[] | undefined {
|
|
129
|
+
if (!subject) return undefined;
|
|
130
|
+
if (tool === "bash" || tool === "monitor") return resolvedPathFacts(bashPathCandidates(subject), { ...opts, spelling: "bash" });
|
|
131
|
+
if (tool === "powershell") return powershellParse ? resolvedPathFacts(powershellPathCandidates(powershellParse), { ...opts, spelling: "powershell" }) : undefined;
|
|
132
|
+
return isPathSubjectTool(tool) ? resolvedPathFacts([subject], opts) : undefined;
|
|
133
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `unmatched`, so a hallucinated name cannot borrow a real HARD rule's authority.
|
|
13
13
|
* The rule's own name is what the user sees; the model's wording is kept as
|
|
14
14
|
* commentary that is never mistaken for fact. This is decision 1 in
|
|
15
|
-
* docs/decisions/auto-mode.md, re-expressed against CC's category scheme in
|
|
15
|
+
* working-docs/decisions/auto-mode.md, re-expressed against CC's category scheme in
|
|
16
16
|
* place of our old H1/S4/A2 ids.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -106,6 +106,23 @@ export function buildCategoryIndex(ruleset: string): RuleIndex {
|
|
|
106
106
|
return { rules, byName };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
const countRules = (section: string): number => section.split("\n").filter((line) => ruleNameFromLine(line) !== undefined).length;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How many rules each section of a ruleset holds, counted like the index (one
|
|
113
|
+
* per top-level bullet). On the embedded ruleset with no user extras, this is
|
|
114
|
+
* what the /permissions Auto mode tab shows as the built-in rules of each
|
|
115
|
+
* section (findings §33: Claude Code shows 70 soft-deny rules because it counts
|
|
116
|
+
* the Artifact consent rule this ruleset does not carry).
|
|
117
|
+
*/
|
|
118
|
+
export function builtinRuleCounts(ruleset: string): { hard_deny: number; soft_deny: number; allow: number } {
|
|
119
|
+
return {
|
|
120
|
+
hard_deny: countRules(sliceSection(ruleset, HARD_HEADING, SOFT_HEADING)),
|
|
121
|
+
soft_deny: countRules(sliceSection(ruleset, SOFT_HEADING, ALLOW_HEADING)),
|
|
122
|
+
allow: countRules(sliceSection(ruleset, ALLOW_HEADING, "\n## ")),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
/** Look up a category the classifier emitted. undefined ⇒ not a known BLOCK rule. */
|
|
110
127
|
export function groundCategory(index: RuleIndex, category: string | undefined): IndexedRule | undefined {
|
|
111
128
|
if (!category) return undefined;
|
|
@@ -17,16 +17,26 @@
|
|
|
17
17
|
* complete to be sound — a `sed -i` on settings.json that the shell evidence
|
|
18
18
|
* does not model as a write is the classifier's to catch, as today.
|
|
19
19
|
*
|
|
20
|
+
* The consent stores (`lib/consent-stores.ts`) are gate controls too: an
|
|
21
|
+
* entry in one approves a repository's hooks, MCP servers, allow rules or
|
|
22
|
+
* language servers for every later session. So are the cached model catalogs
|
|
23
|
+
* (`lib/model-catalog-data.ts catalogCacheFiles`): they decide the model's
|
|
24
|
+
* tier, and the tier decides which fast paths skip the classifier.
|
|
25
|
+
*
|
|
20
26
|
* The target list is deliberately exact files, not directories: ~/.claude also
|
|
21
27
|
* holds memory and skills that the agent writes routinely, and a floor that
|
|
22
28
|
* fires on routine work teaches the user to approve without reading.
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
31
|
+
import { FIND_PATTERN_TESTS, findNegatedPatterns } from "./read-only-options.ts";
|
|
32
|
+
import { analyzeShellCommand, globComponentRegex, hasReadOnlyShellWords, isUnknownTilde, LOOPS, movesDirectory, parseCommand, resolvePayload, scopedTracker } from "./shell-analysis.ts";
|
|
33
|
+
import type { Token } from "./shell-parse.ts";
|
|
34
|
+
import { claudeUserSettingsPath, managedSettingsPaths } from "../lib/claude-settings.ts";
|
|
35
|
+
import { CONSENT_STORE_TAIL, consentStorePaths } from "../lib/consent-stores.ts";
|
|
36
|
+
import { oneCodeProjectSettingsPath, oneCodeSettingsPath } from "../lib/one-code-settings.ts";
|
|
37
|
+
import { CATALOG_CACHE_TAIL, catalogCacheFiles } from "../lib/model-catalog-data.ts";
|
|
38
|
+
import { claudeJsonPath, comparablePath, oneCodeStateDir } from "../lib/paths.ts";
|
|
39
|
+
import { isWritingTool, resolveForContainment, toAbsolute, toAbsoluteBash } from "./paths.ts";
|
|
30
40
|
|
|
31
41
|
/** The same comparison form resolveForContainment's output is in (lib/paths.ts). */
|
|
32
42
|
const fold = comparablePath;
|
|
@@ -53,8 +63,13 @@ const ONECODE_SETTINGS_TAIL = /\/\.onecode\/(projects\/[^/]+\/)?settings\.json$/
|
|
|
53
63
|
function safetyControlFiles(home: string, oneCodeProjectSettings?: string): string[] {
|
|
54
64
|
return [
|
|
55
65
|
// autoMode + user permission rules, and the managed-settings paths (the
|
|
56
|
-
// global ~/.onecode/settings.json here honours ONECODE_STATE_DIR).
|
|
57
|
-
|
|
66
|
+
// global ~/.onecode/settings.json here honours ONECODE_STATE_DIR). Spelled
|
|
67
|
+
// out, not the mode-aware autoModeSettingsPaths: independent mode reads no
|
|
68
|
+
// Claude Code file, but one written there would take effect the moment
|
|
69
|
+
// the user switches back, so the floor guards it in both modes.
|
|
70
|
+
claudeUserSettingsPath(home),
|
|
71
|
+
oneCodeSettingsPath(home),
|
|
72
|
+
...managedSettingsPaths(),
|
|
58
73
|
// Claude Code's global state file also carries permission configuration;
|
|
59
74
|
// claudeJsonPath honours CLAUDE_CONFIG_DIR so a relocated file is still caught.
|
|
60
75
|
claudeJsonPath(home),
|
|
@@ -62,6 +77,12 @@ function safetyControlFiles(home: string, oneCodeProjectSettings?: string): stri
|
|
|
62
77
|
// tail regex catches it under a literal `.onecode`; this catches it when
|
|
63
78
|
// ONECODE_STATE_DIR relocated the state root (review L2).
|
|
64
79
|
...(oneCodeProjectSettings ? [oneCodeProjectSettings] : []),
|
|
80
|
+
// The consent stores: an entry approves a repository's hooks, MCP
|
|
81
|
+
// servers, allow rules or language servers for every later session.
|
|
82
|
+
...consentStorePaths(home),
|
|
83
|
+
// The cached model catalogs decide the model's tier, and the tier decides
|
|
84
|
+
// which fast paths skip the classifier.
|
|
85
|
+
...catalogCacheFiles(oneCodeStateDir(process.env, home)),
|
|
65
86
|
];
|
|
66
87
|
}
|
|
67
88
|
|
|
@@ -71,14 +92,75 @@ function safetyControlFiles(home: string, oneCodeProjectSettings?: string): stri
|
|
|
71
92
|
* by the caller (once per session) so this need not re-walk for the project root.
|
|
72
93
|
*/
|
|
73
94
|
export function isSafetyControlTarget(resolved: string, home: string, oneCodeProjectSettings?: string): boolean {
|
|
95
|
+
return matchesControlFile(resolved, controlFileForms(home, oneCodeProjectSettings));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every comparison form of the control files: each resolved like a write
|
|
100
|
+
* target, or the two sides can disagree about the same file (macOS /var →
|
|
101
|
+
* /private/var), plus its literal spelling.
|
|
102
|
+
*/
|
|
103
|
+
function controlFileForms(home: string, oneCodeProjectSettings?: string): Set<string> {
|
|
104
|
+
const forms = new Set<string>();
|
|
105
|
+
for (const file of safetyControlFiles(home, oneCodeProjectSettings)) {
|
|
106
|
+
forms.add(resolveForContainment(file) ?? fold(file));
|
|
107
|
+
forms.add(fold(file));
|
|
108
|
+
}
|
|
109
|
+
return forms;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchesControlFile(resolved: string, forms: ReadonlySet<string>): boolean {
|
|
74
113
|
const target = fold(resolved);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
return SETTINGS_TAIL.test(target) || ONECODE_SETTINGS_TAIL.test(target) || CONSENT_STORE_TAIL.test(target) || CATALOG_CACHE_TAIL.test(target) || forms.has(target);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const GLOB_CHARS = /[*?[]/;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether a resolved shell path names a control file, literally or as a glob
|
|
121
|
+
* the shell expands onto one (`> .claude/settings.*`, `.claude/s*.json`). A
|
|
122
|
+
* glob leaf is tried against every control file name in its directory; a glob
|
|
123
|
+
* in a directory component stops whenever the leaf could spell a control
|
|
124
|
+
* file's name, since the floor may only ever say "stop". `dotRule` is bash's:
|
|
125
|
+
* a leaf not starting with `.` never matches a name that does.
|
|
126
|
+
*/
|
|
127
|
+
function namesControlFile(resolved: string, forms: ReadonlySet<string>, dotRule: boolean): boolean {
|
|
128
|
+
if (matchesControlFile(resolved, forms)) return true;
|
|
129
|
+
const target = fold(resolved);
|
|
130
|
+
if (!GLOB_CHARS.test(target)) return false;
|
|
131
|
+
const slash = target.lastIndexOf("/");
|
|
132
|
+
const dir = target.slice(0, slash);
|
|
133
|
+
const leaf = target.slice(slash + 1);
|
|
134
|
+
// An uncompilable bracket expression matches nothing: the shell writes the literal name.
|
|
135
|
+
const regex = globComponentRegex(leaf);
|
|
136
|
+
if (!regex) return false;
|
|
137
|
+
const names = new Set(["settings.json", "settings.local.json"]);
|
|
138
|
+
for (const form of forms) names.add(form.slice(form.lastIndexOf("/") + 1));
|
|
139
|
+
const spelled = [...names].filter((name) => regex.test(name) && !(dotRule && name.startsWith(".") && !leaf.startsWith(".")));
|
|
140
|
+
if (spelled.length === 0) return false;
|
|
141
|
+
return GLOB_CHARS.test(dir) || spelled.some((name) => matchesControlFile(`${dir}/${name}`, forms));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A find's negated pattern operands, by token index: those that only exclude
|
|
146
|
+
* files (`findNegatedPatterns`), which name nothing, and the rest, which may
|
|
147
|
+
* select every file but the ones they name. A find that is not read-only acts
|
|
148
|
+
* on every file a negated pattern leaves in (`! -name keep -delete`), and its
|
|
149
|
+
* expression is not read, so there every pattern after any `!`/`-not` selects.
|
|
150
|
+
*/
|
|
151
|
+
function negatedFindPatterns(tokens: readonly Token[], args: readonly Token[]): { excluding: Set<number>; selecting: Set<number> } {
|
|
152
|
+
const offset = tokens.length - args.length;
|
|
153
|
+
const shift = (indexes: Set<number>) => new Set([...indexes].map((index) => index + offset));
|
|
154
|
+
const readOnly = findNegatedPatterns(args);
|
|
155
|
+
if (readOnly) return { excluding: shift(readOnly.excluding), selecting: shift(readOnly.selecting) };
|
|
156
|
+
const selecting = new Set<number>();
|
|
157
|
+
let negated = false;
|
|
158
|
+
for (let index = 0; index + 1 < tokens.length; index++) {
|
|
159
|
+
const word = tokens[index].value;
|
|
160
|
+
if (word === "!" || word === "-not") negated = true;
|
|
161
|
+
else if (negated && FIND_PATTERN_TESTS.has(word)) selecting.add(index + 1);
|
|
162
|
+
}
|
|
163
|
+
return { excluding: new Set(), selecting };
|
|
82
164
|
}
|
|
83
165
|
|
|
84
166
|
export interface FloorInput {
|
|
@@ -96,8 +178,32 @@ export interface FloorInput {
|
|
|
96
178
|
oneCodeProjectSettings?: string;
|
|
97
179
|
}
|
|
98
180
|
|
|
181
|
+
/**
|
|
182
|
+
* A wildcard or negated pattern (`./node_modules/*`, `!**\/node_modules/**`):
|
|
183
|
+
* the command may select a control file without naming one, so "it writes"
|
|
184
|
+
* would be false and a read-only search can pass rewritten without it.
|
|
185
|
+
*/
|
|
186
|
+
const PATTERN_TOKEN = /^!|[*?[]/;
|
|
187
|
+
const PATTERN_REASON = "its pattern ";
|
|
188
|
+
|
|
99
189
|
const REASON = (token: string) =>
|
|
100
|
-
|
|
190
|
+
CATALOG_FILE_NAME.test(token)
|
|
191
|
+
? `it writes ${token}, a model catalog that decides the model tier the auto-mode checks that contain this agent depend on`
|
|
192
|
+
: PATTERN_TOKEN.test(token)
|
|
193
|
+
? `${PATTERN_REASON}${token} can select a file that holds the permission rules and auto-mode configuration that contain this agent`
|
|
194
|
+
: `it writes ${token}, which holds the permission rules and auto-mode configuration that contain this agent`;
|
|
195
|
+
|
|
196
|
+
/** The model-facing block text for a floor reason. The floor re-checks a rewrite, and the classifier still judges it. */
|
|
197
|
+
export function safetyFloorBlockText(reason: string): string {
|
|
198
|
+
const opening = `Auto mode blocked this call without consulting the classifier: ${reason}.`;
|
|
199
|
+
if (reason.startsWith(PATTERN_REASON)) {
|
|
200
|
+
return `${opening} A command that may touch the gate's own configuration is never auto-approved. If this is a read-only search, rewrite it without the wildcard or negated pattern (for example \`grep -r --exclude-dir=node_modules\`); otherwise ask the user to run it themselves.`;
|
|
201
|
+
}
|
|
202
|
+
return `${opening} Writes to the gate's own configuration are never auto-approved. Do not retry or route around this; ask the user to make the change themselves.`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A cached catalog's file name, to word the reason (a false match only rewords it). */
|
|
206
|
+
const CATALOG_FILE_NAME = /(^|[/\\])(models-dev|openrouter|huggingface|refresh-failed)\.json$/i;
|
|
101
207
|
|
|
102
208
|
/**
|
|
103
209
|
* Reason text when this call writes a safety-control file, undefined otherwise.
|
|
@@ -115,12 +221,28 @@ export function safetyControlWrite({ toolName, input, cwd, home, oneCodeProjectS
|
|
|
115
221
|
return resolved && isSafetyControlTarget(resolved, home, perRepoSettings) ? REASON(raw) : undefined;
|
|
116
222
|
}
|
|
117
223
|
|
|
118
|
-
|
|
224
|
+
// `monitor` runs a shell command exactly as `bash` does (review L1).
|
|
225
|
+
if (toolName === "bash" || toolName === "monitor") {
|
|
119
226
|
const command = typeof input.command === "string" ? input.command : "";
|
|
120
227
|
if (!command) return undefined;
|
|
121
228
|
const evidence = analyzeShellCommand({ command, cwd, home });
|
|
229
|
+
const forms = controlFileForms(home, perRepoSettings);
|
|
122
230
|
for (const write of evidence.writes) {
|
|
123
|
-
if (write.resolved &&
|
|
231
|
+
if (write.resolved && namesControlFile(write.resolved, forms, true)) return REASON(write.token);
|
|
232
|
+
}
|
|
233
|
+
// A command the pre-gate cannot prove read-only may write in ways its
|
|
234
|
+
// evidence does not model (an output operand, an option's value, a
|
|
235
|
+
// nested script), and until 2026-09-23 such a write reached neither this
|
|
236
|
+
// floor nor, when the pre-gate wrongly said "safe", the classifier
|
|
237
|
+
// (SECURITY-REVIEW-2026-09-23 H3). So for those the floor is textual, as
|
|
238
|
+
// the PowerShell one below: any unproven word naming a gate-control file
|
|
239
|
+
// stops the call. Proven read-only simple commands are excluded even in
|
|
240
|
+
// an escalated compound line; their redirects and nested code are not.
|
|
241
|
+
// `readOnlyOutside` means every command was proven read-only by its
|
|
242
|
+
// options and only the location escalated: a read, not a hidden write.
|
|
243
|
+
if (evidence.verdict === "escalate" && !evidence.readOnlyOutside) {
|
|
244
|
+
const named = shellNamesControlFile(command, cwd, home, perRepoSettings, 0, forms);
|
|
245
|
+
if (named) return REASON(named);
|
|
124
246
|
}
|
|
125
247
|
}
|
|
126
248
|
|
|
@@ -132,9 +254,11 @@ export function safetyControlWrite({ toolName, input, cwd, home, oneCodeProjectS
|
|
|
132
254
|
// gate (the floor may only ever say "stop" — header).
|
|
133
255
|
const command = typeof input.command === "string" ? input.command : "";
|
|
134
256
|
if (!command) return undefined;
|
|
257
|
+
// PowerShell wildcards (`*`, `?`, `[…]`) have no leading-dot rule.
|
|
258
|
+
const forms = controlFileForms(home, perRepoSettings);
|
|
135
259
|
for (const token of powershellPathTokens(command, home)) {
|
|
136
260
|
const absolute = toAbsolute(cwd, token, home);
|
|
137
|
-
if (
|
|
261
|
+
if (namesControlFile(resolveForContainment(absolute) ?? absolute, forms, false)) return REASON(token);
|
|
138
262
|
}
|
|
139
263
|
}
|
|
140
264
|
|
|
@@ -167,3 +291,222 @@ export function powershellPathTokens(command: string, home: string): string[] {
|
|
|
167
291
|
}
|
|
168
292
|
return out;
|
|
169
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A `{var}>f` redirect, which assigns the descriptor's number to var. The
|
|
297
|
+
* grammar reads `{var}` as a word, or not at all after a compound command, so
|
|
298
|
+
* it is matched in the line's text.
|
|
299
|
+
*/
|
|
300
|
+
const VAR_REDIRECT = /\{[A-Za-z_][A-Za-z0-9_]*\}[<>]/;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The gate-control file spellings the textual floor matches in a command line,
|
|
304
|
+
* lowercased, with `\\` turned to `/` and `/./`, `//` collapsed.
|
|
305
|
+
*/
|
|
306
|
+
const CONTROL_FILE_TEXT =
|
|
307
|
+
/(^|[\s'"=/<>|;&(:])(\.claude\/settings(\.local)?\.json|\.onecode\/(projects\/[^\s'"/]+\/)?settings\.json|\.onecode\/cache\/model-catalog\/(models-dev|openrouter|huggingface|refresh-failed)\.json|managed-settings\.json|\.claude\.json)(?=$|[\s'";|&)<>])/;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* `$_` (or `${_}`, `${#_}`, …) anywhere in the line: bash's last argument of
|
|
311
|
+
* the previous command, which carries a proven read-only command's words into
|
|
312
|
+
* a later one (`echo .claude/settings.json; rm "$_"`). Raw text, so heredoc
|
|
313
|
+
* bodies and quoted spellings count too; a false positive costs one stop.
|
|
314
|
+
*/
|
|
315
|
+
const LAST_ARGUMENT = /\$(?:_|\{[#!]?_)(?![A-Za-z0-9_])/;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The literal text every match of a find `-regex` pattern ends with (find
|
|
319
|
+
* anchors the pattern to the whole path), or undefined when that is not
|
|
320
|
+
* certain: no literal tail, or alternation anywhere (`|` or emacs `\|`).
|
|
321
|
+
* Wildcards, groups, intervals and classes end the tail in either the emacs
|
|
322
|
+
* or the POSIX spelling. Only `\` before one of `.*+?[]^$\/-` is a literal;
|
|
323
|
+
* any other escape (emacs `\'` is an anchor, `\w` a class) ends the tail.
|
|
324
|
+
*/
|
|
325
|
+
export function regexLiteralTail(pattern: string): string | undefined {
|
|
326
|
+
if (pattern.includes("|")) return undefined;
|
|
327
|
+
const units: Array<{ literal: boolean; text: string }> = [];
|
|
328
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
329
|
+
const char = pattern[i];
|
|
330
|
+
if (char === "\\" && i + 1 < pattern.length) {
|
|
331
|
+
const next = pattern[++i];
|
|
332
|
+
units.push({ literal: /[.*+?[\]^$\\/-]/.test(next), text: next });
|
|
333
|
+
} else units.push({ literal: !/[.[\]*+?^$(){}\\]/.test(char), text: char });
|
|
334
|
+
}
|
|
335
|
+
// A trailing unescaped `$` only re-anchors the end find anchors anyway.
|
|
336
|
+
if (units.at(-1)?.text === "$" && !units.at(-1)!.literal) units.pop();
|
|
337
|
+
let tail = "";
|
|
338
|
+
for (let i = units.length - 1; i >= 0 && units[i].literal; i--) tail = units[i].text + tail;
|
|
339
|
+
return tail || undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The first word of a shell line that names a gate-control file, or
|
|
344
|
+
* undefined. Every word not proven read-only counts, plus the value after an `=`
|
|
345
|
+
* (`--output=…`, `of=…`) and the words of a nested `sh -c '…'` script, read
|
|
346
|
+
* from proven words too (`echo '…'` may print a script to a file); `cd`
|
|
347
|
+
* is followed, per subshell scope, so a relative name is resolved where the
|
|
348
|
+
* shell would. Where the directory cannot be known (the line does not parse,
|
|
349
|
+
* a `cd` sits in a loop body that runs more than once, or its target is an
|
|
350
|
+
* expansion), a word whose file name is a control file's stops by name alone.
|
|
351
|
+
* False positives cost one stop; the floor may only ever say "stop".
|
|
352
|
+
*/
|
|
353
|
+
export function shellNamesControlFile(
|
|
354
|
+
command: string,
|
|
355
|
+
cwd: string,
|
|
356
|
+
home: string,
|
|
357
|
+
oneCodeProjectSettings?: string,
|
|
358
|
+
depth = 0,
|
|
359
|
+
/** Resolved once per top-level call, not once per word. */
|
|
360
|
+
forms: ReadonlySet<string> = controlFileForms(home, oneCodeProjectSettings),
|
|
361
|
+
): string | undefined {
|
|
362
|
+
const { segments, parseFailed, unknownQuoting, unattributedExpansion, definesFunction, pipelines } = parseCommand(command);
|
|
363
|
+
const dirs = scopedTracker(cwd);
|
|
364
|
+
const ignoredRanges: { start: number; end: number }[] = [];
|
|
365
|
+
// Functions/eval can replace a read-only command; loop headers, case
|
|
366
|
+
// subjects and `{var}>` redirects can change shell state outside the
|
|
367
|
+
// attributed words. A compound command's redirect (`{ …; } > f`,
|
|
368
|
+
// `fi > f`) or an `exec` one sends a proven command's output, unquoted
|
|
369
|
+
// words and all, to a file a later command can run; /dev/null and
|
|
370
|
+
// descriptor dups capture nothing. Never use a partial parse, such a
|
|
371
|
+
// construct, or a nested script to subtract evidence.
|
|
372
|
+
const capturesOutput = segments.some((segment) =>
|
|
373
|
+
segment.redirects.some((target) => target !== "/dev/null") && (segment.tokens.length === 0 || resolvePayload(segment.tokens).command === "exec"),
|
|
374
|
+
);
|
|
375
|
+
const canProveWords = depth === 0 && !parseFailed && !unknownQuoting && !unattributedExpansion && !definesFunction && !capturesOutput && !VAR_REDIRECT.test(command) && !segments.some((segment) =>
|
|
376
|
+
segment.enclosing.some((construct) => LOOPS.has(construct) || construct === "case_statement") ||
|
|
377
|
+
["eval", "source", ".", "alias", "enable", "trap"].includes(resolvePayload(segment.tokens).command),
|
|
378
|
+
) && !LAST_ARGUMENT.test(command);
|
|
379
|
+
|
|
380
|
+
// Decided before the walk: in a loop, a word read before the `cd` runs after it on the next pass.
|
|
381
|
+
const unknownDir =
|
|
382
|
+
parseFailed ||
|
|
383
|
+
segments.some((segment) => {
|
|
384
|
+
const payload = resolvePayload(segment.tokens);
|
|
385
|
+
// A script run in this shell (`eval "cd .claude"`, `source f`) may `cd` too.
|
|
386
|
+
if (["eval", "source", "."].includes(payload.command)) return true;
|
|
387
|
+
if (!movesDirectory(segment)) return false;
|
|
388
|
+
const target = payload.args.find((token) => !token.value.startsWith("-"));
|
|
389
|
+
// `cd -` goes to $OLDPWD.
|
|
390
|
+
const previous = payload.args.some((token) => token.value === "-");
|
|
391
|
+
return (
|
|
392
|
+
payload.command !== "cd" ||
|
|
393
|
+
previous ||
|
|
394
|
+
// A pipeline's last `cd` moves the later commands of its own shell
|
|
395
|
+
// under lastpipe, inside a substitution too.
|
|
396
|
+
!!segment.pipelineShell ||
|
|
397
|
+
// `false && cd x` may or may not move the commands after it.
|
|
398
|
+
!!segment.conditional ||
|
|
399
|
+
segment.enclosing.some((construct) => LOOPS.has(construct)) ||
|
|
400
|
+
!!target?.dynamic ||
|
|
401
|
+
!!target?.glob ||
|
|
402
|
+
(!!target && isUnknownTilde(target.value))
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
// Lowercased on every platform: a false positive costs one stop.
|
|
406
|
+
const baseName = (path: string) => path.slice(path.replace(/\\/g, "/").lastIndexOf("/") + 1).toLowerCase();
|
|
407
|
+
const controlNames = new Set([...forms].map(baseName));
|
|
408
|
+
// First pass, in order: each segment's directory and whether its words
|
|
409
|
+
// alone are proven read-only. The cd tracking runs here so the second pass
|
|
410
|
+
// can look ahead at a whole pipeline.
|
|
411
|
+
let shellChanged = false;
|
|
412
|
+
const facts = segments.map((segment) => {
|
|
413
|
+
const dir = dirs.get(segment);
|
|
414
|
+
const payload = resolvePayload(segment.tokens);
|
|
415
|
+
// A previous assignment or stateful builtin can change command lookup
|
|
416
|
+
// (PATH, a hash entry, shell options, …). Arithmetic commands and
|
|
417
|
+
// expansions can assign too. A standalone command proof cannot account
|
|
418
|
+
// for that state. Even inert assignments/expansions forfeit it.
|
|
419
|
+
shellChanged ||= !!segment.unknownTarget || !!segment.expandsIntoInput ||
|
|
420
|
+
segment.tokens.some((word) => word.dynamic || /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/.test(word.value)) ||
|
|
421
|
+
["read", "readarray", "mapfile", "getopts", "printf", "export", "declare", "typeset", "local", "readonly", "unset", "set", "shopt", "hash", "let", "wait", "coproc"].includes(payload.command);
|
|
422
|
+
const proven = canProveWords && !shellChanged && !unknownDir &&
|
|
423
|
+
segment.wordRanges?.length === segment.tokens.length && hasReadOnlyShellWords(segment, dir, home);
|
|
424
|
+
if (payload.command === "cd" && !unknownDir) {
|
|
425
|
+
const target = payload.args.find((token) => !token.value.startsWith("-"));
|
|
426
|
+
if (target) dirs.set(segment, toAbsoluteBash(dir, target.value, home));
|
|
427
|
+
}
|
|
428
|
+
return { dir, payload, proven };
|
|
429
|
+
});
|
|
430
|
+
// Pipeline output may itself be a script (`echo '…' | sh`), so a pipeline
|
|
431
|
+
// member keeps its words out of the scan only when the pipeline sits in the
|
|
432
|
+
// top-level shell and every member is one proven read-only command: then
|
|
433
|
+
// no member runs another's output (`find … | sort`). Substitutions and
|
|
434
|
+
// `( … )` stay textual.
|
|
435
|
+
const provenPipelineScopes = new Set<number>();
|
|
436
|
+
for (const members of pipelines) {
|
|
437
|
+
const inMember = members.map((scope) => segments.flatMap((segment, index) => (segment.scopes.length === 1 && segment.scopes[0] === scope ? [index] : [])));
|
|
438
|
+
const nested = segments.some((segment) => segment.scopes.length > 1 && members.includes(segment.scopes[0]));
|
|
439
|
+
if (!nested && inMember.every((indexes) => indexes.length === 1 && facts[indexes[0]].proven)) {
|
|
440
|
+
for (const scope of members) provenPipelineScopes.add(scope);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const [index, segment] of segments.entries()) {
|
|
444
|
+
const { dir, payload, proven } = facts[index];
|
|
445
|
+
const readOnly = proven && (segment.scopes.length === 0 || (segment.scopes.length === 1 && provenPipelineScopes.has(segment.scopes[0])));
|
|
446
|
+
if (readOnly) ignoredRanges.push(...segment.wordRanges!);
|
|
447
|
+
// -regex/-iregex match the whole path, which the file-name glob check
|
|
448
|
+
// cannot model. An unproven find selecting by one stops unless the
|
|
449
|
+
// pattern's fixed tail rules out every control file's name, and only a
|
|
450
|
+
// plain conjunction can rely on that: `! -regex '.*\.orig'` or a `-o`
|
|
451
|
+
// branch selects everything the pattern does not.
|
|
452
|
+
if (!readOnly && payload.command === "find") {
|
|
453
|
+
const uncertain = payload.args.some((token) => ["-regextype", "!", "-not", "-o", "-or", ","].includes(token.value));
|
|
454
|
+
for (const [i, token] of payload.args.entries()) {
|
|
455
|
+
if (token.value !== "-regex" && token.value !== "-iregex") continue;
|
|
456
|
+
const pattern = payload.args[i + 1];
|
|
457
|
+
if (!pattern || pattern.dynamic || uncertain) return pattern?.value ?? token.value;
|
|
458
|
+
const tail = regexLiteralTail(pattern.value)?.toLowerCase();
|
|
459
|
+
const name = tail?.slice(tail.lastIndexOf("/") + 1);
|
|
460
|
+
if (tail === undefined || name === undefined || [...controlNames].some((control) => tail.includes("/") ? control === name : control.endsWith(name))) return pattern.value;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const negated = payload.command === "find" && !readOnly ? negatedFindPatterns(segment.tokens, payload.args) : undefined;
|
|
464
|
+
const words = [
|
|
465
|
+
...segment.tokens.map((token, index) => ({ value: token.value, index, exempt: readOnly })),
|
|
466
|
+
...[...segment.redirects, ...segment.inputs.map((token) => token.value)].map((value) => ({ value, index: -1, exempt: false })),
|
|
467
|
+
];
|
|
468
|
+
for (const { value: word, index: at, exempt } of words) {
|
|
469
|
+
// A proven command's words are still read as a script: its output can
|
|
470
|
+
// reach a file it never names, through an enclosing redirect, an
|
|
471
|
+
// `exec` or a descriptor (`{ echo '…'; } > s.sh; sh s.sh`). Only
|
|
472
|
+
// their path match is exempt.
|
|
473
|
+
if (depth < 3 && /\s/.test(word)) {
|
|
474
|
+
const nested = shellNamesControlFile(word, dir, home, oneCodeProjectSettings, depth + 1, forms);
|
|
475
|
+
if (nested) return nested;
|
|
476
|
+
}
|
|
477
|
+
if (exempt) continue;
|
|
478
|
+
// find never opens or writes a pattern operand, and one that only
|
|
479
|
+
// excludes files names nothing (a loop elsewhere in the line leaves
|
|
480
|
+
// every word unproven). A negated pattern that may select everything
|
|
481
|
+
// else may select a control file.
|
|
482
|
+
if (negated?.excluding.has(at)) continue;
|
|
483
|
+
if (negated?.selecting.has(at)) return word;
|
|
484
|
+
const eq = word.indexOf("=");
|
|
485
|
+
for (const candidate of eq >= 0 ? [word, word.slice(eq + 1)] : [word]) {
|
|
486
|
+
if (!candidate || isUnknownTilde(candidate)) continue;
|
|
487
|
+
const resolved = resolveForContainment(toAbsoluteBash(dir, candidate, home));
|
|
488
|
+
if (resolved && namesControlFile(resolved, forms, true)) return candidate;
|
|
489
|
+
if (unknownDir && controlNames.has(baseName(candidate))) return candidate;
|
|
490
|
+
// find matches names beneath its search roots, not the shell's cwd.
|
|
491
|
+
// An unproven expression may delete/execute on any such match.
|
|
492
|
+
if (payload.command === "find") {
|
|
493
|
+
const pattern = globComponentRegex(baseName(candidate));
|
|
494
|
+
if (pattern && [...controlNames].some((name) => pattern.test(name))) return candidate;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Keep the raw-text fallback for syntax/words the walker cannot attribute,
|
|
500
|
+
// heredoc bodies included. Only exact source ranges of proven command
|
|
501
|
+
// words disappear; never use string replacement (the same text may also
|
|
502
|
+
// occur in a redirect or in an unproven command).
|
|
503
|
+
let unproven = "";
|
|
504
|
+
let at = 0;
|
|
505
|
+
for (const range of ignoredRanges.sort((a, b) => a.start - b.start)) {
|
|
506
|
+
unproven += command.slice(at, range.start) + " ";
|
|
507
|
+
at = range.end;
|
|
508
|
+
}
|
|
509
|
+
unproven += command.slice(at);
|
|
510
|
+
const text = unproven.toLowerCase().replace(/\\/g, "/").replace(/\/\.\//g, "/").replace(/\/{2,}/g, "/");
|
|
511
|
+
return CONTROL_FILE_TEXT.exec(text)?.[2];
|
|
512
|
+
}
|