@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/LICENSE +1 -1
- package/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 +3 -3
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +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 +151 -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 +298 -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 +36 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4203 -493
- 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 +73 -39
- 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/sigint-guard.js +272 -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 +288 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +624 -46
- package/dist/tools/brief.js +224 -0
- package/dist/tools/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-chips.js +257 -0
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/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 +25 -7
- 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,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission gate — canonical 6-mode enforcement ().
|
|
3
|
+
*
|
|
4
|
+
* Single dispatch entry point. Every tool call goes through `gate()`
|
|
5
|
+
* before the executor runs the tool body; the executor surfaces the
|
|
6
|
+
* `PermissionDenied` error as a model-readable sentinel so the model
|
|
7
|
+
* can either reformulate the request or wait for the operator to
|
|
8
|
+
* change the mode.
|
|
9
|
+
*
|
|
10
|
+
* Routing matrix (mode × class):
|
|
11
|
+
*
|
|
12
|
+
* | read | write | dispatch
|
|
13
|
+
* default | ask | ask | ask
|
|
14
|
+
* acceptEdits | allow | allow* | ask
|
|
15
|
+
* plan | allow | deny | deny
|
|
16
|
+
* auto | ask† | ask† | ask†
|
|
17
|
+
* dontAsk | allow | allow | allow
|
|
18
|
+
* bypassPermissions | allow | allow | allow (hooks bypassed)
|
|
19
|
+
*
|
|
20
|
+
* * acceptEdits allows file-write tools (write/edit/multi_edit) only;
|
|
21
|
+
* bash and other write-class tools still ask.
|
|
22
|
+
* † auto-mode consults the classifier (`auto-classifier.ts`): safe
|
|
23
|
+
* regex allowlist → allow; destructive regex denylist → deny;
|
|
24
|
+
* anything else → ask.
|
|
25
|
+
*
|
|
26
|
+
* Protected paths (`.git`, `~/.ssh`, `.pugi/settings.json`, …) NEVER
|
|
27
|
+
* auto-approve regardless of mode — the gate refuses them even in
|
|
28
|
+
* `dontAsk` and `bypassPermissions`. The bypassPermissions circuit-
|
|
29
|
+
* breaker fires on rm -rf / fork-bomb / dd if=/ patterns против root /
|
|
30
|
+
* home / workspace.
|
|
31
|
+
*
|
|
32
|
+
* Ask-mode session cache (`AskAlwaysCache`) survives — `default` and
|
|
33
|
+
* `auto` consult it; `plan` ignores it (structural contract); the
|
|
34
|
+
* permissive modes already allow / refuse без the cache.
|
|
35
|
+
*/
|
|
36
|
+
import { classifyAutoMode } from './auto-classifier.js';
|
|
37
|
+
import { evaluateCircuitBreaker } from './circuit-breaker.js';
|
|
38
|
+
import { getToolClass } from './tool-class.js';
|
|
39
|
+
export const ASK_OPTIONS = Object.freeze([
|
|
40
|
+
'allow-once',
|
|
41
|
+
'always-this-tool',
|
|
42
|
+
'deny-once',
|
|
43
|
+
'always-deny-this-tool',
|
|
44
|
+
]);
|
|
45
|
+
export function createAskAlwaysCache() {
|
|
46
|
+
return {
|
|
47
|
+
alwaysAllowed: new Set(),
|
|
48
|
+
alwaysDenied: new Set(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply the operator's answer to an `ask` decision. Caller invokes this
|
|
53
|
+
* after the operator picks an option так cache stays в sync. Returns
|
|
54
|
+
* the effective decision: `allow-once` / `always-this-tool` become
|
|
55
|
+
* `allow`; `deny-once` / `always-deny-this-tool` become `deny`.
|
|
56
|
+
*
|
|
57
|
+
* `always-*` answers persist to the cache and short-circuit the next
|
|
58
|
+
* gate call для the same tool name внутри session.
|
|
59
|
+
*/
|
|
60
|
+
export function applyAskAnswer(cache, toolName, answer) {
|
|
61
|
+
switch (answer) {
|
|
62
|
+
case 'allow-once':
|
|
63
|
+
return { decision: 'allow', reason: `Allowed once for ${toolName}` };
|
|
64
|
+
case 'always-this-tool':
|
|
65
|
+
cache.alwaysAllowed.add(toolName);
|
|
66
|
+
cache.alwaysDenied.delete(toolName);
|
|
67
|
+
return { decision: 'allow', reason: `Allowed for ${toolName} this session` };
|
|
68
|
+
case 'deny-once':
|
|
69
|
+
return { decision: 'deny', reason: `Denied once for ${toolName}` };
|
|
70
|
+
case 'always-deny-this-tool':
|
|
71
|
+
cache.alwaysDenied.add(toolName);
|
|
72
|
+
cache.alwaysAllowed.delete(toolName);
|
|
73
|
+
return { decision: 'deny', reason: `Denied for ${toolName} this session` };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tools that `acceptEdits` mode auto-allows. Restricted к file-edit
|
|
78
|
+
* surfaces — bash / dispatch / etc fall through к the ask branch so
|
|
79
|
+
* the operator stays in control of "execute arbitrary command" and
|
|
80
|
+
* "spawn child agent" actions.
|
|
81
|
+
*/
|
|
82
|
+
const ACCEPT_EDITS_AUTO_ALLOW = new Set([
|
|
83
|
+
'write',
|
|
84
|
+
'edit',
|
|
85
|
+
'multi_edit',
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* Permission-denied sentinel. Distinguishable from other tool errors
|
|
89
|
+
* (parse errors, IO failures) so the caller can route the message back
|
|
90
|
+
* to the model with the canonical recovery hint.
|
|
91
|
+
*/
|
|
92
|
+
export class PermissionDenied extends Error {
|
|
93
|
+
name = 'PermissionDenied';
|
|
94
|
+
mode;
|
|
95
|
+
toolName;
|
|
96
|
+
toolClass;
|
|
97
|
+
/**
|
|
98
|
+
* Human-friendly reason surfaced в logs / hook payloads. Distinct
|
|
99
|
+
* from `message` so the spec layer can pattern-match the canonical
|
|
100
|
+
* `PERMISSION_DENIED:` sentinel verbatim while operators see the
|
|
101
|
+
* full explanation в console output.
|
|
102
|
+
*/
|
|
103
|
+
reason;
|
|
104
|
+
constructor(toolName, toolClass, mode, reason) {
|
|
105
|
+
// The base Error.message is the canonical sentinel так default
|
|
106
|
+
// toString() / re-throw paths preserve the format the model and
|
|
107
|
+
// the spec layer pattern-match against.
|
|
108
|
+
super(`PERMISSION_DENIED: ${toolName} blocked in ${mode} mode. Operator can switch with /permissions <mode>.`);
|
|
109
|
+
this.mode = mode;
|
|
110
|
+
this.toolName = toolName;
|
|
111
|
+
this.toolClass = toolClass;
|
|
112
|
+
this.reason = reason;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Render the sentinel message the executor surfaces к the model.
|
|
116
|
+
* The string format stable так a parent agent / E2E spec can
|
|
117
|
+
* pattern-match `PERMISSION_DENIED: <tool> blocked in <mode> mode.`
|
|
118
|
+
* verbatim. Equivalent to `this.message`; kept как method так
|
|
119
|
+
* downstream callers can use whichever spelling reads better at the
|
|
120
|
+
* site.
|
|
121
|
+
*/
|
|
122
|
+
toModelMessage() {
|
|
123
|
+
return this.message;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Core dispatch gate. Pure function — no IO, no side effects beyond
|
|
128
|
+
* mutating the caller-supplied `alwaysCache`. Safe to call from any
|
|
129
|
+
* layer (engine adapter, agent-as-tool bridge, doctor command).
|
|
130
|
+
*
|
|
131
|
+
* Argument bag mirrors the executor entry shape:
|
|
132
|
+
* - `toolName` is the registered tool key (e.g. `read`, `write`,
|
|
133
|
+
* `mcp__github__list_issues`).
|
|
134
|
+
* - `args` is the raw arg payload. The gate inspects `args.command`
|
|
135
|
+
* when present (bash tool) for the circuit-breaker + auto-mode
|
|
136
|
+
* classifier. Other tools pass through unused — the contract is
|
|
137
|
+
* stable on purpose.
|
|
138
|
+
* - `ctx` carries mode + session-scoped state.
|
|
139
|
+
*/
|
|
140
|
+
export function gate(toolName, args, ctx) {
|
|
141
|
+
const toolClass = getToolClass(toolName);
|
|
142
|
+
const cache = ctx.alwaysCache;
|
|
143
|
+
const command = extractCommand(args, ctx.commandPreview);
|
|
144
|
+
// Bypass-mode circuit breaker: catastrophic patterns refuse FIRST,
|
|
145
|
+
// before any other routing. Same rule applies when the operator
|
|
146
|
+
// wired `dontAsk` and a destructive command sneaks through — defence
|
|
147
|
+
// in depth.
|
|
148
|
+
if ((ctx.permissionMode === 'bypassPermissions' || ctx.permissionMode === 'dontAsk')
|
|
149
|
+
&& command) {
|
|
150
|
+
const breaker = evaluateCircuitBreaker(command);
|
|
151
|
+
if (breaker.tripped) {
|
|
152
|
+
return {
|
|
153
|
+
decision: 'deny',
|
|
154
|
+
reason: `Circuit-breaker tripped: ${breaker.reason}. Refused в ${ctx.permissionMode} mode regardless of policy.`,
|
|
155
|
+
circuitBreakerReason: breaker.reason,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Ask-mode session memory: an explicit "always-deny" beats any other
|
|
160
|
+
// routing because the operator has actively refused this tool.
|
|
161
|
+
if (cache?.alwaysDenied.has(toolName)) {
|
|
162
|
+
return {
|
|
163
|
+
decision: 'deny',
|
|
164
|
+
reason: `Tool ${toolName} denied for the session via /permissions`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// "Always-allow" в ask-style modes skips the prompt for subsequent
|
|
168
|
+
// calls. `plan` mode IGNORES the always-allow cache because plan
|
|
169
|
+
// mode's contract is structural (read-only), not consent-based.
|
|
170
|
+
// `bypassPermissions` / `dontAsk` already allow so the cache is moot.
|
|
171
|
+
// `acceptEdits` and `auto` honour the cache like `default` does.
|
|
172
|
+
if (cache?.alwaysAllowed.has(toolName)
|
|
173
|
+
&& (ctx.permissionMode === 'default'
|
|
174
|
+
|| ctx.permissionMode === 'acceptEdits'
|
|
175
|
+
|| ctx.permissionMode === 'auto')) {
|
|
176
|
+
return {
|
|
177
|
+
decision: 'allow',
|
|
178
|
+
reason: `Tool ${toolName} always-allowed for this session`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
switch (ctx.permissionMode) {
|
|
182
|
+
case 'plan': {
|
|
183
|
+
if (toolClass === 'read') {
|
|
184
|
+
return { decision: 'allow', reason: `Plan mode: read tools allowed (${toolName})` };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
decision: 'deny',
|
|
188
|
+
reason: `Plan mode: ${toolClass} tools blocked. Switch with /permissions dontAsk.`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
case 'default': {
|
|
192
|
+
return askDecision(toolName, toolClass, ctx.target, 'default');
|
|
193
|
+
}
|
|
194
|
+
case 'acceptEdits': {
|
|
195
|
+
// File-edit surfaces auto-execute; everything else asks.
|
|
196
|
+
if (toolClass === 'read') {
|
|
197
|
+
return { decision: 'allow', reason: `acceptEdits: read allowed (${toolName})` };
|
|
198
|
+
}
|
|
199
|
+
if (toolClass === 'write' && ACCEPT_EDITS_AUTO_ALLOW.has(toolName)) {
|
|
200
|
+
return {
|
|
201
|
+
decision: 'allow',
|
|
202
|
+
reason: `acceptEdits: file-edit auto-allowed (${toolName})`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return askDecision(toolName, toolClass, ctx.target, 'acceptEdits');
|
|
206
|
+
}
|
|
207
|
+
case 'auto': {
|
|
208
|
+
// Phase 1 classifier — regex allowlist / denylist для bash;
|
|
209
|
+
// other tools always fall back to ask. The classifier surfaces
|
|
210
|
+
// an explicit verdict so the spec layer can assert per-pattern.
|
|
211
|
+
if (toolName === 'bash' && command) {
|
|
212
|
+
const verdict = classifyAutoMode(command);
|
|
213
|
+
if (verdict.verdict === 'allow') {
|
|
214
|
+
return {
|
|
215
|
+
decision: 'allow',
|
|
216
|
+
reason: `auto-mode: ${verdict.reason}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (verdict.verdict === 'deny') {
|
|
220
|
+
return {
|
|
221
|
+
decision: 'deny',
|
|
222
|
+
reason: `auto-mode: ${verdict.reason}. Switch with /permissions default to override.`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// verdict === 'ask' — fall through.
|
|
226
|
+
}
|
|
227
|
+
return askDecision(toolName, toolClass, ctx.target, 'auto');
|
|
228
|
+
}
|
|
229
|
+
case 'dontAsk': {
|
|
230
|
+
return {
|
|
231
|
+
decision: 'allow',
|
|
232
|
+
reason: `dontAsk mode: ${toolName} executed (deny-list still applies)`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
case 'bypassPermissions': {
|
|
236
|
+
return {
|
|
237
|
+
decision: 'allow',
|
|
238
|
+
reason: `bypassPermissions: ${toolName} executed (policy hooks skipped)`,
|
|
239
|
+
hooksBypassed: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function askDecision(toolName, toolClass, target, mode) {
|
|
245
|
+
return {
|
|
246
|
+
decision: 'ask',
|
|
247
|
+
reason: `${mode} mode: prompt before ${toolName}`,
|
|
248
|
+
question: buildAskQuestion(toolName, toolClass, target),
|
|
249
|
+
options: ASK_OPTIONS,
|
|
250
|
+
toolClass,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Best-effort extract of the bash command string from the raw tool
|
|
255
|
+
* args. Returns `null` when the shape doesn't match — auto-mode and
|
|
256
|
+
* the circuit-breaker treat null как "no preview available" and fall
|
|
257
|
+
* back to safe defaults (ask / no-trip).
|
|
258
|
+
*/
|
|
259
|
+
function extractCommand(args, fallback) {
|
|
260
|
+
if (typeof fallback === 'string' && fallback.length > 0)
|
|
261
|
+
return fallback;
|
|
262
|
+
if (args && typeof args === 'object') {
|
|
263
|
+
const maybe = args.command;
|
|
264
|
+
if (typeof maybe === 'string' && maybe.length > 0)
|
|
265
|
+
return maybe;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build the operator-facing question string for an ask-mode prompt.
|
|
271
|
+
* Kept в one place так the wording stays consistent across the REPL
|
|
272
|
+
* Ink modal and the simpler stdin fallback.
|
|
273
|
+
*/
|
|
274
|
+
function buildAskQuestion(toolName, toolClass, target) {
|
|
275
|
+
const suffix = target ? ` on ${target}` : '';
|
|
276
|
+
return `Allow ${toolName} (${toolClass})${suffix}?`;
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission gate () public surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the canonical 4-mode types, the tool-class classifier,
|
|
5
|
+
* the dispatch gate, and the workspace + global session-state helpers
|
|
6
|
+
* so callers import from one place:
|
|
7
|
+
*
|
|
8
|
+
* import { gate, resolveMode, PermissionDenied } from '<...>/permissions/index.js';
|
|
9
|
+
*
|
|
10
|
+
* Keeps the internal file split (mode / tool-class / gate / state)
|
|
11
|
+
* invisible to consumers — those files are an implementation detail
|
|
12
|
+
* the engine adapter does not need to know about.
|
|
13
|
+
*/
|
|
14
|
+
export { DEFAULT_PERMISSION_MODE, PERMISSION_MODE_GLOSS, PERMISSION_MODES, isPermissionMode, nextPermissionMode, parsePermissionMode, toLegacyMode, } from './mode.js';
|
|
15
|
+
export { classifyAutoMode, listAutoAllowPatterns, listAutoDenyPatterns, } from './auto-classifier.js';
|
|
16
|
+
export { evaluateCircuitBreaker, listCircuitBreakerPatterns, } from './circuit-breaker.js';
|
|
17
|
+
export { getToolClass, listBuiltInToolClasses, } from './tool-class.js';
|
|
18
|
+
export { ASK_OPTIONS, PermissionDenied, applyAskAnswer, createAskAlwaysCache, gate, } from './gate.js';
|
|
19
|
+
export { getCurrentMode, getGlobalDefaultMode, getPreviousMode, globalConfigPath, resolveMode, sessionStatePath, setCurrentMode, setGlobalDefaultMode, setPreviousMode, } from './state.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission modes — canonical 6-mode taxonomy (the upstream tool parity).
|
|
3
|
+
*
|
|
4
|
+
* Pugi shipped a 4-mode taxonomy (`plan | ask | allow | bypass`) that
|
|
5
|
+
* proved fine для daily use but diverged from the upstream tool's 6-mode
|
|
6
|
+
* surface (`default | acceptEdits | plan | auto | dontAsk |
|
|
7
|
+
* bypassPermissions`). epic #2 closes the parity gap so
|
|
8
|
+
* operators coming from the upstream tool see the same mode names + same
|
|
9
|
+
* Shift+Tab cycle.
|
|
10
|
+
*
|
|
11
|
+
* Canonical 6 modes :
|
|
12
|
+
*
|
|
13
|
+
* - `default` — every tool call asks the operator. Safe
|
|
14
|
+
* ground state, replaces `ask`.
|
|
15
|
+
* - `acceptEdits` — auto-allow write/edit on workspace files,
|
|
16
|
+
* ask for everything else (bash, dispatch).
|
|
17
|
+
* Matches CC's "trust file edits только".
|
|
18
|
+
* - `plan` — read-only proposal mode. Write/dispatch
|
|
19
|
+
* refused with deterministic sentinel; the
|
|
20
|
+
* model surfaces a plan, не executes.
|
|
21
|
+
* - `auto` — classifier decides per-call. Phase 1
|
|
22
|
+
* (this PR) ships regex allowlist (safe
|
|
23
|
+
* commands) + regex denylist (destructive).
|
|
24
|
+
* Anything else falls back to ask.
|
|
25
|
+
* - `dontAsk` — auto-allow everything except `permissions.deny`
|
|
26
|
+
* list. Replaces `allow`.
|
|
27
|
+
* - `bypassPermissions` — skip ALL checks including deny list +
|
|
28
|
+
* policy hooks. Has a circuit-breaker for
|
|
29
|
+
* catastrophic patterns (rm -rf /, fork bomb,
|
|
30
|
+
* dd if=/) that refuses regardless of mode.
|
|
31
|
+
* Replaces `bypass`.
|
|
32
|
+
*
|
|
33
|
+
* Backwards-compat aliases: the short names (`ask`, `allow`, `bypass`)
|
|
34
|
+
* map to the new canonical names via `MODE_ALIASES`. `parsePermissionMode`
|
|
35
|
+
* resolves aliases so existing session.json files keep working.
|
|
36
|
+
*
|
|
37
|
+
* Rename mapping :
|
|
38
|
+
* ask → default
|
|
39
|
+
* allow → dontAsk
|
|
40
|
+
* bypass → bypassPermissions
|
|
41
|
+
* (plan, acceptEdits, auto unchanged / new)
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Closed list — used by Shift+Tab cycle (in order), input validation,
|
|
45
|
+
* and slash-command help. Order matches the upstream tool's documented
|
|
46
|
+
* Shift+Tab progression: default → acceptEdits → plan → auto → dontAsk
|
|
47
|
+
* → bypassPermissions → wrap к default.
|
|
48
|
+
*/
|
|
49
|
+
export const PERMISSION_MODES = Object.freeze([
|
|
50
|
+
'default',
|
|
51
|
+
'acceptEdits',
|
|
52
|
+
'plan',
|
|
53
|
+
'auto',
|
|
54
|
+
'dontAsk',
|
|
55
|
+
'bypassPermissions',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Default mode applied when no `--mode` flag, no per-workspace session
|
|
59
|
+
* state, and no `defaultPermissionMode` в `~/.pugi/config.json`. We
|
|
60
|
+
* default cautious (`default` mode = prompt every call) — an operator
|
|
61
|
+
* who has not configured anything is treated as a new operator who
|
|
62
|
+
* deserves visibility into every tool call.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_PERMISSION_MODE = 'default';
|
|
65
|
+
/**
|
|
66
|
+
* Backwards-compat aliases: short names map to canonical names.
|
|
67
|
+
* `parsePermissionMode` consults this table так existing session.json
|
|
68
|
+
* files + scripts that pass `--mode ask` keep working without breaking.
|
|
69
|
+
*
|
|
70
|
+
* Aliases are one-way: persistence writes the canonical name, so a
|
|
71
|
+
* session that started on `ask` is migrated to `default` on next save.
|
|
72
|
+
*/
|
|
73
|
+
const MODE_ALIASES = Object.freeze({
|
|
74
|
+
ask: 'default',
|
|
75
|
+
allow: 'dontAsk',
|
|
76
|
+
bypass: 'bypassPermissions',
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Type guard для arbitrary string input (CLI flag, session.json
|
|
80
|
+
* deserialization). Returns false for casing variants — caller is
|
|
81
|
+
* expected to lowercase before testing. Aliases are NOT accepted by
|
|
82
|
+
* this predicate; use `parsePermissionMode` for alias resolution.
|
|
83
|
+
*/
|
|
84
|
+
export function isPermissionMode(value) {
|
|
85
|
+
return typeof value === 'string' && PERMISSION_MODES.includes(value);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse + validate a mode string. Returns null для invalid input so the
|
|
89
|
+
* caller can surface a typed error (`unknown mode: <value>`) instead of
|
|
90
|
+
* throwing from a parse helper.
|
|
91
|
+
*
|
|
92
|
+
* Resolves aliases (`ask` → `default`, `allow` → `dontAsk`,
|
|
93
|
+
* `bypass` → `bypassPermissions`) for backwards compatibility.
|
|
94
|
+
*
|
|
95
|
+
* Case-handling: lowercases for canonical names but matches camelCase
|
|
96
|
+
* names case-insensitively too (так `acceptedits` resolves to
|
|
97
|
+
* `acceptEdits`). The aliases table covers the legacy lowercase tokens.
|
|
98
|
+
*/
|
|
99
|
+
export function parsePermissionMode(value) {
|
|
100
|
+
const trimmed = value.trim();
|
|
101
|
+
if (trimmed.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
// Direct canonical match first (preserves camelCase capitalisation).
|
|
104
|
+
if (isPermissionMode(trimmed))
|
|
105
|
+
return trimmed;
|
|
106
|
+
// Case-insensitive canonical match — operator typed `acceptedits`.
|
|
107
|
+
const lower = trimmed.toLowerCase();
|
|
108
|
+
const canonical = PERMISSION_MODES.find((m) => m.toLowerCase() === lower);
|
|
109
|
+
if (canonical)
|
|
110
|
+
return canonical;
|
|
111
|
+
// alias fallthrough.
|
|
112
|
+
const aliased = MODE_ALIASES[lower];
|
|
113
|
+
if (aliased)
|
|
114
|
+
return aliased;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Shift+Tab cycle — advance to the next mode in the canonical
|
|
119
|
+
* order, wrapping from the last back to the first. Pure helper so the
|
|
120
|
+
* TUI binding can call it without re-implementing the cycle logic.
|
|
121
|
+
*/
|
|
122
|
+
export function nextPermissionMode(current) {
|
|
123
|
+
const idx = PERMISSION_MODES.indexOf(current);
|
|
124
|
+
if (idx === -1)
|
|
125
|
+
return DEFAULT_PERMISSION_MODE;
|
|
126
|
+
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length];
|
|
127
|
+
return next ?? DEFAULT_PERMISSION_MODE;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Map the canonical 6-mode taxonomy to the legacy SDK enum used by
|
|
131
|
+
* `@pugi/sdk::permissionModeSchema`. The SDK enum already contains all
|
|
132
|
+
* 6 names so the map is identity для the modes that align; the two
|
|
133
|
+
* -only legacy names (`ask`, `allow`) are not part of the canonical
|
|
134
|
+
* set anymore — `default` and `dontAsk` are их replacements.
|
|
135
|
+
*
|
|
136
|
+
* Callers that need the legacy enum (existing bash classifier, settings
|
|
137
|
+
* persistence) should funnel through this helper so the mapping stays
|
|
138
|
+
* в one place.
|
|
139
|
+
*/
|
|
140
|
+
export function toLegacyMode(mode) {
|
|
141
|
+
switch (mode) {
|
|
142
|
+
case 'default':
|
|
143
|
+
// SDK enum doesn't carry `default`; the closest legacy semantic is
|
|
144
|
+
// `ask` (prompt-every-call). Persistence layers that round-trip
|
|
145
|
+
// через the SDK enum get back `ask`, which `parsePermissionMode`
|
|
146
|
+
// re-maps to `default` via the alias table. Round-trip safe.
|
|
147
|
+
return 'ask';
|
|
148
|
+
case 'acceptEdits':
|
|
149
|
+
return 'acceptEdits';
|
|
150
|
+
case 'plan':
|
|
151
|
+
return 'plan';
|
|
152
|
+
case 'auto':
|
|
153
|
+
return 'auto';
|
|
154
|
+
case 'dontAsk':
|
|
155
|
+
return 'dontAsk';
|
|
156
|
+
case 'bypassPermissions':
|
|
157
|
+
return 'bypassPermissions';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* One-line human-readable summary surfaced by the `/permissions` table,
|
|
162
|
+
* the Ink picker, and `pugi --help` text. Each line carries a safety
|
|
163
|
+
* hint ("safe-by-default" / "use carefully" / "power-user only") so an
|
|
164
|
+
* operator скимming the picker knows the risk profile at a glance.
|
|
165
|
+
*/
|
|
166
|
+
export const PERMISSION_MODE_GLOSS = Object.freeze({
|
|
167
|
+
default: 'Prompt before every tool call. Safe-by-default for new operators.',
|
|
168
|
+
acceptEdits: 'Auto-allow file edit/write; ask for bash + dispatch. Safe-by-default.',
|
|
169
|
+
plan: 'Read-only — propose, never execute. Write + dispatch refused.',
|
|
170
|
+
auto: 'Classifier decides per call (safe regex allowlist; falls back к ask). Use carefully.',
|
|
171
|
+
dontAsk: 'Execute tools without prompts; deny-list still applies. Use carefully.',
|
|
172
|
+
bypassPermissions: 'Skip ALL checks AND policy hooks; circuit-breaker on catastrophic patterns. Power-user only.',
|
|
173
|
+
});
|
|
174
|
+
//# sourceMappingURL=mode.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network egress permission gate.
|
|
3
|
+
*
|
|
4
|
+
* Enforces a deny-by-default allowlist for outbound HTTP(S) requests made by
|
|
5
|
+
* the Pugi CLI runtime. Enterprise customers require provable network
|
|
6
|
+
* boundaries before granting agent autonomy on internal machines.
|
|
7
|
+
*
|
|
8
|
+
* Rule semantics:
|
|
9
|
+
* - Rules are evaluated in declaration order.
|
|
10
|
+
* - First matching rule wins; its `allow` value is final.
|
|
11
|
+
* - If no rule matches, `policy.defaultAction` decides.
|
|
12
|
+
*
|
|
13
|
+
* Pattern syntax (intentionally minimal — full glob is YAGNI here):
|
|
14
|
+
* - Literal: "api.openai.com" — exact case-insensitive match.
|
|
15
|
+
* - Wildcard: "*.pugi.io" — matches any single-or-multi-label
|
|
16
|
+
* subdomain, NOT the apex.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_ALLOWED_HOSTS = [
|
|
19
|
+
'api.anthropic.com',
|
|
20
|
+
'api.openai.com',
|
|
21
|
+
'generativelanguage.googleapis.com',
|
|
22
|
+
'github.com',
|
|
23
|
+
'api.github.com',
|
|
24
|
+
'registry.npmjs.org',
|
|
25
|
+
'*.pugi.io',
|
|
26
|
+
'*.pugi.dev',
|
|
27
|
+
];
|
|
28
|
+
export const DEFAULT_DENY_POLICY = {
|
|
29
|
+
defaultAction: 'deny',
|
|
30
|
+
rules: DEFAULT_ALLOWED_HOSTS.map((host) => ({
|
|
31
|
+
host,
|
|
32
|
+
allow: true,
|
|
33
|
+
reason: `default allowlist: ${host}`,
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Match a hostname against a single pattern.
|
|
38
|
+
*
|
|
39
|
+
* Wildcard rule: a leading "*." pattern matches any host whose suffix
|
|
40
|
+
* (after stripping at least one label) equals the pattern tail. The apex
|
|
41
|
+
* is intentionally NOT matched — "*.pugi.io" does not match "pugi.io".
|
|
42
|
+
* This prevents an allow rule like "*.pugi.io" from also greenlighting
|
|
43
|
+
* "pugi.io.evil.com" (which has the literal string but a different apex).
|
|
44
|
+
*/
|
|
45
|
+
export function matchHost(host, pattern) {
|
|
46
|
+
if (host.length === 0 || pattern.length === 0)
|
|
47
|
+
return false;
|
|
48
|
+
const normalizedHost = host.toLowerCase();
|
|
49
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
50
|
+
if (normalizedPattern.startsWith('*.')) {
|
|
51
|
+
const tail = normalizedPattern.slice(2);
|
|
52
|
+
if (tail.length === 0)
|
|
53
|
+
return false;
|
|
54
|
+
if (normalizedHost === tail)
|
|
55
|
+
return false;
|
|
56
|
+
return normalizedHost.endsWith(`.${tail}`);
|
|
57
|
+
}
|
|
58
|
+
return normalizedHost === normalizedPattern;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check a target URL against a policy. Malformed URLs deny-fail closed.
|
|
62
|
+
*/
|
|
63
|
+
export function checkEgress(targetUrl, policy) {
|
|
64
|
+
let hostname;
|
|
65
|
+
try {
|
|
66
|
+
hostname = new URL(targetUrl).hostname;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {
|
|
70
|
+
allowed: false,
|
|
71
|
+
reason: `malformed URL: ${targetUrl}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (hostname.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
allowed: false,
|
|
77
|
+
reason: `empty hostname in URL: ${targetUrl}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
for (const rule of policy.rules) {
|
|
81
|
+
if (matchHost(hostname, rule.host)) {
|
|
82
|
+
return {
|
|
83
|
+
allowed: rule.allow,
|
|
84
|
+
reason: rule.allow
|
|
85
|
+
? `allowed by rule "${rule.host}": ${rule.reason}`
|
|
86
|
+
: `denied by rule "${rule.host}": ${rule.reason}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (policy.defaultAction === 'allow') {
|
|
91
|
+
return {
|
|
92
|
+
allowed: true,
|
|
93
|
+
reason: `allowed by default policy (no matching rule for ${hostname})`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
allowed: false,
|
|
98
|
+
reason: `denied by default policy (no matching rule for ${hostname})`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function parseHostList(raw) {
|
|
102
|
+
if (raw === undefined || raw.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
return raw
|
|
105
|
+
.split(',')
|
|
106
|
+
.map((entry) => entry.trim())
|
|
107
|
+
.filter((entry) => entry.length > 0);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build a policy by merging env-supplied overrides on top of the default
|
|
111
|
+
* deny policy.
|
|
112
|
+
*
|
|
113
|
+
* Precedence (first match wins inside `checkEgress`):
|
|
114
|
+
* 1. PUGI_EGRESS_DENY hosts (explicit deny — wins over any later allow).
|
|
115
|
+
* 2. PUGI_EGRESS_ALLOW hosts (operator extension of the allowlist).
|
|
116
|
+
* 3. Built-in DEFAULT_DENY_POLICY allowlist.
|
|
117
|
+
* 4. Default action: deny.
|
|
118
|
+
*/
|
|
119
|
+
export function loadPolicyFromEnv(env = process.env) {
|
|
120
|
+
const denyHosts = parseHostList(env['PUGI_EGRESS_DENY']);
|
|
121
|
+
const allowHosts = parseHostList(env['PUGI_EGRESS_ALLOW']);
|
|
122
|
+
const denyRules = denyHosts.map((host) => ({
|
|
123
|
+
host,
|
|
124
|
+
allow: false,
|
|
125
|
+
reason: 'PUGI_EGRESS_DENY override',
|
|
126
|
+
}));
|
|
127
|
+
const allowRules = allowHosts.map((host) => ({
|
|
128
|
+
host,
|
|
129
|
+
allow: true,
|
|
130
|
+
reason: 'PUGI_EGRESS_ALLOW override',
|
|
131
|
+
}));
|
|
132
|
+
return {
|
|
133
|
+
defaultAction: 'deny',
|
|
134
|
+
rules: [...denyRules, ...allowRules, ...DEFAULT_DENY_POLICY.rules],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=network-egress.js.map
|