@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.40
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- 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/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/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- 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-classifier.js +108 -1
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -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/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -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 +86 -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/git.js +65 -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/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/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +992 -36
- 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/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/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/mcp/client.js +75 -6
- 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 +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -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/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -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/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -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/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1899 -38
- package/dist/core/repl/slash-commands.js +406 -21
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- 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/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/session.js +92 -0
- package/dist/core/settings.js +80 -0
- 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/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/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- 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/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3073 -321
- 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/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +390 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -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/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 +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- 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 +32 -0
- 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/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/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- 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/registry.js +46 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +69 -2
- package/dist/tui/markdown-render.js +4 -4
- 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 +303 -13
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +72 -14
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- 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/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -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
|
+
* Leak L18 (2026-05-27) — 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
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-mode classifier — Wave 7 Phase 1 (regex allowlist + denylist).
|
|
3
|
+
*
|
|
4
|
+
* `permissionMode === 'auto'` is the Claude Code parity mode where the
|
|
5
|
+
* classifier decides safe-vs-unsafe per call. Phase 1 ships without
|
|
6
|
+
* ML — just two curated regex lists covering the 80% of "obviously
|
|
7
|
+
* safe" and "obviously catastrophic" patterns. Anything that doesn't
|
|
8
|
+
* match either list returns `ask`, so the operator stays in the loop
|
|
9
|
+
* для the ambiguous middle.
|
|
10
|
+
*
|
|
11
|
+
* Phase 2 (deferred, NOT in this PR): semantic classifier consulting
|
|
12
|
+
* the model with a tight system prompt. The interface (`AutoVerdict`)
|
|
13
|
+
* is stable so we can swap implementations without touching the gate.
|
|
14
|
+
*
|
|
15
|
+
* Design notes:
|
|
16
|
+
*
|
|
17
|
+
* - Patterns are conservative: a read-only command is only
|
|
18
|
+
* allow-listed когда its argv shape is unambiguous (no `-exec`,
|
|
19
|
+
* no `--delete`, no `|` к shell). When в doubt, fall back to ask.
|
|
20
|
+
* - The denylist matches catastrophic patterns even в auto-mode so
|
|
21
|
+
* a misclick can't shred the workspace. The circuit-breaker
|
|
22
|
+
* (`circuit-breaker.ts`) covers the same surface для bypass-mode;
|
|
23
|
+
* this denylist is the auto-mode equivalent.
|
|
24
|
+
* - All matches operate on the FULL command string, not parsed
|
|
25
|
+
* argv. This is deliberately permissive on whitespace but strict
|
|
26
|
+
* on operator characters (`|`, `&`, `;`, `>`, backticks) — a
|
|
27
|
+
* pipe-into-shell или command-chain forces fallback к ask.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Catastrophic patterns — the auto-mode regex denylist. Each entry
|
|
31
|
+
* carries a human-readable reason surfaced в the deny payload so the
|
|
32
|
+
* operator + audit log see why the gate refused. Order matters: most-
|
|
33
|
+
* specific первой так "rm -rf /" reports as that, не the generic
|
|
34
|
+
* "rm -rf".
|
|
35
|
+
*/
|
|
36
|
+
const AUTO_DENY_PATTERNS = [
|
|
37
|
+
{ pattern: /\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\b/i, reason: 'rm -rf (recursive force-delete)' },
|
|
38
|
+
{ pattern: /\bgit\s+push\s+(-{1,2}force\b|\-f\b)/i, reason: 'git push --force (history rewrite)' },
|
|
39
|
+
{ pattern: /\bgit\s+reset\s+--hard\b/i, reason: 'git reset --hard (uncommitted-work loss)' },
|
|
40
|
+
{ pattern: /\bdd\s+if=\/(dev|)/i, reason: 'dd if=/dev/* (raw device read/write)' },
|
|
41
|
+
{ pattern: /\bmkfs(\.|\s|$)/i, reason: 'mkfs (filesystem format)' },
|
|
42
|
+
{ pattern: /\bchmod\s+-R\s+777\b/i, reason: 'chmod -R 777 (world-writable recursive)' },
|
|
43
|
+
{ pattern: /\bchown\s+-R\b/i, reason: 'chown -R (recursive ownership change)' },
|
|
44
|
+
{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:/, reason: 'fork bomb signature' },
|
|
45
|
+
{ pattern: /\bsudo\b/i, reason: 'sudo (privilege escalation)' },
|
|
46
|
+
{ pattern: /\b(npm|pnpm|yarn)\s+publish\b/i, reason: 'package publish (irreversible npm release)' },
|
|
47
|
+
{ pattern: /\b(curl|wget)\b[^|;&]*\|\s*(sh|bash|zsh)\b/i, reason: 'pipe-to-shell installer (curl … | sh)' },
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Safe-by-default patterns — auto-mode regex allowlist. Each regex
|
|
51
|
+
* must match the FULL command (with `^…$` anchors) so a leading
|
|
52
|
+
* `sudo ls` или a trailing `; rm -rf /` does NOT slip through. The
|
|
53
|
+
* caller passes the trimmed command string; whitespace around argv
|
|
54
|
+
* tokens is tolerated.
|
|
55
|
+
*/
|
|
56
|
+
const AUTO_ALLOW_PATTERNS = [
|
|
57
|
+
{ pattern: /^ls(\s+-[a-zA-Z]+)*(\s+[^|;&`>$()\\]+)?$/, reason: 'ls (directory listing)' },
|
|
58
|
+
{ pattern: /^pwd\s*$/, reason: 'pwd (working directory)' },
|
|
59
|
+
{ pattern: /^cat\s+[^|;&`>$()\\]+$/, reason: 'cat (file read)' },
|
|
60
|
+
{ pattern: /^head(\s+-n?\s*\d+)?\s+[^|;&`>$()\\]+$/, reason: 'head (file preview)' },
|
|
61
|
+
{ pattern: /^tail(\s+-n?\s*\d+)?\s+[^|;&`>$()\\]+$/, reason: 'tail (file preview)' },
|
|
62
|
+
{ pattern: /^wc(\s+-[a-z]+)?\s+[^|;&`>$()\\]+$/, reason: 'wc (line/word count)' },
|
|
63
|
+
{ pattern: /^du\s+-sh?\s+[^|;&`>$()\\]+$/, reason: 'du -sh (disk usage summary)' },
|
|
64
|
+
{ pattern: /^df\s+-h\s*$/, reason: 'df -h (filesystem free space)' },
|
|
65
|
+
{ pattern: /^git\s+status(\s+--short|\s+-s)?\s*$/, reason: 'git status' },
|
|
66
|
+
{ pattern: /^git\s+diff(\s+[a-zA-Z0-9_./~^-]+)*\s*$/, reason: 'git diff (read-only)' },
|
|
67
|
+
{ pattern: /^git\s+log(\s+[a-zA-Z0-9_./~^-]+)*\s*$/, reason: 'git log (read-only)' },
|
|
68
|
+
{ pattern: /^git\s+branch(\s+-[a-z]+)?\s*$/, reason: 'git branch (read-only)' },
|
|
69
|
+
{ pattern: /^git\s+remote\s+-v\s*$/, reason: 'git remote -v (read-only)' },
|
|
70
|
+
{ pattern: /^pnpm\s+(typecheck|lint|test\s+--run|test\s+--watch=false)\s*$/, reason: 'pnpm read-only build check' },
|
|
71
|
+
{ pattern: /^npm\s+(--version|-v|run\s+typecheck|run\s+lint)\s*$/, reason: 'npm read-only check' },
|
|
72
|
+
{ pattern: /^node\s+--version\s*$/, reason: 'node --version' },
|
|
73
|
+
{ pattern: /^pnpm\s+--version\s*$/, reason: 'pnpm --version' },
|
|
74
|
+
{ pattern: /^which\s+[a-zA-Z0-9_-]+\s*$/, reason: 'which (command lookup)' },
|
|
75
|
+
{ pattern: /^find\s+\.\s+-type\s+f(\s+-name\s+[^|;&`>$()\\]+)?\s*$/, reason: 'find -type f (read-only)' },
|
|
76
|
+
{ pattern: /^(rg|ripgrep|grep)\s+(-[a-z]+\s+)*[^|;&`>$()\\]+(\s+[^|;&`>$()\\]+)?\s*$/, reason: 'grep/ripgrep (read-only search)' },
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* Classify an auto-mode command. Order:
|
|
80
|
+
* 1. Catastrophic deny patterns — surface the explicit deny reason.
|
|
81
|
+
* 2. Safe allow patterns — surface the matched reason.
|
|
82
|
+
* 3. Fallback к ask.
|
|
83
|
+
*
|
|
84
|
+
* The order matters: a destructive pattern that ALSO looks like a
|
|
85
|
+
* read-only token (e.g. `git diff ; rm -rf .`) hits deny first because
|
|
86
|
+
* the allow patterns require `^…$` anchors that the chained command
|
|
87
|
+
* fails to satisfy. Belt + suspenders.
|
|
88
|
+
*/
|
|
89
|
+
export function classifyAutoMode(command) {
|
|
90
|
+
const trimmed = command.trim();
|
|
91
|
+
if (trimmed.length === 0)
|
|
92
|
+
return { verdict: 'ask' };
|
|
93
|
+
for (const entry of AUTO_DENY_PATTERNS) {
|
|
94
|
+
if (entry.pattern.test(trimmed)) {
|
|
95
|
+
return {
|
|
96
|
+
verdict: 'deny',
|
|
97
|
+
reason: entry.reason,
|
|
98
|
+
pattern: entry.pattern.source,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const entry of AUTO_ALLOW_PATTERNS) {
|
|
103
|
+
if (entry.pattern.test(trimmed)) {
|
|
104
|
+
return {
|
|
105
|
+
verdict: 'allow',
|
|
106
|
+
reason: entry.reason,
|
|
107
|
+
pattern: entry.pattern.source,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { verdict: 'ask' };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Diagnostic accessors — exposed для doctor surfaces + spec coverage.
|
|
115
|
+
* The arrays are frozen at module load so callers can iterate without
|
|
116
|
+
* mutating the source-of-truth.
|
|
117
|
+
*/
|
|
118
|
+
export function listAutoAllowPatterns() {
|
|
119
|
+
return AUTO_ALLOW_PATTERNS.map((e) => ({ pattern: e.pattern.source, reason: e.reason }));
|
|
120
|
+
}
|
|
121
|
+
export function listAutoDenyPatterns() {
|
|
122
|
+
return AUTO_DENY_PATTERNS.map((e) => ({ pattern: e.pattern.source, reason: e.reason }));
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=auto-classifier.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bypassPermissions circuit-breaker — Wave 7.
|
|
3
|
+
*
|
|
4
|
+
* `bypassPermissions` is "skip ALL checks" for trusted scripted runs.
|
|
5
|
+
* Even so, certain commands are catastrophic enough that the gate
|
|
6
|
+
* MUST refuse regardless of mode. This module owns that short list.
|
|
7
|
+
*
|
|
8
|
+
* The breaker is conservative on purpose:
|
|
9
|
+
* - rm -rf against `/`, `~`, or workspace root (`.`)
|
|
10
|
+
* - fork bomb signature (`:(){:|:&};:`)
|
|
11
|
+
* - dd if=/ (raw block-device read or write)
|
|
12
|
+
*
|
|
13
|
+
* False positives are acceptable here — an operator who really wants
|
|
14
|
+
* to nuke their root filesystem can switch to `dontAsk` and re-issue;
|
|
15
|
+
* the breaker is the "are you sure you typed this correctly?" guard,
|
|
16
|
+
* not a hard policy boundary.
|
|
17
|
+
*
|
|
18
|
+
* `evaluateCircuitBreaker` is pure regex matching — no IO, no state.
|
|
19
|
+
* It's called by the gate before any other routing so a bypass-mode
|
|
20
|
+
* session that types `rm -rf /` sees the deny path first.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Pattern list — kept narrow on purpose. Each entry must match the
|
|
24
|
+
* canonical destructive shape; argv variants without the exact form
|
|
25
|
+
* fall through к the regular `dontAsk` / `bypassPermissions` allow
|
|
26
|
+
* path, which is what the operator opted into.
|
|
27
|
+
*/
|
|
28
|
+
const CIRCUIT_BREAKER_PATTERNS = [
|
|
29
|
+
// rm -rf against absolute root, $HOME, ~, $WORKSPACE_ROOT, or `.`
|
|
30
|
+
// with no further token. The negative lookahead на `[/~.]\S` makes
|
|
31
|
+
// sure `rm -rf /tmp/foo` (specific subtree) doesn't trip — only the
|
|
32
|
+
// catastrophic `rm -rf /` or `rm -rf ~` or `rm -rf .` shapes do.
|
|
33
|
+
{
|
|
34
|
+
pattern: /\brm\s+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r)\s+(\/|~|\$HOME|\$\{HOME\}|\.)\s*$/i,
|
|
35
|
+
reason: 'rm -rf against /, $HOME, ~, or workspace root',
|
|
36
|
+
},
|
|
37
|
+
// Fork bomb signature. Whitespace-tolerant но shape-strict.
|
|
38
|
+
{
|
|
39
|
+
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:/,
|
|
40
|
+
reason: 'fork bomb (`:(){ :|:& };:`)',
|
|
41
|
+
},
|
|
42
|
+
// dd to/from raw devices. Either direction is catastrophic enough
|
|
43
|
+
// to warrant the breaker (read of /dev/random into a workspace file
|
|
44
|
+
// can fill the disk, write to /dev/sda destroys the disk).
|
|
45
|
+
{
|
|
46
|
+
pattern: /\bdd\b[^|;&]*\b(if|of)=\/dev\//i,
|
|
47
|
+
reason: 'dd reading/writing /dev/* (catastrophic IO)',
|
|
48
|
+
},
|
|
49
|
+
// mkfs against any disk — single regex covers ext*, xfs, btrfs, vfat.
|
|
50
|
+
{
|
|
51
|
+
pattern: /\bmkfs(\.[a-z0-9]+)?\s+\/dev\//i,
|
|
52
|
+
reason: 'mkfs against /dev/* (filesystem format)',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Test the command against every circuit-breaker pattern. Returns the
|
|
57
|
+
* first match (most-catastrophic-first ordering is encoded в the array
|
|
58
|
+
* order); when no pattern matches, the breaker is `tripped: false` so
|
|
59
|
+
* the caller proceeds to the regular gate decision.
|
|
60
|
+
*
|
|
61
|
+
* Pure function — no IO, no module-scoped state. Safe to call from any
|
|
62
|
+
* surface (gate, doctor command, audit replay).
|
|
63
|
+
*/
|
|
64
|
+
export function evaluateCircuitBreaker(command) {
|
|
65
|
+
const trimmed = command.trim();
|
|
66
|
+
if (trimmed.length === 0)
|
|
67
|
+
return { tripped: false, reason: '' };
|
|
68
|
+
for (const entry of CIRCUIT_BREAKER_PATTERNS) {
|
|
69
|
+
if (entry.pattern.test(trimmed)) {
|
|
70
|
+
return { tripped: true, reason: entry.reason };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { tripped: false, reason: '' };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Diagnostic accessor — exposed для doctor surfaces + spec coverage so
|
|
77
|
+
* the test layer can iterate the full list and assert each entry trips
|
|
78
|
+
* on representative input.
|
|
79
|
+
*/
|
|
80
|
+
export function listCircuitBreakerPatterns() {
|
|
81
|
+
return CIRCUIT_BREAKER_PATTERNS.map((e) => ({ pattern: e.pattern.source, reason: e.reason }));
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission gate — Wave 7 canonical 6-mode enforcement (CC parity).
|
|
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
|