@pugi/cli 0.1.0-beta.9 → 0.1.0-beta.91
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/CHANGELOG.md +132 -0
- package/LICENSE +1 -1
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +3 -3
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +13 -13
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +333 -7
- package/dist/core/edits/format-detector.js +260 -0
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +5 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +29 -29
- package/dist/core/engine/anvil-client.js +214 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +129 -19
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1792 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +46 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +216 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/hooks/worktree-events.js +158 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +551 -41
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +664 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2148 -217
- package/dist/core/repl/slash-commands.js +501 -41
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +324 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +30 -30
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4185 -549
- package/dist/runtime/commands/agents.js +31 -31
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +73 -39
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +27 -4
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +187 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +200 -38
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +12 -12
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +8 -8
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +22 -22
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +89 -28
- package/dist/tools/ask-user-question.js +337 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/cron.js +433 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +99 -4
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-chips.js +315 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/multi-file-diff-approval.js +375 -0
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +36 -1
- package/dist/tui/repl-render.js +176 -25
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +125 -45
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/package.json +31 -16
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +12 -0
- package/test/scenarios/identity.scenario.txt +12 -0
- package/test/scenarios/persona-handoff.scenario.txt +12 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multi_edit tool — β7 .
|
|
3
|
+
*
|
|
4
|
+
* Dispatches an ordered batch of file edits as a single transaction. Each
|
|
5
|
+
* edit is one Layer A (oldString -> newString) operation against one
|
|
6
|
+
* workspace file. Either every edit lands, or none do — failures roll
|
|
7
|
+
* the workspace back to the pre-dispatch state using the same journal +
|
|
8
|
+
* snapshot machinery the β1b Pl8 transactional layer uses for the
|
|
9
|
+
* marker-driven dispatcher.
|
|
10
|
+
*
|
|
11
|
+
* Why multi_edit when `edit` already exists:
|
|
12
|
+
*
|
|
13
|
+
* The single-shot `edit` tool is the right primitive for one mutation;
|
|
14
|
+
* the model uses it dozens of times in a typical session. A coordinated
|
|
15
|
+
* refactor (rename across 8 files, add an import to 12 modules, peel a
|
|
16
|
+
* helper into 5 callers) is currently 8/12/5 separate `edit` calls.
|
|
17
|
+
* Each call is its own audit + permission check + atomic write, which
|
|
18
|
+
* is the right shape for the audit story but means the model can leave
|
|
19
|
+
* the workspace half-mutated when one of the calls fails partway. The
|
|
20
|
+
* model also pays the round-trip latency once per call.
|
|
21
|
+
*
|
|
22
|
+
* `multi_edit` collapses the 8/12/5 calls into one tool dispatch with
|
|
23
|
+
* transactional semantics: snapshot every target file, attempt every
|
|
24
|
+
* edit against an in-memory buffer, then commit the writes only after
|
|
25
|
+
* all in-memory edits succeed. A failure rolls back via journal +
|
|
26
|
+
* in-memory snapshot — same code path as the dispatcher.
|
|
27
|
+
*
|
|
28
|
+
* Security: every target file routes through the same `applySecurityGate`
|
|
29
|
+
* chokepoint Layer A/B/C inherit. A path that escapes the workspace,
|
|
30
|
+
* points at a protected basename (`.env`, `*.pem`, ...), or symlinks
|
|
31
|
+
* outside the tree is refused BEFORE any read.
|
|
32
|
+
*
|
|
33
|
+
* Concurrency: marked `concurrencySafe: false` in the tool registry. The
|
|
34
|
+
* model MUST NOT issue another `multi_edit` (or any write tool) in
|
|
35
|
+
* parallel with one in flight; the journal serialises one dispatch per
|
|
36
|
+
* session.
|
|
37
|
+
*
|
|
38
|
+
* Output cap: a 50-edit batch is the soft ceiling. Beyond that the tool
|
|
39
|
+
* refuses with `too_many_edits` — the operator can split the refactor.
|
|
40
|
+
* Empirically a coordinated refactor that needs 50+ atomic edits should
|
|
41
|
+
* be a per-file Layer C rewrite instead.
|
|
42
|
+
*
|
|
43
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
44
|
+
*/
|
|
45
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
46
|
+
import { applySecurityGate } from '../core/edits/security-gate.js';
|
|
47
|
+
import { appendEntry, snapshotForDispatch, } from '../core/edits/journal.js';
|
|
48
|
+
import { rollbackDispatch } from '../core/edits/dispatch.js';
|
|
49
|
+
import { gateOnCancellation, OperatorAbortedError } from './file-tools.js';
|
|
50
|
+
import { recordFileMutation, recordToolCall, recordToolResult } from '../core/session.js';
|
|
51
|
+
/** Soft cap on per-dispatch edit count. See module docstring. */
|
|
52
|
+
const MULTI_EDIT_MAX = 50;
|
|
53
|
+
/**
|
|
54
|
+
* Apply a batch of file edits transactionally. Returns a structured
|
|
55
|
+
* result; never throws on operator-attributable failure (security,
|
|
56
|
+
* missing file, no_match) — only on infrastructure error (filesystem
|
|
57
|
+
* permission denied mid-write after the snapshot, etc.).
|
|
58
|
+
*/
|
|
59
|
+
export function multiEdit(ctx, edits, opts = {}) {
|
|
60
|
+
const toolCallId = recordToolCall(ctx.session, 'multi_edit', `${edits.length} edits across ${new Set(edits.map((e) => e.file)).size} files`);
|
|
61
|
+
try {
|
|
62
|
+
gateOnCancellation(ctx, 'multi_edit');
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof OperatorAbortedError) {
|
|
66
|
+
recordToolResult(ctx.session, toolCallId, 'cancelled', error.message);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (edits.length === 0) {
|
|
72
|
+
const result = {
|
|
73
|
+
ok: false,
|
|
74
|
+
filesChanged: [],
|
|
75
|
+
editsApplied: 0,
|
|
76
|
+
reason: 'empty_batch',
|
|
77
|
+
detail: 'multi_edit received zero edits',
|
|
78
|
+
perEdit: [],
|
|
79
|
+
};
|
|
80
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'empty_batch');
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
if (edits.length > MULTI_EDIT_MAX) {
|
|
84
|
+
const result = {
|
|
85
|
+
ok: false,
|
|
86
|
+
filesChanged: [],
|
|
87
|
+
editsApplied: 0,
|
|
88
|
+
reason: 'too_many_edits',
|
|
89
|
+
detail: `multi_edit batch of ${edits.length} exceeds cap ${MULTI_EDIT_MAX}; split the refactor`,
|
|
90
|
+
perEdit: [],
|
|
91
|
+
};
|
|
92
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'too_many_edits');
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
// SECURITY GATE pass over every distinct file BEFORE any read.
|
|
96
|
+
// A single rejected file aborts the whole batch — the transactional
|
|
97
|
+
// contract requires we never partial-mutate.
|
|
98
|
+
const uniqueFiles = Array.from(new Set(edits.map((e) => e.file)));
|
|
99
|
+
const resolvedByFile = new Map();
|
|
100
|
+
for (const f of uniqueFiles) {
|
|
101
|
+
const gate = applySecurityGate(f, { cwd: ctx.root, toolName: 'layer-c' });
|
|
102
|
+
if (!gate.ok) {
|
|
103
|
+
const result = {
|
|
104
|
+
ok: false,
|
|
105
|
+
filesChanged: [],
|
|
106
|
+
editsApplied: 0,
|
|
107
|
+
reason: gate.reason,
|
|
108
|
+
detail: `${f}: ${gate.detail}`,
|
|
109
|
+
perEdit: edits.map((e, i) => ({
|
|
110
|
+
index: i,
|
|
111
|
+
file: e.file,
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: gate.reason,
|
|
114
|
+
detail: e.file === f ? gate.detail : 'batch aborted by sibling security failure',
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
recordToolResult(ctx.session, toolCallId, 'error', `${gate.reason}: ${f}`);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
resolvedByFile.set(f, gate.absPath);
|
|
121
|
+
}
|
|
122
|
+
// Snapshot existing files BEFORE any in-memory edit so a partial-write
|
|
123
|
+
// rollback is deterministic. The snapshot also captures sha256 of each
|
|
124
|
+
// pre-existing file so post-failure restore can verify the in-memory
|
|
125
|
+
// buffer still matches.
|
|
126
|
+
const snapshot = snapshotForDispatch(ctx.root, uniqueFiles);
|
|
127
|
+
const preContent = new Map();
|
|
128
|
+
for (const entry of snapshot) {
|
|
129
|
+
if (!entry.existed)
|
|
130
|
+
continue;
|
|
131
|
+
const abs = resolvedByFile.get(entry.path);
|
|
132
|
+
if (!abs)
|
|
133
|
+
continue;
|
|
134
|
+
try {
|
|
135
|
+
preContent.set(entry.path, readFileSync(abs));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Best-effort. A read failure here will surface again when the
|
|
139
|
+
// per-edit phase tries to read the same file — let that path
|
|
140
|
+
// produce the operator-facing error.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// In-memory edit phase. For each edit we work on the latest version
|
|
144
|
+
// of the file (so two edits against the same file stack). Failure
|
|
145
|
+
// here is the common case — `no_match`, `ambiguous_match`, missing
|
|
146
|
+
// file — and aborts the whole batch.
|
|
147
|
+
const bodyByFile = new Map();
|
|
148
|
+
const perEdit = [];
|
|
149
|
+
for (let i = 0; i < edits.length; i += 1) {
|
|
150
|
+
const edit = edits[i];
|
|
151
|
+
const abs = resolvedByFile.get(edit.file);
|
|
152
|
+
if (!abs) {
|
|
153
|
+
// Should be unreachable — every distinct file went through the
|
|
154
|
+
// gate above. Belt + braces.
|
|
155
|
+
perEdit.push({ index: i, file: edit.file, ok: false, reason: 'write_error', detail: 'no resolved path' });
|
|
156
|
+
const result = {
|
|
157
|
+
ok: false,
|
|
158
|
+
filesChanged: [],
|
|
159
|
+
editsApplied: 0,
|
|
160
|
+
reason: 'write_error',
|
|
161
|
+
detail: `${edit.file}: no resolved path`,
|
|
162
|
+
perEdit,
|
|
163
|
+
};
|
|
164
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'write_error');
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
let body = bodyByFile.get(edit.file);
|
|
168
|
+
if (body === undefined) {
|
|
169
|
+
if (!existsSync(abs)) {
|
|
170
|
+
const detail = `file does not exist: ${edit.file}`;
|
|
171
|
+
perEdit.push({ index: i, file: edit.file, ok: false, reason: 'file_missing', detail });
|
|
172
|
+
const result = {
|
|
173
|
+
ok: false,
|
|
174
|
+
filesChanged: [],
|
|
175
|
+
editsApplied: 0,
|
|
176
|
+
reason: 'file_missing',
|
|
177
|
+
detail,
|
|
178
|
+
perEdit,
|
|
179
|
+
};
|
|
180
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'file_missing');
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
body = readFileSync(abs, 'utf8');
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
188
|
+
perEdit.push({ index: i, file: edit.file, ok: false, reason: 'write_error', detail });
|
|
189
|
+
const result = {
|
|
190
|
+
ok: false,
|
|
191
|
+
filesChanged: [],
|
|
192
|
+
editsApplied: 0,
|
|
193
|
+
reason: 'write_error',
|
|
194
|
+
detail: `${edit.file}: ${detail}`,
|
|
195
|
+
perEdit,
|
|
196
|
+
};
|
|
197
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'write_error');
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (edit.oldString === edit.newString) {
|
|
202
|
+
perEdit.push({
|
|
203
|
+
index: i,
|
|
204
|
+
file: edit.file,
|
|
205
|
+
ok: false,
|
|
206
|
+
reason: 'identical_replacement',
|
|
207
|
+
detail: 'oldString and newString are identical',
|
|
208
|
+
});
|
|
209
|
+
const result = {
|
|
210
|
+
ok: false,
|
|
211
|
+
filesChanged: [],
|
|
212
|
+
editsApplied: 0,
|
|
213
|
+
reason: 'identical_replacement',
|
|
214
|
+
detail: `edit ${i} (${edit.file}): oldString and newString are identical`,
|
|
215
|
+
perEdit,
|
|
216
|
+
};
|
|
217
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'identical_replacement');
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
const matches = countOccurrences(body, edit.oldString);
|
|
221
|
+
if (matches === 0) {
|
|
222
|
+
const detail = `edit ${i} (${edit.file}): oldString not found`;
|
|
223
|
+
perEdit.push({ index: i, file: edit.file, ok: false, reason: 'no_match', detail });
|
|
224
|
+
const result = {
|
|
225
|
+
ok: false,
|
|
226
|
+
filesChanged: [],
|
|
227
|
+
editsApplied: 0,
|
|
228
|
+
reason: 'no_match',
|
|
229
|
+
detail,
|
|
230
|
+
perEdit,
|
|
231
|
+
};
|
|
232
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'no_match');
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
if (matches > 1) {
|
|
236
|
+
const detail = `edit ${i} (${edit.file}): oldString matches ${matches} times — expand context to make it unique`;
|
|
237
|
+
perEdit.push({ index: i, file: edit.file, ok: false, reason: 'ambiguous_match', detail });
|
|
238
|
+
const result = {
|
|
239
|
+
ok: false,
|
|
240
|
+
filesChanged: [],
|
|
241
|
+
editsApplied: 0,
|
|
242
|
+
reason: 'ambiguous_match',
|
|
243
|
+
detail,
|
|
244
|
+
perEdit,
|
|
245
|
+
};
|
|
246
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'ambiguous_match');
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
body = body.replace(edit.oldString, edit.newString);
|
|
250
|
+
bodyByFile.set(edit.file, body);
|
|
251
|
+
perEdit.push({ index: i, file: edit.file, ok: true });
|
|
252
|
+
}
|
|
253
|
+
if (opts.dryRun) {
|
|
254
|
+
const result = {
|
|
255
|
+
ok: true,
|
|
256
|
+
filesChanged: Array.from(bodyByFile.keys()),
|
|
257
|
+
editsApplied: edits.length,
|
|
258
|
+
perEdit,
|
|
259
|
+
};
|
|
260
|
+
recordToolResult(ctx.session, toolCallId, 'success', `dry-run ${edits.length} edits ok`);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
// Persist the snapshot to the journal BEFORE the first write. A crash
|
|
264
|
+
// mid-write then has a recoverable trail in `.pugi/sessions/<id>/journal.jsonl`.
|
|
265
|
+
// Best-effort; a journal write failure does not block the edits (the
|
|
266
|
+
// in-memory rollback path still covers same-process failures).
|
|
267
|
+
if (ctx.session.enabled) {
|
|
268
|
+
appendEntry(ctx.root, ctx.session.id, {
|
|
269
|
+
ts: Date.now(),
|
|
270
|
+
taskId: `multi_edit-${toolCallId}`,
|
|
271
|
+
files: snapshot,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// Commit phase. Atomic writes one file at a time. A failure rolls
|
|
275
|
+
// back via the same dispatcher rollback used by the marker layer.
|
|
276
|
+
const written = [];
|
|
277
|
+
for (const [file, body] of bodyByFile) {
|
|
278
|
+
const abs = resolvedByFile.get(file);
|
|
279
|
+
try {
|
|
280
|
+
atomicWrite(abs, body);
|
|
281
|
+
written.push(file);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
285
|
+
// Roll back every file we already touched plus restore the
|
|
286
|
+
// not-yet-touched ones that existed before (defensive — the
|
|
287
|
+
// rollback function is idempotent on untouched paths).
|
|
288
|
+
const rollback = rollbackDispatch(ctx.root, snapshot, preContent);
|
|
289
|
+
if (!rollback.ok) {
|
|
290
|
+
const result = {
|
|
291
|
+
ok: false,
|
|
292
|
+
filesChanged: [],
|
|
293
|
+
editsApplied: 0,
|
|
294
|
+
reason: 'rollback_failed',
|
|
295
|
+
detail: `${file}: ${detail}; rollback also failed: ${rollback.detail}`,
|
|
296
|
+
perEdit,
|
|
297
|
+
};
|
|
298
|
+
recordToolResult(ctx.session, toolCallId, 'error', 'rollback_failed');
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
const result = {
|
|
302
|
+
ok: false,
|
|
303
|
+
filesChanged: [],
|
|
304
|
+
editsApplied: 0,
|
|
305
|
+
reason: 'write_error',
|
|
306
|
+
detail: `${file}: ${detail}`,
|
|
307
|
+
perEdit,
|
|
308
|
+
};
|
|
309
|
+
recordToolResult(ctx.session, toolCallId, 'error', `write_error: ${detail}`);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const file of written) {
|
|
314
|
+
recordFileMutation(ctx.session, {
|
|
315
|
+
toolCallId,
|
|
316
|
+
path: file,
|
|
317
|
+
operation: 'update',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
recordToolResult(ctx.session, toolCallId, 'success', `applied ${edits.length} edits across ${written.length} files`);
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
filesChanged: written,
|
|
324
|
+
editsApplied: edits.length,
|
|
325
|
+
perEdit,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function countOccurrences(haystack, needle) {
|
|
329
|
+
if (needle.length === 0)
|
|
330
|
+
return 0;
|
|
331
|
+
let count = 0;
|
|
332
|
+
let from = 0;
|
|
333
|
+
while (true) {
|
|
334
|
+
const idx = haystack.indexOf(needle, from);
|
|
335
|
+
if (idx === -1)
|
|
336
|
+
return count;
|
|
337
|
+
count += 1;
|
|
338
|
+
from = idx + needle.length;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** Atomic write helper — mirrors Layer A / Layer D. */
|
|
342
|
+
function atomicWrite(absPath, contents) {
|
|
343
|
+
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
344
|
+
const tmp = `${absPath}.pugi-tmp-${suffix}`;
|
|
345
|
+
try {
|
|
346
|
+
writeFileSync(tmp, contents, { encoding: 'utf8', mode: 0o600 });
|
|
347
|
+
renameSync(tmp, absPath);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
try {
|
|
351
|
+
unlinkSync(tmp);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// tmp file may not exist if writeFileSync itself failed.
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Test-only surface. */
|
|
360
|
+
export const __test__ = { MULTI_EDIT_MAX };
|
|
361
|
+
//# sourceMappingURL=multi-edit.js.map
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell tool — .
|
|
3
|
+
*
|
|
4
|
+
* Windows operators cannot run native `*.ps1` scripts via the bash tool
|
|
5
|
+
* (which spawns `/bin/sh`). This tool spawns `pwsh -NoProfile -Command`
|
|
6
|
+
* на cross-platform PowerShell 7+ binary so Windows-first workflows are
|
|
7
|
+
* first-class на Pugi.
|
|
8
|
+
*
|
|
9
|
+
* independent implementation re-implementation. Surface mirrors bashTool's permission
|
|
10
|
+
* gate, env sanitiser, output cap, timeout, and exit-code propagation;
|
|
11
|
+
* the only difference is the shell binary selection. Per-platform
|
|
12
|
+
* resolution:
|
|
13
|
+
* - All OS: try `pwsh` on $PATH first (PowerShell 7+ cross-platform).
|
|
14
|
+
* - Windows fallback: `powershell.exe` (Windows PowerShell 5.1 baked-in).
|
|
15
|
+
* - Other OS without pwsh: tool returns a clear "powershell binary
|
|
16
|
+
* not found" error so the operator can install pwsh or fall back
|
|
17
|
+
* к bash.
|
|
18
|
+
*
|
|
19
|
+
* Permission class: reuses the bash classifier — destructive patterns,
|
|
20
|
+
* sandbox detection, and additional-directories checks are command-string
|
|
21
|
+
* based and apply equally to pwsh and sh.
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { listDestructivePatterns } from '../core/bash-classifier.js';
|
|
25
|
+
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
26
|
+
export const POWERSHELL_OUTPUT_CAP_BYTES = 64 * 1024;
|
|
27
|
+
export const POWERSHELL_DEFAULT_TIMEOUT_MS = 30_000;
|
|
28
|
+
export const POWERSHELL_MAX_TIMEOUT_MS = 120_000;
|
|
29
|
+
/**
|
|
30
|
+
* PowerShell-specific destructive patterns. Layered ON TOP of the
|
|
31
|
+
* shared `listDestructivePatterns()` from the bash classifier (which
|
|
32
|
+
* covers `rm -rf`, `DROP TABLE`, etc — patterns that also surface в
|
|
33
|
+
* pwsh-via-aliases). These are the cmdlet forms unique to pwsh.
|
|
34
|
+
*
|
|
35
|
+
* Patterns are case-insensitive matched against the command string
|
|
36
|
+
* (pwsh cmdlets accept any case: `remove-item -force` == `Remove-Item -Force`).
|
|
37
|
+
*/
|
|
38
|
+
const PWSH_DESTRUCTIVE_PATTERNS = [
|
|
39
|
+
// Recursive force delete via cmdlet
|
|
40
|
+
'remove-item -recurse -force',
|
|
41
|
+
'remove-item -force -recurse',
|
|
42
|
+
'ri -recurse -force',
|
|
43
|
+
'ri -force -recurse',
|
|
44
|
+
'rmdir -recurse -force',
|
|
45
|
+
'rmdir -force -recurse',
|
|
46
|
+
// Disk / volume operations
|
|
47
|
+
'format-volume',
|
|
48
|
+
'clear-disk',
|
|
49
|
+
'reset-physicaldisk',
|
|
50
|
+
// System state
|
|
51
|
+
'stop-computer',
|
|
52
|
+
'restart-computer',
|
|
53
|
+
'shutdown',
|
|
54
|
+
// Security weakening
|
|
55
|
+
'set-executionpolicy unrestricted',
|
|
56
|
+
'set-executionpolicy bypass',
|
|
57
|
+
// Service / process attack surface
|
|
58
|
+
'invoke-webrequest', // common phishing-script vector when piped to iex
|
|
59
|
+
'iex (new-object', // download-execute pattern
|
|
60
|
+
// Credential exfil
|
|
61
|
+
'get-credential | export-clixml',
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Normalize whitespace before pattern matching: collapse runs of
|
|
65
|
+
* whitespace к single space + lowercase. Defends against the
|
|
66
|
+
* `iex(New-Object`/`IEX (New-Object` style bypass where pattern
|
|
67
|
+
* `iex (new-object` would miss the no-space or double-space variant.
|
|
68
|
+
*/
|
|
69
|
+
function normalizeForMatch(text) {
|
|
70
|
+
return text.toLowerCase().replace(/\s+/g, ' ');
|
|
71
|
+
}
|
|
72
|
+
function findPwshDestructiveMatch(cmd) {
|
|
73
|
+
const normalized = normalizeForMatch(cmd);
|
|
74
|
+
for (const pattern of PWSH_DESTRUCTIVE_PATTERNS) {
|
|
75
|
+
if (normalized.includes(normalizeForMatch(pattern)))
|
|
76
|
+
return pattern;
|
|
77
|
+
}
|
|
78
|
+
// Fall back к the shared bash destructive list (covers cross-shell
|
|
79
|
+
// patterns like `rm -rf /`, `DROP DATABASE`). Shared patterns may
|
|
80
|
+
// contain uppercase (case-insensitive SQL verbs); normalize both
|
|
81
|
+
// sides before compare.
|
|
82
|
+
const shared = listDestructivePatterns();
|
|
83
|
+
for (const pattern of shared) {
|
|
84
|
+
if (normalized.includes(normalizeForMatch(pattern)))
|
|
85
|
+
return pattern;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* PowerShell-aware permission decision. Differs from
|
|
91
|
+
* `evaluateBashPermission` в two ways:
|
|
92
|
+
*
|
|
93
|
+
* 1. Default class is `allow` (after destructive check) instead of
|
|
94
|
+
* `unknown → deny`. The bash classifier rejects any first-token
|
|
95
|
+
* it does not recognise — appropriate for bash where every verb
|
|
96
|
+
* is a separate binary, hostile for pwsh where the Verb-Noun
|
|
97
|
+
* cmdlet convention means thousands of legitimate verbs exist
|
|
98
|
+
* (`Get-Process`, `$PSVersionTable`, `Select-Object`, ...).
|
|
99
|
+
*
|
|
100
|
+
* 2. Destructive patterns combine the shared bash denylist (covers
|
|
101
|
+
* cross-shell patterns like `rm -rf`) с pwsh-specific cmdlet
|
|
102
|
+
* forms (`Remove-Item -Recurse -Force`, `Format-Volume`, etc).
|
|
103
|
+
*
|
|
104
|
+
* Mode FSM mirrors bash: plan → deny ALL, ask → ask, auto/bypass → allow,
|
|
105
|
+
* destructive class → deny unless `bypassPermissions + human + ENV override`.
|
|
106
|
+
*/
|
|
107
|
+
function evaluatePwshPermission(cmd, mode, source) {
|
|
108
|
+
const destructive = findPwshDestructiveMatch(cmd);
|
|
109
|
+
if (destructive !== null) {
|
|
110
|
+
const overrideOk = mode === 'bypassPermissions' &&
|
|
111
|
+
source === 'human' &&
|
|
112
|
+
process.env['PUGI_DESTRUCTIVE_OVERRIDE'] === '1';
|
|
113
|
+
if (overrideOk) {
|
|
114
|
+
return {
|
|
115
|
+
decision: 'allow',
|
|
116
|
+
reason: `destructive pwsh pattern '${destructive}' allowed via override (bypassPermissions + human + PUGI_DESTRUCTIVE_OVERRIDE=1)`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
decision: 'deny',
|
|
121
|
+
reason: `destructive pwsh pattern '${destructive}' is always denied (override requires bypassPermissions + human + PUGI_DESTRUCTIVE_OVERRIDE=1)`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Non-destructive pwsh command — mode FSM.
|
|
125
|
+
switch (mode) {
|
|
126
|
+
case 'plan':
|
|
127
|
+
return { decision: 'deny', reason: 'plan mode denies all shell dispatches' };
|
|
128
|
+
case 'ask':
|
|
129
|
+
case 'acceptEdits':
|
|
130
|
+
return { decision: 'ask', reason: 'pwsh command requires operator confirmation' };
|
|
131
|
+
case 'auto':
|
|
132
|
+
case 'dontAsk':
|
|
133
|
+
case 'bypassPermissions':
|
|
134
|
+
return { decision: 'allow', reason: 'pwsh command allowed by mode' };
|
|
135
|
+
default:
|
|
136
|
+
return { decision: 'ask', reason: `unknown mode ${mode}; defaulting к ask` };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Cached binary path so repeated calls inside one session skip the probe. */
|
|
140
|
+
let cachedShellBinary;
|
|
141
|
+
function resolveShellBinary() {
|
|
142
|
+
if (cachedShellBinary !== undefined)
|
|
143
|
+
return cachedShellBinary;
|
|
144
|
+
// Try pwsh (cross-platform PowerShell 7+) first.
|
|
145
|
+
const pwshProbe = spawnSync('pwsh', ['-NoProfile', '-Command', 'exit 0'], {
|
|
146
|
+
encoding: 'utf8',
|
|
147
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
148
|
+
timeout: 3000,
|
|
149
|
+
});
|
|
150
|
+
if (pwshProbe.status === 0) {
|
|
151
|
+
cachedShellBinary = 'pwsh';
|
|
152
|
+
return 'pwsh';
|
|
153
|
+
}
|
|
154
|
+
// Windows fallback к the baked-in PowerShell 5.1.
|
|
155
|
+
if (process.platform === 'win32') {
|
|
156
|
+
const wpsProbe = spawnSync('powershell.exe', ['-NoProfile', '-Command', 'exit 0'], {
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
159
|
+
timeout: 3000,
|
|
160
|
+
});
|
|
161
|
+
if (wpsProbe.status === 0) {
|
|
162
|
+
cachedShellBinary = 'powershell.exe';
|
|
163
|
+
return 'powershell.exe';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
cachedShellBinary = null;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function sanitizeTimeout(value) {
|
|
170
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
171
|
+
return POWERSHELL_DEFAULT_TIMEOUT_MS;
|
|
172
|
+
}
|
|
173
|
+
return Math.min(value, POWERSHELL_MAX_TIMEOUT_MS);
|
|
174
|
+
}
|
|
175
|
+
function buildChildEnv() {
|
|
176
|
+
const env = { ...process.env };
|
|
177
|
+
delete env['PUGI_API_KEY'];
|
|
178
|
+
delete env['PUGI_LOGIN_TOKEN'];
|
|
179
|
+
return env;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Sync PowerShell dispatch. Mirrors bashToolSync shape so dispatchTool
|
|
183
|
+
* can call either tool with the same context shape.
|
|
184
|
+
*/
|
|
185
|
+
export function powerShellToolSync(input, ctx) {
|
|
186
|
+
const cmd = input.cmd ?? '';
|
|
187
|
+
const source = ctx.source ?? 'agent';
|
|
188
|
+
const toolCallId = recordToolCall(ctx.session, 'powershell', cmd);
|
|
189
|
+
// pwsh-aware permission gate (NOT the bash classifier). Bash classifier
|
|
190
|
+
// would reject `$PSVersionTable`, `Get-Process`, etc as "Unrecognized
|
|
191
|
+
// command" → default-deny, making the pwsh tool useless. The pwsh gate
|
|
192
|
+
// applies the shared destructive denylist (rm -rf / DROP TABLE) + a
|
|
193
|
+
// pwsh-specific list (Remove-Item -Recurse -Force / Format-Volume /
|
|
194
|
+
// Set-ExecutionPolicy Unrestricted / iex (New-Object ...)) and
|
|
195
|
+
// defaults non-destructive cmdlets к allow under mode FSM.
|
|
196
|
+
const decision = evaluatePwshPermission(cmd, ctx.settings.permissions.mode, source);
|
|
197
|
+
if (decision.decision !== 'allow') {
|
|
198
|
+
const reason = `Permission ${decision.decision}: ${decision.reason}`;
|
|
199
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
200
|
+
return {
|
|
201
|
+
stdout: '',
|
|
202
|
+
stderr: `Permission ${decision.decision}: ${decision.reason}`,
|
|
203
|
+
exitCode: 126,
|
|
204
|
+
truncated: false,
|
|
205
|
+
timedOut: false,
|
|
206
|
+
shellBinary: 'unresolved',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const shellBinary = resolveShellBinary();
|
|
210
|
+
if (shellBinary === null) {
|
|
211
|
+
const reason = 'powershell binary not found (tried pwsh' +
|
|
212
|
+
(process.platform === 'win32' ? ', powershell.exe' : '') +
|
|
213
|
+
'). Install PowerShell 7+ from https://aka.ms/powershell or use the bash tool instead.';
|
|
214
|
+
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
215
|
+
return {
|
|
216
|
+
stdout: '',
|
|
217
|
+
stderr: reason,
|
|
218
|
+
exitCode: 127,
|
|
219
|
+
truncated: false,
|
|
220
|
+
timedOut: false,
|
|
221
|
+
shellBinary: 'unavailable',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const timeoutMs = sanitizeTimeout(input.timeoutMs);
|
|
225
|
+
const childEnv = buildChildEnv();
|
|
226
|
+
const cwd = input.cwd ?? ctx.root;
|
|
227
|
+
const result = spawnSync(shellBinary, ['-NoProfile', '-Command', cmd], {
|
|
228
|
+
cwd,
|
|
229
|
+
env: childEnv,
|
|
230
|
+
encoding: 'utf8',
|
|
231
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
232
|
+
timeout: timeoutMs,
|
|
233
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
234
|
+
});
|
|
235
|
+
const stdoutFull = (result.stdout ?? '').toString();
|
|
236
|
+
const stderrFull = (result.stderr ?? '').toString();
|
|
237
|
+
const combined = stdoutFull.length + stderrFull.length;
|
|
238
|
+
const truncated = combined > POWERSHELL_OUTPUT_CAP_BYTES;
|
|
239
|
+
let stdoutOut = stdoutFull;
|
|
240
|
+
let stderrOut = stderrFull;
|
|
241
|
+
if (truncated) {
|
|
242
|
+
const halfCap = POWERSHELL_OUTPUT_CAP_BYTES / 2;
|
|
243
|
+
stdoutOut = stdoutFull.slice(0, halfCap);
|
|
244
|
+
stderrOut = stderrFull.slice(0, halfCap);
|
|
245
|
+
}
|
|
246
|
+
const timedOut = result.error?.code === 'ETIMEDOUT' ||
|
|
247
|
+
result.signal === 'SIGTERM';
|
|
248
|
+
const exitCode = timedOut ? 124 : result.status ?? 1;
|
|
249
|
+
if (timedOut) {
|
|
250
|
+
recordToolResult(ctx.session, toolCallId, 'error', `powershell timed out after ${timeoutMs}ms`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
recordToolResult(ctx.session, toolCallId, 'success', `powershell exit=${exitCode} bytes=${combined} binary=${shellBinary}`);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
stdout: stdoutOut,
|
|
257
|
+
stderr: stderrOut,
|
|
258
|
+
exitCode,
|
|
259
|
+
truncated,
|
|
260
|
+
timedOut,
|
|
261
|
+
shellBinary,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Visible-for-spec helper: forces a re-probe on next call. */
|
|
265
|
+
export function _resetShellBinaryCacheForSpec() {
|
|
266
|
+
cachedShellBinary = undefined;
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=powershell.js.map
|