@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
@@ -0,0 +1,238 @@
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 { mcpInfoFromString } from "../mcp/names.js";
28
+ import { analyzeSubcommand, splitSubcommands } from "./bashFileArgs.js";
29
+ import { effectiveRules } from "./loadConfig.js";
30
+ import { pathClassForTool, pathMatches, resolvePatternRoot } from "./pathRules.js";
31
+ import { parseRuleString } from "./parser.js";
32
+ import { shellRuleMatches } from "./shellRules.js";
33
+ // --- tool-name matching (Claude Code's toolMatchesRule, incl. MCP) ---
34
+ function toolNameMatches(ruleTool, callTool) {
35
+ if (ruleTool === callTool)
36
+ return true;
37
+ if (ruleTool === "*")
38
+ return true;
39
+ // MCP: `mcp__server` matches every tool of that server; `mcp__server__tool`
40
+ // matches exactly; `mcp__server__*` is the glob form.
41
+ const ruleInfo = mcpInfoFromString(ruleTool);
42
+ const callInfo = mcpInfoFromString(callTool);
43
+ if (ruleInfo && callInfo) {
44
+ if (ruleInfo.serverName !== callInfo.serverName)
45
+ return false;
46
+ if (ruleInfo.toolName === undefined || ruleInfo.toolName === "*")
47
+ return true;
48
+ return ruleInfo.toolName === callInfo.toolName;
49
+ }
50
+ return false;
51
+ }
52
+ // --- WebFetch domain matching ---
53
+ function domainMatches(pattern, hostname) {
54
+ const p = pattern.toLowerCase().trim();
55
+ const h = hostname.toLowerCase();
56
+ if (p === "*" || p === "domain:*")
57
+ return true;
58
+ if (!p.includes("*"))
59
+ return p === h;
60
+ // `*.example.com` matches any depth of subdomain (docs semantics); a bare
61
+ // `example.com` does NOT match sub.example.com (exact only).
62
+ if (p.startsWith("*.")) {
63
+ const base = p.slice(2);
64
+ // `*.example.com` matches subdomains ONLY (not the apex) — the strict
65
+ // reading of Claude Code's docs; an apex allow belongs in its own rule.
66
+ return h.endsWith("." + base);
67
+ }
68
+ // Other wildcard placements: simple glob (segment-safe).
69
+ const re = new RegExp("^" + p.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^.]*") + "$");
70
+ return re.test(h);
71
+ }
72
+ function hostnameOf(url) {
73
+ try {
74
+ return new URL(url).hostname;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ // --- main evaluation ---
81
+ export function evaluateRules(rules, ctx) {
82
+ const active = effectiveRules(rules, ctx.isProjectTrusted);
83
+ for (const behavior of ["deny", "ask", "allow"]) {
84
+ const hit = firstMatch(active, behavior, ctx);
85
+ if (hit)
86
+ return { verdict: behavior, ...hit };
87
+ }
88
+ return null;
89
+ }
90
+ function firstMatch(rules, behavior, ctx) {
91
+ // Path-class rules evaluate as ONE ordered compile (gitignore semantics:
92
+ // later lines win, !negation works across rules and files). Non-path rules
93
+ // stay first-match (Claude Code's per-rule loop).
94
+ const pathRules = rules.filter((r) => r.behavior === behavior && r.ruleContent !== undefined && pathClassForTool(r.toolName) !== null);
95
+ if (pathRules.length > 0 && pathClassForTool(ctx.toolName) !== null) {
96
+ const hit = pathListMatch(pathRules, behavior, ctx);
97
+ if (hit)
98
+ return hit;
99
+ }
100
+ for (const rule of rules) {
101
+ if (rule.behavior !== behavior)
102
+ continue;
103
+ if (pathRules.includes(rule) && pathClassForTool(ctx.toolName) !== null)
104
+ continue; // handled above
105
+ const via = ruleMatchesCall(rule, ctx);
106
+ if (via)
107
+ return { rule, matchedVia: via };
108
+ }
109
+ return null;
110
+ }
111
+ /** Ordered whole-list evaluation for path-class rules of one behavior. */
112
+ function pathListMatch(pathRules, behavior, ctx) {
113
+ const callClass = pathClassForTool(ctx.toolName);
114
+ if (!callClass)
115
+ return null;
116
+ const path = pathParam(ctx.params);
117
+ const resolveOpts = {
118
+ userStateHome: ctx.userStateHome,
119
+ projectRoot: ctx.projectRoot,
120
+ ...(ctx.homeDir ? { homeDir: ctx.homeDir } : {}),
121
+ };
122
+ // Class applicability (Claude Code parity):
123
+ // - Edit-class rules apply to edit AND read tools (edit implies read).
124
+ // - Read-class DENY/ASK rules also apply to edit tools ("Read deny rules
125
+ // also block Edit/Write on the same path" — their docs); only Read-class
126
+ // ALLOW is read-tool-only (an allow must never widen).
127
+ const applicable = pathRules.filter((r) => {
128
+ const rc = pathClassForTool(r.toolName);
129
+ if (rc === "edit")
130
+ return true;
131
+ return callClass === "read" || behavior !== "allow";
132
+ });
133
+ if (path && applicable.length > 0) {
134
+ const patterns = applicable.map((r) => resolvePatternRoot(r.ruleContent ?? "", r.source, resolveOpts));
135
+ if (anchoredPathMatches(patterns, path, behavior, ctx)) {
136
+ // Report the LAST rule whose pattern decided the outcome (gitignore's
137
+ // decisive line) — the list is user-before-project, in file order.
138
+ return { rule: applicable[applicable.length - 1], matchedVia: "path_pattern" };
139
+ }
140
+ }
141
+ // The bash bridge runs per-rule (bridge matching is per-command-segment,
142
+ // not an ordered list op) — but only for deny/ask.
143
+ if (ctx.toolName === "bash" && behavior !== "allow") {
144
+ for (const rule of applicable) {
145
+ if (bashBridgeMatches(rule, ctx))
146
+ return { rule, matchedVia: "file_args" };
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+ /** Ordered compile + the allow-vs-deny anchoring asymmetry. */
152
+ function anchoredPathMatches(patterns, targetPath, behavior, ctx) {
153
+ if (!pathMatches(patterns, targetPath, { cwd: ctx.cwd }))
154
+ return false;
155
+ if (behavior !== "allow")
156
+ return true;
157
+ // Rootless single-segment allow patterns are narrowed (Claude Code's
158
+ // asymmetry): only anchored or multi-segment allow patterns may match.
159
+ const last = patterns[patterns.length - 1];
160
+ const content = last.relativePattern;
161
+ if (content.startsWith("/") || content.includes("/"))
162
+ return true;
163
+ return false;
164
+ }
165
+ function ruleMatchesCall(rule, ctx) {
166
+ // Tool-name-level match first (bare rules + MCP shapes).
167
+ if (rule.ruleContent === undefined) {
168
+ return toolNameMatches(rule.toolName, ctx.toolName) ? "tool_name" : null;
169
+ }
170
+ if (rule.toolName === "bash") {
171
+ const command = typeof ctx.params.command === "string" ? ctx.params.command : "";
172
+ if (!command)
173
+ return null;
174
+ return shellRuleMatches(rule.ruleContent, command, rule.behavior) ? "bash_pattern" : null;
175
+ }
176
+ if (rule.toolName === "web_fetch") {
177
+ const url = typeof ctx.params.url === "string" ? ctx.params.url : "";
178
+ const host = hostnameOf(url);
179
+ if (!host)
180
+ return null;
181
+ const m = rule.ruleContent.match(/^domain:(.*)$/);
182
+ if (!m)
183
+ return null;
184
+ return domainMatches(m[1], host) ? "domain_pattern" : null;
185
+ }
186
+ // Path-class rules were already handled by pathListMatch in firstMatch;
187
+ // ruleMatchesCall only sees them for the bash bridge (non-path call tool)
188
+ // or not at all. Keep the bridge branch here for direct callers.
189
+ const ruleClass = pathClassForTool(rule.toolName);
190
+ if (ruleClass) {
191
+ if (ctx.toolName === "bash" && rule.behavior !== "allow") {
192
+ if (bashBridgeMatches(rule, ctx))
193
+ return "file_args";
194
+ }
195
+ return null;
196
+ }
197
+ // Non-path tools with content (e.g. Agent(name)) — exact content match.
198
+ if (rule.ruleContent === ctx.toolName)
199
+ return null;
200
+ return null;
201
+ }
202
+ function pathParam(params) {
203
+ const p = params.path ?? params.file_path;
204
+ return typeof p === "string" && p.length > 0 ? p : null;
205
+ }
206
+ /** Bash bridge: does this path rule bite on the command's file arguments? */
207
+ function bashBridgeMatches(rule, ctx) {
208
+ const command = typeof ctx.params.command === "string" ? ctx.params.command : "";
209
+ if (!command)
210
+ return false;
211
+ if (pathClassForTool(rule.toolName) === null)
212
+ return false;
213
+ for (const segment of splitSubcommands(command)) {
214
+ const analysis = analyzeSubcommand(segment);
215
+ if (analysis.kind !== "analyzed")
216
+ continue;
217
+ // Claude Code parity: a deny/ask rule for the path bites on ANY file
218
+ // operation the command performs on it — read-class rules are NOT
219
+ // limited to read ops (their `checkPathConstraints` consults the same
220
+ // patterns for write ops). Fail-safe: a deny is a deny.
221
+ for (const p of analysis.paths) {
222
+ if (pathMatches([resolvePatternRoot(rule.ruleContent ?? "", rule.source, {
223
+ userStateHome: ctx.userStateHome,
224
+ projectRoot: ctx.projectRoot,
225
+ ...(ctx.homeDir ? { homeDir: ctx.homeDir } : {}),
226
+ })], p, { cwd: ctx.cwd })) {
227
+ return true;
228
+ }
229
+ }
230
+ }
231
+ return false;
232
+ }
233
+ /** Parse a raw rule string into a sourced PermissionRule (loader/tests). */
234
+ export function ruleFromRaw(raw, behavior, source) {
235
+ const { toolName, ruleContent } = parseRuleString(raw);
236
+ return { behavior, source, toolName, ruleContent, raw };
237
+ }
238
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Permission-rule settings loader.
3
+ *
4
+ * Reads `permissions: { allow, deny, ask }` from the two YAGNI Code settings
5
+ * files, mirroring the hooks loader (hooks.ts) shape exactly:
6
+ * - user: ~/.yagni-code/config.json — always active
7
+ * - project: .yagni-code/config.json — deny/ask always active;
8
+ * allow gated on workspace trust (Claude Code semantics:
9
+ * "deny and ask rules apply right away; allow rules from a
10
+ * project file wait for trust")
11
+ *
12
+ * Lists UNION across sources (Claude Code: lists merge, never replace).
13
+ * User-source rules are ordered before project-source rules so path-pattern
14
+ * negation (`!exception`) behaves like gitignore lines across files.
15
+ *
16
+ * Fail-soft: a malformed config file is skipped whole with a warning (same
17
+ * posture as hooks.ts). Malformed individual rule strings parse as bare
18
+ * tool names (parser degrades, never throws) and are flagged.
19
+ */
20
+ export type RuleSource = "user" | "project";
21
+ export type RuleBehavior = "allow" | "deny" | "ask";
22
+ /** A parsed, sourced permission rule. */
23
+ export interface PermissionRule {
24
+ behavior: RuleBehavior;
25
+ source: RuleSource;
26
+ toolName: string;
27
+ ruleContent?: string;
28
+ /** The original rule string, for diagnostics and verdict logs. */
29
+ raw: string;
30
+ }
31
+ export interface LoadRulesDiagnostics {
32
+ /** Non-fatal problems: bad files, unparsed rules, unknown keys. */
33
+ warnings: string[];
34
+ /** Rule tool names that no rule consumer consults (Claude-only tools). */
35
+ neverConsultedTools: string[];
36
+ }
37
+ export interface LoadedRules {
38
+ rules: PermissionRule[];
39
+ diagnostics: LoadRulesDiagnostics;
40
+ }
41
+ /** The tools a rule can actually be consulted for at the gate. */
42
+ export declare const KNOWN_RULE_TOOLS: Set<string>;
43
+ /** Load + union rules from both files. Pure I/O; no throw. */
44
+ export declare function loadPermissionRules(opts?: {
45
+ cwd?: string;
46
+ env?: NodeJS.ProcessEnv;
47
+ userHome?: string;
48
+ /** Override the user state home (tests). */
49
+ stateHomeOverride?: string | null;
50
+ }): LoadedRules;
51
+ /** Filter rules by behavior + trust (project allow needs trust; deny/ask always). */
52
+ export declare function effectiveRules(rules: readonly PermissionRule[], isProjectTrusted: boolean): PermissionRule[];
53
+ //# sourceMappingURL=loadConfig.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Permission-rule settings loader.
3
+ *
4
+ * Reads `permissions: { allow, deny, ask }` from the two YAGNI Code settings
5
+ * files, mirroring the hooks loader (hooks.ts) shape exactly:
6
+ * - user: ~/.yagni-code/config.json — always active
7
+ * - project: .yagni-code/config.json — deny/ask always active;
8
+ * allow gated on workspace trust (Claude Code semantics:
9
+ * "deny and ask rules apply right away; allow rules from a
10
+ * project file wait for trust")
11
+ *
12
+ * Lists UNION across sources (Claude Code: lists merge, never replace).
13
+ * User-source rules are ordered before project-source rules so path-pattern
14
+ * negation (`!exception`) behaves like gitignore lines across files.
15
+ *
16
+ * Fail-soft: a malformed config file is skipped whole with a warning (same
17
+ * posture as hooks.ts). Malformed individual rule strings parse as bare
18
+ * tool names (parser degrades, never throws) and are flagged.
19
+ */
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { codeStateHome } from "../stateHome.js";
23
+ import { parseRuleString } from "./parser.js";
24
+ /** The tools a rule can actually be consulted for at the gate. */
25
+ export const KNOWN_RULE_TOOLS = new Set([
26
+ "read", "edit", "write", "grep", "find", "ls", "bash", "web_fetch",
27
+ "ask_yagni", "ask_advisor", "ask_user_question", "file_ticket",
28
+ "update_ticket_status", "review_business_match", "suggest_next_work",
29
+ "record_engineering_context", "record_decision",
30
+ ]);
31
+ function readRulesFromFile(path, source, behavior, out, warnings) {
32
+ try {
33
+ if (!existsSync(path))
34
+ return;
35
+ const raw = readFileSync(path, "utf-8");
36
+ if (raw.trim() === "")
37
+ return;
38
+ const parsed = JSON.parse(raw);
39
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
40
+ warnings.push(`${source} settings: not a JSON object — permissions skipped (${path})`);
41
+ return;
42
+ }
43
+ const permissions = parsed.permissions;
44
+ if (permissions === undefined)
45
+ return;
46
+ if (!permissions || typeof permissions !== "object" || Array.isArray(permissions)) {
47
+ warnings.push(`${source} settings: "permissions" is not an object — skipped (${path})`);
48
+ return;
49
+ }
50
+ const entries = permissions[behavior];
51
+ if (entries === undefined)
52
+ return;
53
+ const list = Array.isArray(entries) ? entries : [entries];
54
+ for (const entry of list) {
55
+ if (typeof entry !== "string") {
56
+ warnings.push(`${source} settings: non-string ${behavior} rule skipped (${path})`);
57
+ continue;
58
+ }
59
+ const { toolName, ruleContent } = parseRuleString(entry);
60
+ out.push({ behavior, source, toolName, ruleContent, raw: entry });
61
+ }
62
+ }
63
+ catch {
64
+ warnings.push(`${source} settings: malformed JSON — permissions skipped (${path})`);
65
+ }
66
+ }
67
+ /** Load + union rules from both files. Pure I/O; no throw. */
68
+ export function loadPermissionRules(opts = {}) {
69
+ const warnings = [];
70
+ const rules = [];
71
+ const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
72
+ const userPath = join(stateHome, "config.json");
73
+ const projectPath = join(opts.cwd ?? process.cwd(), ".yagni-code", "config.json");
74
+ // User first, project second — ordering matters for path-rule negation.
75
+ for (const behavior of ["deny", "ask", "allow"]) {
76
+ readRulesFromFile(userPath, "user", behavior, rules, warnings);
77
+ readRulesFromFile(projectPath, "project", behavior, rules, warnings);
78
+ }
79
+ const neverConsultedTools = [
80
+ ...new Set(rules.map((r) => r.toolName).filter((t) => !KNOWN_RULE_TOOLS.has(t) && !t.startsWith("mcp__"))),
81
+ ];
82
+ return { rules, diagnostics: { warnings, neverConsultedTools } };
83
+ }
84
+ /** Filter rules by behavior + trust (project allow needs trust; deny/ask always). */
85
+ export function effectiveRules(rules, isProjectTrusted) {
86
+ if (isProjectTrusted)
87
+ return [...rules];
88
+ return rules.filter((r) => !(r.source === "project" && r.behavior === "allow"));
89
+ }
90
+ //# sourceMappingURL=loadConfig.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Permission-rule string parser.
3
+ *
4
+ * Parses `ToolName` / `ToolName(content)` rule strings — the exact format
5
+ * Claude Code uses in settings.json `permissions.allow/deny/ask` arrays —
6
+ * so a rule pasted from Claude Code works unchanged.
7
+ *
8
+ * Tool-name aliasing: Claude Code names (Read/Edit/Write/…) map onto this
9
+ * agent's tool names (read/edit/write/…). Both spellings are accepted; rules
10
+ * for tools that do not exist here parse fine but are flagged
11
+ * never-consulted by the loader's diagnostics.
12
+ *
13
+ * Ported from Claude Code's permissionRuleParser.ts (escaping semantics,
14
+ * `ToolName(*)` ≡ tool-wide) with the alias table added.
15
+ */
16
+ export interface ParsedRuleValue {
17
+ /** Normalized tool name this rule targets (after aliasing). */
18
+ toolName: string;
19
+ /** The raw content inside the parens, unescaped; undefined for bare rules. */
20
+ ruleContent?: string;
21
+ }
22
+ /** Normalize a rule's tool name: lowercase → alias → MCP passthrough. */
23
+ export declare function normalizeRuleToolName(raw: string): string;
24
+ /** Escape parens/backslashes in rule content for storage (parser inverse). */
25
+ export declare function escapeRuleContent(content: string): string;
26
+ /** Unescape rule content after parsing. */
27
+ export declare function unescapeRuleContent(content: string): string;
28
+ /**
29
+ * Parse a rule string into its components. Malformed shapes degrade the same
30
+ * way Claude Code's parser degrades them: treat the whole string as a bare
31
+ * tool name (never throw) — the loader's diagnostics flag them.
32
+ *
33
+ * `Bash(*)` and `Bash()` are tool-wide rules, same as bare `Bash`.
34
+ */
35
+ export declare function parseRuleString(ruleString: string): ParsedRuleValue;
36
+ /** Serialize a parsed rule back to its string form (loader diagnostics). */
37
+ export declare function ruleValueToString(value: ParsedRuleValue): string;
38
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Permission-rule string parser.
3
+ *
4
+ * Parses `ToolName` / `ToolName(content)` rule strings — the exact format
5
+ * Claude Code uses in settings.json `permissions.allow/deny/ask` arrays —
6
+ * so a rule pasted from Claude Code works unchanged.
7
+ *
8
+ * Tool-name aliasing: Claude Code names (Read/Edit/Write/…) map onto this
9
+ * agent's tool names (read/edit/write/…). Both spellings are accepted; rules
10
+ * for tools that do not exist here parse fine but are flagged
11
+ * never-consulted by the loader's diagnostics.
12
+ *
13
+ * Ported from Claude Code's permissionRuleParser.ts (escaping semantics,
14
+ * `ToolName(*)` ≡ tool-wide) with the alias table added.
15
+ */
16
+ import { mcpInfoFromString } from "../mcp/names.js";
17
+ /**
18
+ * Claude Code tool names → this agent's tool names. Keys are matched
19
+ * case-insensitively at parse time (Claude Code rules use PascalCase; our
20
+ * registered tools are lowercase). MCP names (`mcp__server__tool`) pass
21
+ * through untouched — naming is already byte-identical (mcp/names.ts).
22
+ */
23
+ const TOOL_NAME_ALIASES = {
24
+ read: "read",
25
+ edit: "edit",
26
+ write: "write",
27
+ grep: "grep",
28
+ find: "find",
29
+ glob: "find", // Claude Code's Glob ≈ our find (pattern-based file search)
30
+ ls: "ls",
31
+ bash: "bash",
32
+ webfetch: "web_fetch", // Claude Code's WebFetch → our web_fetch
33
+ // YAGNI-registered tools, accepted under their own names (aliases exist so
34
+ // a rule copied from a Claude Code setup that wrapped the same capability
35
+ // under a different name still lands on ours where sensible).
36
+ ask_yagni: "ask_yagni",
37
+ ask_advisor: "ask_advisor",
38
+ ask_user_question: "ask_user_question",
39
+ web_fetch: "web_fetch",
40
+ file_ticket: "file_ticket",
41
+ update_ticket_status: "update_ticket_status",
42
+ review_business_match: "review_business_match",
43
+ suggest_next_work: "suggest_next_work",
44
+ record_engineering_context: "record_engineering_context",
45
+ record_decision: "record_decision",
46
+ };
47
+ /** Normalize a rule's tool name: lowercase → alias → MCP passthrough. */
48
+ export function normalizeRuleToolName(raw) {
49
+ const lower = raw.toLowerCase();
50
+ const aliased = TOOL_NAME_ALIASES[lower];
51
+ if (aliased)
52
+ return aliased;
53
+ // MCP names are lowercase by construction (mcp/names.ts); no aliasing.
54
+ if (mcpInfoFromString(raw) !== null)
55
+ return raw;
56
+ // Unknown tool: keep the lowercased name so diagnostics can name it.
57
+ return lower;
58
+ }
59
+ /** Escape parens/backslashes in rule content for storage (parser inverse). */
60
+ export function escapeRuleContent(content) {
61
+ return content
62
+ .replace(/\\/g, "\\\\")
63
+ .replace(/\(/g, "\\(")
64
+ .replace(/\)/g, "\\)");
65
+ }
66
+ /** Unescape rule content after parsing. */
67
+ export function unescapeRuleContent(content) {
68
+ return content
69
+ .replace(/\\\(/g, "(")
70
+ .replace(/\\\)/g, ")")
71
+ .replace(/\\\\/g, "\\");
72
+ }
73
+ /** Index of the first occurrence of `ch` not escaped by an odd backslash run. */
74
+ function findFirstUnescapedChar(str, ch) {
75
+ for (let i = 0; i < str.length; i++) {
76
+ if (str[i] !== ch)
77
+ continue;
78
+ let backslashes = 0;
79
+ let j = i - 1;
80
+ while (j >= 0 && str[j] === "\\") {
81
+ backslashes++;
82
+ j--;
83
+ }
84
+ if (backslashes % 2 === 0)
85
+ return i;
86
+ }
87
+ return -1;
88
+ }
89
+ function findLastUnescapedChar(str, ch) {
90
+ for (let i = str.length - 1; i >= 0; i--) {
91
+ if (str[i] !== ch)
92
+ continue;
93
+ let backslashes = 0;
94
+ let j = i - 1;
95
+ while (j >= 0 && str[j] === "\\") {
96
+ backslashes++;
97
+ j--;
98
+ }
99
+ if (backslashes % 2 === 0)
100
+ return i;
101
+ }
102
+ return -1;
103
+ }
104
+ /**
105
+ * Parse a rule string into its components. Malformed shapes degrade the same
106
+ * way Claude Code's parser degrades them: treat the whole string as a bare
107
+ * tool name (never throw) — the loader's diagnostics flag them.
108
+ *
109
+ * `Bash(*)` and `Bash()` are tool-wide rules, same as bare `Bash`.
110
+ */
111
+ export function parseRuleString(ruleString) {
112
+ const openIdx = findFirstUnescapedChar(ruleString, "(");
113
+ if (openIdx === -1) {
114
+ return { toolName: normalizeRuleToolName(ruleString.trim()) };
115
+ }
116
+ const closeIdx = findLastUnescapedChar(ruleString, ")");
117
+ if (closeIdx === -1 || closeIdx <= openIdx || closeIdx !== ruleString.length - 1) {
118
+ return { toolName: normalizeRuleToolName(ruleString.trim()) };
119
+ }
120
+ const toolName = ruleString.slice(0, openIdx).trim();
121
+ if (!toolName) {
122
+ return { toolName: normalizeRuleToolName(ruleString.trim()) };
123
+ }
124
+ const rawContent = ruleString.slice(openIdx + 1, closeIdx);
125
+ if (rawContent === "" || rawContent === "*") {
126
+ return { toolName: normalizeRuleToolName(toolName) };
127
+ }
128
+ return { toolName: normalizeRuleToolName(toolName), ruleContent: unescapeRuleContent(rawContent) };
129
+ }
130
+ /** Serialize a parsed rule back to its string form (loader diagnostics). */
131
+ export function ruleValueToString(value) {
132
+ if (value.ruleContent === undefined)
133
+ return value.toolName;
134
+ return `${value.toolName}(${escapeRuleContent(value.ruleContent)})`;
135
+ }
136
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Path-based permission rules — ported from Claude Code's
3
+ * filesystem.ts (patternWithRoot + matchingRuleForInput), powered by the
4
+ * vendored `ignore` package (the gitignore engine) so pattern semantics are
5
+ * byte-compatible with both git's .gitignore and Claude Code.
6
+ *
7
+ * Pattern anchors (Claude Code parity):
8
+ * //path absolute from filesystem root
9
+ * ~/path relative to the user's home directory
10
+ * /path relative to the SETTINGS FILE's directory (user config →
11
+ * ~/.yagni-code/, project config → the project root)
12
+ * path relative to the session cwd; a bare filename matches at ANY
13
+ * depth (`.env` equals `**` / `.env` in glob terms)
14
+ *
15
+ * Compile semantics: all patterns for one (tool-class, behavior) compile
16
+ * into ONE ordered `ignore` instance — user-source patterns first, then
17
+ * project — so gitignore's later-line-wins and `!exception` negation work
18
+ * across rules from both files (Claude Code's matchingRuleForInput builds
19
+ * one ignore().add(patterns) per root the same way).
20
+ *
21
+ * Behavior asymmetry (Claude Code parity): allow rules with a rootless
22
+ * single-segment pattern anchor at the settings dir (narrow); deny/ask
23
+ * rules match at any depth (broad — the fail-safe direction).
24
+ */
25
+ import type { RuleSource } from "./loadConfig.js";
26
+ export interface PathMatchOptions {
27
+ cwd: string;
28
+ /** Overrides ~ expansion (tests); defaults to os.homedir(). */
29
+ homeDir?: string;
30
+ }
31
+ export type PathToolClass = "read" | "edit";
32
+ /** Which tools a rule's toolName maps to for path purposes. */
33
+ export declare function pathClassForTool(toolName: string): PathToolClass | null;
34
+ /** One path-pattern rule, resolved to a root + relative pattern. */
35
+ export interface ResolvedPathPattern {
36
+ relativePattern: string;
37
+ /** The directory the pattern resolves against; null = cwd-relative. */
38
+ root: string | null;
39
+ source: RuleSource;
40
+ }
41
+ /**
42
+ * Split an anchored pattern into (root, relativePattern). Mirrors Claude
43
+ * Code's patternWithRoot. Project-source `/` anchors at the project root
44
+ * (passed in); user-source `/` anchors at the YAGNI Code state home
45
+ * (~/.yagni-code).
46
+ */
47
+ export declare function resolvePatternRoot(pattern: string, source: RuleSource, opts: {
48
+ userStateHome: string;
49
+ projectRoot: string | null;
50
+ homeDir?: string;
51
+ }): ResolvedPathPattern;
52
+ /**
53
+ * Does the (absolute or cwd-relative) target path match the given patterns?
54
+ * Patterns are tried in order; later patterns win (gitignore semantics) —
55
+ * so the caller must pass user-source patterns before project-source.
56
+ */
57
+ export declare function pathMatches(patterns: readonly ResolvedPathPattern[], targetPath: string, opts: PathMatchOptions): boolean;
58
+ //# sourceMappingURL=pathRules.d.ts.map