@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.87
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 +96 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- 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 +2 -2
- 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 +12 -12
- 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 +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -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 +322 -0
- package/dist/core/engine/anvil-client.js +140 -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 +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -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 +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -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/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 +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -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 +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- 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 +286 -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 +457 -0
- 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-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4151 -489
- package/dist/runtime/commands/agents.js +30 -30
- 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 +32 -32
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- 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 +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- 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 +128 -0
- 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 +177 -0
- 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 +531 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -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 +556 -0
- package/dist/tools/ask-user-question.js +222 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +623 -45
- package/dist/tools/brief.js +224 -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 +189 -0
- 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 +85 -0
- 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-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/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- 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 +124 -44
- 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/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +23 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bash command classifier — Sprint
|
|
2
|
+
* Bash command classifier — Sprint .
|
|
3
3
|
*
|
|
4
4
|
* Splits a shell command into a 7-class taxonomy so the permission
|
|
5
5
|
* engine can apply class-aware policy instead of the prior bool gate
|
|
6
6
|
* (`destructiveBashPatterns ? deny : ask`).
|
|
7
7
|
*
|
|
8
8
|
* Design notes:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
9
|
+
* - The classifier is a conservative pattern matcher, not a full
|
|
10
|
+
* bash AST parser. M2 will replace it with a real parser (see
|
|
11
|
+
* bash-security.md §4). For M1 the rules are explicit-substring +
|
|
12
|
+
* simple tokenization, which is good enough to gate every command
|
|
13
|
+
* the engine loop currently emits.
|
|
14
|
+
* - Compound commands (`a && b`, `a || b`, `a ; b`, `a | b`) are
|
|
15
|
+
* split on the four separators and every component is classified
|
|
16
|
+
* individually. The overall class is the most dangerous component.
|
|
17
|
+
* - The `destructive` patterns originally lived in
|
|
18
|
+
* `permission.ts::destructiveBashPatterns`. They are now the
|
|
19
|
+
* single source of truth here; `permission.ts` re-exports the
|
|
20
|
+
* hard-deny check through `classifyBash`.
|
|
21
|
+
* - The `unknown` class fires on parse failure (`eval`, deep
|
|
22
|
+
* `$(...)` nesting, `curl | sh` install pipes) so the permission
|
|
23
|
+
* engine can fail closed in interactive modes.
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
26
26
|
* Class rank for worst-component reduction in compound commands.
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* still letting genuine `write_workspace`, `network`, `write_protected`
|
|
35
35
|
* and `destructive` components win when they appear.
|
|
36
36
|
*
|
|
37
|
-
* Code Reviewer P0 retro
|
|
37
|
+
* Code Reviewer P0 retro: previously `unknown: 0` meant
|
|
38
38
|
* `read` (rank 1) won over `unknown` (rank 0) in the worst-component
|
|
39
39
|
* reduction. That bypassed the file-level promise of fail-closed on
|
|
40
40
|
* parse failure.
|
|
@@ -119,6 +119,63 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
119
119
|
// History destruction
|
|
120
120
|
{ pattern: 'history -c' },
|
|
121
121
|
{ pattern: ' >/dev/null 2>&1; rm' },
|
|
122
|
+
// ---------------------------------------------------------------
|
|
123
|
+
// Patterns ported from external utility (Apache-2.0)
|
|
124
|
+
// and `safety-guard.sh` BLOCKED_PATTERNS array. Upstream source:
|
|
125
|
+
// external hooks/destructive-guard.sh (lines 7-13)
|
|
126
|
+
// external hooks/safety-guard.sh (lines 14-50)
|
|
127
|
+
//
|
|
128
|
+
//
|
|
129
|
+
// The patterns below need word-boundary matching because their
|
|
130
|
+
// tokens (kill, halt, reboot, ...) appear as substrings of common
|
|
131
|
+
// unrelated words (skills, default, chrooted-rebooter, etc.).
|
|
132
|
+
// Substring `.includes` cannot express that — `regex` is required.
|
|
133
|
+
// ---------------------------------------------------------------
|
|
134
|
+
// Process termination — `kill`, `pkill`, `killall` at command head
|
|
135
|
+
// or after `sudo`. Matches `kill 1234`, `kill -9 $$`, `sudo killall
|
|
136
|
+
// node`, but NOT `skill issue` (no leading boundary) or
|
|
137
|
+
// `git commit -m "skill kill story"` (the kill is inside a quoted
|
|
138
|
+
// string — quote-aware split handled upstream; here we still need
|
|
139
|
+
// the boundary). Anchored to start-of-component or `sudo ` prefix.
|
|
140
|
+
{
|
|
141
|
+
pattern: 'kill',
|
|
142
|
+
regex: /^(?:sudo\s+)?(?:pkill|killall|kill)\b/,
|
|
143
|
+
},
|
|
144
|
+
// System power state — reboot / shutdown / halt / poweroff / init 0
|
|
145
|
+
// / init 6. External tooling matches these anywhere in the command; we
|
|
146
|
+
// tighten to start-of-component or `sudo ` prefix to avoid FPs on
|
|
147
|
+
// file paths or variable names containing the substring.
|
|
148
|
+
{
|
|
149
|
+
pattern: 'reboot',
|
|
150
|
+
regex: /^(?:sudo\s+)?reboot\b/,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
pattern: 'shutdown',
|
|
154
|
+
regex: /^(?:sudo\s+)?shutdown\b/,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pattern: 'halt',
|
|
158
|
+
regex: /^(?:sudo\s+)?halt\b/,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
pattern: 'poweroff',
|
|
162
|
+
regex: /^(?:sudo\s+)?poweroff\b/,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: 'init 0',
|
|
166
|
+
regex: /^(?:sudo\s+)?init\s+0\b/,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
pattern: 'init 6',
|
|
170
|
+
regex: /^(?:sudo\s+)?init\s+6\b/,
|
|
171
|
+
},
|
|
172
|
+
// `git clean -f` (without -dx) — External tooling lists this as destructive
|
|
173
|
+
// because it still deletes untracked files. Pugi previously only
|
|
174
|
+
// gated `git clean -fdx`; broaden to any `-f` variant.
|
|
175
|
+
{
|
|
176
|
+
pattern: 'git clean -f',
|
|
177
|
+
regex: /\bgit\s+clean\s+-[A-Za-z]*f/,
|
|
178
|
+
},
|
|
122
179
|
];
|
|
123
180
|
/**
|
|
124
181
|
* Compound separators. We split on `&&`, `||`, `;`, `|` to classify
|
|
@@ -133,7 +190,7 @@ const COMPOUND_SEPARATORS = /\s*(?:&&|\|\||;|\|)\s*/;
|
|
|
133
190
|
* that script bodies passed to `awk`, `sed`, `perl`, `python -c` are
|
|
134
191
|
* not mis-split when they contain bare `;` or `|` glyphs.
|
|
135
192
|
*
|
|
136
|
-
* Code Reviewer P0 retro
|
|
193
|
+
* Code Reviewer P0 retro: a naive regex split on
|
|
137
194
|
* `awk 'BEGIN { for (i=0;i<5000;i++) ... }'` produces 3 components
|
|
138
195
|
* (the awk script header + two for-loop fragments) that get
|
|
139
196
|
* classified as `unknown` each and — with the unknown:3 rank above
|
|
@@ -289,6 +346,93 @@ const BUILD_TEST_PREFIXES = [
|
|
|
289
346
|
'tsc -p',
|
|
290
347
|
'eslint',
|
|
291
348
|
'prettier --check',
|
|
349
|
+
// P0 fix (#37 CRITICAL): customer-blocking gap surfaced
|
|
350
|
+
// during dogfood. Engine emitted `chmod +x build.sh`, `node script.js`,
|
|
351
|
+
// `python3 -m pytest`, `git status`, `pnpm build`, `docker ps`, etc.
|
|
352
|
+
// and the classifier returned `unknown` → permission matrix denied
|
|
353
|
+
// в bypassPermissions mode (which the customer expected to auto-allow
|
|
354
|
+
// basic dev tools). Customers could not run ANY real build/test/git
|
|
355
|
+
// workflow through Pugi.
|
|
356
|
+
//
|
|
357
|
+
// The prefixes below cover three classes of developer tooling that
|
|
358
|
+
// are always allowed in `auto`/`dontAsk`/`bypassPermissions` modes,
|
|
359
|
+
// `ask` in interactive modes, and `deny` in `plan` (read-only) mode:
|
|
360
|
+
// - Language runtimes: `node`, `python`, `python3`, `ruby`, etc.
|
|
361
|
+
// - Native build chains: `gcc`, `clang`, `cmake`, `rustc`, etc.
|
|
362
|
+
// - Container/k8s read-class: `docker ps/inspect/logs`, `kubectl get`.
|
|
363
|
+
//
|
|
364
|
+
// Destructive variants are already gated upstream by DESTRUCTIVE_PATTERNS
|
|
365
|
+
// (e.g. `docker system prune`, `kubectl delete --all`). The first-token
|
|
366
|
+
// gate in classifyComponent runs THIS list before the unknown fallback.
|
|
367
|
+
//
|
|
368
|
+
// Language runtime invocations (first-token match, with or without args).
|
|
369
|
+
'node',
|
|
370
|
+
'python',
|
|
371
|
+
'python3',
|
|
372
|
+
'ruby',
|
|
373
|
+
'perl',
|
|
374
|
+
'php',
|
|
375
|
+
'deno',
|
|
376
|
+
'bun',
|
|
377
|
+
'tsx',
|
|
378
|
+
'ts-node',
|
|
379
|
+
// Native build chains.
|
|
380
|
+
'gcc',
|
|
381
|
+
'g++',
|
|
382
|
+
'clang',
|
|
383
|
+
'clang++',
|
|
384
|
+
'cmake',
|
|
385
|
+
'rustc',
|
|
386
|
+
'javac',
|
|
387
|
+
'java',
|
|
388
|
+
// Container/k8s read-class (the destructive subcommands are pre-empted
|
|
389
|
+
// by DESTRUCTIVE_PATTERNS: `docker system prune`, `kubectl delete --all`,
|
|
390
|
+
// `kubectl delete namespace`).
|
|
391
|
+
'docker ps',
|
|
392
|
+
'docker images',
|
|
393
|
+
'docker inspect',
|
|
394
|
+
'docker logs',
|
|
395
|
+
'docker version',
|
|
396
|
+
'docker info',
|
|
397
|
+
'docker exec',
|
|
398
|
+
'docker run',
|
|
399
|
+
'docker stop',
|
|
400
|
+
'docker start',
|
|
401
|
+
'docker restart',
|
|
402
|
+
'docker rm',
|
|
403
|
+
'docker rmi',
|
|
404
|
+
'docker build',
|
|
405
|
+
'docker tag',
|
|
406
|
+
'docker compose',
|
|
407
|
+
'docker-compose',
|
|
408
|
+
'kubectl get',
|
|
409
|
+
'kubectl describe',
|
|
410
|
+
'kubectl logs',
|
|
411
|
+
'kubectl exec',
|
|
412
|
+
'kubectl apply',
|
|
413
|
+
'kubectl create',
|
|
414
|
+
'kubectl rollout',
|
|
415
|
+
'kubectl port-forward',
|
|
416
|
+
'kubectl config',
|
|
417
|
+
// Git read+write surface (network ops already handled by NETWORK_PREFIXES;
|
|
418
|
+
// destructive ops `reset --hard`/`clean -fdx`/`push --force` blocked above).
|
|
419
|
+
// Note: WRITE_WORKSPACE_PREFIXES already covers `git commit/add/checkout/...`.
|
|
420
|
+
// These entries handle plain `git rev-list`, `git cherry-pick`, `git worktree`,
|
|
421
|
+
// `git submodule`, etc that customer scripts commonly invoke.
|
|
422
|
+
'git rev-list',
|
|
423
|
+
'git cherry-pick',
|
|
424
|
+
'git worktree',
|
|
425
|
+
'git submodule',
|
|
426
|
+
'git blame',
|
|
427
|
+
'git describe',
|
|
428
|
+
'git tag --list',
|
|
429
|
+
'git tag -l',
|
|
430
|
+
'git for-each-ref',
|
|
431
|
+
'git ls-remote',
|
|
432
|
+
// gh CLI (GitHub). `gh repo delete` / `gh release delete` reach into
|
|
433
|
+
// network operations but are non-destructive for the local workspace.
|
|
434
|
+
// Permission matrix asks before allowing in auto.
|
|
435
|
+
'gh',
|
|
292
436
|
];
|
|
293
437
|
/** Single-token read-only commands. Argument-free entries match exact. */
|
|
294
438
|
const READ_TOKENS = new Set([
|
|
@@ -327,6 +471,16 @@ const READ_TOKENS = new Set([
|
|
|
327
471
|
'cut',
|
|
328
472
|
'sort',
|
|
329
473
|
'uniq',
|
|
474
|
+
// P0 fix (#37 CRITICAL): structured-data inspection tools
|
|
475
|
+
// are pure stdin/stdout transformers (no FS write, no network) when
|
|
476
|
+
// не paired с `>` redirection (the redirection branch above promotes
|
|
477
|
+
// к write_workspace independently). Common в dev scripts for parsing
|
|
478
|
+
// package.json, tsconfig.json, Helm values.yaml, etc.
|
|
479
|
+
// `tee` is INTENTIONALLY excluded — it writes by definition, even
|
|
480
|
+
// в protected paths (`tee /etc/...` is already in DESTRUCTIVE_PATTERNS).
|
|
481
|
+
'jq',
|
|
482
|
+
'yq',
|
|
483
|
+
'column',
|
|
330
484
|
]);
|
|
331
485
|
const READ_PREFIXES = [
|
|
332
486
|
'git status',
|
|
@@ -361,13 +515,23 @@ const WRITE_WORKSPACE_PREFIXES = [
|
|
|
361
515
|
'git tag',
|
|
362
516
|
'git rebase',
|
|
363
517
|
'git merge',
|
|
518
|
+
// P0 fix (#37 CRITICAL): file-permission ops are common
|
|
519
|
+
// в build scripts (`chmod +x build.sh`, `chown $USER file`). The
|
|
520
|
+
// destructive variants (`chmod 777 /`, `chmod -R 777 /`, `chmod -R
|
|
521
|
+
// 777 ~`, `chown -R root /`, `chown -R / ...`) are pre-empted by
|
|
522
|
+
// DESTRUCTIVE_PATTERNS which runs BEFORE this list — safe to add
|
|
523
|
+
// here for the non-destructive path. detectProtectedWrite's `\bchmod\b`
|
|
524
|
+
// / `\bchown\b` regex also catches writes into protected paths
|
|
525
|
+
// regardless of this list.
|
|
526
|
+
'chmod ',
|
|
527
|
+
'chown ',
|
|
364
528
|
];
|
|
365
529
|
/**
|
|
366
530
|
* Protected-write triggers. If a command writes to any of these paths
|
|
367
531
|
* the class is `write_protected` regardless of the operation type.
|
|
368
532
|
*
|
|
369
533
|
* Wildcards are handled as substring matches (e.g. `/.ssh/` matches
|
|
370
|
-
* `~/.ssh/foo` and
|
|
534
|
+
* `~/.ssh/foo` and `[HOME]/USER/.ssh/bar`).
|
|
371
535
|
*/
|
|
372
536
|
const PROTECTED_PATH_SUBSTRINGS = [
|
|
373
537
|
'/.ssh/',
|
|
@@ -388,6 +552,40 @@ const PROTECTED_PATH_SUBSTRINGS = [
|
|
|
388
552
|
'/usr/',
|
|
389
553
|
'/var/',
|
|
390
554
|
];
|
|
555
|
+
/**
|
|
556
|
+
* Protected basename triggers — files whose CONTENT must never leak
|
|
557
|
+
* through the bash surface, even when the literal path is workspace-
|
|
558
|
+
* local. Mirrors `permission.ts::protectedBasenames` and `.env.*`
|
|
559
|
+
* pattern so the read-tool gate (which fires on `read .env`) and the
|
|
560
|
+
* bash gate (which fires on `cat .env`) stay symmetric.
|
|
561
|
+
*
|
|
562
|
+
* P0 fix (Codex audit): before this list existed, the
|
|
563
|
+
* engine model could circumvent the `read` tool's `protectedTargetReason`
|
|
564
|
+
* check by emitting `bash cat .env` — the classifier saw `cat` (read
|
|
565
|
+
* token) + `.env` (not in PROTECTED_PATH_SUBSTRINGS) and returned class
|
|
566
|
+
* `read`, which the permission matrix allows under every mode. The
|
|
567
|
+
* `local-first-invariants` spec proved the leak: `pugi explain .env`
|
|
568
|
+
* surfaced `SECRET=should_never_leak` in the engine summary.
|
|
569
|
+
*
|
|
570
|
+
* Match shape: the substring must touch a `.` boundary (`/.env`,
|
|
571
|
+
* ` .env`, `.env\b`) or appear as the full token so a path like
|
|
572
|
+
* `apps/codeforge/file.env-template` (no real secret) does not
|
|
573
|
+
* over-trigger.
|
|
574
|
+
*/
|
|
575
|
+
const PROTECTED_BASENAME_PATTERNS = [
|
|
576
|
+
// `.env`, `.env.production`, `.env.local` — anywhere in the command.
|
|
577
|
+
// Boundary on the left is start/whitespace/quote/`/`, on the right
|
|
578
|
+
// start/whitespace/end/quote/`>`/`|`/`;`.
|
|
579
|
+
/(^|[\s'"\/=])\.env(\.[A-Za-z0-9_-]+)?($|[\s'"<>|;&])/,
|
|
580
|
+
// SSH key basenames (covers both `id_rsa` and `id_ed25519` even
|
|
581
|
+
// outside `~/.ssh/`). The `/.ssh/` substring above gates the
|
|
582
|
+
// directory case; this catches a key file copied to the workspace.
|
|
583
|
+
/(^|[\s'"\/])id_(rsa|ed25519|ecdsa|dsa)(\.pub)?($|[\s'"<>|;&])/,
|
|
584
|
+
// Other credential basenames mirrored from permission.ts.
|
|
585
|
+
/(^|[\s'"\/])\.npmrc($|[\s'"<>|;&])/,
|
|
586
|
+
/(^|[\s'"\/])\.pypirc($|[\s'"<>|;&])/,
|
|
587
|
+
/(^|[\s'"\/])\.gitconfig($|[\s'"<>|;&])/,
|
|
588
|
+
];
|
|
391
589
|
/**
|
|
392
590
|
* Obfuscation triggers — any of these forces the `unknown` class so
|
|
393
591
|
* the permission engine can fail closed.
|
|
@@ -400,16 +598,16 @@ const OBFUSCATION_TRIGGERS = [
|
|
|
400
598
|
* Classify a single (non-compound) command component.
|
|
401
599
|
*
|
|
402
600
|
* Order of checks (most-specific first):
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
601
|
+
* 1. destructive substring (hard deny path)
|
|
602
|
+
* 2. obfuscation (curl|sh, deep $() nesting, raw eval)
|
|
603
|
+
* 3. cd-escape (covered by classifyBash for the overall command;
|
|
604
|
+
* single-component cd is handled here too)
|
|
605
|
+
* 4. protected-write (redirection or write op into a protected path)
|
|
606
|
+
* 5. write_workspace (mkdir/touch/cp/mv/git-write etc)
|
|
607
|
+
* 6. network (curl/wget/ssh/installers)
|
|
608
|
+
* 7. build_test (pnpm test, cargo build, ...)
|
|
609
|
+
* 8. read (pwd, ls, cat, ...)
|
|
610
|
+
* 9. unknown (default)
|
|
413
611
|
*/
|
|
414
612
|
function classifyComponent(cmd, ctx) {
|
|
415
613
|
const trimmed = cmd.trim();
|
|
@@ -459,7 +657,7 @@ function classifyComponent(cmd, ctx) {
|
|
|
459
657
|
// `cat ~/.ssh/id_ed25519` still win when matched (they run before
|
|
460
658
|
// this check).
|
|
461
659
|
//
|
|
462
|
-
// Code Reviewer P0 retro
|
|
660
|
+
// Code Reviewer P0 retro: previously these reads fell
|
|
463
661
|
// through to READ_TOKENS and were allowed in every mode.
|
|
464
662
|
const protectedRead = detectProtectedRead(trimmed);
|
|
465
663
|
if (protectedRead) {
|
|
@@ -469,6 +667,26 @@ function classifyComponent(cmd, ctx) {
|
|
|
469
667
|
matched: protectedRead.matched,
|
|
470
668
|
};
|
|
471
669
|
}
|
|
670
|
+
// 4a-bis. Parent-traversal in read arguments. The file-tools layer
|
|
671
|
+
// refuses `..` segments via `resolveWorkspacePath`, but the bash
|
|
672
|
+
// surface had no equivalent gate — the engine could emit
|
|
673
|
+
// `cat ../README.md` or `ls ..` to enumerate / read outside the
|
|
674
|
+
// workspace, sidestepping the path-security check that the `read`
|
|
675
|
+
// and `glob` tools enforce.
|
|
676
|
+
//
|
|
677
|
+
// P0 fix (Codex audit): treat `..` as a path segment
|
|
678
|
+
// (`../`, ` ..`, `..\n`) in any read-class command as a workspace
|
|
679
|
+
// escape. We classify it as `write_protected` so the auto/dontAsk
|
|
680
|
+
// modes refuse, mirroring the `Path escapes workspace` semantics
|
|
681
|
+
// the file-tools layer already provides.
|
|
682
|
+
const traversal = detectParentTraversalRead(trimmed);
|
|
683
|
+
if (traversal) {
|
|
684
|
+
return {
|
|
685
|
+
class: 'write_protected',
|
|
686
|
+
reason: traversal.reason,
|
|
687
|
+
matched: traversal.matched,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
472
690
|
// 4b. .env writes are always protected, even inside the workspace
|
|
473
691
|
// (CEO directive feedback_never_delete_untracked_env.md).
|
|
474
692
|
const envWrite = detectEnvWrite(trimmed);
|
|
@@ -525,6 +743,25 @@ function classifyComponent(cmd, ctx) {
|
|
|
525
743
|
if (trimmed === 'make' || trimmed.startsWith('make ')) {
|
|
526
744
|
return { class: 'build_test', reason: 'make runner', matched: 'make' };
|
|
527
745
|
}
|
|
746
|
+
// 7c. Operator-override safe tokens (P0 fix #37).
|
|
747
|
+
// `PUGI_CLASSIFIER_EXTRA_SAFE=tool1,tool2,...` extends the BUILD_TEST
|
|
748
|
+
// first-token list at runtime. This is a security-sensitive escape
|
|
749
|
+
// hatch — operators can add their custom build tools without a
|
|
750
|
+
// recompile. Destructive patterns ALREADY ran above (step 1) so this
|
|
751
|
+
// cannot whitelist `rm`, `mkfs`, `git push --force`, etc. The match
|
|
752
|
+
// is strict first-token equality — not substring — and the env var
|
|
753
|
+
// is read fresh on every classify call so tests can mutate it.
|
|
754
|
+
const extraSafe = readExtraSafeTokens();
|
|
755
|
+
if (extraSafe.size > 0) {
|
|
756
|
+
const firstTokenForExtraSafe = trimmed.split(/\s+/)[0] ?? '';
|
|
757
|
+
if (extraSafe.has(firstTokenForExtraSafe)) {
|
|
758
|
+
return {
|
|
759
|
+
class: 'build_test',
|
|
760
|
+
reason: `PUGI_CLASSIFIER_EXTRA_SAFE override: ${firstTokenForExtraSafe}`,
|
|
761
|
+
matched: firstTokenForExtraSafe,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
528
765
|
// 7c. Bare `cd <path>` (inside workspace — the cwd-escape detector
|
|
529
766
|
// upgrades the class to write_protected when the target is
|
|
530
767
|
// outside). Standalone `cd` (HOME) is escape, also handled by the
|
|
@@ -568,7 +805,15 @@ function classifyComponent(cmd, ctx) {
|
|
|
568
805
|
}
|
|
569
806
|
function findDestructiveMatch(cmd) {
|
|
570
807
|
const upper = cmd.toUpperCase();
|
|
571
|
-
for (const { pattern, caseInsensitive } of DESTRUCTIVE_PATTERNS) {
|
|
808
|
+
for (const { pattern, caseInsensitive, regex } of DESTRUCTIVE_PATTERNS) {
|
|
809
|
+
if (regex) {
|
|
810
|
+
// Word-boundary regex form (external-derived patterns). Match
|
|
811
|
+
// against the trimmed component so `^` anchors to command head,
|
|
812
|
+
// not surrounding whitespace from the compound split.
|
|
813
|
+
if (regex.test(cmd.trim()))
|
|
814
|
+
return pattern;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
572
817
|
if (caseInsensitive) {
|
|
573
818
|
if (upper.includes(pattern))
|
|
574
819
|
return pattern;
|
|
@@ -637,14 +882,57 @@ function nestingDepth(cmd, open, close) {
|
|
|
637
882
|
function escapeRegex(s) {
|
|
638
883
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
639
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Operator-override safe tokens. Read from `PUGI_CLASSIFIER_EXTRA_SAFE`
|
|
887
|
+
* (comma-separated). Allows operators to extend the BUILD_TEST first-
|
|
888
|
+
* token list at runtime for site-specific tooling без recompile.
|
|
889
|
+
*
|
|
890
|
+
* Security note: destructive substring patterns run BEFORE this gate
|
|
891
|
+
* (step 1 in classifyComponent), so this cannot whitelist `rm`, `mkfs`,
|
|
892
|
+
* `git push --force`, etc. The env var only adds tools to the benign
|
|
893
|
+
* build_test class. Invalid entries (empty strings, тokens containing
|
|
894
|
+
* shell metas) are silently dropped to avoid surprising classifications.
|
|
895
|
+
*
|
|
896
|
+
* Read fresh on every call so per-test mutations work и so operators
|
|
897
|
+
* can update without restarting the agent loop. The cost (one env var
|
|
898
|
+
* read + Set construction per call) is negligible for the classifier's
|
|
899
|
+
* call frequency.
|
|
900
|
+
*/
|
|
901
|
+
function readExtraSafeTokens() {
|
|
902
|
+
const raw = process.env.PUGI_CLASSIFIER_EXTRA_SAFE;
|
|
903
|
+
if (!raw || raw.trim() === '')
|
|
904
|
+
return new Set();
|
|
905
|
+
const tokens = new Set();
|
|
906
|
+
for (const candidate of raw.split(',')) {
|
|
907
|
+
const trimmed = candidate.trim();
|
|
908
|
+
if (trimmed === '')
|
|
909
|
+
continue;
|
|
910
|
+
// Reject anything containing shell metas or whitespace — only bare
|
|
911
|
+
// tool names allowed. Defends against accidental
|
|
912
|
+
// `PUGI_CLASSIFIER_EXTRA_SAFE='rm -rf /'` smuggling.
|
|
913
|
+
if (/[\s;|&<>$`(){}\[\]'"\\]/.test(trimmed))
|
|
914
|
+
continue;
|
|
915
|
+
tokens.add(trimmed);
|
|
916
|
+
}
|
|
917
|
+
return tokens;
|
|
918
|
+
}
|
|
640
919
|
function detectProtectedWrite(cmd, ctx) {
|
|
641
920
|
// Surface every write target this command produces so we can both
|
|
642
921
|
// protected-path-check and outside-workspace-check them uniformly.
|
|
643
922
|
// Captures `sort -o`, `uniq <in> <out>`, `sed -i` files, `awk '... > "file"'`,
|
|
644
923
|
// and `>` / `>>` redirections without surrounding whitespace.
|
|
645
924
|
const writeTargets = extractWriteTargets(cmd);
|
|
925
|
+
// Strip heredoc bodies before substring scan. Heredoc payloads are
|
|
926
|
+
// DATA (file contents the script writes), not commands the shell
|
|
927
|
+
// executes — a `package.json` body containing `/usr/local/bin/...`
|
|
928
|
+
// would FP as "Write into protected path: /usr/" under the broad
|
|
929
|
+
// includes() scan below. The per-target check at the bottom of this
|
|
930
|
+
// function still catches real `cat > /usr/file << EOF` attempts
|
|
931
|
+
// because extractWriteTargets reads the redirection target, not the
|
|
932
|
+
// heredoc body. CEO dogfood (#28 follow-up).
|
|
933
|
+
const cmdForScan = stripHeredocBodies(cmd);
|
|
646
934
|
for (const needle of PROTECTED_PATH_SUBSTRINGS) {
|
|
647
|
-
if (!
|
|
935
|
+
if (!cmdForScan.includes(needle))
|
|
648
936
|
continue;
|
|
649
937
|
// Reading from a protected path is allowed at the classifier
|
|
650
938
|
// layer (the permission engine still gates `read`); writing is
|
|
@@ -695,17 +983,67 @@ function detectProtectedWrite(cmd, ctx) {
|
|
|
695
983
|
}
|
|
696
984
|
/**
|
|
697
985
|
* Extract every write-target path the command produces. Covers:
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
986
|
+
* - shell redirection `> file`, `>> file` (with optional whitespace,
|
|
987
|
+
* skipping `>&1`, `>&2`, etc.)
|
|
988
|
+
* - `sort -o file`
|
|
989
|
+
* - `uniq <input> <output>` (the two-arg form)
|
|
990
|
+
* - `sed -i <file>...` (in-place edit treats every trailing file as a
|
|
991
|
+
* write target)
|
|
992
|
+
* - `awk '... > "file"'` (quoted redirection inside an awk script)
|
|
705
993
|
*
|
|
706
994
|
* Conservative — we do not try to resolve shell vars or globs; the
|
|
707
995
|
* caller still gates absolute paths via `looksAbsoluteOutsideWorkspace`.
|
|
708
996
|
*/
|
|
997
|
+
/**
|
|
998
|
+
* Strip heredoc bodies so substring scans (e.g. `cmd.includes('/usr/')`)
|
|
999
|
+
* do not false-positive on file content the script is *writing*. A
|
|
1000
|
+
* heredoc starts с `<< 'WORD'` (or `<< WORD` / `<<-WORD`) and ends на a
|
|
1001
|
+
* line containing only WORD. The body between is DATA, not commands.
|
|
1002
|
+
*
|
|
1003
|
+
* Best-effort: handles single-heredoc-per-command (the common case)
|
|
1004
|
+
* AND multiple sequential heredocs. Nested heredocs (heredoc-inside-
|
|
1005
|
+
* heredoc) are rare and out of scope — the substring scan still gates
|
|
1006
|
+
* the outer command, just без stripping the nested body. Per-target
|
|
1007
|
+
* detection at detectProtectedWrite's tail loop catches real
|
|
1008
|
+
* `cat > /usr/file << EOF` attacks regardless of body content.
|
|
1009
|
+
*
|
|
1010
|
+
* CEO dogfood : `cat > package.json << 'EOF'\n{"bin":
|
|
1011
|
+
* "/usr/local/bin/foo"}\nEOF` was rejected as "Write into protected
|
|
1012
|
+
* path: /usr/" because the broad substring scan saw `/usr/` in the
|
|
1013
|
+
* JSON body. With heredoc-body stripping, the scan now sees only
|
|
1014
|
+
* `cat > package.json << 'EOF' EOF` which contains no protected path.
|
|
1015
|
+
*/
|
|
1016
|
+
function stripHeredocBodies(cmd) {
|
|
1017
|
+
// Match `<< [-]'WORD'` or `<< [-]"WORD"` or `<< [-]WORD` (quoted form
|
|
1018
|
+
// disables variable expansion in real bash; we treat all three the
|
|
1019
|
+
// same for stripping). Capture the WORD so we can find the close
|
|
1020
|
+
// marker.
|
|
1021
|
+
const heredocStart = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
|
|
1022
|
+
let out = cmd;
|
|
1023
|
+
let safetyLoops = 0;
|
|
1024
|
+
let match;
|
|
1025
|
+
while ((match = heredocStart.exec(out)) !== null) {
|
|
1026
|
+
if (++safetyLoops > 16)
|
|
1027
|
+
break;
|
|
1028
|
+
const word = match[2];
|
|
1029
|
+
if (!word)
|
|
1030
|
+
continue;
|
|
1031
|
+
const headEnd = match.index + match[0].length;
|
|
1032
|
+
// Find the close-marker line: `\n<optional indent>WORD<\n|$>`.
|
|
1033
|
+
const closeRegex = new RegExp(`\\n\\s*${word}(?:\\n|$)`);
|
|
1034
|
+
closeRegex.lastIndex = headEnd;
|
|
1035
|
+
const closeMatch = closeRegex.exec(out.slice(headEnd));
|
|
1036
|
+
if (!closeMatch)
|
|
1037
|
+
break;
|
|
1038
|
+
const closeStart = headEnd + closeMatch.index;
|
|
1039
|
+
const closeEnd = closeStart + closeMatch[0].length;
|
|
1040
|
+
// Replace heredoc body + close marker с single space so the regex
|
|
1041
|
+
// iterator's lastIndex stays meaningful.
|
|
1042
|
+
out = out.slice(0, headEnd) + ' ' + out.slice(closeEnd);
|
|
1043
|
+
heredocStart.lastIndex = headEnd + 1;
|
|
1044
|
+
}
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
709
1047
|
function extractWriteTargets(cmd) {
|
|
710
1048
|
const targets = [];
|
|
711
1049
|
// Shell redirection (`>`, `>>`) with optional whitespace. Skip
|
|
@@ -777,14 +1115,72 @@ function detectProtectedRead(cmd) {
|
|
|
777
1115
|
firstToken === 'find';
|
|
778
1116
|
if (!isReadTool)
|
|
779
1117
|
return null;
|
|
1118
|
+
// Strip heredoc bodies so `cat > config << 'EOF'\n... /etc/... \nEOF`
|
|
1119
|
+
// does not FP as "Read from protected path" when first-token=`cat` +
|
|
1120
|
+
// redirection writes к workspace-local file. Heredoc payload is data.
|
|
1121
|
+
// CEO dogfood .
|
|
1122
|
+
const cmdForScan = stripHeredocBodies(cmd);
|
|
780
1123
|
for (const needle of PROTECTED_PATH_SUBSTRINGS) {
|
|
781
|
-
if (
|
|
1124
|
+
if (cmdForScan.includes(needle)) {
|
|
782
1125
|
return {
|
|
783
1126
|
reason: `Read from protected path: ${needle}`,
|
|
784
1127
|
matched: needle,
|
|
785
1128
|
};
|
|
786
1129
|
}
|
|
787
1130
|
}
|
|
1131
|
+
// P0 fix: extend protected-read detection to credential
|
|
1132
|
+
// basenames (`cat .env`, `head id_rsa`, `grep TOKEN .env.production`).
|
|
1133
|
+
// Without this branch, the engine model can bypass the `read` tool's
|
|
1134
|
+
// `protectedTargetReason` gate by emitting a bash `cat` — the read
|
|
1135
|
+
// tool refuses, the model falls back to bash, and the classifier
|
|
1136
|
+
// (which only knew about full-path substrings) classified `cat .env`
|
|
1137
|
+
// as benign `read`. The `local-first-invariants` spec proved the leak.
|
|
1138
|
+
for (const pattern of PROTECTED_BASENAME_PATTERNS) {
|
|
1139
|
+
const match = cmd.match(pattern);
|
|
1140
|
+
if (match) {
|
|
1141
|
+
return {
|
|
1142
|
+
reason: `Read from protected basename: ${match[0].trim()}`,
|
|
1143
|
+
matched: match[0].trim(),
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Detect parent-traversal segments (`..`) inside read-class commands.
|
|
1151
|
+
* The file-tools layer (`resolveWorkspacePath`) refuses these for the
|
|
1152
|
+
* `read`/`glob`/`grep` tools, but bash had no equivalent gate. We
|
|
1153
|
+
* trigger on the SAME shape `path-security.ts` rejects: a `..` segment
|
|
1154
|
+
* separated by `/` or whitespace. Quoted/escaped variants get the same
|
|
1155
|
+
* treatment.
|
|
1156
|
+
*
|
|
1157
|
+
* Returns null on the safe path (no `..` segment) so the caller falls
|
|
1158
|
+
* through to the regular read classification.
|
|
1159
|
+
*/
|
|
1160
|
+
function detectParentTraversalRead(cmd) {
|
|
1161
|
+
const firstToken = cmd.split(/\s+/)[0] ?? '';
|
|
1162
|
+
const isReadTool = READ_TOKENS.has(firstToken) ||
|
|
1163
|
+
READ_PREFIX_TOKENS.has(firstToken) ||
|
|
1164
|
+
firstToken === 'sed' ||
|
|
1165
|
+
firstToken === 'awk' ||
|
|
1166
|
+
firstToken === 'find';
|
|
1167
|
+
if (!isReadTool)
|
|
1168
|
+
return null;
|
|
1169
|
+
// Match `..` as a path segment: preceded by start/whitespace/quote/`/`
|
|
1170
|
+
// and followed by `/`, end-of-string, whitespace, or shell metas.
|
|
1171
|
+
// Avoids over-matching `v1..v2` (range syntax inside a single token)
|
|
1172
|
+
// and `1..5` (numeric ranges) because those lack the path boundary.
|
|
1173
|
+
const traversalPattern = /(^|[\s'"\/])\.\.(\/|$|[\s'"<>|;&])/;
|
|
1174
|
+
const m = cmd.match(traversalPattern);
|
|
1175
|
+
if (m) {
|
|
1176
|
+
return {
|
|
1177
|
+
reason: 'Read command escapes workspace via parent traversal',
|
|
1178
|
+
matched: '..',
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
// Absolute path read of /etc, /usr, /var, etc is already covered by
|
|
1182
|
+
// PROTECTED_PATH_SUBSTRINGS in detectProtectedRead — no extra branch
|
|
1183
|
+
// needed here.
|
|
788
1184
|
return null;
|
|
789
1185
|
}
|
|
790
1186
|
function detectEnvWrite(cmd) {
|