@yagni-app/code 1.0.5 → 1.0.7

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.
Files changed (111) hide show
  1. package/README.md +89 -27
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -3
  5. package/dist/claudePlugins.js +129 -19
  6. package/dist/cli.js +28 -4
  7. package/dist/doctor.d.ts +42 -4
  8. package/dist/doctor.js +150 -15
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +18 -5
  12. package/dist/extension/index.js +319 -72
  13. package/dist/extension/mcp/approval.d.ts +45 -0
  14. package/dist/extension/mcp/approval.js +164 -0
  15. package/dist/extension/mcp/auth.d.ts +124 -0
  16. package/dist/extension/mcp/auth.js +560 -0
  17. package/dist/extension/mcp/authStore.d.ts +61 -0
  18. package/dist/extension/mcp/authStore.js +105 -0
  19. package/dist/extension/mcp/callbackPage.d.ts +31 -0
  20. package/dist/extension/mcp/callbackPage.js +222 -0
  21. package/dist/extension/mcp/cliConfig.d.ts +12 -0
  22. package/dist/extension/mcp/cliConfig.js +12 -0
  23. package/dist/extension/mcp/config.d.ts +153 -0
  24. package/dist/extension/mcp/config.js +381 -0
  25. package/dist/extension/mcp/log.d.ts +28 -0
  26. package/dist/extension/mcp/log.js +82 -0
  27. package/dist/extension/mcp/manager.d.ts +100 -0
  28. package/dist/extension/mcp/manager.js +273 -0
  29. package/dist/extension/mcp/names.d.ts +25 -0
  30. package/dist/extension/mcp/names.js +40 -0
  31. package/dist/extension/mcp/panel.d.ts +33 -0
  32. package/dist/extension/mcp/panel.js +268 -0
  33. package/dist/extension/mcp/prompts.d.ts +23 -0
  34. package/dist/extension/mcp/prompts.js +93 -0
  35. package/dist/extension/mcp/startup.d.ts +55 -0
  36. package/dist/extension/mcp/startup.js +152 -0
  37. package/dist/extension/mcp/tools.d.ts +31 -0
  38. package/dist/extension/mcp/tools.js +117 -0
  39. package/dist/extension/mcp/transports.d.ts +17 -0
  40. package/dist/extension/mcp/transports.js +44 -0
  41. package/dist/extension/permission/gate.d.ts +104 -2
  42. package/dist/extension/permission/gate.js +387 -31
  43. package/dist/extension/permission/guardian.d.ts +24 -5
  44. package/dist/extension/permission/guardian.js +162 -24
  45. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  46. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  47. package/dist/extension/permissionRules/engine.d.ts +50 -0
  48. package/dist/extension/permissionRules/engine.js +238 -0
  49. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  50. package/dist/extension/permissionRules/loadConfig.js +90 -0
  51. package/dist/extension/permissionRules/parser.d.ts +38 -0
  52. package/dist/extension/permissionRules/parser.js +136 -0
  53. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  54. package/dist/extension/permissionRules/pathRules.js +120 -0
  55. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  56. package/dist/extension/permissionRules/shellRules.js +221 -0
  57. package/dist/extension/pipeline/invocation.d.ts +3 -6
  58. package/dist/extension/pipeline/invocation.js +3 -6
  59. package/dist/extension/pipeline/personas.js +5 -0
  60. package/dist/extension/pipeline/runner.d.ts +0 -1
  61. package/dist/extension/pipeline/runner.js +6 -14
  62. package/dist/extension/plugins/inventory.d.ts +88 -0
  63. package/dist/extension/plugins/inventory.js +144 -0
  64. package/dist/extension/plugins/panel.d.ts +45 -0
  65. package/dist/extension/plugins/panel.js +293 -0
  66. package/dist/extension/sandbox/bash.d.ts +99 -0
  67. package/dist/extension/sandbox/bash.js +190 -0
  68. package/dist/extension/sandbox/config.d.ts +114 -0
  69. package/dist/extension/sandbox/config.js +366 -0
  70. package/dist/extension/sandbox/manager.d.ts +98 -0
  71. package/dist/extension/sandbox/manager.js +216 -0
  72. package/dist/extension/sandbox/panel.d.ts +111 -0
  73. package/dist/extension/sandbox/panel.js +342 -0
  74. package/dist/extension/sandbox/session.d.ts +85 -0
  75. package/dist/extension/sandbox/session.js +775 -0
  76. package/dist/extension/telemetry/attrs.d.ts +96 -0
  77. package/dist/extension/telemetry/attrs.js +149 -0
  78. package/dist/extension/telemetry/config.d.ts +99 -0
  79. package/dist/extension/telemetry/config.js +193 -0
  80. package/dist/extension/telemetry/index.d.ts +7 -0
  81. package/dist/extension/telemetry/index.js +7 -0
  82. package/dist/extension/telemetry/probe.d.ts +29 -0
  83. package/dist/extension/telemetry/probe.js +122 -0
  84. package/dist/extension/telemetry/register.d.ts +40 -0
  85. package/dist/extension/telemetry/register.js +192 -0
  86. package/dist/extension/telemetry/sdk.d.ts +63 -0
  87. package/dist/extension/telemetry/sdk.js +207 -0
  88. package/dist/extension/telemetry/tracker.d.ts +131 -0
  89. package/dist/extension/telemetry/tracker.js +551 -0
  90. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  91. package/dist/extension/vendor/ignore.d.ts +86 -0
  92. package/dist/extension/vendor/ignore.js +788 -0
  93. package/dist/goHeadless.d.ts +1 -1
  94. package/dist/goHeadless.js +2 -2
  95. package/dist/launch.d.ts +4 -3
  96. package/dist/launch.js +7 -4
  97. package/dist/mcpCommand.d.ts +122 -0
  98. package/dist/mcpCommand.js +787 -0
  99. package/dist/otel.d.ts +69 -63
  100. package/dist/otel.js +154 -119
  101. package/dist/paths.d.ts +13 -0
  102. package/dist/paths.js +18 -0
  103. package/dist/pluginCommand.d.ts +43 -0
  104. package/dist/pluginCommand.js +499 -0
  105. package/dist/pluginStore.d.ts +170 -0
  106. package/dist/pluginStore.js +554 -0
  107. package/dist/upgrade.d.ts +11 -2
  108. package/dist/upgrade.js +48 -8
  109. package/package.json +20 -3
  110. package/dist/extension/mcpTools.d.ts +0 -57
  111. package/dist/extension/mcpTools.js +0 -132
@@ -99,6 +99,14 @@ export interface CircuitBreakerResult {
99
99
  reason?: string;
100
100
  }
101
101
  export declare function checkCircuitBreaker(state: GuardianState, limits: GuardianLimits): CircuitBreakerResult;
102
+ export interface ParsedVerdict {
103
+ verdict: GuardianVerdict;
104
+ /** True when the strict parse failed and the lenient repair ladder salvaged it. */
105
+ repaired: boolean;
106
+ }
107
+ export declare function parseVerdictDetailed(raw: string): ParsedVerdict | null;
108
+ /** Strict-shaped convenience wrapper: the verdict, or null. Callers that need
109
+ * the repaired signal use {@link parseVerdictDetailed}. */
102
110
  export declare function parseVerdict(raw: string): GuardianVerdict | null;
103
111
  export declare function formatGuardianSubtotal(state: GuardianState, limits: GuardianLimits): string;
104
112
  export type GuardianError = "timeout" | "malformed" | "network" | "empty" | "aborted";
@@ -106,11 +114,17 @@ export interface ReviewResult {
106
114
  verdict: GuardianVerdict | null;
107
115
  error?: GuardianError;
108
116
  cost: number;
117
+ /** True when the verdict came from the lenient repair ladder (strict parse
118
+ * failed first). The verdict is real and flows through the normal
119
+ * allow/ask/deny handling; this flag only marks it for telemetry. */
120
+ repaired?: boolean;
109
121
  /**
110
122
  * Scrubbed + capped copy of the model output when the verdict failed to
111
- * parse (`error: "malformed"`). Present so the sink can capture the exact
112
- * failure shape. Never contains the raw command unredacted: `scrubSecrets`
113
- * removes secret-shaped values before this is stored.
123
+ * parse (`error: "malformed"`), or of the pre-repair extracted block when
124
+ * the repair ladder salvaged it (`repaired: true`). Present so the sink can
125
+ * capture the exact failure shape either way. Never contains the raw
126
+ * command unredacted: `scrubSecrets` removes secret-shaped values before
127
+ * this is stored.
114
128
  */
115
129
  rawOutput?: string;
116
130
  }
@@ -152,14 +166,18 @@ export interface GuardianDiagnosticEvent {
152
166
  outcome: GuardianOutcome | GuardianError;
153
167
  durationMs?: number;
154
168
  tier?: string;
169
+ /** True when the lenient repair ladder salvaged a broken verdict — the
170
+ * outcome is still the REAL verdict (allow/ask/deny); this flag marks it
171
+ * for telemetry so repair hit-rate is measurable. */
172
+ repaired?: true;
155
173
  /** Debug-only: command hash for correlation (never the raw command). */
156
174
  commandHash?: string;
157
175
  /** Debug-only: the Guardian's rationale. */
158
176
  rationale?: string;
159
177
  /**
160
178
  * Scrubbed + capped copy of the unparseable model output, present only for
161
- * `outcome: "malformed"`. Always-on (NOT debug-gated): it is already
162
- * `scrubSecrets`-redacted and size-capped at the source.
179
+ * `outcome: "malformed"` or `repaired: true`. Always-on (NOT debug-gated):
180
+ * it is already `scrubSecrets`-redacted and size-capped at the source.
163
181
  */
164
182
  rawOutput?: string;
165
183
  }
@@ -170,6 +188,7 @@ export interface GuardianDiagnosticEvent {
170
188
  export declare function buildDiagnosticEvent(outcome: GuardianOutcome | GuardianError, opts: {
171
189
  durationMs?: number;
172
190
  tier?: string;
191
+ repaired?: boolean;
173
192
  rationale?: string;
174
193
  commandHash?: string;
175
194
  rawOutput?: string;
@@ -128,33 +128,158 @@ export function checkCircuitBreaker(state, limits) {
128
128
  }
129
129
  return { tripped: false };
130
130
  }
131
- // --- Verdict parsing (fail closed on malformed) ---
132
- export function parseVerdict(raw) {
131
+ const VALID_RISK_LEVELS = ["low", "medium", "high", "critical"];
132
+ /** Validate an already-parsed object into a verdict. The SINGLE validation
133
+ * gate: strict and repaired parses both end here, so the enum checks stay
134
+ * exactly as strict after repair as before. */
135
+ function validateVerdictObject(parsed) {
136
+ if (!parsed)
137
+ return null;
138
+ const outcome = parsed.outcome;
139
+ if (outcome !== "allow" && outcome !== "ask" && outcome !== "deny")
140
+ return null;
141
+ const riskLevel = parsed.riskLevel;
142
+ return {
143
+ outcome,
144
+ riskLevel: typeof riskLevel === "string" && VALID_RISK_LEVELS.includes(riskLevel)
145
+ ? riskLevel
146
+ : "medium",
147
+ rationale: typeof parsed.rationale === "string" && parsed.rationale.trim().length > 0
148
+ ? parsed.rationale.trim()
149
+ : "No rationale provided.",
150
+ };
151
+ }
152
+ /** Extract the first {...} block (efficient-tier models may wrap JSON in
153
+ * markdown fences despite instructions to output raw JSON). */
154
+ function extractJsonBlock(raw) {
155
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
156
+ return jsonMatch ? jsonMatch[0] : raw;
157
+ }
158
+ /**
159
+ * The lenient repair ladder — applied ONLY after a strict `JSON.parse` of the
160
+ * extracted block has already thrown. Each rung fixes one observed model
161
+ * failure shape and immediately retries the strict parse; the ladder runs in
162
+ * cheapest-first order and the content-synthesizing re-quote is always LAST.
163
+ * Every rung is end-anchored or last-occurrence-anchored and linear-time.
164
+ * Returns the repaired JSON string (parsable) or null if no rung fits.
165
+ *
166
+ * Observed shapes (captured live on deepseek-v4-flash, all previously "unclear
167
+ * verdict"): illegal backslash escapes (\d written literally), a doubled
168
+ * closing quote, and unescaped quotes inside the rationale. The trailing
169
+ * comma is the most common LLM JSON slip in the wild, not (yet) observed here.
170
+ *
171
+ * What the ladder deliberately does NOT fix (all fail closed):
172
+ * single quotes as structure (ambiguous with English apostrophes), glued
173
+ * multi-object output (never pick one verdict of two), raw newlines inside
174
+ * strings (indistinguishable from legal pretty-printed whitespace without
175
+ * parsing), a broken prefix before the rationale anchor, and truncations.
176
+ */
177
+ function repairVerdictJson(s) {
178
+ // The ladder is SEQUENTIAL: each rung transforms the previous result and
179
+ // immediately retries the strict parse. Composing matters — a blob can
180
+ // carry more than one failure shape (an illegal backslash escape AND a
181
+ // trailing comma, say), and each rung alone would leave the other broken.
182
+ let cur = s;
183
+ // Rung 1 — illegal backslash escapes (\d, \w, \( … become \\d). Legal JSON
184
+ // escapes (" \\ \/ b f n r t and \uXXXX) are left untouched by the lookahead.
185
+ const escapedBackslashes = cur.replace(/\\(?!["\\\/bfnrtu])/g, "\\\\");
186
+ if (escapedBackslashes !== cur) {
187
+ try {
188
+ JSON.parse(escapedBackslashes);
189
+ return escapedBackslashes;
190
+ }
191
+ catch {
192
+ cur = escapedBackslashes;
193
+ }
194
+ }
195
+ // Rung 2 — doubled closing quote before }: anchored to END so a doubled
196
+ // quote mid-rationale is untouched (the re-quote rung handles that shape).
197
+ const collapsedQuote = cur.replace(/""(\s*\})$/, '"$1');
198
+ if (collapsedQuote !== cur) {
199
+ try {
200
+ JSON.parse(collapsedQuote);
201
+ return collapsedQuote;
202
+ }
203
+ catch {
204
+ cur = collapsedQuote;
205
+ }
206
+ }
207
+ // Rung 3 — trailing comma before }: anchored to END so a comma inside a
208
+ // rationale value ("a, b") can never be stripped.
209
+ const strippedComma = cur.replace(/,(\s*\})$/, '$1');
210
+ if (strippedComma !== cur) {
211
+ try {
212
+ JSON.parse(strippedComma);
213
+ return strippedComma;
214
+ }
215
+ catch {
216
+ cur = strippedComma;
217
+ }
218
+ }
219
+ // Rung 4 (last resort, the only rung that synthesizes content) — re-quote
220
+ // the trailing rationale value. Applies only when the shape is
221
+ // {…"rationale":"<rest-to-end>}: the prefix before the anchor must carry no
222
+ // closing brace (rules out prose and glued multi-object output — an earlier
223
+ // sibling object always leaves a } behind), and the extracted rationale text
224
+ // must carry no braces either (rules out a glued TAIL). Every " and \ in the
225
+ // tail is escaped in ONE pass so whatever the model wrote inside the
226
+ // rationale becomes literal text. Anchor ambiguity resolves to the LAST
227
+ // "rationale":" occurrence; single pass, no loops.
228
+ const anchor = cur.lastIndexOf('"rationale":"');
229
+ if (anchor !== -1) {
230
+ const prefix = cur.slice(0, anchor);
231
+ const tail = cur.slice(anchor + '"rationale":"'.length);
232
+ // The tail must be the trailing value closing the object: non-empty
233
+ // content, an optional-whitespace + } at the very end.
234
+ const tailMatch = tail.match(/^(.+?)(\s*\})$/);
235
+ if (tailMatch && !prefix.includes("}")) {
236
+ const rawRationale = tailMatch[1].replace(/"+$/, "");
237
+ if (rawRationale.length > 0 &&
238
+ !rawRationale.includes("{") &&
239
+ !rawRationale.includes("}")) {
240
+ const reQuoted = prefix +
241
+ '"rationale":"' +
242
+ rawRationale.replace(/["\\]/g, "\\$&") +
243
+ '"}';
244
+ try {
245
+ JSON.parse(reQuoted);
246
+ return reQuoted;
247
+ }
248
+ catch {
249
+ /* fail closed */
250
+ }
251
+ }
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+ export function parseVerdictDetailed(raw) {
257
+ const jsonStr = extractJsonBlock(raw);
133
258
  try {
134
- // Efficient-tier models may wrap JSON in markdown fences despite
135
- // instructions to output raw JSON. Extract the first {...} block.
136
- const jsonMatch = raw.match(/\{[\s\S]*\}/);
137
- const jsonStr = jsonMatch ? jsonMatch[0] : raw;
138
259
  const parsed = JSON.parse(jsonStr);
139
- const outcome = parsed?.outcome;
140
- if (outcome !== "allow" && outcome !== "ask" && outcome !== "deny")
141
- return null;
142
- const riskLevel = parsed.riskLevel;
143
- const validLevels = ["low", "medium", "high", "critical"];
144
- return {
145
- outcome,
146
- riskLevel: typeof riskLevel === "string" && validLevels.includes(riskLevel)
147
- ? riskLevel
148
- : "medium",
149
- rationale: typeof parsed.rationale === "string" && parsed.rationale.trim().length > 0
150
- ? parsed.rationale.trim()
151
- : "No rationale provided.",
152
- };
260
+ const verdict = validateVerdictObject(parsed);
261
+ return verdict ? { verdict, repaired: false } : null;
153
262
  }
154
263
  catch {
155
- return null;
264
+ // Strict parse failed — try the repair ladder on the same extracted block.
265
+ const repairedJson = repairVerdictJson(jsonStr);
266
+ if (repairedJson === null)
267
+ return null;
268
+ try {
269
+ const parsed = JSON.parse(repairedJson);
270
+ const verdict = validateVerdictObject(parsed);
271
+ return verdict ? { verdict, repaired: true } : null;
272
+ }
273
+ catch {
274
+ return null;
275
+ }
156
276
  }
157
277
  }
278
+ /** Strict-shaped convenience wrapper: the verdict, or null. Callers that need
279
+ * the repaired signal use {@link parseVerdictDetailed}. */
280
+ export function parseVerdict(raw) {
281
+ return parseVerdictDetailed(raw)?.verdict ?? null;
282
+ }
158
283
  // --- /cost subtotal ---
159
284
  export function formatGuardianSubtotal(state, limits) {
160
285
  if (state.reviews === 0)
@@ -236,8 +361,8 @@ export async function reviewCommand(command, deps) {
236
361
  }
237
362
  return { verdict: null, error: "empty", cost };
238
363
  }
239
- const verdict = parseVerdict(output);
240
- if (!verdict) {
364
+ const parsed = parseVerdictDetailed(output);
365
+ if (!parsed) {
241
366
  // Scrubbed + capped so the local sink and Sentry can see the exact
242
367
  // failure shape without carrying a raw command or a secret it echoed.
243
368
  return {
@@ -247,7 +372,19 @@ export async function reviewCommand(command, deps) {
247
372
  rawOutput: scrubSecrets(output).slice(0, GUARDIAN_RAW_OUTPUT_CAP),
248
373
  };
249
374
  }
250
- return { verdict, cost };
375
+ if (parsed.repaired) {
376
+ // The repair ladder salvaged a broken-but-salvageable verdict. The
377
+ // verdict is real (validation is exactly as strict as the happy path);
378
+ // capture the pre-repair shape so telemetry can keep watching what the
379
+ // model is still emitting wrong.
380
+ return {
381
+ verdict: parsed.verdict,
382
+ cost,
383
+ repaired: true,
384
+ rawOutput: scrubSecrets(output).slice(0, GUARDIAN_RAW_OUTPUT_CAP),
385
+ };
386
+ }
387
+ return { verdict: parsed.verdict, cost };
251
388
  }
252
389
  catch (err) {
253
390
  // Distinguish the caller aborting (user hit ESC — must NOT be treated as
@@ -275,6 +412,7 @@ export function buildDiagnosticEvent(outcome, opts) {
275
412
  outcome,
276
413
  ...(opts.durationMs !== undefined ? { durationMs: opts.durationMs } : {}),
277
414
  ...(opts.tier !== undefined ? { tier: opts.tier } : {}),
415
+ ...(opts.repaired ? { repaired: true } : {}),
278
416
  ...(opts.rawOutput !== undefined ? { rawOutput: opts.rawOutput } : {}),
279
417
  };
280
418
  if (opts.debug) {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Curated bash command → file-operation table.
3
+ *
4
+ * The bridge that makes Read/Edit deny rules bite on bash commands: for a
5
+ * curated set of commands, extract the file paths they touch, classify each
6
+ * touch as read or write, and run those paths through the same path-rule
7
+ * engine. `cat .env`, `sed -i .env`, `cp .env x`, `echo x > .env` are all
8
+ * caught by a `Read(.env)`/`Edit(.env)` deny.
9
+ *
10
+ * Ported from Claude Code's BashTool/pathValidation.ts (PATH_EXTRACTORS +
11
+ * COMMAND_OPERATION_TYPE), adapted to shellParse tokens. Guardrails (same
12
+ * posture as Claude Code):
13
+ * - unknown flags on commands where flags can change path meaning
14
+ * (mv/cp --target-directory) → the command is NOT auto-analyzed; it
15
+ * degrades to the caller's "ask" path, never silently allowed.
16
+ * - glob arguments in write position → not analyzed (write globs bypass
17
+ * checks in Claude Code; we degrade to ask).
18
+ * - anything unextractable → no opinion; caller decides.
19
+ */
20
+ export type FileOperation = "read" | "write";
21
+ /** A command's opinion: extracted (op, paths) or needs-ask. */
22
+ export type BashFileArgsResult = {
23
+ kind: "analyzed";
24
+ operation: FileOperation;
25
+ paths: string[];
26
+ } | {
27
+ kind: "unknown";
28
+ };
29
+ /** Commands whose file paths we can extract confidently. */
30
+ export declare const COMMAND_OPERATION: Record<string, FileOperation>;
31
+ /**
32
+ * Analyze one bash SUBCOMMAND string. `kind: "unknown"` means no confident
33
+ * opinion — the caller degrades to ask for deny-evaluation purposes (never
34
+ * to silent allow).
35
+ */
36
+ export declare function analyzeSubcommand(subcommand: string): BashFileArgsResult;
37
+ /** Split a full command into subcommand strings (reuses shellRules' splitter). */
38
+ export { splitSubcommands } from "./shellRules.js";
39
+ //# sourceMappingURL=bashFileArgs.d.ts.map
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Curated bash command → file-operation table.
3
+ *
4
+ * The bridge that makes Read/Edit deny rules bite on bash commands: for a
5
+ * curated set of commands, extract the file paths they touch, classify each
6
+ * touch as read or write, and run those paths through the same path-rule
7
+ * engine. `cat .env`, `sed -i .env`, `cp .env x`, `echo x > .env` are all
8
+ * caught by a `Read(.env)`/`Edit(.env)` deny.
9
+ *
10
+ * Ported from Claude Code's BashTool/pathValidation.ts (PATH_EXTRACTORS +
11
+ * COMMAND_OPERATION_TYPE), adapted to shellParse tokens. Guardrails (same
12
+ * posture as Claude Code):
13
+ * - unknown flags on commands where flags can change path meaning
14
+ * (mv/cp --target-directory) → the command is NOT auto-analyzed; it
15
+ * degrades to the caller's "ask" path, never silently allowed.
16
+ * - glob arguments in write position → not analyzed (write globs bypass
17
+ * checks in Claude Code; we degrade to ask).
18
+ * - anything unextractable → no opinion; caller decides.
19
+ */
20
+ import { shellParse } from "../permission/execPolicy.js";
21
+ /** Commands whose file paths we can extract confidently. */
22
+ export const COMMAND_OPERATION = {
23
+ // read-only
24
+ cat: "read", head: "read", tail: "read", sort: "read", uniq: "read",
25
+ wc: "read", cut: "read", paste: "read", column: "read", file: "read",
26
+ stat: "read", diff: "read", awk: "read", strings: "read", hexdump: "read",
27
+ od: "read", base64: "read", nl: "read", grep: "read", rg: "read",
28
+ sha256sum: "read", sha1sum: "read", md5sum: "read", jq: "read",
29
+ ls: "read", find: "read", git: "read",
30
+ // mutating on their targets
31
+ rm: "write", rmdir: "write", mv: "write", cp: "write", touch: "write",
32
+ mkdir: "write", sed: "write", tee: "write",
33
+ };
34
+ // --- token helpers (over shellParse output) ---
35
+ function argTokens(command) {
36
+ const parsed = shellParse(command);
37
+ const tokens = [];
38
+ const redirects = [];
39
+ for (const t of parsed) {
40
+ if (typeof t === "string") {
41
+ tokens.push(t);
42
+ }
43
+ else if ("op" in t) {
44
+ if (t.op === "redirect" && t.direction === "out" && typeof t.target === "string") {
45
+ redirects.push(t.target);
46
+ }
47
+ else if (t.op !== "background") {
48
+ // substitution / in-redirect / background: can't analyze confidently
49
+ return null;
50
+ }
51
+ }
52
+ }
53
+ return { tokens, redirects };
54
+ }
55
+ /** Drop flag tokens, honoring `--` end-of-options (Claude's filterOutFlags). */
56
+ function filterOutFlags(args) {
57
+ const out = [];
58
+ let afterDoubleDash = false;
59
+ for (const a of args) {
60
+ if (!afterDoubleDash && a === "--") {
61
+ afterDoubleDash = true;
62
+ continue;
63
+ }
64
+ if (!afterDoubleDash && a.startsWith("-"))
65
+ continue;
66
+ out.push(a);
67
+ }
68
+ return out;
69
+ }
70
+ /** Pattern-first commands (grep/rg/awk/sed-style): first positional is the pattern. */
71
+ function patternCommandPaths(args, flagsWithValues, defaults) {
72
+ const out = [];
73
+ let skipNext = false;
74
+ let patternFound = false;
75
+ let afterDoubleDash = false;
76
+ for (const a of args) {
77
+ if (skipNext) {
78
+ skipNext = false;
79
+ continue;
80
+ }
81
+ if (!afterDoubleDash && a === "--") {
82
+ afterDoubleDash = true;
83
+ continue;
84
+ }
85
+ if (!afterDoubleDash && a.startsWith("-")) {
86
+ const flag = a.split("=")[0];
87
+ if (flagsWithValues.has(flag) && !a.includes("="))
88
+ skipNext = true;
89
+ continue;
90
+ }
91
+ if (!patternFound) {
92
+ patternFound = true;
93
+ continue;
94
+ }
95
+ out.push(a);
96
+ }
97
+ return out.length > 0 ? out : defaults;
98
+ }
99
+ const GREP_FLAGS = new Set(["-e", "--regexp", "-f", "--file", "--exclude", "--include", "--exclude-dir", "--include-dir", "-m", "--max-count", "-A", "--after-context", "-B", "--before-context", "-C", "--context"]);
100
+ const RG_FLAGS = new Set(["-e", "--regexp", "-f", "--file", "-t", "--type", "-T", "--type-not", "-g", "--glob", "-m", "--max-count", "--max-depth", "-r", "--replace", "-A", "--after-context", "-B", "--before-context", "-C", "--context"]);
101
+ // mv/cp validators: ANY flag → unknown (Claude blocks all flags for these —
102
+ // --target-directory=PATH changes path meaning).
103
+ function hasFlag(args) {
104
+ let afterDoubleDash = false;
105
+ for (const a of args) {
106
+ if (!afterDoubleDash && a === "--") {
107
+ afterDoubleDash = true;
108
+ continue;
109
+ }
110
+ if (!afterDoubleDash && a.startsWith("-") && a !== "-")
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ const GLOBISH = /[*?[]/;
116
+ /**
117
+ * Analyze one bash SUBCOMMAND string. `kind: "unknown"` means no confident
118
+ * opinion — the caller degrades to ask for deny-evaluation purposes (never
119
+ * to silent allow).
120
+ */
121
+ export function analyzeSubcommand(subcommand) {
122
+ const parsed = argTokens(subcommand);
123
+ if (!parsed || parsed.tokens.length === 0)
124
+ return { kind: "unknown" };
125
+ // Redirect targets are ALWAYS write access, regardless of the command
126
+ // being curated (Claude Code validates redirect targets independently of
127
+ // the command table). `echo x > .env` must not escape the .env deny.
128
+ if (parsed.redirects.length > 0) {
129
+ const [cmd, ...args] = parsed.tokens;
130
+ const operation = COMMAND_OPERATION[cmd];
131
+ const paths = operation
132
+ ? extractPaths(cmd, args)
133
+ : [];
134
+ if (paths === null)
135
+ return { kind: "unknown" };
136
+ const base = paths.filter((p) => p.length > 0);
137
+ if (operation === "write" && base.some((p) => GLOBISH.test(p)))
138
+ return { kind: "unknown" };
139
+ return { kind: "analyzed", operation: "write", paths: [...base, ...parsed.redirects] };
140
+ }
141
+ const [cmd, ...args] = parsed.tokens;
142
+ const operation = COMMAND_OPERATION[cmd];
143
+ if (!operation)
144
+ return { kind: "unknown" };
145
+ const paths = extractPaths(cmd, args);
146
+ if (paths === null)
147
+ return { kind: "unknown" };
148
+ if (operation === "write" && paths.some((p) => GLOBISH.test(p))) {
149
+ return { kind: "unknown" };
150
+ }
151
+ return { kind: "analyzed", operation, paths: paths.filter((p) => p.length > 0) };
152
+ }
153
+ function extractPaths(cmd, args) {
154
+ switch (cmd) {
155
+ case "grep":
156
+ return patternCommandPaths(args, GREP_FLAGS, []);
157
+ case "rg":
158
+ return patternCommandPaths(args, RG_FLAGS, ["."]);
159
+ case "ls": {
160
+ const out = filterOutFlags(args);
161
+ return out.length > 0 ? out : ["."];
162
+ }
163
+ case "find": {
164
+ // collect positionals until the first real flag
165
+ const out = [];
166
+ let hitFlag = false;
167
+ for (const a of args) {
168
+ if (a.startsWith("-")) {
169
+ hitFlag = true;
170
+ continue;
171
+ }
172
+ if (!hitFlag)
173
+ out.push(a);
174
+ }
175
+ return out.length > 0 ? out : ["."];
176
+ }
177
+ case "git":
178
+ // Only `git diff --no-index` reaches arbitrary files outside the repo;
179
+ // other git subcommands are covered by the repo's own boundaries.
180
+ if (args[0] === "diff" && args.includes("--no-index")) {
181
+ return filterOutFlags(args.slice(1)).slice(0, 2);
182
+ }
183
+ return [];
184
+ case "sed": {
185
+ // -f FILE reads a script file; positionals after the script are files.
186
+ const out = [];
187
+ let skipNext = false;
188
+ let scriptFound = false;
189
+ let afterDoubleDash = false;
190
+ for (const a of args) {
191
+ if (skipNext) {
192
+ skipNext = false;
193
+ continue;
194
+ }
195
+ if (!afterDoubleDash && a === "--") {
196
+ afterDoubleDash = true;
197
+ continue;
198
+ }
199
+ if (!afterDoubleDash && a.startsWith("-")) {
200
+ if (a === "-f" || a === "--file") {
201
+ const f = args[args.indexOf(a) + 1];
202
+ if (f)
203
+ out.push(f);
204
+ skipNext = true;
205
+ scriptFound = true;
206
+ }
207
+ else if (a === "-e" || a === "--expression") {
208
+ skipNext = true;
209
+ scriptFound = true;
210
+ }
211
+ else if (a.includes("e") || a.includes("f")) {
212
+ scriptFound = true;
213
+ }
214
+ continue;
215
+ }
216
+ if (!scriptFound) {
217
+ scriptFound = true;
218
+ continue;
219
+ }
220
+ out.push(a);
221
+ }
222
+ return out;
223
+ }
224
+ case "mv":
225
+ case "cp":
226
+ // ANY flag → unknown (--target-directory=PATH changes path meaning).
227
+ if (hasFlag(args))
228
+ return null;
229
+ return filterOutFlags(args);
230
+ default:
231
+ return filterOutFlags(args);
232
+ }
233
+ }
234
+ /** Split a full command into subcommand strings (reuses shellRules' splitter). */
235
+ export { splitSubcommands } from "./shellRules.js";
236
+ //# sourceMappingURL=bashFileArgs.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Permission-rule evaluation engine.
3
+ *
4
+ * The single entry the gate calls BEFORE hooks/exec-policy/grants/Guardian:
5
+ *
6
+ * evaluateRules(rules, { toolName, params, cwd, isProjectTrusted, ... })
7
+ * → { verdict: "deny" | "ask" | "allow", rule } | null (no opinion)
8
+ *
9
+ * Evaluation order is Claude Code's: for each behavior in deny → ask → allow,
10
+ * find the first rule that matches the tool call. Deny/ask are FINAL from the
11
+ * gate's perspective (nothing downstream may override); allow short-circuits
12
+ * the Guardian but CANNOT lift the exec-policy forbidden band or the
13
+ * alwaysConfirmTools contract (those floors live in the gate wiring, which
14
+ * re-checks them after an allow verdict).
15
+ *
16
+ * Matching by tool:
17
+ * - bash: Bash(command-pattern): prefix, :* suffix, or * wildcards; compound split
18
+ * - read/edit/write: Read(path)/Edit(path) — gitignore path patterns
19
+ * (read-class rules also consult grep/find/ls paths)
20
+ * - web_fetch: WebFetch(domain:pattern) — hostname globs
21
+ * - others: bare tool-name rules (and mcp__server / mcp__server__tool)
22
+ *
23
+ * Edit-class deny also applies to read access of the same path (Claude Code:
24
+ * a Read deny blocks Edit/Write on the path; an Edit deny blocks reads too —
25
+ * "edit access implies read access" cuts both ways for denies).
26
+ */
27
+ import type { PermissionRule, RuleSource } from "./loadConfig.js";
28
+ export type RuleVerdict = "deny" | "ask" | "allow";
29
+ export interface RuleEvaluationContext {
30
+ toolName: string;
31
+ params: Record<string, unknown>;
32
+ cwd: string;
33
+ isProjectTrusted: boolean;
34
+ /** ~/.yagni-code (user `/`-anchor base). */
35
+ userStateHome: string;
36
+ /** Project root for project-source `/`-anchored patterns; null outside a repo. */
37
+ projectRoot: string | null;
38
+ /** Overrides ~ expansion (tests); defaults to os.homedir(). */
39
+ homeDir?: string;
40
+ }
41
+ export interface RuleEvaluation {
42
+ verdict: RuleVerdict;
43
+ rule: PermissionRule;
44
+ /** Which matcher produced the hit (verdict logs / deny messages). */
45
+ matchedVia: "tool_name" | "bash_pattern" | "path_pattern" | "domain_pattern" | "file_args";
46
+ }
47
+ export declare function evaluateRules(rules: readonly PermissionRule[], ctx: RuleEvaluationContext): RuleEvaluation | null;
48
+ /** Parse a raw rule string into a sourced PermissionRule (loader/tests). */
49
+ export declare function ruleFromRaw(raw: string, behavior: PermissionRule["behavior"], source: RuleSource): PermissionRule;
50
+ //# sourceMappingURL=engine.d.ts.map