@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.
- package/README.md +84 -46
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +13 -0
- package/dist/extension/index.js +230 -43
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/permission/gate.d.ts +97 -2
- package/dist/extension/permission/gate.js +375 -26
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
- package/dist/extension/permissionRules/loadConfig.js +90 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +58 -0
- package/dist/extension/permissionRules/pathRules.js +120 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +775 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +99 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +40 -0
- package/dist/extension/telemetry/register.js +192 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- 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
|