@yagni-app/code 1.0.7 → 1.0.9
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 +12 -1
- package/dist/extension/askUserQuestionTool.js +7 -2
- package/dist/extension/config.d.ts +6 -0
- package/dist/extension/hooks.d.ts +3 -3
- package/dist/extension/hooks.js +30 -5
- package/dist/extension/index.d.ts +6 -0
- package/dist/extension/index.js +56 -38
- package/dist/extension/permission/gate.d.ts +5 -1
- package/dist/extension/permission/gate.js +138 -44
- package/dist/extension/permissionRules/loadConfig.d.ts +23 -12
- package/dist/extension/permissionRules/loadConfig.js +29 -14
- package/dist/extension/permissionRules/pathRules.d.ts +9 -7
- package/dist/extension/permissionRules/pathRules.js +10 -8
- package/dist/extension/sandbox/config.d.ts +15 -14
- package/dist/extension/sandbox/config.js +62 -40
- package/dist/extension/sandbox/manager.d.ts +10 -0
- package/dist/extension/sandbox/manager.js +28 -1
- package/dist/extension/sandbox/session.js +150 -96
- package/dist/extension/settingsFiles.d.ts +50 -0
- package/dist/extension/settingsFiles.js +206 -0
- package/dist/extension/telemetry/attrs.d.ts +7 -0
- package/dist/extension/telemetry/attrs.js +7 -0
- package/dist/extension/telemetry/config.d.ts +5 -1
- package/dist/extension/telemetry/register.d.ts +7 -0
- package/dist/extension/telemetry/register.js +15 -0
- package/dist/extension/telemetry/tracker.js +8 -1
- package/dist/upgrade.js +10 -1
- package/package.json +2 -2
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission-rule settings loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads `permissions: { allow, deny, ask }` from the
|
|
5
|
-
* files, mirroring the hooks loader (hooks.ts) shape exactly:
|
|
6
|
-
* - user:
|
|
7
|
-
* - project:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Reads `permissions: { allow, deny, ask }` from the three YAGNI Code
|
|
5
|
+
* settings 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
|
+
* - local: .yagni-code/config.local.json — the personal per-project
|
|
12
|
+
* tier; trust-gated EXACTLY like project (a hostile repo can
|
|
13
|
+
* commit a local file — the gitignore convention only covers
|
|
14
|
+
* untracked files). Deliberately stricter than Claude Code,
|
|
15
|
+
* which does not trust-gate localSettings.
|
|
11
16
|
*
|
|
12
17
|
* Lists UNION across sources (Claude Code: lists merge, never replace).
|
|
13
|
-
* User-source rules are ordered before project-source rules
|
|
14
|
-
* negation (`!exception`) behaves like gitignore
|
|
18
|
+
* User-source rules are ordered before project-source rules, project before
|
|
19
|
+
* local, so path-pattern negation (`!exception`) behaves like gitignore
|
|
20
|
+
* lines across files — higher-precedence sources come later (later wins).
|
|
15
21
|
*
|
|
16
22
|
* Fail-soft: a malformed config file is skipped whole with a warning (same
|
|
17
23
|
* posture as hooks.ts). Malformed individual rule strings parse as bare
|
|
18
24
|
* tool names (parser degrades, never throws) and are flagged.
|
|
19
25
|
*/
|
|
20
|
-
export type RuleSource = "user" | "project";
|
|
26
|
+
export type RuleSource = "user" | "project" | "local";
|
|
21
27
|
export type RuleBehavior = "allow" | "deny" | "ask";
|
|
22
28
|
/** A parsed, sourced permission rule. */
|
|
23
29
|
export interface PermissionRule {
|
|
@@ -40,7 +46,7 @@ export interface LoadedRules {
|
|
|
40
46
|
}
|
|
41
47
|
/** The tools a rule can actually be consulted for at the gate. */
|
|
42
48
|
export declare const KNOWN_RULE_TOOLS: Set<string>;
|
|
43
|
-
/** Load + union rules from
|
|
49
|
+
/** Load + union rules from all three files. Pure I/O; no throw. */
|
|
44
50
|
export declare function loadPermissionRules(opts?: {
|
|
45
51
|
cwd?: string;
|
|
46
52
|
env?: NodeJS.ProcessEnv;
|
|
@@ -48,6 +54,11 @@ export declare function loadPermissionRules(opts?: {
|
|
|
48
54
|
/** Override the user state home (tests). */
|
|
49
55
|
stateHomeOverride?: string | null;
|
|
50
56
|
}): LoadedRules;
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Filter rules by behavior + trust. Untrusted repos keep deny/ask from every
|
|
59
|
+
* source but lose allow rules from BOTH project and local files — a
|
|
60
|
+
* committed config.local.json must not smuggle allow rules past the trust
|
|
61
|
+
* gate (see the module header; stricter than Claude Code's localSettings).
|
|
62
|
+
*/
|
|
52
63
|
export declare function effectiveRules(rules: readonly PermissionRule[], isProjectTrusted: boolean): PermissionRule[];
|
|
53
64
|
//# sourceMappingURL=loadConfig.d.ts.map
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission-rule settings loader.
|
|
3
3
|
*
|
|
4
|
-
* Reads `permissions: { allow, deny, ask }` from the
|
|
5
|
-
* files, mirroring the hooks loader (hooks.ts) shape exactly:
|
|
6
|
-
* - user:
|
|
7
|
-
* - project:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Reads `permissions: { allow, deny, ask }` from the three YAGNI Code
|
|
5
|
+
* settings 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
|
+
* - local: .yagni-code/config.local.json — the personal per-project
|
|
12
|
+
* tier; trust-gated EXACTLY like project (a hostile repo can
|
|
13
|
+
* commit a local file — the gitignore convention only covers
|
|
14
|
+
* untracked files). Deliberately stricter than Claude Code,
|
|
15
|
+
* which does not trust-gate localSettings.
|
|
11
16
|
*
|
|
12
17
|
* Lists UNION across sources (Claude Code: lists merge, never replace).
|
|
13
|
-
* User-source rules are ordered before project-source rules
|
|
14
|
-
* negation (`!exception`) behaves like gitignore
|
|
18
|
+
* User-source rules are ordered before project-source rules, project before
|
|
19
|
+
* local, so path-pattern negation (`!exception`) behaves like gitignore
|
|
20
|
+
* lines across files — higher-precedence sources come later (later wins).
|
|
15
21
|
*
|
|
16
22
|
* Fail-soft: a malformed config file is skipped whole with a warning (same
|
|
17
23
|
* posture as hooks.ts). Malformed individual rule strings parse as bare
|
|
@@ -64,27 +70,36 @@ function readRulesFromFile(path, source, behavior, out, warnings) {
|
|
|
64
70
|
warnings.push(`${source} settings: malformed JSON — permissions skipped (${path})`);
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
/** Load + union rules from
|
|
73
|
+
/** Load + union rules from all three files. Pure I/O; no throw. */
|
|
68
74
|
export function loadPermissionRules(opts = {}) {
|
|
69
75
|
const warnings = [];
|
|
70
76
|
const rules = [];
|
|
71
77
|
const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
|
|
78
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
72
79
|
const userPath = join(stateHome, "config.json");
|
|
73
|
-
const projectPath = join(
|
|
74
|
-
|
|
80
|
+
const projectPath = join(cwd, ".yagni-code", "config.json");
|
|
81
|
+
const localPath = join(cwd, ".yagni-code", "config.local.json");
|
|
82
|
+
// User, then project, then local — ordering matters for path-rule
|
|
83
|
+
// negation (later sources win, matching scalar precedence).
|
|
75
84
|
for (const behavior of ["deny", "ask", "allow"]) {
|
|
76
85
|
readRulesFromFile(userPath, "user", behavior, rules, warnings);
|
|
77
86
|
readRulesFromFile(projectPath, "project", behavior, rules, warnings);
|
|
87
|
+
readRulesFromFile(localPath, "local", behavior, rules, warnings);
|
|
78
88
|
}
|
|
79
89
|
const neverConsultedTools = [
|
|
80
90
|
...new Set(rules.map((r) => r.toolName).filter((t) => !KNOWN_RULE_TOOLS.has(t) && !t.startsWith("mcp__"))),
|
|
81
91
|
];
|
|
82
92
|
return { rules, diagnostics: { warnings, neverConsultedTools } };
|
|
83
93
|
}
|
|
84
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Filter rules by behavior + trust. Untrusted repos keep deny/ask from every
|
|
96
|
+
* source but lose allow rules from BOTH project and local files — a
|
|
97
|
+
* committed config.local.json must not smuggle allow rules past the trust
|
|
98
|
+
* gate (see the module header; stricter than Claude Code's localSettings).
|
|
99
|
+
*/
|
|
85
100
|
export function effectiveRules(rules, isProjectTrusted) {
|
|
86
101
|
if (isProjectTrusted)
|
|
87
102
|
return [...rules];
|
|
88
|
-
return rules.filter((r) => !(r.source
|
|
103
|
+
return rules.filter((r) => !(r.source !== "user" && r.behavior === "allow"));
|
|
89
104
|
}
|
|
90
105
|
//# sourceMappingURL=loadConfig.js.map
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
* //path absolute from filesystem root
|
|
9
9
|
* ~/path relative to the user's home directory
|
|
10
10
|
* /path relative to the SETTINGS FILE's directory (user config →
|
|
11
|
-
* ~/.yagni-code/, project
|
|
11
|
+
* ~/.yagni-code/, project AND local configs → the project root —
|
|
12
|
+
* local lives in the same .yagni-code dir as project)
|
|
12
13
|
* path relative to the session cwd; a bare filename matches at ANY
|
|
13
14
|
* depth (`.env` equals `**` / `.env` in glob terms)
|
|
14
15
|
*
|
|
15
16
|
* Compile semantics: all patterns for one (tool-class, behavior) compile
|
|
16
17
|
* into ONE ordered `ignore` instance — user-source patterns first, then
|
|
17
|
-
* project — so gitignore's later-line-wins and `!exception`
|
|
18
|
-
* across rules from
|
|
19
|
-
* one ignore().add(patterns) per root the same
|
|
18
|
+
* project, then local — so gitignore's later-line-wins and `!exception`
|
|
19
|
+
* negation work across rules from all three files (Claude Code's
|
|
20
|
+
* matchingRuleForInput builds one ignore().add(patterns) per root the same
|
|
21
|
+
* way; local patterns are the last/highest-precedence "lines").
|
|
20
22
|
*
|
|
21
23
|
* Behavior asymmetry (Claude Code parity): allow rules with a rootless
|
|
22
24
|
* single-segment pattern anchor at the settings dir (narrow); deny/ask
|
|
@@ -40,9 +42,9 @@ export interface ResolvedPathPattern {
|
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Split an anchored pattern into (root, relativePattern). Mirrors Claude
|
|
43
|
-
* Code's patternWithRoot. Project-source `/` anchors at the
|
|
44
|
-
* (passed in)
|
|
45
|
-
* (~/.yagni-code).
|
|
45
|
+
* Code's patternWithRoot. Project- and local-source `/` anchors at the
|
|
46
|
+
* project root (passed in) — local shares project's .yagni-code dir;
|
|
47
|
+
* user-source `/` anchors at the YAGNI Code state home (~/.yagni-code).
|
|
46
48
|
*/
|
|
47
49
|
export declare function resolvePatternRoot(pattern: string, source: RuleSource, opts: {
|
|
48
50
|
userStateHome: string;
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
* //path absolute from filesystem root
|
|
9
9
|
* ~/path relative to the user's home directory
|
|
10
10
|
* /path relative to the SETTINGS FILE's directory (user config →
|
|
11
|
-
* ~/.yagni-code/, project
|
|
11
|
+
* ~/.yagni-code/, project AND local configs → the project root —
|
|
12
|
+
* local lives in the same .yagni-code dir as project)
|
|
12
13
|
* path relative to the session cwd; a bare filename matches at ANY
|
|
13
14
|
* depth (`.env` equals `**` / `.env` in glob terms)
|
|
14
15
|
*
|
|
15
16
|
* Compile semantics: all patterns for one (tool-class, behavior) compile
|
|
16
17
|
* into ONE ordered `ignore` instance — user-source patterns first, then
|
|
17
|
-
* project — so gitignore's later-line-wins and `!exception`
|
|
18
|
-
* across rules from
|
|
19
|
-
* one ignore().add(patterns) per root the same
|
|
18
|
+
* project, then local — so gitignore's later-line-wins and `!exception`
|
|
19
|
+
* negation work across rules from all three files (Claude Code's
|
|
20
|
+
* matchingRuleForInput builds one ignore().add(patterns) per root the same
|
|
21
|
+
* way; local patterns are the last/highest-precedence "lines").
|
|
20
22
|
*
|
|
21
23
|
* Behavior asymmetry (Claude Code parity): allow rules with a rootless
|
|
22
24
|
* single-segment pattern anchor at the settings dir (narrow); deny/ask
|
|
@@ -42,9 +44,9 @@ export function pathClassForTool(toolName) {
|
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
46
|
* Split an anchored pattern into (root, relativePattern). Mirrors Claude
|
|
45
|
-
* Code's patternWithRoot. Project-source `/` anchors at the
|
|
46
|
-
* (passed in)
|
|
47
|
-
* (~/.yagni-code).
|
|
47
|
+
* Code's patternWithRoot. Project- and local-source `/` anchors at the
|
|
48
|
+
* project root (passed in) — local shares project's .yagni-code dir;
|
|
49
|
+
* user-source `/` anchors at the YAGNI Code state home (~/.yagni-code).
|
|
48
50
|
*/
|
|
49
51
|
export function resolvePatternRoot(pattern, source, opts) {
|
|
50
52
|
if (pattern.startsWith("//")) {
|
|
@@ -55,7 +57,7 @@ export function resolvePatternRoot(pattern, source, opts) {
|
|
|
55
57
|
return { relativePattern: rel, root: opts.homeDir ?? homedir(), source };
|
|
56
58
|
}
|
|
57
59
|
if (pattern.startsWith("/") && !pattern.startsWith("//")) {
|
|
58
|
-
const base = source === "
|
|
60
|
+
const base = source === "user" ? opts.userStateHome : opts.projectRoot;
|
|
59
61
|
return { relativePattern: pattern, root: base ?? opts.userStateHome, source };
|
|
60
62
|
}
|
|
61
63
|
// Rootless: `./x` normalized to `x`; bare names keep any-depth semantics.
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sandbox settings schema + layered config load (user + project),
|
|
3
|
-
* Claude Code's SandboxSettingsSchema subset so a Claude
|
|
4
|
-
* copied verbatim parses and resolves identically.
|
|
2
|
+
* Sandbox settings schema + layered config load (user + project + local),
|
|
3
|
+
* shaped after Claude Code's SandboxSettingsSchema subset so a Claude
|
|
4
|
+
* `sandbox` block copied verbatim parses and resolves identically.
|
|
5
5
|
*
|
|
6
|
-
* Files: the `sandbox` key in ~/.yagni-code/config.json (user)
|
|
7
|
-
* .yagni-code/config.json (project)
|
|
8
|
-
*
|
|
6
|
+
* Files: the `sandbox` key in ~/.yagni-code/config.json (user),
|
|
7
|
+
* .yagni-code/config.json (project), and .yagni-code/config.local.json
|
|
8
|
+
* (local — the personal per-project tier; the /sandbox panel and toggle
|
|
9
|
+
* persist HERE).
|
|
9
10
|
*
|
|
10
|
-
* Merge semantics (Claude parity): scalars —
|
|
11
|
-
* defaults; arrays — union + dedupe across sources (lists merge,
|
|
12
|
-
* replace). Fail-soft: a malformed file is skipped whole with a
|
|
13
|
-
* (same posture as permissionRules/loadConfig.ts).
|
|
11
|
+
* Merge semantics (Claude parity): scalars — local wins over project over
|
|
12
|
+
* user over defaults; arrays — union + dedupe across sources (lists merge,
|
|
13
|
+
* never replace). Fail-soft: a malformed file is skipped whole with a
|
|
14
|
+
* warning (same posture as permissionRules/loadConfig.ts).
|
|
14
15
|
*
|
|
15
16
|
* Read/write filesystem semantics (srt, verified in the M0 spike): reads are
|
|
16
17
|
* allowed by default with denyRead regions + allowRead re-allow; writes are
|
|
@@ -64,10 +65,10 @@ export interface LoadedSandboxConfig {
|
|
|
64
65
|
*/
|
|
65
66
|
export declare function readSandboxSettingsFromFile(configPath: string, warnings: string[], unknownKeys: string[]): SandboxSettings | undefined;
|
|
66
67
|
/**
|
|
67
|
-
* Load + merge sandbox settings from
|
|
68
|
-
* beats user; arrays: union. Defaults for scalars land here
|
|
69
|
-
* parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands
|
|
70
|
-
* everything else unset/false.
|
|
68
|
+
* Load + merge sandbox settings from all three config files. Scalars: local
|
|
69
|
+
* beats project beats user; arrays: union. Defaults for scalars land here
|
|
70
|
+
* too (Claude parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands
|
|
71
|
+
* true, everything else unset/false.
|
|
71
72
|
*/
|
|
72
73
|
export declare function loadSandboxSettings(opts?: {
|
|
73
74
|
cwd?: string;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sandbox settings schema + layered config load (user + project),
|
|
3
|
-
* Claude Code's SandboxSettingsSchema subset so a Claude
|
|
4
|
-
* copied verbatim parses and resolves identically.
|
|
2
|
+
* Sandbox settings schema + layered config load (user + project + local),
|
|
3
|
+
* shaped after Claude Code's SandboxSettingsSchema subset so a Claude
|
|
4
|
+
* `sandbox` block copied verbatim parses and resolves identically.
|
|
5
5
|
*
|
|
6
|
-
* Files: the `sandbox` key in ~/.yagni-code/config.json (user)
|
|
7
|
-
* .yagni-code/config.json (project)
|
|
8
|
-
*
|
|
6
|
+
* Files: the `sandbox` key in ~/.yagni-code/config.json (user),
|
|
7
|
+
* .yagni-code/config.json (project), and .yagni-code/config.local.json
|
|
8
|
+
* (local — the personal per-project tier; the /sandbox panel and toggle
|
|
9
|
+
* persist HERE).
|
|
9
10
|
*
|
|
10
|
-
* Merge semantics (Claude parity): scalars —
|
|
11
|
-
* defaults; arrays — union + dedupe across sources (lists merge,
|
|
12
|
-
* replace). Fail-soft: a malformed file is skipped whole with a
|
|
13
|
-
* (same posture as permissionRules/loadConfig.ts).
|
|
11
|
+
* Merge semantics (Claude parity): scalars — local wins over project over
|
|
12
|
+
* user over defaults; arrays — union + dedupe across sources (lists merge,
|
|
13
|
+
* never replace). Fail-soft: a malformed file is skipped whole with a
|
|
14
|
+
* warning (same posture as permissionRules/loadConfig.ts).
|
|
14
15
|
*
|
|
15
16
|
* Read/write filesystem semantics (srt, verified in the M0 spike): reads are
|
|
16
17
|
* allowed by default with denyRead regions + allowRead re-allow; writes are
|
|
@@ -68,7 +69,11 @@ export function readSandboxSettingsFromFile(configPath, warnings, unknownKeys) {
|
|
|
68
69
|
parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
69
70
|
}
|
|
70
71
|
catch (err) {
|
|
71
|
-
|
|
72
|
+
// Error CLASS only, never the thrown message: V8's JSON.parse message
|
|
73
|
+
// embeds a snippet of the file's own content, and these warnings are
|
|
74
|
+
// forwarded verbatim into the error sink — a corrupt/hostile config's
|
|
75
|
+
// contents must not ride the trail. Path + class stays diagnosable.
|
|
76
|
+
warnings.push(`sandbox: could not parse ${configPath}: ${err instanceof Error ? err.constructor.name : typeof err}`);
|
|
72
77
|
return undefined;
|
|
73
78
|
}
|
|
74
79
|
if (!isPlainObject(parsed) || parsed.sandbox === undefined)
|
|
@@ -178,54 +183,69 @@ function union(...lists) {
|
|
|
178
183
|
return undefined;
|
|
179
184
|
return [...new Set(present.flat())];
|
|
180
185
|
}
|
|
181
|
-
function mergeNetwork(user, project) {
|
|
186
|
+
function mergeNetwork(user, project, local) {
|
|
182
187
|
const u = user ?? {};
|
|
183
188
|
const p = project ?? {};
|
|
189
|
+
const l = local ?? {};
|
|
184
190
|
return {
|
|
185
|
-
allowedDomains: union(u.allowedDomains, p.allowedDomains) ?? [],
|
|
186
|
-
deniedDomains: union(u.deniedDomains, p.deniedDomains) ?? [],
|
|
187
|
-
allowUnixSockets: union(u.allowUnixSockets, p.allowUnixSockets),
|
|
188
|
-
allowAllUnixSockets: p.allowAllUnixSockets ?? u.allowAllUnixSockets,
|
|
189
|
-
allowLocalBinding: p.allowLocalBinding ?? u.allowLocalBinding,
|
|
190
|
-
httpProxyPort: p.httpProxyPort ?? u.httpProxyPort,
|
|
191
|
-
socksProxyPort: p.socksProxyPort ?? u.socksProxyPort,
|
|
191
|
+
allowedDomains: union(u.allowedDomains, p.allowedDomains, l.allowedDomains) ?? [],
|
|
192
|
+
deniedDomains: union(u.deniedDomains, p.deniedDomains, l.deniedDomains) ?? [],
|
|
193
|
+
allowUnixSockets: union(u.allowUnixSockets, p.allowUnixSockets, l.allowUnixSockets),
|
|
194
|
+
allowAllUnixSockets: l.allowAllUnixSockets ?? p.allowAllUnixSockets ?? u.allowAllUnixSockets,
|
|
195
|
+
allowLocalBinding: l.allowLocalBinding ?? p.allowLocalBinding ?? u.allowLocalBinding,
|
|
196
|
+
httpProxyPort: l.httpProxyPort ?? p.httpProxyPort ?? u.httpProxyPort,
|
|
197
|
+
socksProxyPort: l.socksProxyPort ?? p.socksProxyPort ?? u.socksProxyPort,
|
|
192
198
|
};
|
|
193
199
|
}
|
|
194
|
-
function mergeFilesystem(user, project) {
|
|
200
|
+
function mergeFilesystem(user, project, local) {
|
|
195
201
|
const u = user ?? {};
|
|
196
202
|
const p = project ?? {};
|
|
203
|
+
const l = local ?? {};
|
|
197
204
|
return {
|
|
198
|
-
allowRead: union(u.allowRead, p.allowRead),
|
|
199
|
-
denyRead: union(u.denyRead, p.denyRead) ?? [],
|
|
200
|
-
allowWrite: union(u.allowWrite, p.allowWrite) ?? [],
|
|
201
|
-
denyWrite: union(u.denyWrite, p.denyWrite) ?? [],
|
|
205
|
+
allowRead: union(u.allowRead, p.allowRead, l.allowRead),
|
|
206
|
+
denyRead: union(u.denyRead, p.denyRead, l.denyRead) ?? [],
|
|
207
|
+
allowWrite: union(u.allowWrite, p.allowWrite, l.allowWrite) ?? [],
|
|
208
|
+
denyWrite: union(u.denyWrite, p.denyWrite, l.denyWrite) ?? [],
|
|
202
209
|
};
|
|
203
210
|
}
|
|
204
211
|
/**
|
|
205
|
-
* Load + merge sandbox settings from
|
|
206
|
-
* beats user; arrays: union. Defaults for scalars land here
|
|
207
|
-
* parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands
|
|
208
|
-
* everything else unset/false.
|
|
212
|
+
* Load + merge sandbox settings from all three config files. Scalars: local
|
|
213
|
+
* beats project beats user; arrays: union. Defaults for scalars land here
|
|
214
|
+
* too (Claude parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands
|
|
215
|
+
* true, everything else unset/false.
|
|
209
216
|
*/
|
|
210
217
|
export function loadSandboxSettings(opts = {}) {
|
|
211
218
|
const warnings = [];
|
|
212
219
|
const unknownKeys = [];
|
|
213
220
|
const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
|
|
221
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
214
222
|
const userPath = join(stateHome, "config.json");
|
|
215
|
-
const projectPath = join(
|
|
223
|
+
const projectPath = join(cwd, ".yagni-code", "config.json");
|
|
224
|
+
const localPath = join(cwd, ".yagni-code", "config.local.json");
|
|
216
225
|
const userSettings = readSandboxSettingsFromFile(userPath, warnings, unknownKeys);
|
|
217
226
|
const projectSettings = readSandboxSettingsFromFile(projectPath, warnings, unknownKeys);
|
|
227
|
+
const localSettings = readSandboxSettingsFromFile(localPath, warnings, unknownKeys);
|
|
218
228
|
const settings = {
|
|
219
|
-
enabled: projectSettings?.enabled ?? userSettings?.enabled ?? false,
|
|
220
|
-
autoAllowBashIfSandboxed:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
enabled: localSettings?.enabled ?? projectSettings?.enabled ?? userSettings?.enabled ?? false,
|
|
230
|
+
autoAllowBashIfSandboxed: localSettings?.autoAllowBashIfSandboxed ??
|
|
231
|
+
projectSettings?.autoAllowBashIfSandboxed ??
|
|
232
|
+
userSettings?.autoAllowBashIfSandboxed ??
|
|
233
|
+
true,
|
|
234
|
+
allowUnsandboxedCommands: localSettings?.allowUnsandboxedCommands ??
|
|
235
|
+
projectSettings?.allowUnsandboxedCommands ??
|
|
236
|
+
userSettings?.allowUnsandboxedCommands ??
|
|
237
|
+
true,
|
|
238
|
+
failIfUnavailable: localSettings?.failIfUnavailable ?? projectSettings?.failIfUnavailable ?? userSettings?.failIfUnavailable ?? false,
|
|
239
|
+
excludedCommands: union(userSettings?.excludedCommands, projectSettings?.excludedCommands, localSettings?.excludedCommands) ?? [],
|
|
240
|
+
network: mergeNetwork(userSettings?.network, projectSettings?.network, localSettings?.network),
|
|
241
|
+
filesystem: mergeFilesystem(userSettings?.filesystem, projectSettings?.filesystem, localSettings?.filesystem),
|
|
242
|
+
ignoreViolations: localSettings?.ignoreViolations ?? projectSettings?.ignoreViolations ?? userSettings?.ignoreViolations,
|
|
243
|
+
enableWeakerNestedSandbox: localSettings?.enableWeakerNestedSandbox ??
|
|
244
|
+
projectSettings?.enableWeakerNestedSandbox ??
|
|
245
|
+
userSettings?.enableWeakerNestedSandbox,
|
|
246
|
+
enableWeakerNetworkIsolation: localSettings?.enableWeakerNetworkIsolation ??
|
|
247
|
+
projectSettings?.enableWeakerNetworkIsolation ??
|
|
248
|
+
userSettings?.enableWeakerNetworkIsolation,
|
|
229
249
|
};
|
|
230
250
|
return { settings, diagnostics: { warnings, unknownKeys } };
|
|
231
251
|
}
|
|
@@ -343,8 +363,10 @@ export function mergeRulesIntoSandbox(settings, rules, base) {
|
|
|
343
363
|
// Protected paths: always denyWrite, never exempted (Claude parity + our
|
|
344
364
|
// own surfaces). Note srt denyWrite also denies read-of-ignored writes.
|
|
345
365
|
denyWrite.add(join(base.userStateHome, "config.json"));
|
|
346
|
-
if (base.projectRoot)
|
|
366
|
+
if (base.projectRoot) {
|
|
347
367
|
denyWrite.add(join(base.projectRoot, ".yagni-code", "config.json"));
|
|
368
|
+
denyWrite.add(join(base.projectRoot, ".yagni-code", "config.local.json"));
|
|
369
|
+
}
|
|
348
370
|
denyWrite.add(base.userStateHome);
|
|
349
371
|
if (base.projectRoot)
|
|
350
372
|
denyWrite.add(join(base.projectRoot, ".yagni-code"));
|
|
@@ -46,7 +46,17 @@ export declare class YagniSandboxManager {
|
|
|
46
46
|
private state;
|
|
47
47
|
private askHandler;
|
|
48
48
|
private readonly opts;
|
|
49
|
+
/** Config warnings already logged this process — buildRuntimeMerge runs
|
|
50
|
+
* per grant/setRules refresh, and re-logging the same corrupt-file warning
|
|
51
|
+
* per call would flood the trail with one line per grant. */
|
|
52
|
+
private loggedConfigWarnings;
|
|
49
53
|
constructor(opts: SandboxSessionOptions);
|
|
54
|
+
/** Surface loader diagnostics (warnings + unknown keys) once per string
|
|
55
|
+
* per process: a corrupt or wrong-shaped sandbox block in any of the
|
|
56
|
+
* three config files is skipped whole (fail-soft), and a silently ignored
|
|
57
|
+
* config (or a typo'd key) needs its signal — same posture as index.ts
|
|
58
|
+
* surfacing the rule-loader warnings. */
|
|
59
|
+
private surfaceConfigDiagnostics;
|
|
50
60
|
get initialized(): boolean;
|
|
51
61
|
get settings(): SandboxSettings | null;
|
|
52
62
|
get dependencies(): SandboxDependencyStatus | null;
|
|
@@ -24,10 +24,33 @@ export class YagniSandboxManager {
|
|
|
24
24
|
state = { initialized: false, dependencies: null, settings: null };
|
|
25
25
|
askHandler = null;
|
|
26
26
|
opts;
|
|
27
|
+
/** Config warnings already logged this process — buildRuntimeMerge runs
|
|
28
|
+
* per grant/setRules refresh, and re-logging the same corrupt-file warning
|
|
29
|
+
* per call would flood the trail with one line per grant. */
|
|
30
|
+
loggedConfigWarnings = new Set();
|
|
27
31
|
constructor(opts) {
|
|
28
32
|
this.opts = opts;
|
|
29
33
|
this.askHandler = opts.askHandler ?? null;
|
|
30
34
|
}
|
|
35
|
+
/** Surface loader diagnostics (warnings + unknown keys) once per string
|
|
36
|
+
* per process: a corrupt or wrong-shaped sandbox block in any of the
|
|
37
|
+
* three config files is skipped whole (fail-soft), and a silently ignored
|
|
38
|
+
* config (or a typo'd key) needs its signal — same posture as index.ts
|
|
39
|
+
* surfacing the rule-loader warnings. */
|
|
40
|
+
surfaceConfigDiagnostics(diagnostics) {
|
|
41
|
+
const messages = [
|
|
42
|
+
...diagnostics.warnings,
|
|
43
|
+
...(diagnostics.unknownKeys.length > 0
|
|
44
|
+
? [`sandbox: unknown keys ignored: ${diagnostics.unknownKeys.join(", ")}`]
|
|
45
|
+
: []),
|
|
46
|
+
];
|
|
47
|
+
for (const message of messages) {
|
|
48
|
+
if (this.loggedConfigWarnings.has(message))
|
|
49
|
+
continue;
|
|
50
|
+
this.loggedConfigWarnings.add(message);
|
|
51
|
+
logEvent({ source: "sandbox", level: "warn", event: "sandbox_config_warning", fields: { warning: message } });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
get initialized() { return this.state.initialized; }
|
|
32
55
|
get settings() { return this.state.settings; }
|
|
33
56
|
get dependencies() { return this.state.dependencies; }
|
|
@@ -75,12 +98,13 @@ export class YagniSandboxManager {
|
|
|
75
98
|
* tests; initialize() consumes it internally.
|
|
76
99
|
*/
|
|
77
100
|
buildRuntimeMerge(rules) {
|
|
78
|
-
const { settings } = loadSandboxSettings({
|
|
101
|
+
const { settings, diagnostics } = loadSandboxSettings({
|
|
79
102
|
cwd: this.opts.cwd,
|
|
80
103
|
env: this.opts.env,
|
|
81
104
|
userHome: this.opts.userHome,
|
|
82
105
|
stateHomeOverride: this.opts.stateHomeOverride,
|
|
83
106
|
});
|
|
107
|
+
this.surfaceConfigDiagnostics(diagnostics);
|
|
84
108
|
return mergeRulesIntoSandbox(settings, rules, {
|
|
85
109
|
cwd: this.opts.cwd,
|
|
86
110
|
userStateHome: this.opts.stateHomeOverride ?? codeStateHome(null, this.opts.env, this.opts.userHome),
|
|
@@ -134,6 +158,9 @@ export class YagniSandboxManager {
|
|
|
134
158
|
userHome: this.opts.userHome,
|
|
135
159
|
stateHomeOverride: this.opts.stateHomeOverride,
|
|
136
160
|
});
|
|
161
|
+
// Same warning surfacing as buildRuntimeMerge — initialize() skips
|
|
162
|
+
// buildRuntimeMerge on the disabled path, so its own load reports too.
|
|
163
|
+
this.surfaceConfigDiagnostics(load.diagnostics);
|
|
137
164
|
if (!load.settings.enabled && !opts.force)
|
|
138
165
|
return "sandbox disabled by config";
|
|
139
166
|
const deps = this.checkDependencies();
|