@yagni-app/code 1.0.6 → 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.
- package/README.md +84 -46
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +13 -0
- package/dist/extension/index.js +230 -43
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/permission/gate.d.ts +97 -2
- package/dist/extension/permission/gate.js +375 -26
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
- package/dist/extension/permissionRules/loadConfig.js +90 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +58 -0
- package/dist/extension/permissionRules/pathRules.js +120 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +775 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +99 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +40 -0
- package/dist/extension/telemetry/register.js +192 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/package.json +19 -3
|
@@ -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
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { homedir } from "node:os";
|
|
26
|
+
import { isAbsolute, posix, sep } from "node:path";
|
|
27
|
+
import ignore from "../vendor/ignore.js";
|
|
28
|
+
/** Which tools a rule's toolName maps to for path purposes. */
|
|
29
|
+
export function pathClassForTool(toolName) {
|
|
30
|
+
switch (toolName) {
|
|
31
|
+
case "read":
|
|
32
|
+
case "grep":
|
|
33
|
+
case "find":
|
|
34
|
+
case "ls":
|
|
35
|
+
return "read";
|
|
36
|
+
case "edit":
|
|
37
|
+
case "write":
|
|
38
|
+
return "edit";
|
|
39
|
+
default:
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Split an anchored pattern into (root, relativePattern). Mirrors Claude
|
|
45
|
+
* Code's patternWithRoot. Project-source `/` anchors at the project root
|
|
46
|
+
* (passed in); user-source `/` anchors at the YAGNI Code state home
|
|
47
|
+
* (~/.yagni-code).
|
|
48
|
+
*/
|
|
49
|
+
export function resolvePatternRoot(pattern, source, opts) {
|
|
50
|
+
if (pattern.startsWith("//")) {
|
|
51
|
+
return { relativePattern: pattern.slice(1), root: "/", source };
|
|
52
|
+
}
|
|
53
|
+
if (pattern.startsWith("~/") || pattern === "~") {
|
|
54
|
+
const rel = pattern === "~" ? "" : pattern.slice(1);
|
|
55
|
+
return { relativePattern: rel, root: opts.homeDir ?? homedir(), source };
|
|
56
|
+
}
|
|
57
|
+
if (pattern.startsWith("/") && !pattern.startsWith("//")) {
|
|
58
|
+
const base = source === "project" ? opts.projectRoot : opts.userStateHome;
|
|
59
|
+
return { relativePattern: pattern, root: base ?? opts.userStateHome, source };
|
|
60
|
+
}
|
|
61
|
+
// Rootless: `./x` normalized to `x`; bare names keep any-depth semantics.
|
|
62
|
+
let normalized = pattern;
|
|
63
|
+
if (normalized.startsWith("./"))
|
|
64
|
+
normalized = normalized.slice(2);
|
|
65
|
+
return { relativePattern: normalized, root: null, source };
|
|
66
|
+
}
|
|
67
|
+
/** Strip a trailing `/**` — the ignore lib treats `path` as dir+children. */
|
|
68
|
+
function forIgnore(relativePattern) {
|
|
69
|
+
if (relativePattern.endsWith("/**"))
|
|
70
|
+
return relativePattern.slice(0, -3);
|
|
71
|
+
return relativePattern;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Does the (absolute or cwd-relative) target path match the given patterns?
|
|
75
|
+
* Patterns are tried in order; later patterns win (gitignore semantics) —
|
|
76
|
+
* so the caller must pass user-source patterns before project-source.
|
|
77
|
+
*/
|
|
78
|
+
export function pathMatches(patterns, targetPath, opts) {
|
|
79
|
+
if (patterns.length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
const target = toPosix(targetPath);
|
|
82
|
+
const groups = new Map();
|
|
83
|
+
for (const p of patterns) {
|
|
84
|
+
const key = p.root;
|
|
85
|
+
const list = groups.get(key) ?? [];
|
|
86
|
+
list.push(forIgnore(p.relativePattern));
|
|
87
|
+
groups.set(key, list);
|
|
88
|
+
}
|
|
89
|
+
for (const [root, plist] of groups) {
|
|
90
|
+
const ig = ignore().add(plist);
|
|
91
|
+
const base = root ?? opts.cwd;
|
|
92
|
+
// Lexical normalization BEFORE relativization: `a/../.env` and `a//b`
|
|
93
|
+
// must resolve against the same relative form the pattern matches, or a
|
|
94
|
+
// traversal-shaped spelling of a denied path slips past (probe:
|
|
95
|
+
// `/work/proj/../.env` was NOT caught by `Read(./.env)` without this).
|
|
96
|
+
// posix.normalize keeps this lexical — no filesystem access, so a
|
|
97
|
+
// symlinked target still needs its own rule (documented limitation,
|
|
98
|
+
// same as pi's own tools resolving paths literally).
|
|
99
|
+
const absTarget = isAbsolute(target) ? posix.normalize(target) : posix.normalize(posix.join(opts.cwd, target));
|
|
100
|
+
const rel = toPosix(posixRelative(base, absTarget));
|
|
101
|
+
if (rel.startsWith(".."))
|
|
102
|
+
continue; // outside this root — not matched
|
|
103
|
+
// ignore() rejects empty paths; an empty relative means the target IS
|
|
104
|
+
// the root itself (e.g. `rg` defaulting to "."). Nothing matches that
|
|
105
|
+
// unless a pattern covers "" — treat as not-matched (safe default: a
|
|
106
|
+
// cwd-wide read goes through the normal gate, not a deny).
|
|
107
|
+
if (rel === "" || rel === ".")
|
|
108
|
+
continue;
|
|
109
|
+
if (ig.ignores(rel))
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function toPosix(p) {
|
|
115
|
+
return sep === "\\" ? p.replaceAll("\\", "/") : p;
|
|
116
|
+
}
|
|
117
|
+
function posixRelative(from, to) {
|
|
118
|
+
return posix.relative(toPosix(from), toPosix(to));
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=pathRules.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash command-rule matching — ported from Claude Code's
|
|
3
|
+
* shellRuleMatching.ts + the matching half of BashTool/bashPermissions.ts.
|
|
4
|
+
*
|
|
5
|
+
* A bash rule's content is one of:
|
|
6
|
+
* - exact: `npm run build` — matches that exact command string
|
|
7
|
+
* - prefix: `npm install:*` — `:*` suffix = trailing wildcard
|
|
8
|
+
* - wildcard: `git log *` — `*` anywhere; `*` matches any text
|
|
9
|
+
* including spaces; `\*` literal
|
|
10
|
+
*
|
|
11
|
+
* Security invariants (Claude Code parity, enforced here):
|
|
12
|
+
* - compound commands are split on ;, &&, ||, |, and newlines, and EVERY
|
|
13
|
+
* subcommand must pass the rule check independently. Wildcards and
|
|
14
|
+
* prefixes never run against the raw compound string (`cd /x && rm -rf`
|
|
15
|
+
* must not ride a `cd:*` allow).
|
|
16
|
+
* - prefix matching requires a word boundary: `ls:*` matches `ls` and
|
|
17
|
+
* `ls -la` but never `lsof`.
|
|
18
|
+
* - a trailing ` *` wildcard that is the rule's ONLY wildcard also matches
|
|
19
|
+
* the bare command (`git log *` matches `git log`).
|
|
20
|
+
* - safe wrappers (timeout/time/nice/nohup) are stripped before matching,
|
|
21
|
+
* so `Bash(npm install:*)` matches `timeout 10 npm install foo`.
|
|
22
|
+
* - for DENY and ASK rules, ALL leading env-var assignments are stripped
|
|
23
|
+
* (fixed-point, mirroring Claude's stripAllLeadingEnvVars) so
|
|
24
|
+
* `FOO=bar denied_cmd` stays denied. For ALLOW rules only the safe-list
|
|
25
|
+
* env prefixes are stripped — an env var must never widen an allow
|
|
26
|
+
* (Claude's asymmetry, HackerOne #3543050).
|
|
27
|
+
*/
|
|
28
|
+
export type ShellRuleShape = {
|
|
29
|
+
type: "exact";
|
|
30
|
+
command: string;
|
|
31
|
+
} | {
|
|
32
|
+
type: "prefix";
|
|
33
|
+
prefix: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "wildcard";
|
|
36
|
+
pattern: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function parseShellRule(ruleContent: string): ShellRuleShape;
|
|
39
|
+
/** Match a command string against a `*`-wildcard pattern. */
|
|
40
|
+
export declare function matchWildcardPattern(pattern: string, command: string): boolean;
|
|
41
|
+
/** Split a command into its subcommand strings (operators are boundaries). */
|
|
42
|
+
export declare function splitSubcommands(command: string): string[];
|
|
43
|
+
/** Strip ALL leading env assignments — deny/ask rules only. Fixed-point. */
|
|
44
|
+
export declare function stripAllEnvVars(command: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Does this bash rule match the full command? Mirrors Claude Code's
|
|
47
|
+
* matchingRulesForInput: compound commands split, every subcommand must
|
|
48
|
+
* pass, candidates include stripped variants, and deny/ask additionally
|
|
49
|
+
* try all-env-stripped variants.
|
|
50
|
+
*/
|
|
51
|
+
export declare function shellRuleMatches(ruleContent: string, command: string, behavior: "allow" | "deny" | "ask"): boolean;
|
|
52
|
+
//# sourceMappingURL=shellRules.d.ts.map
|