@yagni-app/code 1.0.6 → 1.0.8

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