@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.
Files changed (85) hide show
  1. package/README.md +84 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +13 -0
  12. package/dist/extension/index.js +230 -43
  13. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  14. package/dist/extension/mcp/cliConfig.js +1 -1
  15. package/dist/extension/mcp/config.d.ts +24 -2
  16. package/dist/extension/mcp/config.js +75 -3
  17. package/dist/extension/mcp/manager.d.ts +3 -1
  18. package/dist/extension/mcp/manager.js +2 -2
  19. package/dist/extension/mcp/panel.d.ts +0 -1
  20. package/dist/extension/mcp/panel.js +13 -3
  21. package/dist/extension/mcp/startup.js +8 -6
  22. package/dist/extension/permission/gate.d.ts +97 -2
  23. package/dist/extension/permission/gate.js +375 -26
  24. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  25. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  26. package/dist/extension/permissionRules/engine.d.ts +50 -0
  27. package/dist/extension/permissionRules/engine.js +238 -0
  28. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  29. package/dist/extension/permissionRules/loadConfig.js +90 -0
  30. package/dist/extension/permissionRules/parser.d.ts +38 -0
  31. package/dist/extension/permissionRules/parser.js +136 -0
  32. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  33. package/dist/extension/permissionRules/pathRules.js +120 -0
  34. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  35. package/dist/extension/permissionRules/shellRules.js +221 -0
  36. package/dist/extension/pipeline/invocation.d.ts +3 -6
  37. package/dist/extension/pipeline/invocation.js +3 -6
  38. package/dist/extension/pipeline/runner.d.ts +0 -1
  39. package/dist/extension/pipeline/runner.js +6 -14
  40. package/dist/extension/plugins/inventory.d.ts +88 -0
  41. package/dist/extension/plugins/inventory.js +144 -0
  42. package/dist/extension/plugins/panel.d.ts +45 -0
  43. package/dist/extension/plugins/panel.js +293 -0
  44. package/dist/extension/sandbox/bash.d.ts +99 -0
  45. package/dist/extension/sandbox/bash.js +190 -0
  46. package/dist/extension/sandbox/config.d.ts +114 -0
  47. package/dist/extension/sandbox/config.js +366 -0
  48. package/dist/extension/sandbox/manager.d.ts +98 -0
  49. package/dist/extension/sandbox/manager.js +216 -0
  50. package/dist/extension/sandbox/panel.d.ts +111 -0
  51. package/dist/extension/sandbox/panel.js +342 -0
  52. package/dist/extension/sandbox/session.d.ts +85 -0
  53. package/dist/extension/sandbox/session.js +775 -0
  54. package/dist/extension/telemetry/attrs.d.ts +96 -0
  55. package/dist/extension/telemetry/attrs.js +149 -0
  56. package/dist/extension/telemetry/config.d.ts +99 -0
  57. package/dist/extension/telemetry/config.js +193 -0
  58. package/dist/extension/telemetry/index.d.ts +7 -0
  59. package/dist/extension/telemetry/index.js +7 -0
  60. package/dist/extension/telemetry/probe.d.ts +29 -0
  61. package/dist/extension/telemetry/probe.js +122 -0
  62. package/dist/extension/telemetry/register.d.ts +40 -0
  63. package/dist/extension/telemetry/register.js +192 -0
  64. package/dist/extension/telemetry/sdk.d.ts +63 -0
  65. package/dist/extension/telemetry/sdk.js +207 -0
  66. package/dist/extension/telemetry/tracker.d.ts +131 -0
  67. package/dist/extension/telemetry/tracker.js +551 -0
  68. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  69. package/dist/extension/vendor/ignore.d.ts +86 -0
  70. package/dist/extension/vendor/ignore.js +788 -0
  71. package/dist/goHeadless.d.ts +1 -1
  72. package/dist/goHeadless.js +2 -2
  73. package/dist/launch.d.ts +4 -3
  74. package/dist/launch.js +7 -4
  75. package/dist/mcpCommand.d.ts +10 -1
  76. package/dist/mcpCommand.js +42 -10
  77. package/dist/otel.d.ts +67 -90
  78. package/dist/otel.js +152 -195
  79. package/dist/paths.d.ts +13 -0
  80. package/dist/paths.js +18 -0
  81. package/dist/pluginCommand.d.ts +43 -0
  82. package/dist/pluginCommand.js +499 -0
  83. package/dist/pluginStore.d.ts +170 -0
  84. package/dist/pluginStore.js +554 -0
  85. package/package.json +19 -3
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Sandbox settings schema + layered config load (user + project), shaped after
3
+ * Claude Code's SandboxSettingsSchema subset so a Claude `sandbox` block
4
+ * copied verbatim parses and resolves identically.
5
+ *
6
+ * Files: the `sandbox` key in ~/.yagni-code/config.json (user) and
7
+ * .yagni-code/config.json (project) — the same pair permissionRules reads,
8
+ * so one settings file drives both the permission layer and the sandbox.
9
+ *
10
+ * Merge semantics (Claude parity): scalars — project wins over user over
11
+ * defaults; arrays — union + dedupe across sources (lists merge, never
12
+ * replace). Fail-soft: a malformed file is skipped whole with a warning
13
+ * (same posture as permissionRules/loadConfig.ts).
14
+ *
15
+ * Read/write filesystem semantics (srt, verified in the M0 spike): reads are
16
+ * allowed by default with denyRead regions + allowRead re-allow; writes are
17
+ * allow-only (default-deny, /tmp included). Network: proxy-enforced
18
+ * allowlist; loopback needs allowLocalBinding (allowedDomains cannot open it
19
+ * — loopback bypasses the proxy via no_proxy).
20
+ */
21
+ import type { PermissionRule } from "../permissionRules/loadConfig.js";
22
+ export interface SandboxNetworkSettings {
23
+ allowedDomains?: string[];
24
+ deniedDomains?: string[];
25
+ allowUnixSockets?: string[];
26
+ allowAllUnixSockets?: boolean;
27
+ allowLocalBinding?: boolean;
28
+ httpProxyPort?: number;
29
+ socksProxyPort?: number;
30
+ }
31
+ export interface SandboxFilesystemSettings {
32
+ allowRead?: string[];
33
+ denyRead?: string[];
34
+ allowWrite?: string[];
35
+ denyWrite?: string[];
36
+ }
37
+ export interface SandboxSettings {
38
+ enabled?: boolean;
39
+ /** Sandboxed bash auto-allows (explicit deny/ask rules still win). Default true. */
40
+ autoAllowBashIfSandboxed?: boolean;
41
+ /** false = strict mode: dangerouslyDisableSandbox is ignored entirely. Default true. */
42
+ allowUnsandboxedCommands?: boolean;
43
+ /** Exit-with-error posture when enabled but deps/platform unavailable. Default false. */
44
+ failIfUnavailable?: boolean;
45
+ excludedCommands?: string[];
46
+ network?: SandboxNetworkSettings;
47
+ filesystem?: SandboxFilesystemSettings;
48
+ ignoreViolations?: Record<string, string[]>;
49
+ enableWeakerNestedSandbox?: boolean;
50
+ enableWeakerNetworkIsolation?: boolean;
51
+ }
52
+ export interface SandboxConfigDiagnostics {
53
+ warnings: string[];
54
+ unknownKeys: string[];
55
+ }
56
+ export interface LoadedSandboxConfig {
57
+ settings: SandboxSettings;
58
+ diagnostics: SandboxConfigDiagnostics;
59
+ }
60
+ /**
61
+ * Read the `sandbox` object from one config file. Returns undefined when the
62
+ * file is missing, unreadable, malformed, or has no sandbox key. Collects
63
+ * unknown keys for diagnostics (ignored, never fatal).
64
+ */
65
+ export declare function readSandboxSettingsFromFile(configPath: string, warnings: string[], unknownKeys: string[]): SandboxSettings | undefined;
66
+ /**
67
+ * Load + merge sandbox settings from both config files. Scalars: project
68
+ * beats user; arrays: union. Defaults for scalars land here too (Claude
69
+ * parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands true,
70
+ * everything else unset/false.
71
+ */
72
+ export declare function loadSandboxSettings(opts?: {
73
+ cwd?: string;
74
+ env?: NodeJS.ProcessEnv;
75
+ userHome?: string;
76
+ stateHomeOverride?: string | null;
77
+ }): LoadedSandboxConfig;
78
+ export interface SandboxRuntimePaths {
79
+ cwd: string;
80
+ /** ~/.yagni-code (user `/`-anchor base + protected paths). */
81
+ userStateHome: string;
82
+ /** Project root; null outside a repo. */
83
+ projectRoot: string | null;
84
+ homeDir?: string;
85
+ }
86
+ export interface SandboxRuntimeMerge {
87
+ network: SandboxNetworkSettings & {
88
+ allowedDomains: string[];
89
+ deniedDomains: string[];
90
+ };
91
+ filesystem: SandboxFilesystemSettings & {
92
+ allowRead: string[];
93
+ denyRead: string[];
94
+ allowWrite: string[];
95
+ denyWrite: string[];
96
+ };
97
+ /** Permission rules that could not map into sandbox restrictions (surfaced
98
+ * in diagnostics — never silently dropped). */
99
+ droppedRules: string[];
100
+ }
101
+ /**
102
+ * Merge sandbox settings + permission rules into srt's effective filesystem /
103
+ * network restriction sets. Rule mapping (Claude parity):
104
+ * Edit(path) allow -> allowWrite
105
+ * Edit(path) deny -> denyWrite
106
+ * Read(path) deny -> denyRead
107
+ * WebFetch(domain:) allow/deny -> allowedDomains / deniedDomains
108
+ *
109
+ * Rule paths use the permission-rule anchors (~/, //, settings-relative /,
110
+ * cwd-relative bare) resolved per-source; sandbox.filesystem.* uses standard
111
+ * path semantics (/ = absolute) exactly like Claude's two resolvers.
112
+ */
113
+ export declare function mergeRulesIntoSandbox(settings: SandboxSettings, rules: readonly PermissionRule[], base: SandboxRuntimePaths): SandboxRuntimeMerge;
114
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Sandbox settings schema + layered config load (user + project), shaped after
3
+ * Claude Code's SandboxSettingsSchema subset so a Claude `sandbox` block
4
+ * copied verbatim parses and resolves identically.
5
+ *
6
+ * Files: the `sandbox` key in ~/.yagni-code/config.json (user) and
7
+ * .yagni-code/config.json (project) — the same pair permissionRules reads,
8
+ * so one settings file drives both the permission layer and the sandbox.
9
+ *
10
+ * Merge semantics (Claude parity): scalars — project wins over user over
11
+ * defaults; arrays — union + dedupe across sources (lists merge, never
12
+ * replace). Fail-soft: a malformed file is skipped whole with a warning
13
+ * (same posture as permissionRules/loadConfig.ts).
14
+ *
15
+ * Read/write filesystem semantics (srt, verified in the M0 spike): reads are
16
+ * allowed by default with denyRead regions + allowRead re-allow; writes are
17
+ * allow-only (default-deny, /tmp included). Network: proxy-enforced
18
+ * allowlist; loopback needs allowLocalBinding (allowedDomains cannot open it
19
+ * — loopback bypasses the proxy via no_proxy).
20
+ */
21
+ import { existsSync, readFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { codeStateHome } from "../stateHome.js";
25
+ const KNOWN_TOP_LEVEL_KEYS = new Set([
26
+ "enabled",
27
+ "autoAllowBashIfSandboxed",
28
+ "allowUnsandboxedCommands",
29
+ "failIfUnavailable",
30
+ "excludedCommands",
31
+ "network",
32
+ "filesystem",
33
+ "ignoreViolations",
34
+ "enableWeakerNestedSandbox",
35
+ "enableWeakerNetworkIsolation",
36
+ ]);
37
+ const KNOWN_NETWORK_KEYS = new Set([
38
+ "allowedDomains",
39
+ "deniedDomains",
40
+ "allowUnixSockets",
41
+ "allowAllUnixSockets",
42
+ "allowLocalBinding",
43
+ "httpProxyPort",
44
+ "socksProxyPort",
45
+ ]);
46
+ const KNOWN_FILESYSTEM_KEYS = new Set(["allowRead", "denyRead", "allowWrite", "denyWrite"]);
47
+ const ARRAY_KEYS_BY_SECTION = {
48
+ "": ["excludedCommands"],
49
+ network: ["allowedDomains", "deniedDomains", "allowUnixSockets"],
50
+ filesystem: ["allowRead", "denyRead", "allowWrite", "denyWrite"],
51
+ };
52
+ function isPlainObject(v) {
53
+ return typeof v === "object" && v !== null && !Array.isArray(v);
54
+ }
55
+ function stringArray(v) {
56
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : undefined;
57
+ }
58
+ /**
59
+ * Read the `sandbox` object from one config file. Returns undefined when the
60
+ * file is missing, unreadable, malformed, or has no sandbox key. Collects
61
+ * unknown keys for diagnostics (ignored, never fatal).
62
+ */
63
+ export function readSandboxSettingsFromFile(configPath, warnings, unknownKeys) {
64
+ if (!existsSync(configPath))
65
+ return undefined;
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(readFileSync(configPath, "utf-8"));
69
+ }
70
+ catch (err) {
71
+ warnings.push(`sandbox: could not parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
72
+ return undefined;
73
+ }
74
+ if (!isPlainObject(parsed) || parsed.sandbox === undefined)
75
+ return undefined;
76
+ if (!isPlainObject(parsed.sandbox)) {
77
+ warnings.push(`sandbox: "sandbox" in ${configPath} must be an object — ignored`);
78
+ return undefined;
79
+ }
80
+ const out = {};
81
+ const src = parsed.sandbox;
82
+ for (const [k, v] of Object.entries(src)) {
83
+ if (k === "network") {
84
+ if (isPlainObject(v)) {
85
+ const net = {};
86
+ for (const [nk, nv] of Object.entries(v)) {
87
+ if (!KNOWN_NETWORK_KEYS.has(nk)) {
88
+ unknownKeys.push(`network.${nk}`);
89
+ continue;
90
+ }
91
+ if ((nk === "allowAllUnixSockets" || nk === "allowLocalBinding") && typeof nv === "boolean") {
92
+ net[nk] = nv;
93
+ }
94
+ else if ((nk === "httpProxyPort" || nk === "socksProxyPort") && typeof nv === "number") {
95
+ net[nk] = nv;
96
+ }
97
+ else {
98
+ const arr = stringArray(nv);
99
+ if (arr === undefined) {
100
+ warnings.push(`sandbox: network.${nk} has an unsupported value type — ignored`);
101
+ }
102
+ else {
103
+ net[nk] = arr;
104
+ }
105
+ }
106
+ }
107
+ out.network = net;
108
+ }
109
+ else {
110
+ warnings.push(`sandbox: network in ${configPath} must be an object — ignored`);
111
+ }
112
+ continue;
113
+ }
114
+ if (k === "filesystem") {
115
+ if (isPlainObject(v)) {
116
+ const fs = {};
117
+ for (const [fk, fv] of Object.entries(v)) {
118
+ if (!KNOWN_FILESYSTEM_KEYS.has(fk)) {
119
+ unknownKeys.push(`filesystem.${fk}`);
120
+ continue;
121
+ }
122
+ const arr = stringArray(fv);
123
+ if (arr === undefined) {
124
+ warnings.push(`sandbox: filesystem.${fk} has an unsupported value type — ignored`);
125
+ }
126
+ else {
127
+ fs[fk] = arr;
128
+ }
129
+ }
130
+ out.filesystem = fs;
131
+ }
132
+ else {
133
+ warnings.push(`sandbox: filesystem in ${configPath} must be an object — ignored`);
134
+ }
135
+ continue;
136
+ }
137
+ if (!KNOWN_TOP_LEVEL_KEYS.has(k)) {
138
+ unknownKeys.push(k);
139
+ continue;
140
+ }
141
+ if (k === "ignoreViolations") {
142
+ if (isPlainObject(v)) {
143
+ const iv = {};
144
+ for (const [pat, arr] of Object.entries(v)) {
145
+ const list = stringArray(arr);
146
+ if (list === undefined) {
147
+ warnings.push(`sandbox: ignoreViolations["${pat}"] must be a string array — ignored`);
148
+ }
149
+ else {
150
+ iv[pat] = list;
151
+ }
152
+ }
153
+ out.ignoreViolations = iv;
154
+ }
155
+ else {
156
+ warnings.push(`sandbox: ignoreViolations in ${configPath} must be an object — ignored`);
157
+ }
158
+ continue;
159
+ }
160
+ if (k === "excludedCommands") {
161
+ const list = stringArray(v);
162
+ if (list === undefined)
163
+ warnings.push(`sandbox: excludedCommands must be a string array — ignored`);
164
+ else
165
+ out.excludedCommands = list;
166
+ continue;
167
+ }
168
+ if (typeof v === "boolean")
169
+ out[k] = v;
170
+ else
171
+ warnings.push(`sandbox: ${k} must be a boolean — ignored`);
172
+ }
173
+ return out;
174
+ }
175
+ function union(...lists) {
176
+ const present = lists.filter((l) => Array.isArray(l));
177
+ if (present.length === 0)
178
+ return undefined;
179
+ return [...new Set(present.flat())];
180
+ }
181
+ function mergeNetwork(user, project) {
182
+ const u = user ?? {};
183
+ const p = project ?? {};
184
+ 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,
192
+ };
193
+ }
194
+ function mergeFilesystem(user, project) {
195
+ const u = user ?? {};
196
+ const p = project ?? {};
197
+ 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) ?? [],
202
+ };
203
+ }
204
+ /**
205
+ * Load + merge sandbox settings from both config files. Scalars: project
206
+ * beats user; arrays: union. Defaults for scalars land here too (Claude
207
+ * parity): autoAllowBashIfSandboxed true, allowUnsandboxedCommands true,
208
+ * everything else unset/false.
209
+ */
210
+ export function loadSandboxSettings(opts = {}) {
211
+ const warnings = [];
212
+ const unknownKeys = [];
213
+ const stateHome = opts.stateHomeOverride ?? codeStateHome(null, opts.env, opts.userHome);
214
+ const userPath = join(stateHome, "config.json");
215
+ const projectPath = join(opts.cwd ?? process.cwd(), ".yagni-code", "config.json");
216
+ const userSettings = readSandboxSettingsFromFile(userPath, warnings, unknownKeys);
217
+ const projectSettings = readSandboxSettingsFromFile(projectPath, warnings, unknownKeys);
218
+ const settings = {
219
+ enabled: projectSettings?.enabled ?? userSettings?.enabled ?? false,
220
+ autoAllowBashIfSandboxed: projectSettings?.autoAllowBashIfSandboxed ?? userSettings?.autoAllowBashIfSandboxed ?? true,
221
+ allowUnsandboxedCommands: projectSettings?.allowUnsandboxedCommands ?? userSettings?.allowUnsandboxedCommands ?? true,
222
+ failIfUnavailable: projectSettings?.failIfUnavailable ?? userSettings?.failIfUnavailable ?? false,
223
+ excludedCommands: union(userSettings?.excludedCommands, projectSettings?.excludedCommands) ?? [],
224
+ network: mergeNetwork(userSettings?.network, projectSettings?.network),
225
+ filesystem: mergeFilesystem(userSettings?.filesystem, projectSettings?.filesystem),
226
+ ignoreViolations: projectSettings?.ignoreViolations ?? userSettings?.ignoreViolations,
227
+ enableWeakerNestedSandbox: projectSettings?.enableWeakerNestedSandbox ?? userSettings?.enableWeakerNestedSandbox,
228
+ enableWeakerNetworkIsolation: projectSettings?.enableWeakerNetworkIsolation ?? userSettings?.enableWeakerNetworkIsolation,
229
+ };
230
+ return { settings, diagnostics: { warnings, unknownKeys } };
231
+ }
232
+ function expandHome(p, homeDir) {
233
+ if (p === "~")
234
+ return homeDir;
235
+ if (p.startsWith("~/"))
236
+ return join(homeDir, p.slice(2));
237
+ return p;
238
+ }
239
+ function resolveSandboxPath(pattern, base) {
240
+ const homeDir = base.homeDir ?? homeDirDefault();
241
+ if (pattern.startsWith("//"))
242
+ return pattern.slice(1); // absolute from root (Claude legacy)
243
+ if (pattern.startsWith("/")) {
244
+ // Claude permission-rule convention: settings-dir-relative. User rules
245
+ // resolve against ~/.yagni-code, project rules against the project root.
246
+ return join(base.userStateHome, pattern.slice(1));
247
+ }
248
+ return expandHome(pattern, homeDir);
249
+ }
250
+ function resolveSandboxFsPath(pattern, base) {
251
+ // sandbox.filesystem.* uses STANDARD path semantics (Claude #30067):
252
+ // /path -> absolute (as written)
253
+ // //path -> absolute (legacy permission-rule escape, kept for compat)
254
+ // ~/path, ./path, path -> expanded / settings-dir-relative
255
+ const homeDir = base.homeDir ?? homeDirDefault();
256
+ if (pattern.startsWith("//"))
257
+ return pattern.slice(1);
258
+ if (pattern.startsWith("/"))
259
+ return pattern;
260
+ return expandHome(pattern, homeDir);
261
+ }
262
+ let homeDirDefault = () => homeDirImpl();
263
+ import { homedir as homeDirImpl } from "node:os";
264
+ function homeDirDefaultFn() { return homeDirImpl(); }
265
+ homeDirDefault = homeDirDefaultFn;
266
+ /**
267
+ * Merge sandbox settings + permission rules into srt's effective filesystem /
268
+ * network restriction sets. Rule mapping (Claude parity):
269
+ * Edit(path) allow -> allowWrite
270
+ * Edit(path) deny -> denyWrite
271
+ * Read(path) deny -> denyRead
272
+ * WebFetch(domain:) allow/deny -> allowedDomains / deniedDomains
273
+ *
274
+ * Rule paths use the permission-rule anchors (~/, //, settings-relative /,
275
+ * cwd-relative bare) resolved per-source; sandbox.filesystem.* uses standard
276
+ * path semantics (/ = absolute) exactly like Claude's two resolvers.
277
+ */
278
+ export function mergeRulesIntoSandbox(settings, rules, base) {
279
+ const homeDir = base.homeDir ?? homeDirDefault();
280
+ const allowWrite = new Set([base.cwd, tmpdir()]);
281
+ const denyWrite = new Set();
282
+ const denyRead = new Set();
283
+ const allowRead = new Set();
284
+ const allowedDomains = new Set(settings.network?.allowedDomains ?? []);
285
+ const deniedDomains = new Set(settings.network?.deniedDomains ?? []);
286
+ /** Permission rules that could not map into sandbox restrictions —
287
+ * surfaced to the caller (diagnostics), never silently dropped. */
288
+ const droppedRules = [];
289
+ // Permission rules: user rules resolve /-anchors against the user state
290
+ // home, project rules against the project root (pathRules semantics).
291
+ for (const rule of rules) {
292
+ const anchorBase = {
293
+ ...base,
294
+ userStateHome: rule.source === "user" ? base.userStateHome : (base.projectRoot ?? base.userStateHome),
295
+ };
296
+ if (rule.toolName === "edit" || rule.toolName === "write") {
297
+ if (!rule.ruleContent)
298
+ continue;
299
+ const p = resolveSandboxPath(rule.ruleContent, anchorBase);
300
+ if (rule.behavior === "allow")
301
+ allowWrite.add(p);
302
+ // ask and deny BOTH deny in the sandbox: an ask rule's contract is
303
+ // "consult the user for writes here", and sandbox auto-allow would
304
+ // silently skip that consultation for bash — degrading ask to allow.
305
+ // denyWrite blocks bash at the OS level; the file tools still honor
306
+ // ask through the ordinary gate flow.
307
+ else if (rule.behavior === "deny" || rule.behavior === "ask")
308
+ denyWrite.add(p);
309
+ }
310
+ else if (rule.toolName === "read") {
311
+ if (!rule.ruleContent)
312
+ continue;
313
+ const p = resolveSandboxPath(rule.ruleContent, anchorBase);
314
+ if (rule.behavior === "deny")
315
+ denyRead.add(p);
316
+ }
317
+ else if (rule.toolName === "web_fetch") {
318
+ const m = rule.ruleContent?.match(/^domain:(.*)$/);
319
+ if (!m) {
320
+ // Non-domain web_fetch rules (bare tool-name form) cannot map into
321
+ // the network lists — surface instead of silently dropping.
322
+ droppedRules.push(rule.raw);
323
+ continue;
324
+ }
325
+ const domain = m[1];
326
+ if (rule.behavior === "allow")
327
+ allowedDomains.add(domain);
328
+ else if (rule.behavior === "deny")
329
+ deniedDomains.add(domain);
330
+ else if (rule.behavior === "ask")
331
+ deniedDomains.add(domain); // same ask-wins reasoning
332
+ }
333
+ }
334
+ // sandbox.filesystem.*: standard path semantics (NOT settings-relative).
335
+ for (const p of settings.filesystem?.allowWrite ?? [])
336
+ allowWrite.add(resolveSandboxFsPath(p, base));
337
+ for (const p of settings.filesystem?.denyWrite ?? [])
338
+ denyWrite.add(resolveSandboxFsPath(p, base));
339
+ for (const p of settings.filesystem?.denyRead ?? [])
340
+ denyRead.add(resolveSandboxFsPath(p, base));
341
+ for (const p of settings.filesystem?.allowRead ?? [])
342
+ allowRead.add(resolveSandboxFsPath(p, base));
343
+ // Protected paths: always denyWrite, never exempted (Claude parity + our
344
+ // own surfaces). Note srt denyWrite also denies read-of-ignored writes.
345
+ denyWrite.add(join(base.userStateHome, "config.json"));
346
+ if (base.projectRoot)
347
+ denyWrite.add(join(base.projectRoot, ".yagni-code", "config.json"));
348
+ denyWrite.add(base.userStateHome);
349
+ if (base.projectRoot)
350
+ denyWrite.add(join(base.projectRoot, ".yagni-code"));
351
+ return {
352
+ network: {
353
+ ...settings.network,
354
+ allowedDomains: [...allowedDomains],
355
+ deniedDomains: [...deniedDomains],
356
+ },
357
+ filesystem: {
358
+ allowRead: [...allowRead],
359
+ denyRead: [...denyRead],
360
+ allowWrite: [...allowWrite],
361
+ denyWrite: [...denyWrite],
362
+ },
363
+ droppedRules,
364
+ };
365
+ }
366
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Sandbox manager adapter — wraps @anthropic-ai/sandbox-runtime's
3
+ * SandboxManager with YAGNI Code wiring: dependency check against the
4
+ * vendored ripgrep, settings-derived runtime config (config.ts merge), and
5
+ * an injectable network ask-callback so the UI layer owns prompting (M5)
6
+ * while headless fails closed (deny).
7
+ *
8
+ * All state is session-scoped behind a single holder; nothing module-global
9
+ * is mutated except the underlying srt manager, which initialize/reset
10
+ * bracket. Pure functions (config building) live in config.ts.
11
+ */
12
+ import { type SandboxSettings, type SandboxRuntimeMerge } from "./config.js";
13
+ import type { PermissionRule } from "../permissionRules/loadConfig.js";
14
+ /** Where the network ask-callback surfaces a decision. */
15
+ export type NetworkAskHandler = (host: string) => Promise<boolean>;
16
+ export interface SandboxDependencyStatus {
17
+ /** Fatal: sandbox cannot run. */
18
+ errors: string[];
19
+ warnings: string[];
20
+ }
21
+ export interface SandboxManagerEvents {
22
+ onStateChange?: (enabled: boolean) => void;
23
+ }
24
+ export interface SandboxSessionOptions {
25
+ cwd: string;
26
+ env?: NodeJS.ProcessEnv;
27
+ userHome?: string;
28
+ stateHomeOverride?: string | null;
29
+ projectRoot?: string | null;
30
+ /** Ripgrep binary for srt's dependency check (default: vendored rg). */
31
+ rgPath?: string;
32
+ askHandler?: NetworkAskHandler | null;
33
+ events?: SandboxManagerEvents;
34
+ }
35
+ export interface SandboxSessionState {
36
+ /** srt initialized and commands will be wrapped. */
37
+ initialized: boolean;
38
+ /** Last dependency status (memoized per init attempt). */
39
+ dependencies: SandboxDependencyStatus | null;
40
+ /** The settings snapshot this session initialized with. */
41
+ settings: SandboxSettings | null;
42
+ }
43
+ /** Default vendored rg shipped with the CLI install. */
44
+ export declare function defaultRgPath(env?: NodeJS.ProcessEnv): string | undefined;
45
+ export declare class YagniSandboxManager {
46
+ private state;
47
+ private askHandler;
48
+ private readonly opts;
49
+ constructor(opts: SandboxSessionOptions);
50
+ get initialized(): boolean;
51
+ get settings(): SandboxSettings | null;
52
+ get dependencies(): SandboxDependencyStatus | null;
53
+ setAskHandler(handler: NetworkAskHandler | null): void;
54
+ /** The failure-trail wrapper handed to srt — exported for tests so the
55
+ * catch-and-log contract (fail closed + sink line) is observable without
56
+ * driving a real wrapped network connection. */
57
+ networkAsk(host: string): Promise<boolean>;
58
+ checkDependencies(): SandboxDependencyStatus;
59
+ /**
60
+ * Build the runtime filesystem/network config from current settings +
61
+ * permission rules. Exposed for diagnostics (/sandbox config display) and
62
+ * tests; initialize() consumes it internally.
63
+ */
64
+ buildRuntimeMerge(rules: readonly PermissionRule[]): SandboxRuntimeMerge;
65
+ private runtimeConfig;
66
+ /**
67
+ * Initialize srt for this session. Returns an error string when the sandbox
68
+ * cannot start (deps/platform) — the caller decides fail-soft (notify) vs
69
+ * fail-hard (refuse to run, `failIfUnavailable`). Idempotent: a second call
70
+ * while initialized is a no-op.
71
+ *
72
+ * `force` bypasses the disk `enabled` check: the session-level toggle
73
+ * (/sandbox enable, Alt+S) must be able to START the sandbox in a
74
+ * default-off config — that is its entire purpose. Config-driven starts
75
+ * (session_start) never pass force.
76
+ */
77
+ initialize(rules: readonly PermissionRule[], opts?: {
78
+ force?: boolean;
79
+ }): Promise<string | undefined>;
80
+ /**
81
+ * Rebuild + push the srt config without reset (settings changed or a grant
82
+ * landed). No-op when not initialized.
83
+ */
84
+ refreshConfig(rules: readonly PermissionRule[]): void;
85
+ /**
86
+ * Re-init from scratch (config grant changed semantics srt can't hot-swap,
87
+ * or a toggle). Tears down proxies + violation store first.
88
+ */
89
+ reinitialize(rules: readonly PermissionRule[]): Promise<string | undefined>;
90
+ /** Tear down srt (proxies, violation store, seatbelt state). */
91
+ reset(): Promise<void>;
92
+ wrapWithSandbox(command: string, binShell?: string): Promise<string>;
93
+ cleanupAfterCommand(): void;
94
+ annotateStderrWithSandboxFailures(command: string, stderr: string): string;
95
+ getProxyPort(): number | undefined;
96
+ getSocksProxyPort(): number | undefined;
97
+ }
98
+ //# sourceMappingURL=manager.d.ts.map