@yagni-app/code-staging 1.0.7-staging.1268.1 → 1.0.7-staging.1272.1
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 +8 -1
- 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 +19 -13
- 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/config.d.ts +5 -1
- package/dist/extension/telemetry/register.d.ts +7 -0
- package/dist/extension/telemetry/register.js +15 -0
- package/package.json +2 -2
|
@@ -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();
|