@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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* — Output-style state persistence.
|
|
3
|
+
*
|
|
4
|
+
* Two-tier storage:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Workspace** — `<workspaceRoot>/.pugi/config.json`. Set by
|
|
7
|
+
* `/style <name>` from inside the REPL or `pugi style <name>`
|
|
8
|
+
* without `--persist`. Overrides the user default for the
|
|
9
|
+
* current workspace only. Survives sessions because the same
|
|
10
|
+
* `.pugi/` survives sessions.
|
|
11
|
+
*
|
|
12
|
+
* 2. **User default** — `~/.pugi/config.json` (PUGI_HOME-aware).
|
|
13
|
+
* Set by `pugi style <name> --persist` or
|
|
14
|
+
* `/style <name> --persist`. Applies to every workspace that
|
|
15
|
+
* has no workspace-level override.
|
|
16
|
+
*
|
|
17
|
+
* Precedence (highest → lowest):
|
|
18
|
+
*
|
|
19
|
+
* workspace value > user value > DEFAULT_OUTPUT_STYLE ('default')
|
|
20
|
+
*
|
|
21
|
+
* Both files live under the same `pugi-config-v1` JSON envelope as
|
|
22
|
+
* other settings (permissionMode, privacy, model, preferredEndpoint).
|
|
23
|
+
* The schema is intentionally NOT shared with `runtime/commands/config.ts`'s
|
|
24
|
+
* strict Zod schema — `outputStyle` is read/written ONLY through this
|
|
25
|
+
* module so `pugi config set outputStyle=…` is NOT a supported path
|
|
26
|
+
* (it would silently bypass the slug validator). Operators get a
|
|
27
|
+
* single surface: `/style` + `pugi style`.
|
|
28
|
+
*
|
|
29
|
+
* File layout (one config.json, multiple keys; this module owns the
|
|
30
|
+
* `outputStyle` key only):
|
|
31
|
+
*
|
|
32
|
+
* {
|
|
33
|
+
* "permissionMode": "ask",
|
|
34
|
+
* "outputStyle": "terse",
|
|
35
|
+
* ...
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* The reader tolerates:
|
|
39
|
+
* - missing file (returns the default slug),
|
|
40
|
+
* - empty file (returns the default slug),
|
|
41
|
+
* - malformed JSON (returns the default slug — DO NOT crash REPL
|
|
42
|
+
* boot because of a hand-edited config),
|
|
43
|
+
* - unknown slug (returns the default slug + emits no error; the
|
|
44
|
+
* operator can `/style` to see the table and re-set).
|
|
45
|
+
*
|
|
46
|
+
* The writer is a read-modify-write to preserve neighbouring keys
|
|
47
|
+
* (permissionMode etc.) — overwriting the whole file would clobber
|
|
48
|
+
* the other tier's settings.
|
|
49
|
+
*
|
|
50
|
+
* Test surface: `test/commands/output-style-state.spec.ts` exercises
|
|
51
|
+
* precedence, malformed-config tolerance, persistence across reads,
|
|
52
|
+
* the `--persist` (user-default) path, and reset semantics.
|
|
53
|
+
*/
|
|
54
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
55
|
+
import { homedir } from 'node:os';
|
|
56
|
+
import { dirname, resolve } from 'node:path';
|
|
57
|
+
import { DEFAULT_OUTPUT_STYLE, isOutputStyleSlug, } from './presets.js';
|
|
58
|
+
/**
|
|
59
|
+
* Env override for `~/.pugi` so the spec can sandbox both tiers
|
|
60
|
+
* without touching the developer's real config. Matches the existing
|
|
61
|
+
* `runtime/commands/config.ts` convention.
|
|
62
|
+
*/
|
|
63
|
+
export const PUGI_HOME_ENV = 'PUGI_HOME';
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the active output style for the workspace, applying the
|
|
66
|
+
* precedence ladder (workspace > user > default).
|
|
67
|
+
*
|
|
68
|
+
* Pure read. Never writes, never throws — every IO failure degrades
|
|
69
|
+
* to the default slug. The function returns the source label too so
|
|
70
|
+
* the CLI surface can show the operator where the value came from.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveOutputStyle(io) {
|
|
73
|
+
const workspaceSlug = readSlugFromFile(workspaceConfigPath(io.workspaceRoot));
|
|
74
|
+
if (workspaceSlug)
|
|
75
|
+
return { slug: workspaceSlug, source: 'workspace' };
|
|
76
|
+
const userSlug = readSlugFromFile(userConfigPath(io.env ?? process.env));
|
|
77
|
+
if (userSlug)
|
|
78
|
+
return { slug: userSlug, source: 'user' };
|
|
79
|
+
return { slug: DEFAULT_OUTPUT_STYLE, source: 'default' };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Write `slug` to the workspace tier. Creates `<workspaceRoot>/.pugi/`
|
|
83
|
+
* if missing. Preserves neighbouring config keys via read-modify-write.
|
|
84
|
+
*/
|
|
85
|
+
export function setWorkspaceOutputStyle(slug, io) {
|
|
86
|
+
writeSlugToFile(workspaceConfigPath(io.workspaceRoot), slug);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Write `slug` to the user tier (`~/.pugi/config.json`).
|
|
90
|
+
*
|
|
91
|
+
* Mirrors the workspace writer's read-modify-write so the user's
|
|
92
|
+
* `permissionMode` / `privacy` / `model` keys survive a style flip.
|
|
93
|
+
*/
|
|
94
|
+
export function setUserOutputStyle(slug, io) {
|
|
95
|
+
writeSlugToFile(userConfigPath(io.env ?? process.env), slug);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Clear the workspace tier's `outputStyle` key. The user tier (and
|
|
99
|
+
* therefore the eventual resolved style) is left untouched.
|
|
100
|
+
*
|
|
101
|
+
* Used by `/style --reset` so the operator can revert a workspace
|
|
102
|
+
* override without nuking the rest of their workspace config.
|
|
103
|
+
*/
|
|
104
|
+
export function clearWorkspaceOutputStyle(io) {
|
|
105
|
+
clearSlugInFile(workspaceConfigPath(io.workspaceRoot));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Clear the user tier's `outputStyle` key. Lower-blast-radius reset
|
|
109
|
+
* for operators who want every workspace to fall back to `default`
|
|
110
|
+
* unless an explicit workspace value is set.
|
|
111
|
+
*/
|
|
112
|
+
export function clearUserOutputStyle(io) {
|
|
113
|
+
clearSlugInFile(userConfigPath(io.env ?? process.env));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Workspace config path. Exported for the spec; production callers
|
|
117
|
+
* should use the `setWorkspace…` / `resolveOutputStyle` helpers.
|
|
118
|
+
*/
|
|
119
|
+
export function workspaceConfigPath(workspaceRoot) {
|
|
120
|
+
return resolve(workspaceRoot, '.pugi', 'config.json');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* User config path resolved against `PUGI_HOME` (or `~/.pugi`).
|
|
124
|
+
* Exported for the spec.
|
|
125
|
+
*/
|
|
126
|
+
export function userConfigPath(env = process.env) {
|
|
127
|
+
const home = env[PUGI_HOME_ENV] ?? resolve(homedir(), '.pugi');
|
|
128
|
+
return resolve(home, 'config.json');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read + parse a config file. Returns an empty object on any IO or
|
|
132
|
+
* parse error. Caller-provided JSON must be a plain object; arrays /
|
|
133
|
+
* scalars / null are treated as "no config" so a hand-edited file
|
|
134
|
+
* never crashes the REPL.
|
|
135
|
+
*/
|
|
136
|
+
function readConfigFile(path) {
|
|
137
|
+
if (!existsSync(path))
|
|
138
|
+
return {};
|
|
139
|
+
let raw;
|
|
140
|
+
try {
|
|
141
|
+
raw = readFileSync(path, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
if (raw.trim().length === 0)
|
|
147
|
+
return {};
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(raw);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
156
|
+
return {};
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
function writeConfigFile(path, config) {
|
|
160
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
161
|
+
// 0o600 mirrors `runtime/commands/config.ts` — the config file may
|
|
162
|
+
// hold `preferredEndpoint` URLs that should not be world-readable.
|
|
163
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
164
|
+
encoding: 'utf8',
|
|
165
|
+
mode: 0o600,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function readSlugFromFile(path) {
|
|
169
|
+
const config = readConfigFile(path);
|
|
170
|
+
const candidate = config.outputStyle;
|
|
171
|
+
return isOutputStyleSlug(candidate) ? candidate : null;
|
|
172
|
+
}
|
|
173
|
+
function writeSlugToFile(path, slug) {
|
|
174
|
+
const config = readConfigFile(path);
|
|
175
|
+
config.outputStyle = slug;
|
|
176
|
+
writeConfigFile(path, config);
|
|
177
|
+
}
|
|
178
|
+
function clearSlugInFile(path) {
|
|
179
|
+
const config = readConfigFile(path);
|
|
180
|
+
if (!('outputStyle' in config))
|
|
181
|
+
return;
|
|
182
|
+
delete config.outputStyle;
|
|
183
|
+
writeConfigFile(path, config);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -1,12 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Path-security gate for Pugi CLI file operations.
|
|
3
|
+
*
|
|
4
|
+
* The original `resolveWorkspacePath` (preserved below) is the read-path
|
|
5
|
+
* gate: it stops relative-path traversal, URL-encoded traversal, and
|
|
6
|
+
* symlink escapes at the leaf. It is kept as the existing call surface
|
|
7
|
+
* for `read` and other non-mutating tools.
|
|
8
|
+
*
|
|
9
|
+
* `assertSafeWritePath` is a stricter, write-only gate ported from
|
|
10
|
+
* external's Rust `path_guard.rs` (Apache-2.0). It adds:
|
|
11
|
+
*
|
|
12
|
+
* 1. `..` segment rejection BEFORE any filesystem touch.
|
|
13
|
+
* 2. Walk-up canonicalize: finds the deepest existing ancestor,
|
|
14
|
+
* canonicalizes it (resolving every symlink in the existing
|
|
15
|
+
* prefix), then reattaches the non-existent tail. This closes
|
|
16
|
+
* the "parent's parent is a symlink" bypass that catches naive
|
|
17
|
+
* canonicalize-the-parent implementations.
|
|
18
|
+
* 3. Leaf-symlink reject: refuse to write through a symlink even
|
|
19
|
+
* when the file already exists. Covers dangling symlinks too.
|
|
20
|
+
* 4. Fail-CLOSED on empty allowed roots: an empty PUGI_ALLOWED_ROOTS
|
|
21
|
+
* means "no writes anywhere" rather than "writes allowed
|
|
22
|
+
* everywhere".
|
|
23
|
+
* 5. Containment check against canonicalized allowed roots.
|
|
24
|
+
* 6. Denylist of system, credential, and shell-init paths even
|
|
25
|
+
* when they sit inside an allowed root (defense in depth — a
|
|
26
|
+
* misconfigured root pointing at `$HOME` still cannot clobber
|
|
27
|
+
* `~/.ssh/id_ed25519`).
|
|
28
|
+
*
|
|
29
|
+
* ---
|
|
30
|
+
* Portions of `assertSafeWritePath` and `canonicalizeWithWalkUp` are
|
|
31
|
+
* derived from external `kei-mcp/src/handlers/safe_tools/
|
|
32
|
+
* path_guard.rs` (Apache-2.0). See `licenses/EXTERNAL-LICENSE-NOTICE.md`
|
|
33
|
+
* at the repo root for attribution and the full upstream license
|
|
34
|
+
* reference.
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, lstatSync, realpathSync, } from 'node:fs';
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep, } from 'node:path';
|
|
3
39
|
/**
|
|
4
40
|
* Resolve and validate that an inputPath stays within the workspace.
|
|
5
41
|
*
|
|
6
42
|
* Defends against:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
43
|
+
* 1. relative-path traversal (`../etc/passwd`)
|
|
44
|
+
* 2. URL-encoded traversal (`..%2Fetc%2Fpasswd`)
|
|
45
|
+
* 3. symlink escapes at the target itself (`alias-link -> /etc/passwd`)
|
|
10
46
|
*
|
|
11
47
|
* The previous implementation also resolved the parent's realpath and
|
|
12
48
|
* compared to the workspace root. That broke `pugi explain .` on macOS
|
|
@@ -60,4 +96,250 @@ function isInsideWorkspace(child, workspaceRoot) {
|
|
|
60
96
|
const rel = relative(workspaceRoot, child);
|
|
61
97
|
return Boolean(rel) && !rel.startsWith('..') && rel !== '..';
|
|
62
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Apply the strict write-path guard. Returns the canonical
|
|
101
|
+
* absolute path on success, throws on rejection.
|
|
102
|
+
*
|
|
103
|
+
* Use this for any operation that mutates the filesystem (`write`,
|
|
104
|
+
* `edit`, atomic-rename targets). The error message is safe to surface
|
|
105
|
+
* to the operator — it never echoes filesystem secrets.
|
|
106
|
+
*/
|
|
107
|
+
export function assertSafeWritePath(inputPath, options = {}) {
|
|
108
|
+
if (typeof inputPath !== 'string' || inputPath.length === 0) {
|
|
109
|
+
throw new Error('file_path: empty');
|
|
110
|
+
}
|
|
111
|
+
if (inputPath.includes('\0')) {
|
|
112
|
+
throw new Error('file_path: null byte rejected');
|
|
113
|
+
}
|
|
114
|
+
// step: reject `..` segments before any FS work. We split on
|
|
115
|
+
// both `/` and the platform separator so a Windows-style `..\foo`
|
|
116
|
+
// input cannot smuggle a traversal through on macOS test paths.
|
|
117
|
+
const decoded = decodeURIComponent(inputPath);
|
|
118
|
+
const segments = decoded.split(/[/\\]/);
|
|
119
|
+
if (segments.some((seg) => seg === '..')) {
|
|
120
|
+
throw new Error(`file_path: '..' segment not allowed in ${inputPath}`);
|
|
121
|
+
}
|
|
122
|
+
// step: refuse to write through a symlink leaf. We must
|
|
123
|
+
// check the LITERAL input path (pre-canonicalize) because `realpath`
|
|
124
|
+
// unconditionally resolves symlinks — so by the time we have the
|
|
125
|
+
// canonical path the symlink fingerprint is already gone. `lstatSync`
|
|
126
|
+
// does NOT follow symlinks, which is exactly what we need.
|
|
127
|
+
const cwdRoot = options.root ?? process.cwd();
|
|
128
|
+
const literalAbsolute = isAbsolute(decoded) ? decoded : resolve(cwdRoot, decoded);
|
|
129
|
+
try {
|
|
130
|
+
const meta = lstatSync(literalAbsolute);
|
|
131
|
+
if (meta.isSymbolicLink()) {
|
|
132
|
+
throw new Error(`file_path: leaf is a symlink (refusing to follow): ${literalAbsolute}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const code = error.code;
|
|
137
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
138
|
+
throw error;
|
|
139
|
+
// ENOENT/ENOTDIR is fine — the leaf simply does not exist yet
|
|
140
|
+
// (write-create path). The walk-up canonicalize below handles it.
|
|
141
|
+
}
|
|
142
|
+
const canonical = canonicalizeWithWalkUp(decoded, cwdRoot);
|
|
143
|
+
// step: fail-CLOSED on empty allowed roots.
|
|
144
|
+
const roots = computeAllowedRoots(options);
|
|
145
|
+
if (roots.length === 0) {
|
|
146
|
+
throw new Error("file_path: allowed_roots is empty — refusing all writes " +
|
|
147
|
+
'(set PUGI_ALLOWED_ROOTS to a non-empty value or run from a real cwd)');
|
|
148
|
+
}
|
|
149
|
+
// step: containment check against canonicalized allowed
|
|
150
|
+
// roots. Each root is normalized to end in `sep` so `/tmp/wsX` does
|
|
151
|
+
// not accidentally pass containment for `/tmp/ws`.
|
|
152
|
+
const inAllowedRoot = roots.some((r) => isContainedIn(canonical, r));
|
|
153
|
+
if (!inAllowedRoot) {
|
|
154
|
+
throw new Error(`file_path: outside allowed roots ${JSON.stringify(roots)}: ${canonical}`);
|
|
155
|
+
}
|
|
156
|
+
// step: denylist. Applied AFTER containment so a
|
|
157
|
+
// misconfigured root pointing at `/` or `$HOME` still cannot clobber
|
|
158
|
+
// sensitive files. We canonicalize HOME so a $HOME that lives under
|
|
159
|
+
// a symlinked prefix (macOS /var → /private/var, Linux /home →
|
|
160
|
+
// /usr/home on some FreeBSD-style mounts) still matches the
|
|
161
|
+
// canonical write target.
|
|
162
|
+
assertNotDenylisted(canonical, canonicalizeHomeDir(options.homeDir ?? homedir()));
|
|
163
|
+
return canonical;
|
|
164
|
+
}
|
|
165
|
+
function canonicalizeHomeDir(raw) {
|
|
166
|
+
if (!raw)
|
|
167
|
+
return raw;
|
|
168
|
+
try {
|
|
169
|
+
return realpathSync.native(raw);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return raw;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Ported from an external utility. Finds the deepest
|
|
177
|
+
* existing ancestor, canonicalizes it (resolving every symlink in the
|
|
178
|
+
* existing prefix), then reattaches the non-existent tail components.
|
|
179
|
+
*
|
|
180
|
+
* This closes the "parent's parent is a symlink" bypass where naive
|
|
181
|
+
* implementations canonicalize only the immediate parent.
|
|
182
|
+
*/
|
|
183
|
+
export function canonicalizeWithWalkUp(inputPath, cwd) {
|
|
184
|
+
const absolute = isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath);
|
|
185
|
+
let current = absolute;
|
|
186
|
+
const tail = [];
|
|
187
|
+
// Hard cap on walk-up iterations — defense against pathological
|
|
188
|
+
// inputs that somehow evade `dirname` termination.
|
|
189
|
+
const maxIterations = 4096;
|
|
190
|
+
let iterations = 0;
|
|
191
|
+
while (true) {
|
|
192
|
+
iterations += 1;
|
|
193
|
+
if (iterations > maxIterations) {
|
|
194
|
+
throw new Error(`file_path: walk-up exceeded ${maxIterations} iterations: ${absolute}`);
|
|
195
|
+
}
|
|
196
|
+
if (existsSync(current)) {
|
|
197
|
+
let canonicalExisting;
|
|
198
|
+
try {
|
|
199
|
+
canonicalExisting = realpathSync.native(current);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw new Error(`file_path: canonicalize ${current}: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
// Reattach tail in original order (we pushed leaf-first).
|
|
205
|
+
let result = canonicalExisting;
|
|
206
|
+
for (let i = tail.length - 1; i >= 0; i -= 1) {
|
|
207
|
+
result = resolve(result, tail[i]);
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
const name = basename(current);
|
|
212
|
+
const parent = dirname(current);
|
|
213
|
+
if (!name || parent === current) {
|
|
214
|
+
throw new Error(`file_path: walked to root without finding existing dir: ${absolute}`);
|
|
215
|
+
}
|
|
216
|
+
tail.push(name);
|
|
217
|
+
current = parent;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function computeAllowedRoots(options) {
|
|
221
|
+
const envValue = options.allowedRootsEnv ?? process.env.PUGI_ALLOWED_ROOTS;
|
|
222
|
+
if (envValue !== undefined) {
|
|
223
|
+
return envValue
|
|
224
|
+
.split(':')
|
|
225
|
+
.filter((s) => s.length > 0)
|
|
226
|
+
.map(canonicalizeRoot)
|
|
227
|
+
.filter((s) => s !== null);
|
|
228
|
+
}
|
|
229
|
+
// No env override: implicit root is the caller-supplied cwd (defaults
|
|
230
|
+
// to `process.cwd()`).
|
|
231
|
+
const cwd = options.root ?? process.cwd();
|
|
232
|
+
const canon = canonicalizeRoot(cwd);
|
|
233
|
+
return canon ? [canon] : [];
|
|
234
|
+
}
|
|
235
|
+
function canonicalizeRoot(raw) {
|
|
236
|
+
if (!raw)
|
|
237
|
+
return null;
|
|
238
|
+
let canon;
|
|
239
|
+
try {
|
|
240
|
+
canon = realpathSync.native(raw);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
canon = resolve(raw);
|
|
244
|
+
}
|
|
245
|
+
if (!canon)
|
|
246
|
+
return null;
|
|
247
|
+
return canon.endsWith(sep) ? canon : canon + sep;
|
|
248
|
+
}
|
|
249
|
+
function isContainedIn(canonicalChild, rootWithSep) {
|
|
250
|
+
const rootNoSep = rootWithSep.endsWith(sep)
|
|
251
|
+
? rootWithSep.slice(0, -1)
|
|
252
|
+
: rootWithSep;
|
|
253
|
+
if (canonicalChild === rootNoSep)
|
|
254
|
+
return true;
|
|
255
|
+
return canonicalChild.startsWith(rootWithSep);
|
|
256
|
+
}
|
|
257
|
+
function assertNotDenylisted(canonical, homeDir) {
|
|
258
|
+
// System paths — match as prefix-with-separator so `/etc-fake/` is
|
|
259
|
+
// not denied while `/etc/passwd` is.
|
|
260
|
+
const systemDenyPrefixes = [
|
|
261
|
+
'/etc/',
|
|
262
|
+
'/usr/',
|
|
263
|
+
'/System/',
|
|
264
|
+
'/Library/Application Support/',
|
|
265
|
+
'/var/db/',
|
|
266
|
+
'/var/log/',
|
|
267
|
+
'/var/root/',
|
|
268
|
+
'/private/etc/',
|
|
269
|
+
'/private/usr/',
|
|
270
|
+
'/private/var/db/',
|
|
271
|
+
'/private/var/log/',
|
|
272
|
+
'/private/var/root/',
|
|
273
|
+
'/root/',
|
|
274
|
+
'/bin/',
|
|
275
|
+
'/sbin/',
|
|
276
|
+
];
|
|
277
|
+
for (const prefix of systemDenyPrefixes) {
|
|
278
|
+
if (canonical.startsWith(prefix)) {
|
|
279
|
+
throw new Error(`file_path: denied (system dir): ${canonical}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!homeDir)
|
|
283
|
+
return;
|
|
284
|
+
// Credential and substrate directories.
|
|
285
|
+
const homeDirSecrets = [
|
|
286
|
+
'.ssh/',
|
|
287
|
+
'.aws/',
|
|
288
|
+
'.gnupg/',
|
|
289
|
+
'.config/gcloud/',
|
|
290
|
+
'.kube/',
|
|
291
|
+
'.docker/',
|
|
292
|
+
'.claude/',
|
|
293
|
+
'.grok/',
|
|
294
|
+
'.gemini/',
|
|
295
|
+
'.copilot/',
|
|
296
|
+
'.kimi/',
|
|
297
|
+
'.pugi/credentials/',
|
|
298
|
+
];
|
|
299
|
+
for (const secret of homeDirSecrets) {
|
|
300
|
+
const full = joinHome(homeDir, secret);
|
|
301
|
+
if (canonical.startsWith(full)) {
|
|
302
|
+
throw new Error(`file_path: denied (secret/substrate dir): ${canonical}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Credential files (exact match).
|
|
306
|
+
const homeFileSecrets = [
|
|
307
|
+
'.npmrc',
|
|
308
|
+
'.cargo/credentials',
|
|
309
|
+
'.cargo/credentials.toml',
|
|
310
|
+
'.docker/config.json',
|
|
311
|
+
'.netrc',
|
|
312
|
+
'.pgpass',
|
|
313
|
+
];
|
|
314
|
+
for (const secret of homeFileSecrets) {
|
|
315
|
+
const full = joinHome(homeDir, secret);
|
|
316
|
+
if (canonical === full) {
|
|
317
|
+
throw new Error(`file_path: denied (credential file): ${canonical}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Shell-init files (exact match).
|
|
321
|
+
const initFiles = [
|
|
322
|
+
'.zshrc',
|
|
323
|
+
'.bashrc',
|
|
324
|
+
'.profile',
|
|
325
|
+
'.bash_profile',
|
|
326
|
+
'.zprofile',
|
|
327
|
+
'.zshenv',
|
|
328
|
+
'.bash_login',
|
|
329
|
+
'.bash_logout',
|
|
330
|
+
'.inputrc',
|
|
331
|
+
'.gitconfig',
|
|
332
|
+
'.config/fish/config.fish',
|
|
333
|
+
];
|
|
334
|
+
for (const file of initFiles) {
|
|
335
|
+
const full = joinHome(homeDir, file);
|
|
336
|
+
if (canonical === full) {
|
|
337
|
+
throw new Error(`file_path: denied (shell-init file): ${canonical}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function joinHome(homeDir, rest) {
|
|
342
|
+
const trimmedHome = homeDir.endsWith(sep) ? homeDir.slice(0, -1) : homeDir;
|
|
343
|
+
return `${trimmedHome}${sep}${rest}`;
|
|
344
|
+
}
|
|
63
345
|
//# sourceMappingURL=path-security.js.map
|
package/dist/core/permission.js
CHANGED
|
@@ -68,8 +68,8 @@ const protectedSuffixes = ['.pem', '.key', '.crt', '.p12', '.dump', '.sql'];
|
|
|
68
68
|
* it as a list for callers (doctor, debug tooling) that need to
|
|
69
69
|
* audit the rule set without re-running `classifyBash`.
|
|
70
70
|
*
|
|
71
|
-
* Code Reviewer P2 retro
|
|
72
|
-
* duplicated here as `destructiveBashPatterns`. Sprint
|
|
71
|
+
* Code Reviewer P2 retro: previously this list was
|
|
72
|
+
* duplicated here as `destructiveBashPatterns`. Sprint moves it
|
|
73
73
|
* into the classifier so the permission engine and the doctor surface
|
|
74
74
|
* cannot drift.
|
|
75
75
|
*/
|
|
@@ -79,23 +79,23 @@ export function destructiveBashPatternsList() {
|
|
|
79
79
|
/**
|
|
80
80
|
* Class-aware bash permission decision. The matrix:
|
|
81
81
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
82
|
+
* | plan | ask | acceptEdits | auto | dontAsk | bypass
|
|
83
|
+
* read | allow| allow| allow | allow | allow | allow
|
|
84
|
+
* build_test | deny | ask | ask | allow | allow* | allow
|
|
85
|
+
* network | deny | ask | ask | ask | allow* | allow
|
|
86
|
+
* write_workspace | deny | ask | allow | allow | allow* | allow
|
|
87
|
+
* write_protected | deny | ask | ask | ask | deny | ask
|
|
88
|
+
* destructive | deny | deny | deny | deny | deny | deny**
|
|
89
|
+
* unknown | deny | ask | ask | ask | deny | ask
|
|
90
90
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
91
|
+
* * dontAsk allows non-destructive classes when no settings rule
|
|
92
|
+
* contradicts; the bare-mode policy is encoded in the table below.
|
|
93
|
+
* ** destructive can be unlocked ONLY when ALL three hold:
|
|
94
|
+
* - mode === 'bypassPermissions'
|
|
95
|
+
* - PUGI_DESTRUCTIVE_OVERRIDE === '1'
|
|
96
|
+
* - source === 'human'
|
|
97
|
+
* The agent loop never sets `source: 'human'`, so even a runaway
|
|
98
|
+
* agent in bypass mode cannot trigger a destructive deletion.
|
|
99
99
|
*/
|
|
100
100
|
export function evaluateBashPermission(cmd, mode, ctx) {
|
|
101
101
|
const classification = classifyBash(cmd, {
|
|
@@ -236,6 +236,18 @@ export function decidePermission(action, settings, root) {
|
|
|
236
236
|
if (protectedReason) {
|
|
237
237
|
return decisionForMode(settings.permissions.mode, protectedReason, 'protected_file', 'high');
|
|
238
238
|
}
|
|
239
|
+
// task — operator-declared read-only paths gate edits
|
|
240
|
+
// and writes ahead of the generic deny check so the audit trail
|
|
241
|
+
// shows `source: 'readonly_paths'` (operator intent), not
|
|
242
|
+
// `settings.deny` (generic rule). Reads always pass through; the
|
|
243
|
+
// contract is "you can look but не touch".
|
|
244
|
+
if (action.kind === 'edit' && matchesAny(action.target, settings.permissions.readonlyPaths)) {
|
|
245
|
+
return {
|
|
246
|
+
decision: 'deny',
|
|
247
|
+
reason: `Read-only path: ${action.target}`,
|
|
248
|
+
source: 'readonly_paths',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
239
251
|
const signature = `${action.kind}:${action.target}`;
|
|
240
252
|
if (matchesAny(signature, settings.permissions.deny)) {
|
|
241
253
|
return { decision: 'deny', reason: `Denied by rule: ${signature}`, source: 'settings.deny' };
|
|
@@ -297,13 +309,61 @@ function riskForAction(action) {
|
|
|
297
309
|
return 'medium';
|
|
298
310
|
return 'medium';
|
|
299
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Expand `${VAR}` and `$VAR` references in a rule string against
|
|
314
|
+
* `process.env`. Unknown variables expand к the empty string so a
|
|
315
|
+
* typo'd `${HMOE}` cannot accidentally match every signature via a
|
|
316
|
+
* literal `${HMOE}` substring; the expanded `mcp:fs/` is innocuous.
|
|
317
|
+
*
|
|
318
|
+
* task #23 — operator wants `mcp:secrets/${USER}-*`
|
|
319
|
+
* style rules so a single settings file fits multiple machines.
|
|
320
|
+
*
|
|
321
|
+
* The implementation deliberately avoids shell-style features like
|
|
322
|
+
* `$()` or default-value `${VAR:-x}`; permission rules should be
|
|
323
|
+
* easy to reason about at audit time.
|
|
324
|
+
*/
|
|
325
|
+
export function expandEnvVars(rule, env = process.env) {
|
|
326
|
+
// Single combined regex. Sequential `${VAR}` then `$VAR` passes are
|
|
327
|
+
// unsafe — if `${X}` expands to a value containing `$Y`, the second
|
|
328
|
+
// pass would also expand that. An attacker controlling one env var
|
|
329
|
+
// could rewrite permission rules. Substitute each variable exactly
|
|
330
|
+
// once by matching both forms in a single sweep.
|
|
331
|
+
return rule.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_m, braced, bare) => {
|
|
332
|
+
const name = braced ?? bare ?? '';
|
|
333
|
+
return env[name] ?? '';
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Convert a glob-style rule to a RegExp anchored full-string.
|
|
338
|
+
* Supports: `*` (greedy any), `?` (single char), all other regex
|
|
339
|
+
* metacharacters escaped. The simpler tail-`*` form remains valid —
|
|
340
|
+
* `mcp:fs/ *` works the same way as before, just via regex instead of
|
|
341
|
+
* the old `startsWith` branch. Power users get `mcp:* / write*` style
|
|
342
|
+
* cross-server denies.
|
|
343
|
+
*/
|
|
344
|
+
const REGEX_META = new Set(['\\', '^', '$', '.', '|', '+', '(', ')', '[', ']', '{', '}']);
|
|
345
|
+
function ruleToRegExp(rule) {
|
|
346
|
+
let out = '';
|
|
347
|
+
for (const ch of rule) {
|
|
348
|
+
if (ch === '*')
|
|
349
|
+
out += '.*';
|
|
350
|
+
else if (ch === '?')
|
|
351
|
+
out += '.';
|
|
352
|
+
else if (REGEX_META.has(ch))
|
|
353
|
+
out += '\\' + ch;
|
|
354
|
+
else
|
|
355
|
+
out += ch;
|
|
356
|
+
}
|
|
357
|
+
return new RegExp('^' + out + '$');
|
|
358
|
+
}
|
|
300
359
|
function matchesAny(value, rules) {
|
|
301
360
|
return rules.some((rule) => {
|
|
302
|
-
|
|
361
|
+
const expanded = expandEnvVars(rule);
|
|
362
|
+
if (expanded === value)
|
|
303
363
|
return true;
|
|
304
|
-
if (
|
|
305
|
-
return
|
|
306
|
-
return
|
|
364
|
+
if (!expanded.includes('*') && !expanded.includes('?'))
|
|
365
|
+
return false;
|
|
366
|
+
return ruleToRegExp(expanded).test(value);
|
|
307
367
|
});
|
|
308
368
|
}
|
|
309
369
|
//# sourceMappingURL=permission.js.map
|