@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.
- package/README.md +89 -27
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -3
- package/dist/claudePlugins.js +129 -19
- package/dist/cli.js +28 -4
- package/dist/doctor.d.ts +42 -4
- package/dist/doctor.js +150 -15
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +18 -5
- package/dist/extension/index.js +319 -72
- package/dist/extension/mcp/approval.d.ts +45 -0
- package/dist/extension/mcp/approval.js +164 -0
- package/dist/extension/mcp/auth.d.ts +124 -0
- package/dist/extension/mcp/auth.js +560 -0
- package/dist/extension/mcp/authStore.d.ts +61 -0
- package/dist/extension/mcp/authStore.js +105 -0
- package/dist/extension/mcp/callbackPage.d.ts +31 -0
- package/dist/extension/mcp/callbackPage.js +222 -0
- package/dist/extension/mcp/cliConfig.d.ts +12 -0
- package/dist/extension/mcp/cliConfig.js +12 -0
- package/dist/extension/mcp/config.d.ts +153 -0
- package/dist/extension/mcp/config.js +381 -0
- package/dist/extension/mcp/log.d.ts +28 -0
- package/dist/extension/mcp/log.js +82 -0
- package/dist/extension/mcp/manager.d.ts +100 -0
- package/dist/extension/mcp/manager.js +273 -0
- package/dist/extension/mcp/names.d.ts +25 -0
- package/dist/extension/mcp/names.js +40 -0
- package/dist/extension/mcp/panel.d.ts +33 -0
- package/dist/extension/mcp/panel.js +268 -0
- package/dist/extension/mcp/prompts.d.ts +23 -0
- package/dist/extension/mcp/prompts.js +93 -0
- package/dist/extension/mcp/startup.d.ts +55 -0
- package/dist/extension/mcp/startup.js +152 -0
- package/dist/extension/mcp/tools.d.ts +31 -0
- package/dist/extension/mcp/tools.js +117 -0
- package/dist/extension/mcp/transports.d.ts +17 -0
- package/dist/extension/mcp/transports.js +44 -0
- package/dist/extension/permission/gate.d.ts +104 -2
- package/dist/extension/permission/gate.js +387 -31
- package/dist/extension/permission/guardian.d.ts +24 -5
- package/dist/extension/permission/guardian.js +162 -24
- 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/personas.js +5 -0
- 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 +122 -0
- package/dist/mcpCommand.js +787 -0
- package/dist/otel.d.ts +69 -63
- package/dist/otel.js +154 -119
- 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/dist/upgrade.d.ts +11 -2
- package/dist/upgrade.js +48 -8
- package/package.json +20 -3
- package/dist/extension/mcpTools.d.ts +0 -57
- package/dist/extension/mcpTools.js +0 -132
|
@@ -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
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
import { shellParse } from "../permission/execPolicy.js";
|
|
29
|
+
/** `npm install:*` → prefix `npm install`; else null. */
|
|
30
|
+
function extractPrefix(ruleContent) {
|
|
31
|
+
const m = ruleContent.match(/^(.+):\*$/);
|
|
32
|
+
return m?.[1] ?? null;
|
|
33
|
+
}
|
|
34
|
+
/** Any unescaped `*` that is not the `:*` trailing legacy form? */
|
|
35
|
+
function hasWildcards(pattern) {
|
|
36
|
+
if (pattern.endsWith(":*"))
|
|
37
|
+
return false;
|
|
38
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
39
|
+
if (pattern[i] !== "*")
|
|
40
|
+
continue;
|
|
41
|
+
let backslashes = 0;
|
|
42
|
+
let j = i - 1;
|
|
43
|
+
while (j >= 0 && pattern[j] === "\\") {
|
|
44
|
+
backslashes++;
|
|
45
|
+
j--;
|
|
46
|
+
}
|
|
47
|
+
if (backslashes % 2 === 0)
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
export function parseShellRule(ruleContent) {
|
|
53
|
+
const prefix = extractPrefix(ruleContent);
|
|
54
|
+
if (prefix !== null)
|
|
55
|
+
return { type: "prefix", prefix };
|
|
56
|
+
if (hasWildcards(ruleContent))
|
|
57
|
+
return { type: "wildcard", pattern: ruleContent };
|
|
58
|
+
return { type: "exact", command: ruleContent };
|
|
59
|
+
}
|
|
60
|
+
// --- Wildcard → regex (Claude Code shellRuleMatching.ts) ---
|
|
61
|
+
const ESCAPED_STAR = "\x00ESCAPED_STAR\x00";
|
|
62
|
+
const ESCAPED_BACKSLASH = "\x00ESCAPED_BACKSLASH\x00";
|
|
63
|
+
/** Match a command string against a `*`-wildcard pattern. */
|
|
64
|
+
export function matchWildcardPattern(pattern, command) {
|
|
65
|
+
const trimmed = pattern.trim();
|
|
66
|
+
let processed = "";
|
|
67
|
+
let i = 0;
|
|
68
|
+
while (i < trimmed.length) {
|
|
69
|
+
const ch = trimmed[i];
|
|
70
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
71
|
+
const next = trimmed[i + 1];
|
|
72
|
+
if (next === "*") {
|
|
73
|
+
processed += ESCAPED_STAR;
|
|
74
|
+
i += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (next === "\\") {
|
|
78
|
+
processed += ESCAPED_BACKSLASH;
|
|
79
|
+
i += 2;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
processed += ch;
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
const escaped = processed.replace(/[.+?^${}()|[\]\\'"]/g, "\\$&");
|
|
87
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
88
|
+
let regexPattern = withWildcards
|
|
89
|
+
.replace(new RegExp(ESCAPED_STAR, "g"), "\\*")
|
|
90
|
+
.replace(new RegExp(ESCAPED_BACKSLASH, "g"), "\\\\");
|
|
91
|
+
// A trailing ` *` wildcard that is the ONLY wildcard also matches the bare
|
|
92
|
+
// command — aligns `git log *` with `git log:*` prefix semantics.
|
|
93
|
+
const starCount = (processed.match(/\*/g) ?? []).length;
|
|
94
|
+
if (regexPattern.endsWith(" .*") && starCount === 1) {
|
|
95
|
+
regexPattern = regexPattern.slice(0, -3) + "( .*)?";
|
|
96
|
+
}
|
|
97
|
+
// `s` (dotAll): `*` spans embedded newlines within a single subcommand.
|
|
98
|
+
return new RegExp(`^${regexPattern}$`, "s").test(command);
|
|
99
|
+
}
|
|
100
|
+
// --- Command splitting / stripping ---
|
|
101
|
+
/** Split a command into its subcommand strings (operators are boundaries). */
|
|
102
|
+
export function splitSubcommands(command) {
|
|
103
|
+
const tokens = shellParse(command);
|
|
104
|
+
const segments = [];
|
|
105
|
+
let current = [];
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (current.length > 0) {
|
|
108
|
+
segments.push(current.join(" "));
|
|
109
|
+
current = [];
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const t of tokens) {
|
|
113
|
+
if (typeof t === "object" && ("op" in t) && (t.op === "pipe" || t.op === "and" || t.op === "or" || t.op === "semi" || t.op === "substitution")) {
|
|
114
|
+
flush();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Redirects/background are constructs, not boundaries — a subcommand
|
|
118
|
+
// carrying them is matched WITH the construct (the exec policy's
|
|
119
|
+
// construct floor handles the safety side; for rules we keep the segment).
|
|
120
|
+
current.push(typeof t === "string" ? t : renderOp(t));
|
|
121
|
+
}
|
|
122
|
+
flush();
|
|
123
|
+
return segments;
|
|
124
|
+
}
|
|
125
|
+
function renderOp(t) {
|
|
126
|
+
if (t.op === "redirect" && "target" in t)
|
|
127
|
+
return `>${t.target}`;
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Safe wrappers stripped before rule matching (Claude Code parity):
|
|
132
|
+
* timeout <n>, time, nice, nohup. Allow rules ALSO strip safe env prefixes
|
|
133
|
+
* (NODE_ENV=test go build). Deny/ask use stripAllEnvVars instead.
|
|
134
|
+
*/
|
|
135
|
+
const SAFE_WRAPPER_RE = /^(?:timeout\s+\d+(?:\.\d+)?[a-z]*)?/;
|
|
136
|
+
function stripSafeWrappers(cmd) {
|
|
137
|
+
let s = cmd.trim();
|
|
138
|
+
for (;;) {
|
|
139
|
+
const next = s
|
|
140
|
+
.replace(/^timeout\s+\S+\s+/, "")
|
|
141
|
+
.replace(/^(?:time|nice|nohup)\s+/, "")
|
|
142
|
+
.replace(SAFE_WRAPPER_RE, "");
|
|
143
|
+
if (next === s)
|
|
144
|
+
return s;
|
|
145
|
+
s = next.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const SAFE_ENV_VARS = new Set([
|
|
149
|
+
"NODE_ENV", "CI", "FORCE_COLOR", "NO_COLOR", "DEBUG", "VERBOSE",
|
|
150
|
+
"GOOS", "GOARCH", "CGO_ENABLED", "PWD", "LANG", "LC_ALL", "TERM",
|
|
151
|
+
]);
|
|
152
|
+
function stripSafeEnvPrefixes(cmd) {
|
|
153
|
+
let s = cmd.trim();
|
|
154
|
+
for (;;) {
|
|
155
|
+
const m = s.match(/^([A-Za-z_][A-Za-z0-9_]*)=(\S*)\s+/);
|
|
156
|
+
if (!m || !SAFE_ENV_VARS.has(m[1]))
|
|
157
|
+
return s;
|
|
158
|
+
s = s.slice(m[0].length).trim();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Broader env-assignment pattern for deny/ask stripping (Claude parity). */
|
|
162
|
+
const ENV_VAR_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?)\+?=(?:'[^'\n\r]*'|"(?:\\.|[^"$`\\\n\r])*"|\\.|[^ \t\n\r$`;|&()<>\\'"])*[ \t]+/;
|
|
163
|
+
/** Strip ALL leading env assignments — deny/ask rules only. Fixed-point. */
|
|
164
|
+
export function stripAllEnvVars(command) {
|
|
165
|
+
let stripped = command.trim();
|
|
166
|
+
for (;;) {
|
|
167
|
+
const m = stripped.match(ENV_VAR_PATTERN);
|
|
168
|
+
if (!m)
|
|
169
|
+
return stripped;
|
|
170
|
+
stripped = stripped.slice(m[0].length).trim();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Prepare a subcommand for matching: candidate strings to try. */
|
|
174
|
+
function candidatesFor(segment, behavior) {
|
|
175
|
+
const stripped = stripSafeWrappers(stripSafeEnvPrefixes(segment));
|
|
176
|
+
const base = [segment, stripped];
|
|
177
|
+
if (behavior === "deny" || behavior === "ask") {
|
|
178
|
+
base.push(stripAllEnvVars(stripped));
|
|
179
|
+
// fixed-point: env prefix may reveal another wrapper (`nohup FOO=bar …`)
|
|
180
|
+
base.push(stripSafeWrappers(stripAllEnvVars(stripped)));
|
|
181
|
+
}
|
|
182
|
+
return [...new Set(base.filter((c) => c.length > 0))];
|
|
183
|
+
}
|
|
184
|
+
/** Match one prepared candidate against one parsed rule. */
|
|
185
|
+
function matchOne(rule, candidate) {
|
|
186
|
+
switch (rule.type) {
|
|
187
|
+
case "exact":
|
|
188
|
+
return rule.command === candidate;
|
|
189
|
+
case "prefix":
|
|
190
|
+
if (candidate === rule.prefix)
|
|
191
|
+
return true;
|
|
192
|
+
return candidate.startsWith(rule.prefix + " ");
|
|
193
|
+
case "wildcard":
|
|
194
|
+
return matchWildcardPattern(rule.pattern, candidate);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Does this bash rule match the full command? Mirrors Claude Code's
|
|
199
|
+
* matchingRulesForInput: compound commands split, every subcommand must
|
|
200
|
+
* pass, candidates include stripped variants, and deny/ask additionally
|
|
201
|
+
* try all-env-stripped variants.
|
|
202
|
+
*/
|
|
203
|
+
export function shellRuleMatches(ruleContent, command, behavior) {
|
|
204
|
+
const rule = parseShellRule(ruleContent);
|
|
205
|
+
const trimmed = command.trim();
|
|
206
|
+
if (!trimmed)
|
|
207
|
+
return false;
|
|
208
|
+
const segments = splitSubcommands(trimmed);
|
|
209
|
+
if (segments.length === 0)
|
|
210
|
+
return false;
|
|
211
|
+
// Compound: EVERY segment must match (a deny rule needs only ONE matching
|
|
212
|
+
// segment to deny the whole; an allow rule must clear every segment).
|
|
213
|
+
if (segments.length > 1) {
|
|
214
|
+
if (behavior === "allow") {
|
|
215
|
+
return segments.every((seg) => candidatesFor(seg, behavior).some((c) => matchOne(rule, c)));
|
|
216
|
+
}
|
|
217
|
+
return segments.some((seg) => candidatesFor(seg, behavior).some((c) => matchOne(rule, c)));
|
|
218
|
+
}
|
|
219
|
+
return candidatesFor(segments[0], behavior).some((c) => matchOne(rule, c));
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=shellRules.js.map
|
|
@@ -66,15 +66,12 @@ export declare function buildStageInvocation(stage: PipelineStage, ctx: {
|
|
|
66
66
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
67
67
|
* left untouched.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* /go run's cost
|
|
72
|
-
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
73
|
-
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
69
|
+
* OTel export rides the inherited env (the launcher's `YAGNI_OTEL_EXPORT`
|
|
70
|
+
* gate plus the OTLP variables): our extension exports child LLM spend too,
|
|
71
|
+
* so stage children — the bulk of a /go run's cost — are never undercounted.
|
|
74
72
|
*/
|
|
75
73
|
export declare function groundedChildArgv(stageArgv: string[], opts: {
|
|
76
74
|
piCli: string;
|
|
77
75
|
extensionPath: string;
|
|
78
|
-
otelExtensionPath?: string;
|
|
79
76
|
}): string[];
|
|
80
77
|
//# sourceMappingURL=invocation.d.ts.map
|
|
@@ -85,11 +85,9 @@ export function buildStageInvocation(stage, ctx) {
|
|
|
85
85
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
86
86
|
* left untouched.
|
|
87
87
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* /go run's cost
|
|
91
|
-
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
92
|
-
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
88
|
+
* OTel export rides the inherited env (the launcher's `YAGNI_OTEL_EXPORT`
|
|
89
|
+
* gate plus the OTLP variables): our extension exports child LLM spend too,
|
|
90
|
+
* so stage children — the bulk of a /go run's cost — are never undercounted.
|
|
93
91
|
*/
|
|
94
92
|
export function groundedChildArgv(stageArgv, opts) {
|
|
95
93
|
const userChoseProvider = stageArgv.includes("--provider");
|
|
@@ -97,7 +95,6 @@ export function groundedChildArgv(stageArgv, opts) {
|
|
|
97
95
|
opts.piCli,
|
|
98
96
|
"-e",
|
|
99
97
|
opts.extensionPath,
|
|
100
|
-
...(opts.otelExtensionPath ? ["-e", opts.otelExtensionPath] : []),
|
|
101
98
|
...(userChoseProvider ? [] : ["--provider", "yagni"]),
|
|
102
99
|
...stageArgv,
|
|
103
100
|
];
|
|
@@ -183,6 +183,11 @@ Rationale rules:
|
|
|
183
183
|
Output ONLY a JSON object with this exact shape:
|
|
184
184
|
{"outcome":"allow"|"ask"|"deny","riskLevel":"low"|"medium"|"high"|"critical","rationale":"see rationale rules"}
|
|
185
185
|
|
|
186
|
+
Your output must parse as strict JSON on the first try:
|
|
187
|
+
- Inside the rationale string, never write a bare double quote. Quote commands, flags, and paths with backticks instead: \`cat ~/.zshrc\`, not "cat ~/.zshrc".
|
|
188
|
+
- A backslash inside a JSON string is legal only as the two-character JSON escapes \\, \\n, \\t, or \\uXXXX, and a quote inside the rationale as \\". Never write regex-style escapes like \\d or \\w literally — write "digit" or "word character" in words instead.
|
|
189
|
+
- Output exactly one JSON object: no trailing comma, no doubled closing quote, no second object, nothing after the closing brace.
|
|
190
|
+
|
|
186
191
|
Do not output anything else after the JSON. No markdown fences, only the JSON object.`;
|
|
187
192
|
const TITLE_BODY = `You produce a session title from a user's prompt. Output ONLY a concise, sentence-case title of 3-7 words that captures the main topic or goal. Capitalize only the first word and proper nouns. Do not include a ticket code in the title text itself (the caller prepends it). No markdown, no prose, no quotes — just the title on one line.
|
|
188
193
|
|
|
@@ -62,18 +62,14 @@ async function defaultWritePrompt(body) {
|
|
|
62
62
|
* Default child resolution: the parent process IS pi (so `process.argv[1]` is
|
|
63
63
|
* pi's cli), and our compiled extension entry sits one dir up from this module.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* existsSync so a stale env value degrades to an untraced child, never a
|
|
69
|
-
* child that fails to boot.
|
|
65
|
+
* OTel export needs no extra child argv: the extension itself exports when
|
|
66
|
+
* the inherited env carries the launcher's `YAGNI_OTEL_EXPORT` gate (see the
|
|
67
|
+
* CLI's otel.ts), so stage children's LLM spend is traced automatically.
|
|
70
68
|
*/
|
|
71
69
|
function defaultResolveChild() {
|
|
72
70
|
const piCli = process.argv[1] ?? "pi";
|
|
73
71
|
const extensionPath = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
74
|
-
|
|
75
|
-
const otelExtensionPath = otelPath && fs.existsSync(otelPath) ? otelPath : undefined;
|
|
76
|
-
return { piCli, extensionPath, ...(otelExtensionPath ? { otelExtensionPath } : {}) };
|
|
72
|
+
return { piCli, extensionPath };
|
|
77
73
|
}
|
|
78
74
|
const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
79
75
|
/** Truncate to PER_TASK_OUTPUT_CAP bytes, never exceeding the cap. */
|
|
@@ -124,12 +120,8 @@ export async function runStage(stage, ctx, deps) {
|
|
|
124
120
|
lens: ctx.lens,
|
|
125
121
|
...(tierCap ? { tierCap } : {}),
|
|
126
122
|
});
|
|
127
|
-
const { piCli, extensionPath
|
|
128
|
-
const argv = groundedChildArgv(passthrough, {
|
|
129
|
-
piCli,
|
|
130
|
-
extensionPath,
|
|
131
|
-
...(otelExtensionPath ? { otelExtensionPath } : {}),
|
|
132
|
-
});
|
|
123
|
+
const { piCli, extensionPath } = resolveChild();
|
|
124
|
+
const argv = groundedChildArgv(passthrough, { piCli, extensionPath });
|
|
133
125
|
// Fold events into a fixed-size accumulator as they stream — never retain
|
|
134
126
|
// the full array (that OOMs on a verbose stage; YAG-317 follow-up).
|
|
135
127
|
const acc = newEventAccumulator();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's view of Claude Code plugins, and how a mid-session change
|
|
3
|
+
* reaches pi without a restart.
|
|
4
|
+
*
|
|
5
|
+
* The launcher (yagni-code-cli) owns discovery, trust, and the ledger. This
|
|
6
|
+
* module only talks to it: `yagni plugin inventory --json` (via
|
|
7
|
+
* `YAGNI_CLI_PATH`) returns what would load for the cwd right now. Reload is
|
|
8
|
+
* pi's own `/reload` flow: before calling it we rewrite this process's env so
|
|
9
|
+
* the re-created extension runtime sees the fresh MCP payload and agent dirs,
|
|
10
|
+
* and `resources_discover` hands pi the skill/prompt paths of plugins that
|
|
11
|
+
* were not in the launch argv (see panel.ts). Everything is fail-soft.
|
|
12
|
+
*/
|
|
13
|
+
import { type PluginMcpEnvEntry } from "../mcp/config.js";
|
|
14
|
+
/** Launcher → extension: the yagni CLI entry to run `plugin …` with. */
|
|
15
|
+
export declare const CLI_PATH_ENV = "YAGNI_CLI_PATH";
|
|
16
|
+
/** Launcher → extension: `name@marketplace` keys wired through argv at launch. */
|
|
17
|
+
export declare const PLUGIN_KEYS_ENV = "YAGNI_CLAUDE_PLUGIN_KEYS";
|
|
18
|
+
/** Set by /reload-plugins before pi reloads: the fresh inventory (JSON). */
|
|
19
|
+
export declare const INVENTORY_ENV = "YAGNI_CLAUDE_PLUGIN_INVENTORY";
|
|
20
|
+
export interface InventoryPlugin {
|
|
21
|
+
key: string;
|
|
22
|
+
name: string;
|
|
23
|
+
origin: "yagni" | "claude-code" | "repo-marketplace";
|
|
24
|
+
version: string | null;
|
|
25
|
+
root: string;
|
|
26
|
+
gated: boolean;
|
|
27
|
+
skillPaths: string[];
|
|
28
|
+
commandPaths: string[];
|
|
29
|
+
agentDirs: string[];
|
|
30
|
+
mcpServers: string[];
|
|
31
|
+
unsupported: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface PluginInventory {
|
|
34
|
+
plugins: InventoryPlugin[];
|
|
35
|
+
mcp: PluginMcpEnvEntry[];
|
|
36
|
+
pendingTrust: string[];
|
|
37
|
+
agentDirs: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface CliResult {
|
|
40
|
+
code: number;
|
|
41
|
+
stdout: string;
|
|
42
|
+
stderr: string;
|
|
43
|
+
}
|
|
44
|
+
export type CliRunner = (args: string[], opts: {
|
|
45
|
+
cliPath: string;
|
|
46
|
+
cwd: string;
|
|
47
|
+
env: NodeJS.ProcessEnv;
|
|
48
|
+
}) => Promise<CliResult>;
|
|
49
|
+
export declare const defaultCliRunner: CliRunner;
|
|
50
|
+
export declare function cliPath(env: NodeJS.ProcessEnv): string | null;
|
|
51
|
+
export declare function launchKeys(env: NodeJS.ProcessEnv): Set<string>;
|
|
52
|
+
/** The inventory recorded by the last /reload-plugins in this process, if any. */
|
|
53
|
+
export declare function storedInventory(env: NodeJS.ProcessEnv): PluginInventory | null;
|
|
54
|
+
export declare function parseInventory(value: unknown): PluginInventory;
|
|
55
|
+
export declare function fetchInventory(opts: {
|
|
56
|
+
cwd: string;
|
|
57
|
+
env: NodeJS.ProcessEnv;
|
|
58
|
+
runCli?: CliRunner;
|
|
59
|
+
}): Promise<PluginInventory>;
|
|
60
|
+
/**
|
|
61
|
+
* The plugins the session knows about right now: launch-time keys plus
|
|
62
|
+
* anything a previous /reload-plugins in this process brought in.
|
|
63
|
+
*/
|
|
64
|
+
export declare function knownKeys(env: NodeJS.ProcessEnv): Set<string>;
|
|
65
|
+
export interface ReloadPlan {
|
|
66
|
+
added: InventoryPlugin[];
|
|
67
|
+
removed: string[];
|
|
68
|
+
/** Env values to set on this process before pi reloads. */
|
|
69
|
+
env: Record<string, string | undefined>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Diff a fresh inventory against what the session already has, and the env
|
|
73
|
+
* rewrite that makes the reloaded runtime pick the fresh state up: the MCP
|
|
74
|
+
* payload and agent dirs are re-read by the extension on startup, and the
|
|
75
|
+
* inventory itself feeds `resources_discover` for skills/prompts.
|
|
76
|
+
*/
|
|
77
|
+
export declare function planReload(inventory: PluginInventory, env: NodeJS.ProcessEnv): ReloadPlan;
|
|
78
|
+
export declare function applyEnv(target: NodeJS.ProcessEnv, values: Record<string, string | undefined>): void;
|
|
79
|
+
/**
|
|
80
|
+
* Skill/prompt paths pi does not already hold through the launch argv: the
|
|
81
|
+
* plugins that arrived via /reload-plugins. Returned from `resources_discover`
|
|
82
|
+
* on every reason so a `/new` after a reload keeps them too.
|
|
83
|
+
*/
|
|
84
|
+
export declare function discoverPaths(env: NodeJS.ProcessEnv): {
|
|
85
|
+
skillPaths: string[];
|
|
86
|
+
promptPaths: string[];
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=inventory.d.ts.map
|