@yagni-app/code 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +89 -27
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -3
  5. package/dist/claudePlugins.js +129 -19
  6. package/dist/cli.js +28 -4
  7. package/dist/doctor.d.ts +42 -4
  8. package/dist/doctor.js +150 -15
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +18 -5
  12. package/dist/extension/index.js +319 -72
  13. package/dist/extension/mcp/approval.d.ts +45 -0
  14. package/dist/extension/mcp/approval.js +164 -0
  15. package/dist/extension/mcp/auth.d.ts +124 -0
  16. package/dist/extension/mcp/auth.js +560 -0
  17. package/dist/extension/mcp/authStore.d.ts +61 -0
  18. package/dist/extension/mcp/authStore.js +105 -0
  19. package/dist/extension/mcp/callbackPage.d.ts +31 -0
  20. package/dist/extension/mcp/callbackPage.js +222 -0
  21. package/dist/extension/mcp/cliConfig.d.ts +12 -0
  22. package/dist/extension/mcp/cliConfig.js +12 -0
  23. package/dist/extension/mcp/config.d.ts +153 -0
  24. package/dist/extension/mcp/config.js +381 -0
  25. package/dist/extension/mcp/log.d.ts +28 -0
  26. package/dist/extension/mcp/log.js +82 -0
  27. package/dist/extension/mcp/manager.d.ts +100 -0
  28. package/dist/extension/mcp/manager.js +273 -0
  29. package/dist/extension/mcp/names.d.ts +25 -0
  30. package/dist/extension/mcp/names.js +40 -0
  31. package/dist/extension/mcp/panel.d.ts +33 -0
  32. package/dist/extension/mcp/panel.js +268 -0
  33. package/dist/extension/mcp/prompts.d.ts +23 -0
  34. package/dist/extension/mcp/prompts.js +93 -0
  35. package/dist/extension/mcp/startup.d.ts +55 -0
  36. package/dist/extension/mcp/startup.js +152 -0
  37. package/dist/extension/mcp/tools.d.ts +31 -0
  38. package/dist/extension/mcp/tools.js +117 -0
  39. package/dist/extension/mcp/transports.d.ts +17 -0
  40. package/dist/extension/mcp/transports.js +44 -0
  41. package/dist/extension/permission/gate.d.ts +104 -2
  42. package/dist/extension/permission/gate.js +387 -31
  43. package/dist/extension/permission/guardian.d.ts +24 -5
  44. package/dist/extension/permission/guardian.js +162 -24
  45. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  46. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  47. package/dist/extension/permissionRules/engine.d.ts +50 -0
  48. package/dist/extension/permissionRules/engine.js +238 -0
  49. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  50. package/dist/extension/permissionRules/loadConfig.js +90 -0
  51. package/dist/extension/permissionRules/parser.d.ts +38 -0
  52. package/dist/extension/permissionRules/parser.js +136 -0
  53. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  54. package/dist/extension/permissionRules/pathRules.js +120 -0
  55. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  56. package/dist/extension/permissionRules/shellRules.js +221 -0
  57. package/dist/extension/pipeline/invocation.d.ts +3 -6
  58. package/dist/extension/pipeline/invocation.js +3 -6
  59. package/dist/extension/pipeline/personas.js +5 -0
  60. package/dist/extension/pipeline/runner.d.ts +0 -1
  61. package/dist/extension/pipeline/runner.js +6 -14
  62. package/dist/extension/plugins/inventory.d.ts +88 -0
  63. package/dist/extension/plugins/inventory.js +144 -0
  64. package/dist/extension/plugins/panel.d.ts +45 -0
  65. package/dist/extension/plugins/panel.js +293 -0
  66. package/dist/extension/sandbox/bash.d.ts +99 -0
  67. package/dist/extension/sandbox/bash.js +190 -0
  68. package/dist/extension/sandbox/config.d.ts +114 -0
  69. package/dist/extension/sandbox/config.js +366 -0
  70. package/dist/extension/sandbox/manager.d.ts +98 -0
  71. package/dist/extension/sandbox/manager.js +216 -0
  72. package/dist/extension/sandbox/panel.d.ts +111 -0
  73. package/dist/extension/sandbox/panel.js +342 -0
  74. package/dist/extension/sandbox/session.d.ts +85 -0
  75. package/dist/extension/sandbox/session.js +775 -0
  76. package/dist/extension/telemetry/attrs.d.ts +96 -0
  77. package/dist/extension/telemetry/attrs.js +149 -0
  78. package/dist/extension/telemetry/config.d.ts +99 -0
  79. package/dist/extension/telemetry/config.js +193 -0
  80. package/dist/extension/telemetry/index.d.ts +7 -0
  81. package/dist/extension/telemetry/index.js +7 -0
  82. package/dist/extension/telemetry/probe.d.ts +29 -0
  83. package/dist/extension/telemetry/probe.js +122 -0
  84. package/dist/extension/telemetry/register.d.ts +40 -0
  85. package/dist/extension/telemetry/register.js +192 -0
  86. package/dist/extension/telemetry/sdk.d.ts +63 -0
  87. package/dist/extension/telemetry/sdk.js +207 -0
  88. package/dist/extension/telemetry/tracker.d.ts +131 -0
  89. package/dist/extension/telemetry/tracker.js +551 -0
  90. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  91. package/dist/extension/vendor/ignore.d.ts +86 -0
  92. package/dist/extension/vendor/ignore.js +788 -0
  93. package/dist/goHeadless.d.ts +1 -1
  94. package/dist/goHeadless.js +2 -2
  95. package/dist/launch.d.ts +4 -3
  96. package/dist/launch.js +7 -4
  97. package/dist/mcpCommand.d.ts +122 -0
  98. package/dist/mcpCommand.js +787 -0
  99. package/dist/otel.d.ts +69 -63
  100. package/dist/otel.js +154 -119
  101. package/dist/paths.d.ts +13 -0
  102. package/dist/paths.js +18 -0
  103. package/dist/pluginCommand.d.ts +43 -0
  104. package/dist/pluginCommand.js +499 -0
  105. package/dist/pluginStore.d.ts +170 -0
  106. package/dist/pluginStore.js +554 -0
  107. package/dist/upgrade.d.ts +11 -2
  108. package/dist/upgrade.js +48 -8
  109. package/package.json +20 -3
  110. package/dist/extension/mcpTools.d.ts +0 -57
  111. package/dist/extension/mcpTools.js +0 -132
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Bash-tool sandbox wiring: decides whether a bash command runs
3
+ * sandboxed, and provides the spawnHook that wraps the command with the OS
4
+ * sandbox (srt) while pi's local exec backend keeps owning process
5
+ * lifecycle, timeout, and abort handling.
6
+ *
7
+ * The wrap uses pi's BashToolOptions.spawnHook (command/cwd/env rewrite
8
+ * before spawn) rather than replacing BashOperations wholesale — pi's
9
+ * battle-tested exec backend stays in charge; only the command string and
10
+ * proxy env change. user_bash (! commands) routes through the same wrap.
11
+ *
12
+ `dangerouslyDisableSandbox` mirrors Claude Code's bash tool parameter: the
13
+ * model may set it when it has evidence a sandbox restriction caused a
14
+ * failure; the parameter NEVER skips the permission gate (gate.ts routes the
15
+ * retry through the normal execPolicy/Guardian flow) — it only skips the OS
16
+ * wrap, and only when sandbox.allowUnsandboxedCommands is true.
17
+ *
18
+ * Child-process stdio-grace handling derived from pi-sandbox (MIT,
19
+ * carderne/pi-sandbox src/sandbox-runtime.ts) — not needed here because pi's
20
+ * local backend owns the child; the reference is credited for the operational
21
+ * lessons (process-group kill, stdio release).
22
+ */
23
+ import { existsSync } from "node:fs";
24
+ function stripLeadingSafeEnvVars(command) {
25
+ // Best-effort normalization for excludedCommands matching (not a security
26
+ // boundary — the permission gate is). Strips leading VAR=val pairs whose
27
+ // name can't affect binary resolution.
28
+ const SAFE = /^[A-Z_][A-Z0-9_]*=(?:[A-Za-z0-9_@./:+~-]+|"[^"\n]*"|'[^'\n]*')/;
29
+ let out = command.trimStart();
30
+ for (;;) {
31
+ const m = out.match(SAFE);
32
+ if (!m)
33
+ return out;
34
+ out = out.slice(m[0].length).trimStart();
35
+ }
36
+ }
37
+ function commandSegments(command) {
38
+ // Split on shell separators for excludedCommands matching against compound
39
+ // commands. Deliberately naive (no quote-awareness beyond the safe-env
40
+ // strip): excludedCommands is a convenience, never a security boundary.
41
+ // & (background) joins ;, &&, ||, | — a compound rider must not smuggle an
42
+ // excluded command's segment past the match.
43
+ return command
44
+ .split(/(?:\n|&&|\|\||;|\||&)/)
45
+ .map((s) => stripLeadingSafeEnvVars(s).trim())
46
+ .filter(Boolean);
47
+ }
48
+ function matchesExcludedPattern(pattern, candidate) {
49
+ if (pattern.endsWith(":*")) {
50
+ const prefix = pattern.slice(0, -2);
51
+ return candidate === prefix || candidate.startsWith(prefix + " ");
52
+ }
53
+ return candidate === pattern;
54
+ }
55
+ /**
56
+ * Does an excludedCommands pattern match any segment of the command?
57
+ * (Compound-aware: "echo hi && docker ps" must not ride an "echo:*" exclude.)
58
+ */
59
+ export function commandHasExcludedSegment(command, excludedPatterns) {
60
+ if (excludedPatterns.length === 0)
61
+ return false;
62
+ const segments = commandSegments(command);
63
+ return segments.some((seg) => excludedPatterns.some((p) => matchesExcludedPattern(p, seg)));
64
+ }
65
+ /**
66
+ * Should this bash tool call run inside the OS sandbox?
67
+ * Pure — reads the decision inputs, no I/O.
68
+ */
69
+ export function shouldUseSandbox(input, manager, settings) {
70
+ if (!manager.initialized)
71
+ return false;
72
+ if (!input.command)
73
+ return false;
74
+ if (input.dangerouslyDisableSandbox && settings.allowUnsandboxedCommands !== false)
75
+ return false;
76
+ if (commandHasExcludedSegment(input.command, settings.excludedCommands ?? []))
77
+ return false;
78
+ return true;
79
+ }
80
+ /**
81
+ * Build the spawnHook that wraps the command with the sandbox. The hook is
82
+ * async-ignoring by design: spawnHook is sync in pi's interface, so the
83
+ * command wrap must already be resolved — use preWrappedCommand() below to
84
+ * pre-wrap and stash, or build the hook from a pre-wrapped string.
85
+ *
86
+ * In practice we pre-wrap in tool_call (async, where we can await the wrap)
87
+ * by mutating event.input.command to the wrapped string, and the spawnHook
88
+ * only injects proxy env. That keeps a single code path for tool calls.
89
+ */
90
+ /**
91
+ * Env additions every sandboxed process needs (srt injects proxy vars into
92
+ * the WRAPPED command's env; the spawnHook passes them through).
93
+ */
94
+ export function sandboxSpawnEnv(manager) {
95
+ const env = {};
96
+ // Node >= 22.21 / >= 24: make undici honor proxy env vars, so fetch() in
97
+ // sandboxed node processes routes through the srt proxy like curl/git do.
98
+ const [major, minor] = process.versions.node.split(".").map(Number);
99
+ if ((major === 22 && minor >= 21) || major >= 24) {
100
+ env.NODE_USE_ENV_PROXY = "1";
101
+ }
102
+ return env;
103
+ }
104
+ /**
105
+ * spawnHook for the sandboxed bash tool: injects proxy-support env vars.
106
+ * The command itself was already wrapped (pre-wrap in tool_call, async).
107
+ */
108
+ export function makeSandboxSpawnHook(manager) {
109
+ const extraEnv = sandboxSpawnEnv(manager);
110
+ return (ctx) => ({
111
+ ...ctx,
112
+ env: { ...ctx.env, ...extraEnv },
113
+ });
114
+ }
115
+ /**
116
+ * Wrap a raw command for OS-sandboxed execution. Returns the original string
117
+ * when the manager is not initialized (fail-open to plain execution — the
118
+ * permission gate still ran; the sandbox simply is not active).
119
+ */
120
+ export async function preWrappedCommand(manager, command, binShell) {
121
+ if (!manager.initialized)
122
+ return command;
123
+ return manager.wrapWithSandbox(command, binShell);
124
+ }
125
+ /**
126
+ * Detect "Operation not permitted" style sandbox denials in bash output so
127
+ * callers (tool_result) can annotate and the model can react. Returns the
128
+ * offending path when found (pi-sandbox's regex, credited MIT).
129
+ */
130
+ export function extractBlockedWritePath(output) {
131
+ const match = output.match(/(?:\/bin\/bash|bash|sh): (?:line \d+: )?(\/[^\s:]+): Operation not permitted/);
132
+ return match ? match[1] : null;
133
+ }
134
+ /**
135
+ * Wrap decision for user_bash (! commands): same rules as the tool, minus
136
+ * the dangerouslyDisableSandbox parameter (users running ! commands are
137
+ * already the principal, not the model).
138
+ */
139
+ export function shouldUseSandboxForUserCommand(command, manager, settings) {
140
+ if (!manager.initialized)
141
+ return false;
142
+ if (!command)
143
+ return false;
144
+ return !commandHasExcludedSegment(command, settings.excludedCommands ?? []);
145
+ }
146
+ /**
147
+ * Post-execution violation annotation: annotate a command's combined output
148
+ * with sandbox violation details when the sandbox is active. Returns the
149
+ * annotated output (or the original when nothing to annotate).
150
+ */
151
+ export function annotateCommandOutput(manager, command, output) {
152
+ if (!manager.initialized)
153
+ return output;
154
+ if (!output.includes("Operation not permitted"))
155
+ return output;
156
+ return manager.annotateStderrWithSandboxFailures(command, output);
157
+ }
158
+ /** cwd must exist before spawn (guard borrowed from pi's local ops). */
159
+ export function assertSpawnableCwd(cwd) {
160
+ if (!existsSync(cwd))
161
+ throw new Error(`Working directory does not exist: ${cwd}`);
162
+ }
163
+ /**
164
+ * The gate's sandbox auto-allow decision — the production predicate index.ts
165
+ * wires into registerPermissionGate's sandboxAutoAllow dep. Exported so the
166
+ * precedence table is pinned by real tests, not mirrored tautologies.
167
+ *
168
+ * Declines (returns false): non-bash tools, autoAllow disabled by setting,
169
+ * manager not initialized, empty command, and — critically —
170
+ * dangerouslyDisableSandbox retries that will run UNSANDBOXED (they must pay
171
+ * the normal permission price; only strict-mode-forced sandboxing rides the
172
+ * auto-allow). Otherwise: shouldUseSandbox's decision (excludedCommands
173
+ * included — an excluded command runs unsandboxed through the ordinary flow).
174
+ */
175
+ export function sandboxAutoAllowDecision(toolName, params, manager, settings) {
176
+ if (toolName !== "bash")
177
+ return false;
178
+ if (settings.autoAllowBashIfSandboxed === false)
179
+ return false;
180
+ if (!manager.initialized)
181
+ return false;
182
+ const command = typeof params.command === "string" ? params.command : "";
183
+ if (!command)
184
+ return false;
185
+ const retryUnsandboxed = params.dangerouslyDisableSandbox === true && settings.allowUnsandboxedCommands !== false;
186
+ if (retryUnsandboxed)
187
+ return false;
188
+ return shouldUseSandbox({ command, dangerouslyDisableSandbox: params.dangerouslyDisableSandbox === true }, manager, settings);
189
+ }
190
+ //# sourceMappingURL=bash.js.map
@@ -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