one-code-extension 0.4.1 → 0.6.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 +40 -28
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +26 -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 +124 -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 +642 -0
- package/extensions/background/index.ts +614 -248
- package/extensions/background/loop-fire.ts +173 -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 +56 -0
- package/extensions/background/wakeup.ts +178 -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 +66 -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 +250 -78
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +436 -63
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +219 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +35 -15
- package/extensions/context-management/index.ts +36 -28
- 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 +141 -25
- 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 +420 -42
- package/extensions/file-tracker/replay.ts +137 -24
- package/extensions/file-tracker/restore.ts +66 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +45 -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 +209 -58
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/prompt-context.ts +59 -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 +26 -12
- 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/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +146 -0
- package/extensions/lib/agent-loader.ts +18 -3
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-auth.ts +98 -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 +52 -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 +97 -0
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +359 -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/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +71 -19
- 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 +178 -0
- package/extensions/lib/model-catalog/models-dev.json +18533 -0
- package/extensions/lib/model-catalog/openrouter.json +5050 -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 +357 -73
- 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 +38 -2
- package/extensions/lib/plan-mode-channels.ts +42 -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/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +132 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +370 -358
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +547 -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 +24 -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 +112 -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 +129 -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 +159 -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 +76 -12
- 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 +90 -43
- 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 +220 -121
- 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 +164 -73
- 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/model-default/index.ts +86 -19
- 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 +1426 -398
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +11 -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 +276 -92
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/run-state.ts +26 -0
- 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 +64 -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 +14 -7
- package/extensions/recap/index.ts +73 -71
- 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/announce.ts +30 -0
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +469 -132
- package/extensions/skill/invoke.ts +118 -75
- 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 +146 -31
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +82 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1594 -852
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +5 -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/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +135 -80
- package/extensions/subagents/runs.ts +44 -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 +165 -47
- package/extensions/system-prompt/template.ts +34 -51
- 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 +204 -70
- 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/index.ts +366 -144
- 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 +108 -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 +157 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +33 -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 +162 -44
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +21 -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
- package/extensions/tool-search/announce.ts +0 -42
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PowerShell pre-gate's verdicts, judged from PowerShell's own parse
|
|
3
|
+
* (lib/powershell-parser.ts; decisions/windows.md "The PowerShell pre-gate
|
|
4
|
+
* reads PowerShell's own parse"). Pure apart from the filesystem reads the
|
|
5
|
+
* path checks make.
|
|
6
|
+
*
|
|
7
|
+
* Two verdicts, each positive proof only:
|
|
8
|
+
*
|
|
9
|
+
* - {@link powershellTreeReadOnly}: every statement is a pipeline of the read
|
|
10
|
+
* cmdlets in `powershell-cmdlets.ts`, called with parameters their tables
|
|
11
|
+
* clear, reading paths inside the working directory or a readable root.
|
|
12
|
+
* - {@link powershellTreeContainedEdits}: every statement is one of Claude
|
|
13
|
+
* Code's acceptEdits cmdlets (`Set-Content`, `Add-Content`, `Remove-Item`,
|
|
14
|
+
* `Clear-Content`), optionally piped into an output tail, writing inside the
|
|
15
|
+
* working space. The caller offers it only where Claude Code's fast paths
|
|
16
|
+
* apply (frontier and workhorse models, acceptEdits and auto mode).
|
|
17
|
+
*
|
|
18
|
+
* Fail closed by construction: each check approves the nodes it has judged,
|
|
19
|
+
* and a line with any node left unapproved (a syntax shape no check models) is
|
|
20
|
+
* not cleared. The command names and parameters are PowerShell's resolution,
|
|
21
|
+
* not ours: aliases are the running PowerShell's own (on macOS and Linux `ls`
|
|
22
|
+
* is `/bin/ls`, not `Get-ChildItem`), and parameters are the static binder's
|
|
23
|
+
* canonical names with positionals already placed.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readdirSync } from "node:fs";
|
|
27
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
28
|
+
import { isSensitivePath } from "../auto-mode/sensitive.ts";
|
|
29
|
+
import { isWithin, resolveForContainment, touchesGuardedPath } from "../auto-mode/paths.ts";
|
|
30
|
+
import { isGuardedWritePath, removalLoses } from "../auto-mode/shell-analysis.ts";
|
|
31
|
+
import type { PowerShellAstNode, PowerShellParse } from "../lib/powershell-parser.ts";
|
|
32
|
+
import {
|
|
33
|
+
applicationArgument,
|
|
34
|
+
type CmdletSpec,
|
|
35
|
+
isSwitchSpelling,
|
|
36
|
+
parameterKind,
|
|
37
|
+
READ_APPLICATIONS,
|
|
38
|
+
READ_CMDLETS,
|
|
39
|
+
SELECT_STRING_SOURCES,
|
|
40
|
+
TRUSTED_MODULES,
|
|
41
|
+
WRITE_CMDLETS,
|
|
42
|
+
WRITE_TAILS,
|
|
43
|
+
} from "./powershell-cmdlets.ts";
|
|
44
|
+
import { hasPowerShellWildcard, powershellPathAbsolute, powershellPathProblem, powershellUnescaped } from "./powershell-paths.ts";
|
|
45
|
+
import { comparablePath, system32Path } from "../lib/paths.ts";
|
|
46
|
+
|
|
47
|
+
export interface PowerShellTreeOptions {
|
|
48
|
+
/** The working directory the command runs in. */
|
|
49
|
+
cwd: string;
|
|
50
|
+
home: string;
|
|
51
|
+
/** REALPATH-resolved directories a read may point into besides the working directory (matcher.ts `sessionReadableRoots`). */
|
|
52
|
+
readableRoots?: string[];
|
|
53
|
+
/** REALPATH-resolved directories a write may land in besides the working directory (workspace directories). */
|
|
54
|
+
writableRoots?: string[];
|
|
55
|
+
/** Directories no write may touch (permissions/protected-paths.ts, the harness's own). */
|
|
56
|
+
protectedDirs?: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PowerShellTreeVerdict {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
/** Why not, for the decision log. */
|
|
62
|
+
reason?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The tree with each node's children, and the set of nodes a check has approved. */
|
|
66
|
+
interface Tree {
|
|
67
|
+
nodes: PowerShellAstNode[];
|
|
68
|
+
children: number[][];
|
|
69
|
+
approved: Set<number>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class Refusal extends Error {}
|
|
73
|
+
|
|
74
|
+
function refuse(reason: string): never {
|
|
75
|
+
throw new Refusal(reason);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildTree(parse: PowerShellParse): Tree {
|
|
79
|
+
const children: number[][] = parse.nodes.map(() => []);
|
|
80
|
+
parse.nodes.forEach((node, i) => {
|
|
81
|
+
if (node.parent >= 0 && node.parent < i) children[node.parent].push(i);
|
|
82
|
+
});
|
|
83
|
+
return { nodes: parse.nodes, children, approved: new Set() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The top-level pipelines of the line, each as its CommandAst node indices in order. Refuses anything that is not a plain pipeline or chain of them. */
|
|
87
|
+
function pipelines(tree: Tree): number[][] {
|
|
88
|
+
const { nodes, children } = tree;
|
|
89
|
+
if (nodes[0]?.type !== "ScriptBlockAst") refuse("the line did not parse as a script");
|
|
90
|
+
tree.approved.add(0);
|
|
91
|
+
const blocks = children[0];
|
|
92
|
+
if (blocks.length !== 1 || nodes[blocks[0]].type !== "NamedBlockAst") refuse("a param block, using statement or named block");
|
|
93
|
+
tree.approved.add(blocks[0]);
|
|
94
|
+
const result: number[][] = [];
|
|
95
|
+
const statement = (i: number) => {
|
|
96
|
+
const node = nodes[i];
|
|
97
|
+
if (node.type === "PipelineChainAst") {
|
|
98
|
+
if (node.background) refuse("a background job (`&`)");
|
|
99
|
+
tree.approved.add(i);
|
|
100
|
+
for (const side of children[i]) statement(side);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (node.type !== "PipelineAst") refuse(`${aNode(node.type)} statement`);
|
|
104
|
+
if (node.background) refuse("a background job (`&`)");
|
|
105
|
+
tree.approved.add(i);
|
|
106
|
+
const elements = children[i];
|
|
107
|
+
for (const element of elements) {
|
|
108
|
+
if (nodes[element].type !== "CommandAst") refuse("an expression as a pipeline source, whose value this check cannot see");
|
|
109
|
+
}
|
|
110
|
+
result.push(elements);
|
|
111
|
+
};
|
|
112
|
+
for (const i of children[blocks[0]]) statement(i);
|
|
113
|
+
if (result.length === 0) refuse("empty command");
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A node type as words: `InvokeMemberExpressionAst` is "invoke member expression". */
|
|
118
|
+
function statementName(type: string): string {
|
|
119
|
+
return type.replace(/(Statement)?Ast$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** "a" or "an" before a node type's words. */
|
|
123
|
+
function aNode(type: string): string {
|
|
124
|
+
const words = statementName(type);
|
|
125
|
+
return `${/^[aeiou]/.test(words) ? "an" : "a"} ${words}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The constant strings a value node holds, approving it, or undefined when it is not a constant. */
|
|
129
|
+
function constantValues(tree: Tree, i: number): string[] | undefined {
|
|
130
|
+
const node = tree.nodes[i];
|
|
131
|
+
if (node.type === "StringConstantExpressionAst" || node.type === "ConstantExpressionAst") {
|
|
132
|
+
if (tree.children[i].length > 0) return undefined;
|
|
133
|
+
tree.approved.add(i);
|
|
134
|
+
return [node.value ?? ""];
|
|
135
|
+
}
|
|
136
|
+
if (node.type === "ArrayLiteralAst") {
|
|
137
|
+
const values: string[] = [];
|
|
138
|
+
for (const child of tree.children[i]) {
|
|
139
|
+
const inner = constantValues(tree, child);
|
|
140
|
+
if (inner === undefined) return undefined;
|
|
141
|
+
values.push(...inner);
|
|
142
|
+
}
|
|
143
|
+
tree.approved.add(i);
|
|
144
|
+
return values;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const SAFE_VARIABLES = new Set(["_", "psitem", "true", "false", "null"]);
|
|
150
|
+
|
|
151
|
+
/** Binary operators a checked script block may use: comparison, logic, arithmetic and string operators. No range (`..`), `-as` or `-is` (types). */
|
|
152
|
+
const SAFE_BINARY = new Set(
|
|
153
|
+
[
|
|
154
|
+
...["eq", "ne", "ge", "gt", "lt", "le", "like", "notlike", "match", "notmatch", "replace", "contains", "notcontains", "in", "notin", "split"].flatMap((op) => [`I${op}`, `C${op}`]),
|
|
155
|
+
"And",
|
|
156
|
+
"Or",
|
|
157
|
+
"Xor",
|
|
158
|
+
"Join",
|
|
159
|
+
"Plus",
|
|
160
|
+
"Minus",
|
|
161
|
+
"Multiply",
|
|
162
|
+
"Divide",
|
|
163
|
+
"Rem",
|
|
164
|
+
"Format",
|
|
165
|
+
].map((op) => op.toLowerCase()),
|
|
166
|
+
);
|
|
167
|
+
const SAFE_UNARY = new Set(["not", "exclaim", "minus", "plus"]);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A script block that only reads `$_` and compares: `{ $_.Length -gt 1kb }`,
|
|
171
|
+
* `{ $_.Name }`. No command, method call, assignment, variable other than
|
|
172
|
+
* `$_`, type or dynamic member name.
|
|
173
|
+
*/
|
|
174
|
+
function checkScriptBlock(tree: Tree, i: number): void {
|
|
175
|
+
const { nodes, children } = tree;
|
|
176
|
+
if (nodes[i].type !== "ScriptBlockExpressionAst") refuse("a script block this check cannot read");
|
|
177
|
+
const block = children[i];
|
|
178
|
+
if (block.length !== 1 || nodes[block[0]].type !== "ScriptBlockAst") refuse("a script block this check cannot read");
|
|
179
|
+
const named = children[block[0]];
|
|
180
|
+
if (named.length !== 1 || nodes[named[0]].type !== "NamedBlockAst") refuse("a script block with a param or named block");
|
|
181
|
+
tree.approved.add(i).add(block[0]).add(named[0]);
|
|
182
|
+
for (const statement of children[named[0]]) checkExpressionStatement(tree, statement);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A pipeline of exactly one expression, the only statement a checked script block or parenthesis may hold. */
|
|
186
|
+
function checkExpressionStatement(tree: Tree, i: number): void {
|
|
187
|
+
const { nodes, children } = tree;
|
|
188
|
+
if (nodes[i].type !== "PipelineAst" || children[i].length !== 1 || nodes[children[i][0]].type !== "CommandExpressionAst") {
|
|
189
|
+
refuse("a script block that runs a command");
|
|
190
|
+
}
|
|
191
|
+
// `{ $_ & }` starts a background job per input object, the job the top-level check refuses.
|
|
192
|
+
if (nodes[i].background) refuse("a background job (`&`) inside a script block");
|
|
193
|
+
const expression = children[i][0];
|
|
194
|
+
if (children[expression].length !== 1) refuse("a redirection inside a script block");
|
|
195
|
+
tree.approved.add(i).add(expression);
|
|
196
|
+
checkExpression(tree, children[expression][0]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function checkExpression(tree: Tree, i: number): void {
|
|
200
|
+
const { nodes, children } = tree;
|
|
201
|
+
const node = nodes[i];
|
|
202
|
+
switch (node.type) {
|
|
203
|
+
case "StringConstantExpressionAst":
|
|
204
|
+
case "ConstantExpressionAst":
|
|
205
|
+
if (children[i].length === 0) {
|
|
206
|
+
tree.approved.add(i);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
case "VariableExpressionAst":
|
|
211
|
+
if (!node.splatted && SAFE_VARIABLES.has((node.name ?? "").toLowerCase())) {
|
|
212
|
+
tree.approved.add(i);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
refuse("a variable other than $_ in a script block");
|
|
216
|
+
break;
|
|
217
|
+
case "MemberExpressionAst": {
|
|
218
|
+
const [target, member] = children[i];
|
|
219
|
+
if (node.static || nodes[member]?.type !== "StringConstantExpressionAst") refuse("a static or computed member access");
|
|
220
|
+
tree.approved.add(i).add(member);
|
|
221
|
+
checkExpression(tree, target);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case "BinaryExpressionAst":
|
|
225
|
+
if (!SAFE_BINARY.has((node.operator ?? "").toLowerCase())) refuse(`the -${node.operator} operator in a script block`);
|
|
226
|
+
tree.approved.add(i);
|
|
227
|
+
for (const child of children[i]) checkExpression(tree, child);
|
|
228
|
+
return;
|
|
229
|
+
case "UnaryExpressionAst":
|
|
230
|
+
if (!SAFE_UNARY.has((node.operator ?? "").toLowerCase())) refuse(`the ${node.operator} operator in a script block`);
|
|
231
|
+
tree.approved.add(i);
|
|
232
|
+
for (const child of children[i]) checkExpression(tree, child);
|
|
233
|
+
return;
|
|
234
|
+
case "ParenExpressionAst":
|
|
235
|
+
if (children[i].length !== 1) break;
|
|
236
|
+
tree.approved.add(i);
|
|
237
|
+
checkExpressionStatement(tree, children[i][0]);
|
|
238
|
+
return;
|
|
239
|
+
case "ArrayLiteralAst":
|
|
240
|
+
tree.approved.add(i);
|
|
241
|
+
for (const child of children[i]) checkExpression(tree, child);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
refuse(`${aNode(node.type)} in a script block`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** One command whose name, bindings and elements are checked; returns its lowercase canonical name and the path values it binds. */
|
|
248
|
+
interface CheckedCommand {
|
|
249
|
+
name: string;
|
|
250
|
+
/** The values of its path parameters. */
|
|
251
|
+
paths: string[];
|
|
252
|
+
/** The canonical names of every parameter it binds. */
|
|
253
|
+
bound: Set<string>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
type SpecFor = (name: string) => CmdletSpec | undefined;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check one CommandAst: a static name that resolves to a PowerShell cmdlet in
|
|
260
|
+
* `specFor`'s table (or a read-only application, when `applications` is set),
|
|
261
|
+
* called with parameters the table clears, holding constant values, with no
|
|
262
|
+
* redirection except to `$null` or a stream merge.
|
|
263
|
+
*/
|
|
264
|
+
function checkCommand(tree: Tree, i: number, specFor: SpecFor, applications: boolean): CheckedCommand {
|
|
265
|
+
const { nodes, children } = tree;
|
|
266
|
+
const node = nodes[i];
|
|
267
|
+
if (node.operator && node.operator !== "Unknown") refuse("the call (&) or dot-source operator");
|
|
268
|
+
if (!node.name) refuse("a command name computed at run time");
|
|
269
|
+
const elements = children[i];
|
|
270
|
+
const nameElement = elements[0];
|
|
271
|
+
if (nameElement === undefined || nodes[nameElement].type !== "StringConstantExpressionAst") refuse("a command name computed at run time");
|
|
272
|
+
tree.approved.add(i).add(nameElement);
|
|
273
|
+
const shown = node.name;
|
|
274
|
+
|
|
275
|
+
for (const element of elements.slice(1)) checkRedirection(tree, element);
|
|
276
|
+
|
|
277
|
+
if (node.commandType === "Application" && applications && READ_APPLICATIONS[shown.toLowerCase()]) {
|
|
278
|
+
// The program PowerShell will run, not the name typed: a `findstr.exe`
|
|
279
|
+
// earlier on PATH than Windows' own is some other program.
|
|
280
|
+
const expected = system32Path(shown.toLowerCase().endsWith(".exe") ? shown : `${shown}.exe`);
|
|
281
|
+
if (!node.resolvedName || comparablePath(node.resolvedName) !== comparablePath(expected)) refuse(`\`${shown}\` resolves to ${node.resolvedName ?? "no program"}, not Windows' own`);
|
|
282
|
+
// The parse server resolved the name in its own directory; a relative
|
|
283
|
+
// PATH entry (`.`, `bin`) resolves against the call's, where it can name
|
|
284
|
+
// another program.
|
|
285
|
+
if (pathHasRelativeEntry(process.env)) refuse(`PATH has a relative entry, so \`${shown}\` may resolve to another program where it runs`);
|
|
286
|
+
const paths: string[] = [];
|
|
287
|
+
for (const element of elements.slice(1)) {
|
|
288
|
+
if (nodes[element].type.endsWith("RedirectionAst")) continue;
|
|
289
|
+
const values = constantValues(tree, element);
|
|
290
|
+
if (values === undefined) refuse(`an argument to ${shown} this check cannot read`);
|
|
291
|
+
for (const value of values) {
|
|
292
|
+
const kind = applicationArgument(shown, value);
|
|
293
|
+
if (kind === "refused") refuse(`${value} is not a switch this check clears for ${shown}`);
|
|
294
|
+
if (kind === "path") paths.push(value);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { name: shown.toLowerCase(), paths, bound: new Set() };
|
|
298
|
+
}
|
|
299
|
+
if (node.commandType !== "Cmdlet" || !TRUSTED_MODULES.has((node.module ?? "").toLowerCase()) || !node.resolvedName) {
|
|
300
|
+
refuse(node.commandType ? `\`${shown}\` is not a read-only cmdlet` : `\`${shown}\` is not a command this PowerShell knows`);
|
|
301
|
+
}
|
|
302
|
+
const name = node.resolvedName.toLowerCase();
|
|
303
|
+
const spec = specFor(name);
|
|
304
|
+
if (!spec) refuse(`\`${node.resolvedName}\` is not a cmdlet this check clears here`);
|
|
305
|
+
if (node.bindingUnavailable) refuse("PowerShell's parameter binder is not available");
|
|
306
|
+
if ((node.bindingErrors ?? []).length > 0) refuse(`an argument PowerShell cannot bind to ${node.resolvedName}`);
|
|
307
|
+
|
|
308
|
+
const paths: string[] = [];
|
|
309
|
+
const bound = new Set<string>();
|
|
310
|
+
for (const binding of node.bindings ?? []) {
|
|
311
|
+
const kind = parameterKind(spec, binding.parameter);
|
|
312
|
+
if (!kind) refuse(`-${binding.parameter} is not a parameter this check clears for ${node.resolvedName}`);
|
|
313
|
+
bound.add(binding.parameter.toLowerCase());
|
|
314
|
+
if (binding.value === -1) {
|
|
315
|
+
if (kind !== "switch") refuse(`-${binding.parameter} without a value`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (kind === "switch") {
|
|
319
|
+
const value = nodes[binding.value];
|
|
320
|
+
if (value?.type !== "VariableExpressionAst" || !["true", "false"].includes((value.name ?? "").toLowerCase())) refuse(`-${binding.parameter} given a value that is not $true or $false`);
|
|
321
|
+
tree.approved.add(binding.value);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (kind === "scriptBlock" && nodes[binding.value]?.type === "ScriptBlockExpressionAst") {
|
|
325
|
+
checkScriptBlock(tree, binding.value);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const values = binding.value === -2 ? collectedValues(tree, binding.elements) : constantValues(tree, binding.value);
|
|
329
|
+
if (values === undefined) refuse(`-${binding.parameter} given a value computed at run time`);
|
|
330
|
+
if (kind === "path") paths.push(...values);
|
|
331
|
+
}
|
|
332
|
+
// A switch given `$false` binds nothing (the binder drops it, as
|
|
333
|
+
// PowerShell does: the switch is off), so its name was never checked
|
|
334
|
+
// against the table; it must still spell one of the cmdlet's switches.
|
|
335
|
+
for (const element of elements.slice(1)) {
|
|
336
|
+
if (nodes[element].type !== "CommandParameterAst") continue;
|
|
337
|
+
tree.approved.add(element);
|
|
338
|
+
for (const argument of children[element]) {
|
|
339
|
+
const value = nodes[argument];
|
|
340
|
+
if (tree.approved.has(argument) || value.type !== "VariableExpressionAst" || (value.name ?? "").toLowerCase() !== "false") continue;
|
|
341
|
+
if (!isSwitchSpelling(spec, nodes[element].name ?? "")) refuse(`-${nodes[element].name}:$false is not a switch this check clears for ${node.resolvedName}`);
|
|
342
|
+
tree.approved.add(argument);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { name, paths, bound };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The constants a remaining-arguments parameter collected, each an argument of the command. */
|
|
349
|
+
function collectedValues(tree: Tree, elements: number[] | undefined): string[] | undefined {
|
|
350
|
+
if (!elements || elements.length === 0 || elements.some((i) => i < 0)) return undefined;
|
|
351
|
+
const values: string[] = [];
|
|
352
|
+
for (const element of elements) {
|
|
353
|
+
const inner = constantValues(tree, element);
|
|
354
|
+
if (inner === undefined) return undefined;
|
|
355
|
+
values.push(...inner);
|
|
356
|
+
}
|
|
357
|
+
return values;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Whether any PATH entry is relative, so a command name resolves differently in another directory. */
|
|
361
|
+
function pathHasRelativeEntry(env: NodeJS.ProcessEnv): boolean {
|
|
362
|
+
const key = Object.keys(env).find((name) => name.toLowerCase() === "path") ?? "PATH";
|
|
363
|
+
return (env[key] ?? "").split(delimiter).some((entry) => entry.trim() !== "" && !isAbsolute(entry.trim()));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** A stream merge (`2>&1`) and a redirection to `$null` write no file; any other redirection is not cleared. */
|
|
367
|
+
function checkRedirection(tree: Tree, i: number): void {
|
|
368
|
+
const { nodes, children } = tree;
|
|
369
|
+
const node = nodes[i];
|
|
370
|
+
if (node.type === "MergingRedirectionAst") {
|
|
371
|
+
tree.approved.add(i);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (node.type !== "FileRedirectionAst") return;
|
|
375
|
+
const [location] = children[i];
|
|
376
|
+
if (children[i].length === 1 && nodes[location].type === "VariableExpressionAst" && (nodes[location].name ?? "").toLowerCase() === "null") {
|
|
377
|
+
tree.approved.add(i).add(location);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
refuse("a redirection that writes a file");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function readRoots(opts: PowerShellTreeOptions): string[] {
|
|
384
|
+
return [resolveForContainment(opts.cwd) ?? opts.cwd, ...(opts.readableRoots ?? [])];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Every node must have been approved by a check; the first one left over names what was not modelled. */
|
|
388
|
+
function requireAllApproved(tree: Tree): void {
|
|
389
|
+
for (let i = 0; i < tree.nodes.length; i++) {
|
|
390
|
+
if (!tree.approved.has(i)) refuse(`${aNode(tree.nodes[i].type)} this check does not model`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Verdicts already reached for a parse. One tool call asks the same question
|
|
396
|
+
* of its parse several times (decide() runs up to three times, then the
|
|
397
|
+
* classifier's pre-gate), and each answer costs a tree walk and filesystem
|
|
398
|
+
* reads; a parse lives for one call, so the answers cannot go stale.
|
|
399
|
+
*/
|
|
400
|
+
const judged = new WeakMap<PowerShellParse, Map<string, PowerShellTreeVerdict>>();
|
|
401
|
+
|
|
402
|
+
function verdict(parse: PowerShellParse, kind: "read" | "edit", opts: PowerShellTreeOptions, judge: () => void): PowerShellTreeVerdict {
|
|
403
|
+
const key = JSON.stringify([kind, opts.cwd, opts.home, opts.readableRoots, opts.writableRoots, opts.protectedDirs]);
|
|
404
|
+
let answers = judged.get(parse);
|
|
405
|
+
if (!answers) judged.set(parse, (answers = new Map()));
|
|
406
|
+
const known = answers.get(key);
|
|
407
|
+
if (known) return known;
|
|
408
|
+
let answer: PowerShellTreeVerdict;
|
|
409
|
+
try {
|
|
410
|
+
judge();
|
|
411
|
+
answer = { ok: true };
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (!(error instanceof Refusal)) throw error;
|
|
414
|
+
answer = { ok: false, reason: error.message };
|
|
415
|
+
}
|
|
416
|
+
answers.set(key, answer);
|
|
417
|
+
return answer;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The most entries a directory may hold for its entries to be checked one by one. */
|
|
421
|
+
const ENTRY_LIMIT = 2_000;
|
|
422
|
+
|
|
423
|
+
/** Whether every entry of `directory` (not recursing) resolves inside a root and to no credential path. */
|
|
424
|
+
function entriesInside(directory: string, roots: string[]): boolean {
|
|
425
|
+
let entries: string[];
|
|
426
|
+
try {
|
|
427
|
+
entries = readdirSync(directory);
|
|
428
|
+
} catch {
|
|
429
|
+
return true; // nothing to read through
|
|
430
|
+
}
|
|
431
|
+
if (entries.length > ENTRY_LIMIT) return false;
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
const resolved = resolveForContainment(join(directory, entry));
|
|
434
|
+
if (resolved === undefined || !roots.some((root) => isWithin(root, resolved)) || isSensitivePath(resolved)) return false;
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Whether the tree under `directory` holds a symbolic link or junction, or
|
|
441
|
+
* is too large to walk. Windows PowerShell 5.1's recursive `Get-ChildItem`
|
|
442
|
+
* and `Remove-Item` follow them (PowerShell 7's do not), so a link deep in
|
|
443
|
+
* the tree lists or deletes what it points at, which the target's own
|
|
444
|
+
* containment check never sees.
|
|
445
|
+
*/
|
|
446
|
+
function linkInTree(directory: string): boolean {
|
|
447
|
+
return touchesGuardedPath(directory, directory, (_path, entry) => entry.isSymbolicLink());
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Whether every statement of the line only reads inside the working directory
|
|
452
|
+
* and the readable roots. See the module header.
|
|
453
|
+
*/
|
|
454
|
+
export function powershellTreeReadOnly(parse: PowerShellParse, opts: PowerShellTreeOptions): PowerShellTreeVerdict {
|
|
455
|
+
return verdict(parse, "read", opts, () => {
|
|
456
|
+
if (parse.errors.length > 0) refuse("PowerShell could not parse the line");
|
|
457
|
+
const tree = buildTree(parse);
|
|
458
|
+
const roots = readRoots(opts);
|
|
459
|
+
for (const pipeline of pipelines(tree)) {
|
|
460
|
+
const checked: CheckedCommand[] = [];
|
|
461
|
+
pipeline.forEach((command, position) => {
|
|
462
|
+
const current = checkCommand(tree, command, (name) => READ_CMDLETS[name], true);
|
|
463
|
+
const spec = READ_CMDLETS[current.name];
|
|
464
|
+
if (position > 0 && spec && !spec.tail) refuse(`\`${tree.nodes[command].resolvedName}\` after a pipe reads the paths its input names`);
|
|
465
|
+
if (position > 0 && current.name === "select-string") checkSelectStringSources(checked, opts, roots);
|
|
466
|
+
if (current.name === "get-childitem" && (current.bound.has("recurse") || current.bound.has("depth"))) {
|
|
467
|
+
for (const directory of current.paths.length > 0 ? current.paths : ["."]) {
|
|
468
|
+
const absolute = hasPowerShellWildcard(directory) ? undefined : powershellPathAbsolute(directory, opts);
|
|
469
|
+
const resolved = absolute === undefined ? undefined : resolveForContainment(absolute);
|
|
470
|
+
if (resolved === undefined || linkInTree(resolved)) refuse("a recursive Get-ChildItem over a tree with a link in it, which Windows PowerShell 5.1 follows");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const path of current.paths) {
|
|
474
|
+
const problem = powershellPathProblem(path, opts, roots);
|
|
475
|
+
if (problem) refuse(problem);
|
|
476
|
+
}
|
|
477
|
+
checked.push(current);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
requireAllApproved(tree);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* `Select-String` after a pipe reads the file behind each `FileInfo` it
|
|
486
|
+
* receives. Its sources must be cmdlets whose output is text or a real
|
|
487
|
+
* file object; a `Get-ChildItem` among them must not recurse (a link deep in
|
|
488
|
+
* the tree is never seen), and every entry it lists must resolve inside.
|
|
489
|
+
*/
|
|
490
|
+
function checkSelectStringSources(upstream: CheckedCommand[], opts: PowerShellTreeOptions, roots: string[]): void {
|
|
491
|
+
for (const source of upstream) {
|
|
492
|
+
if (!SELECT_STRING_SOURCES.has(source.name)) refuse(`Select-String after \`${source.name}\`, whose output can name any file`);
|
|
493
|
+
if (source.name !== "get-childitem") continue;
|
|
494
|
+
if (source.bound.has("recurse") || source.bound.has("depth")) {
|
|
495
|
+
refuse("Select-String over a recursive Get-ChildItem, which reads through links inside the tree");
|
|
496
|
+
}
|
|
497
|
+
const directories = source.paths.length > 0 ? source.paths : ["."];
|
|
498
|
+
for (const directory of directories) {
|
|
499
|
+
// A wildcard lists its matches, each judged as a path already, but
|
|
500
|
+
// `-LiteralPath 'sub[1]'` and `-Path 'sub`[1`]'` list the directory
|
|
501
|
+
// `sub[1]` itself: judge the entries of both spellings, whichever exists.
|
|
502
|
+
const absolute = powershellPathAbsolute(directory, opts);
|
|
503
|
+
if (absolute === undefined) refuse(`Select-String over ${directory}, a directory this check cannot resolve`);
|
|
504
|
+
for (const spelling of new Set([absolute, powershellUnescaped(absolute)])) {
|
|
505
|
+
const resolved = resolveForContainment(spelling);
|
|
506
|
+
if (resolved === undefined || !entriesInside(resolved, roots)) refuse("Select-String over a directory with an entry outside the working directory");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Whether every statement of the line is one of Claude Code's acceptEdits
|
|
514
|
+
* cmdlets writing inside the working space, optionally piped into an output
|
|
515
|
+
* tail. See the module header.
|
|
516
|
+
*/
|
|
517
|
+
export function powershellTreeContainedEdits(parse: PowerShellParse, opts: PowerShellTreeOptions): PowerShellTreeVerdict {
|
|
518
|
+
return verdict(parse, "edit", opts, () => {
|
|
519
|
+
if (parse.errors.length > 0) refuse("PowerShell could not parse the line");
|
|
520
|
+
const tree = buildTree(parse);
|
|
521
|
+
const root = resolveForContainment(opts.cwd) ?? opts.cwd;
|
|
522
|
+
const writable = [root, ...(opts.writableRoots ?? [])];
|
|
523
|
+
for (const pipeline of pipelines(tree)) {
|
|
524
|
+
pipeline.forEach((command, position) => {
|
|
525
|
+
const current = checkCommand(tree, command, (name) => (position === 0 ? WRITE_CMDLETS[name] : WRITE_TAILS.has(name) ? READ_CMDLETS[name] : undefined), false);
|
|
526
|
+
if (position > 0) return;
|
|
527
|
+
if (current.paths.length === 0) refuse(`${tree.nodes[command].resolvedName} without a path`);
|
|
528
|
+
const removes = current.name === "remove-item";
|
|
529
|
+
for (const path of current.paths) checkWriteTarget(path, removes, removes && current.bound.has("recurse"), opts, writable);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
requireAllApproved(tree);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The bash gate's write-target check (shell-analysis.ts `checkWriteTarget`),
|
|
538
|
+
* for a PowerShell path: resolved inside the working space, and not a
|
|
539
|
+
* credential, execution-primitive or protected path. A removal must not take
|
|
540
|
+
* a working root, a `.git`, or a directory holding one or a guarded path,
|
|
541
|
+
* and a recursive removal must not reach a link.
|
|
542
|
+
*/
|
|
543
|
+
function checkWriteTarget(value: string, removes: boolean, recurses: boolean, opts: PowerShellTreeOptions, writable: string[]): void {
|
|
544
|
+
if (hasPowerShellWildcard(value)) refuse(`writes to ${value}, a wildcard whose matches this check does not judge`);
|
|
545
|
+
// `-Path` drops a backtick escape (`a``b` writes `a`b`) and `-LiteralPath` keeps it.
|
|
546
|
+
if (value.includes("`")) refuse(`writes to ${value}, whose backtick escapes this check does not judge`);
|
|
547
|
+
const absolute = powershellPathAbsolute(value, opts);
|
|
548
|
+
if (absolute === undefined) refuse(`writes to ${value}, a path this check cannot resolve`);
|
|
549
|
+
const resolved = resolveForContainment(absolute);
|
|
550
|
+
if (resolved === undefined || !writable.some((dir) => isWithin(dir, resolved))) refuse(`writes to ${value}, which is outside the working directory`);
|
|
551
|
+
if ([absolute, resolved].some((path) => isGuardedWritePath(path, opts.cwd, opts.protectedDirs))) refuse(`writes to ${value}, a credential, execution-primitive or protected path`);
|
|
552
|
+
if (!removes) return;
|
|
553
|
+
if (removalLoses(absolute, resolved, writable, opts.cwd, opts.protectedDirs)) {
|
|
554
|
+
refuse(`removes ${value}, which is a working root, or is or holds a git repository or a protected, credential or execution-primitive path`);
|
|
555
|
+
}
|
|
556
|
+
if (recurses && linkInTree(resolved)) refuse(`removes ${value} recursively, and a link inside it would be followed by Windows PowerShell 5.1`);
|
|
557
|
+
}
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { dirname
|
|
22
|
-
import {
|
|
21
|
+
import { dirname } from "node:path";
|
|
22
|
+
import { consentStorePath } from "../lib/consent-stores.ts";
|
|
23
23
|
|
|
24
24
|
interface ApprovalStore {
|
|
25
25
|
version: 1;
|
|
@@ -27,7 +27,7 @@ interface ApprovalStore {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function projectAllowStorePath(): string {
|
|
30
|
-
return
|
|
30
|
+
return consentStorePath("projectAllow");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Order-insensitive: the same rules in another order are one consent. */
|
|
@@ -66,16 +66,32 @@ export function persistProjectAllowApproval(projectRoot: string, rules: readonly
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The consent dialog's text: what the repository wants pre-approved (allow
|
|
71
|
+
* rules, and workspace directories whose files it wants readable), and what
|
|
72
|
+
* the current call would use: the rule it matches, or the directory it reads
|
|
73
|
+
* inside.
|
|
74
|
+
*/
|
|
75
|
+
export type TrustFiring = { rule: string } | { dir: string };
|
|
76
|
+
|
|
77
|
+
export function describeProjectAllow(rules: readonly string[], firing: TrustFiring, dirs: readonly string[] = []): { title: string; message: string } {
|
|
78
|
+
const list = (items: readonly string[]) => {
|
|
79
|
+
const shown = items.slice(0, 8);
|
|
80
|
+
return `${shown.join("\n")}${items.length > shown.length ? `\n… and ${items.length - shown.length} more` : ""}`;
|
|
81
|
+
};
|
|
82
|
+
const parts = [
|
|
83
|
+
...(rules.length > 0 ? [`pre-approve ${rules.length} permission rule(s):\n${list(rules)}`] : []),
|
|
84
|
+
...(dirs.length > 0 ? [`add ${dirs.length} workspace director${dirs.length === 1 ? "y" : "ies"}, whose files would be read without a prompt and, in accept-edits mode, edited without one:\n${list(dirs)}`] : []),
|
|
85
|
+
];
|
|
73
86
|
return {
|
|
74
|
-
title: "Trust this repository's allow rules?",
|
|
87
|
+
title: dirs.length > 0 ? "Trust this repository's permission settings?" : "Trust this repository's allow rules?",
|
|
75
88
|
message:
|
|
76
|
-
`This repository's .claude settings
|
|
77
|
-
`The current call matches "${firing}" and would run without a prompt` +
|
|
89
|
+
`This repository's .claude settings ${parts.join("\n\nand ")}\n\n` +
|
|
90
|
+
`The current call ${"dir" in firing ? `reads inside the workspace directory ${firing.dir}` : `matches "${firing.rule}"`} and would run without a prompt` +
|
|
78
91
|
" (in auto mode, without the classifier). Whoever committed the file granted this, not you." +
|
|
79
|
-
" Approval is remembered until
|
|
92
|
+
" Approval is remembered until these settings change; declining keeps them off for this session.",
|
|
80
93
|
};
|
|
81
94
|
}
|
|
95
|
+
|
|
96
|
+
/** How a repository's workspace directory enters the consent list: distinct from any rule, and hashed with them. */
|
|
97
|
+
export const projectDirectoryConsentEntry = (raw: string): string => `additionalDirectories: ${raw}`;
|
|
@@ -37,16 +37,28 @@ const PROTECTED_DIRS = [
|
|
|
37
37
|
"library/launchagents",
|
|
38
38
|
".config/fish",
|
|
39
39
|
".local/bin",
|
|
40
|
+
// SSH keys, config and authorized_keys: a write there adds a login or a
|
|
41
|
+
// ProxyCommand that runs on the next connection. Sensitive for reads too
|
|
42
|
+
// (auto-mode/sensitive.ts).
|
|
43
|
+
".ssh",
|
|
40
44
|
];
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
|
-
* `.claude/worktrees` is where the agent keeps its own git worktrees
|
|
47
|
+
* `.claude/worktrees` is where the agent keeps its own git worktrees
|
|
48
|
+
* (`.onecode/worktrees` in independent mode, lib/config-mode.ts), and
|
|
44
49
|
* `.onecode/plans` holds plan-mode documents (rendered to the user, never
|
|
45
50
|
* executed) — ordinary working space rather than configuration.
|
|
46
51
|
*/
|
|
47
|
-
const PROTECTED_DIR_EXCEPTIONS = [".claude/worktrees", ".onecode/plans"];
|
|
52
|
+
const PROTECTED_DIR_EXCEPTIONS = [".claude/worktrees", ".onecode/worktrees", ".onecode/plans"];
|
|
48
53
|
|
|
49
54
|
const PROTECTED_FILES = new Set([
|
|
55
|
+
// A `.git` FILE is a gitlink (`gitdir: <path>`): written into a subdirectory
|
|
56
|
+
// it points an in-project `git -C sub …` at any repository, and that
|
|
57
|
+
// repository's config (SECURITY-REVIEW-2026-09-23 H2). The `.git` directory
|
|
58
|
+
// is covered by PROTECTED_DIRS; this covers the file form. Checked before
|
|
59
|
+
// PROTECTED_DIR_EXCEPTIONS on purpose: a worktree's own gitlink under
|
|
60
|
+
// `.claude/worktrees` is git metadata, not working space (git writes it).
|
|
61
|
+
".git",
|
|
50
62
|
".gitconfig",
|
|
51
63
|
".gitmodules",
|
|
52
64
|
".bashrc",
|
|
@@ -78,6 +78,12 @@ export function sessionGrant(input: SessionGrantInput): SessionGrant | undefined
|
|
|
78
78
|
return mint(`${tool}(${absoluteDirPattern(cwd)})`, `Yes, and allow ${tool} anywhere in the working directory this session`);
|
|
79
79
|
}
|
|
80
80
|
const dir = dirname(absolute);
|
|
81
|
+
// A file directly in the home directory (or higher) grants itself
|
|
82
|
+
// only: its directory holds every dot-directory of the user's, so a
|
|
83
|
+
// grant for `~/.zshrc` would cover `~/.ssh/config` too.
|
|
84
|
+
if (isAtOrInsideDir(home, dir, cwd)) {
|
|
85
|
+
return mint(`${tool}(/${escapeLiteral(toPosixPath(resolve(absolute)))})`, `Yes, and allow ${tool} of ${tildify(absolute, home)} this session`);
|
|
86
|
+
}
|
|
81
87
|
return mint(`${tool}(${absoluteDirPattern(dir)})`, `Yes, and allow ${tool} under ${tildify(dir, home)} this session`);
|
|
82
88
|
}
|
|
83
89
|
case "url": {
|