@garuda-agent/garuda 0.0.1 → 0.15.0
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/CHANGELOG.md +77 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +1097 -0
- package/SECURITY.md +32 -0
- package/dist/acpCommand-FZ2P5JFO.js +153 -0
- package/dist/anthropic-NQDBCHJU.js +327 -0
- package/dist/chunk-23FXBNBB.js +46 -0
- package/dist/chunk-2S62ZNTC.js +320 -0
- package/dist/chunk-2TAXSFWN.js +49 -0
- package/dist/chunk-36GQIVGT.js +526 -0
- package/dist/chunk-3CEPHS4C.js +231 -0
- package/dist/chunk-3NJNVZTK.js +123 -0
- package/dist/chunk-42DSA4EA.js +116 -0
- package/dist/chunk-4JSUUDFQ.js +118 -0
- package/dist/chunk-4P26OKTP.js +100 -0
- package/dist/chunk-5DEWP3YX.js +139 -0
- package/dist/chunk-A2QAIRQL.js +45 -0
- package/dist/chunk-ATW7BNIG.js +31 -0
- package/dist/chunk-B25VL4RU.js +242 -0
- package/dist/chunk-EPKWDCV4.js +9 -0
- package/dist/chunk-EU7PA45O.js +324 -0
- package/dist/chunk-FARS4R67.js +206 -0
- package/dist/chunk-G2AX7MGS.js +36 -0
- package/dist/chunk-HRGX53VC.js +173 -0
- package/dist/chunk-HUIK3WXO.js +234 -0
- package/dist/chunk-INRZR772.js +73 -0
- package/dist/chunk-K3SIUOAG.js +138 -0
- package/dist/chunk-K7KK7U44.js +9358 -0
- package/dist/chunk-KKB5W47S.js +231 -0
- package/dist/chunk-N4I6YS3E.js +73 -0
- package/dist/chunk-NXJ4XLB5.js +19 -0
- package/dist/chunk-OUCV3FCT.js +205 -0
- package/dist/chunk-PQNT34LY.js +1247 -0
- package/dist/chunk-PZS3V6P3.js +46 -0
- package/dist/chunk-S77XDSD4.js +157 -0
- package/dist/chunk-SI2QJYCR.js +1341 -0
- package/dist/chunk-WMOLXTBU.js +61 -0
- package/dist/chunk-YFQJXRIJ.js +193 -0
- package/dist/chunk-YQTGMCU6.js +111 -0
- package/dist/chunk-ZAE25JM2.js +158 -0
- package/dist/cli/index.js +1203 -0
- package/dist/create-N4I7TIZE.js +223 -0
- package/dist/deadline-F5RJJ2QW.js +86 -0
- package/dist/detect-LARDSLLZ.js +10 -0
- package/dist/evalCommand-3SSFMZQD.js +2798 -0
- package/dist/export-HN3CT7AV.js +158 -0
- package/dist/inkChat-34BTFG2K.js +1193 -0
- package/dist/install-VRPBZY5H.js +17 -0
- package/dist/job-3NYDMP2M.js +31 -0
- package/dist/jobCommand-XJ5GULS5.js +30 -0
- package/dist/launchd-QVHQ3VNN.js +40 -0
- package/dist/lspCommand-XNTBHXLA.js +99 -0
- package/dist/manager-4PMSXT47.js +14 -0
- package/dist/manager-CSI76AWF.js +847 -0
- package/dist/nightCommand-CR6ZHYGP.js +247 -0
- package/dist/oauth-W6OK6XXO.js +23 -0
- package/dist/openaiCompatible-CJWVJP5M.js +501 -0
- package/dist/policy-EJPLEEXP.js +31 -0
- package/dist/proxy-XJAHCZB2.js +192 -0
- package/dist/remove-LE7Q7K46.js +59 -0
- package/dist/run-ML6GSPN4.js +888 -0
- package/dist/server-R3GNIC33.js +676 -0
- package/dist/servers-6YDGWOZ6.js +24 -0
- package/dist/text-J2GD647J.js +11 -0
- package/package.json +84 -3
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
realRelative
|
|
4
|
+
} from "./chunk-WMOLXTBU.js";
|
|
5
|
+
import {
|
|
6
|
+
isCommandDisallowedByPolicy,
|
|
7
|
+
isHostBlockedByPolicy,
|
|
8
|
+
isPathDeniedByPolicy,
|
|
9
|
+
isSandboxRequiredByPolicy
|
|
10
|
+
} from "./chunk-HUIK3WXO.js";
|
|
11
|
+
import {
|
|
12
|
+
CASE_INSENSITIVE_FS,
|
|
13
|
+
formatRule,
|
|
14
|
+
fullGlob,
|
|
15
|
+
parseRule,
|
|
16
|
+
pathMatches,
|
|
17
|
+
ruleMatches
|
|
18
|
+
} from "./chunk-3NJNVZTK.js";
|
|
19
|
+
|
|
20
|
+
// src/net/allowlist.ts
|
|
21
|
+
var NETWORK_PRESETS = {
|
|
22
|
+
npm: ["registry.npmjs.org", "registry.yarnpkg.com"],
|
|
23
|
+
pypi: ["pypi.org", "files.pythonhosted.org"],
|
|
24
|
+
maven: ["repo.maven.apache.org", "repo1.maven.org", "plugins.gradle.org", "services.gradle.org"],
|
|
25
|
+
go: ["proxy.golang.org", "sum.golang.org"],
|
|
26
|
+
cargo: ["crates.io", "index.crates.io", "static.crates.io"],
|
|
27
|
+
github: [
|
|
28
|
+
"github.com",
|
|
29
|
+
"api.github.com",
|
|
30
|
+
"codeload.github.com",
|
|
31
|
+
"objects.githubusercontent.com",
|
|
32
|
+
"raw.githubusercontent.com"
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
var PRESET_NAMES = Object.keys(NETWORK_PRESETS);
|
|
36
|
+
var NETWORK_PORTS = [80, 443];
|
|
37
|
+
var HOST = /^(\*\.)?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
38
|
+
function expandAllowlist(entries) {
|
|
39
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
40
|
+
const problems = [];
|
|
41
|
+
for (const raw of entries) {
|
|
42
|
+
const entry = raw.trim().toLowerCase();
|
|
43
|
+
const preset = NETWORK_PRESETS[entry];
|
|
44
|
+
if (preset !== void 0) {
|
|
45
|
+
for (const host of preset) hosts.add(host);
|
|
46
|
+
} else if (HOST.test(entry)) {
|
|
47
|
+
hosts.add(entry);
|
|
48
|
+
} else {
|
|
49
|
+
problems.push(
|
|
50
|
+
`"${raw}" is not a preset (${PRESET_NAMES.join(", ")}) or a host name (example.com, *.example.com).`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { hosts: [...hosts], problems };
|
|
55
|
+
}
|
|
56
|
+
function hostAllowed(host, patterns) {
|
|
57
|
+
const h = host.toLowerCase().replace(/\.$/, "");
|
|
58
|
+
return patterns.some((p) => p.startsWith("*.") ? h.endsWith(p.slice(1)) : h === p);
|
|
59
|
+
}
|
|
60
|
+
function describeAllowlist(entries) {
|
|
61
|
+
return entries.map((raw) => {
|
|
62
|
+
const preset = NETWORK_PRESETS[raw.trim().toLowerCase()];
|
|
63
|
+
return preset === void 0 ? raw : `${raw}: ${preset.join(", ")}`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/permissions/sandboxPaths.ts
|
|
68
|
+
import { realpathSync } from "fs";
|
|
69
|
+
import { homedir, tmpdir } from "os";
|
|
70
|
+
import { isAbsolute, join, resolve } from "path";
|
|
71
|
+
import { globbySync } from "globby";
|
|
72
|
+
var DENY_READ_IN_HOME = [
|
|
73
|
+
".ssh",
|
|
74
|
+
".aws",
|
|
75
|
+
".azure",
|
|
76
|
+
".gnupg",
|
|
77
|
+
".kube",
|
|
78
|
+
".docker",
|
|
79
|
+
".config/gcloud",
|
|
80
|
+
".config/gh",
|
|
81
|
+
".netrc",
|
|
82
|
+
".npmrc",
|
|
83
|
+
".pypirc",
|
|
84
|
+
".git-credentials",
|
|
85
|
+
"Library/Keychains",
|
|
86
|
+
// Garuda's OAuth tokens for remote MCP servers (0.4). The rest of ~/.garuda stays readable:
|
|
87
|
+
// skills and their scripts live there (0.5).
|
|
88
|
+
".garuda/mcp-auth.json",
|
|
89
|
+
// Claude Code: its config can hold MCP server tokens in env values; its credentials on Linux.
|
|
90
|
+
".claude.json",
|
|
91
|
+
".claude/.credentials.json"
|
|
92
|
+
];
|
|
93
|
+
var PROTECTED_IN_ROOT = [".git/hooks", ".git/config", ".garuda"];
|
|
94
|
+
var WRITE_IN_HOME = [
|
|
95
|
+
".cache",
|
|
96
|
+
".npm",
|
|
97
|
+
".local/share/pnpm",
|
|
98
|
+
"Library/Caches",
|
|
99
|
+
"Library/pnpm"
|
|
100
|
+
];
|
|
101
|
+
function sandboxPaths(root, settings = {}, home = homedir()) {
|
|
102
|
+
const expand = (path) => path === "~" ? home : path.startsWith("~/") ? join(home, path.slice(2)) : resolve(root, path);
|
|
103
|
+
const temp = [.../* @__PURE__ */ new Set([real(tmpdir()), real("/tmp")])];
|
|
104
|
+
return {
|
|
105
|
+
writePaths: unique([
|
|
106
|
+
root,
|
|
107
|
+
...temp,
|
|
108
|
+
...WRITE_IN_HOME.map((p) => join(home, p)),
|
|
109
|
+
...(settings.writePaths ?? []).map(expand)
|
|
110
|
+
]),
|
|
111
|
+
denyWritePaths: PROTECTED_IN_ROOT.map((p) => join(root, p)),
|
|
112
|
+
denyReadPaths: unique([
|
|
113
|
+
...DENY_READ_IN_HOME.map((p) => join(home, p)),
|
|
114
|
+
...(settings.denyRead ?? []).map(expand)
|
|
115
|
+
])
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function real(path) {
|
|
119
|
+
try {
|
|
120
|
+
return realpathSync(path);
|
|
121
|
+
} catch {
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function unique(paths) {
|
|
126
|
+
return [...new Set(paths.filter((p) => isAbsolute(p)))];
|
|
127
|
+
}
|
|
128
|
+
var MAX_POLICY_DENIED_PATHS = 1e3;
|
|
129
|
+
function policyDeniedPaths(root, patterns, ignoreCase = CASE_INSENSITIVE_FS) {
|
|
130
|
+
if (patterns.length === 0) return [];
|
|
131
|
+
const found = globbySync(patterns.map(fullGlob), {
|
|
132
|
+
cwd: root,
|
|
133
|
+
absolute: true,
|
|
134
|
+
dot: true,
|
|
135
|
+
onlyFiles: false,
|
|
136
|
+
followSymbolicLinks: false,
|
|
137
|
+
gitignore: false,
|
|
138
|
+
caseSensitiveMatch: !ignoreCase,
|
|
139
|
+
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
140
|
+
});
|
|
141
|
+
return found.slice(0, MAX_POLICY_DENIED_PATHS);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/sandbox/process.ts
|
|
145
|
+
import { spawn } from "child_process";
|
|
146
|
+
|
|
147
|
+
// src/sandbox/capture.ts
|
|
148
|
+
var OutputCapture = class {
|
|
149
|
+
headMax;
|
|
150
|
+
tailMax;
|
|
151
|
+
head = Buffer.alloc(0);
|
|
152
|
+
tail = [];
|
|
153
|
+
tailSize = 0;
|
|
154
|
+
total = 0;
|
|
155
|
+
constructor(maxBytes) {
|
|
156
|
+
this.headMax = Math.ceil(maxBytes / 2);
|
|
157
|
+
this.tailMax = Math.floor(maxBytes / 2);
|
|
158
|
+
}
|
|
159
|
+
push(chunk) {
|
|
160
|
+
this.total += chunk.length;
|
|
161
|
+
let rest = chunk;
|
|
162
|
+
if (this.head.length < this.headMax) {
|
|
163
|
+
const room = this.headMax - this.head.length;
|
|
164
|
+
this.head = Buffer.concat([this.head, rest.subarray(0, room)]);
|
|
165
|
+
rest = rest.subarray(room);
|
|
166
|
+
}
|
|
167
|
+
if (rest.length === 0 || this.tailMax === 0) return;
|
|
168
|
+
this.tail.push(rest);
|
|
169
|
+
this.tailSize += rest.length;
|
|
170
|
+
while (this.tail.length > 1 && this.tailSize - (this.tail[0]?.length ?? 0) >= this.tailMax) {
|
|
171
|
+
this.tailSize -= this.tail.shift()?.length ?? 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
result() {
|
|
175
|
+
const tail = Buffer.concat(this.tail);
|
|
176
|
+
const keptTail = tail.subarray(Math.max(0, tail.length - this.tailMax));
|
|
177
|
+
const truncated = this.head.length + keptTail.length < this.total;
|
|
178
|
+
if (!truncated) {
|
|
179
|
+
return {
|
|
180
|
+
text: Buffer.concat([this.head, keptTail]).toString("utf8"),
|
|
181
|
+
truncated,
|
|
182
|
+
totalBytes: this.total
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const cut2 = this.total - this.head.length - keptTail.length;
|
|
186
|
+
const text = `${this.head.toString("utf8")}
|
|
187
|
+
\u2026 [${cut2} bytes cut] \u2026
|
|
188
|
+
${keptTail.toString("utf8")}`;
|
|
189
|
+
return { text, truncated, totalBytes: this.total };
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/sandbox/daemon.ts
|
|
194
|
+
var DEFAULT_MAX_LOG_LINES = 2e3;
|
|
195
|
+
var MAX_LOG_LINE_CHARS = 4e3;
|
|
196
|
+
var MAX_PENDING_CHARS = 64e3;
|
|
197
|
+
var MAX_LOGS_CHARS = 3e4;
|
|
198
|
+
var cut = (line) => line.length <= MAX_LOG_LINE_CHARS ? line : `${line.slice(0, MAX_LOG_LINE_CHARS)}\u2026 [${line.length - MAX_LOG_LINE_CHARS} more chars]`;
|
|
199
|
+
var DaemonManager = class {
|
|
200
|
+
constructor(executor) {
|
|
201
|
+
this.executor = executor;
|
|
202
|
+
}
|
|
203
|
+
executor;
|
|
204
|
+
nextId = 1;
|
|
205
|
+
daemons = /* @__PURE__ */ new Map();
|
|
206
|
+
spawn(command, policy, options = {}) {
|
|
207
|
+
const id = `daemon_${this.nextId++}`;
|
|
208
|
+
const maxBufferLines = options.maxBufferLines ?? DEFAULT_MAX_LOG_LINES;
|
|
209
|
+
const process2 = this.executor.start(["bash", "-c", command], policy, options.env);
|
|
210
|
+
process2.stdin.end();
|
|
211
|
+
const entry = {
|
|
212
|
+
info: {
|
|
213
|
+
id,
|
|
214
|
+
pid: process2.pid,
|
|
215
|
+
command,
|
|
216
|
+
startTime: Date.now(),
|
|
217
|
+
status: "running"
|
|
218
|
+
},
|
|
219
|
+
process: process2,
|
|
220
|
+
logs: [],
|
|
221
|
+
maxBufferLines,
|
|
222
|
+
stdoutRemainder: "",
|
|
223
|
+
stderrRemainder: "",
|
|
224
|
+
stopRequested: false
|
|
225
|
+
};
|
|
226
|
+
const appendChunk = (chunk, stream) => {
|
|
227
|
+
const text = (stream === "stdout" ? entry.stdoutRemainder : entry.stderrRemainder) + chunk.toString("utf8");
|
|
228
|
+
const lines = text.split("\n");
|
|
229
|
+
let remainder = lines.pop() ?? "";
|
|
230
|
+
if (remainder.length > MAX_PENDING_CHARS) {
|
|
231
|
+
lines.push(remainder);
|
|
232
|
+
remainder = "";
|
|
233
|
+
}
|
|
234
|
+
if (stream === "stdout") entry.stdoutRemainder = remainder;
|
|
235
|
+
else entry.stderrRemainder = remainder;
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
if (entry.logs.length >= entry.maxBufferLines) {
|
|
239
|
+
entry.logs.shift();
|
|
240
|
+
}
|
|
241
|
+
entry.logs.push({ timestamp: now, stream, line: cut(line) });
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
process2.stdout.on("data", (chunk) => appendChunk(chunk, "stdout"));
|
|
245
|
+
process2.stderr.on("data", (chunk) => appendChunk(chunk, "stderr"));
|
|
246
|
+
process2.onExit((code, signal) => {
|
|
247
|
+
if (entry.stdoutRemainder.length > 0) {
|
|
248
|
+
if (entry.logs.length >= entry.maxBufferLines) entry.logs.shift();
|
|
249
|
+
entry.logs.push({
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
stream: "stdout",
|
|
252
|
+
line: cut(entry.stdoutRemainder)
|
|
253
|
+
});
|
|
254
|
+
entry.stdoutRemainder = "";
|
|
255
|
+
}
|
|
256
|
+
if (entry.stderrRemainder.length > 0) {
|
|
257
|
+
if (entry.logs.length >= entry.maxBufferLines) entry.logs.shift();
|
|
258
|
+
entry.logs.push({
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
stream: "stderr",
|
|
261
|
+
line: cut(entry.stderrRemainder)
|
|
262
|
+
});
|
|
263
|
+
entry.stderrRemainder = "";
|
|
264
|
+
}
|
|
265
|
+
entry.info.endTime = Date.now();
|
|
266
|
+
entry.info.exitCode = code;
|
|
267
|
+
entry.info.signal = signal;
|
|
268
|
+
if (entry.stopRequested || signal !== null || code === 0) {
|
|
269
|
+
entry.info.status = "stopped";
|
|
270
|
+
} else {
|
|
271
|
+
entry.info.status = "failed";
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
process2.onError((error) => {
|
|
275
|
+
if (entry.logs.length >= entry.maxBufferLines) entry.logs.shift();
|
|
276
|
+
entry.logs.push({
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
stream: "stderr",
|
|
279
|
+
line: `Process error: ${error.message}`
|
|
280
|
+
});
|
|
281
|
+
entry.info.endTime = Date.now();
|
|
282
|
+
entry.info.status = "failed";
|
|
283
|
+
});
|
|
284
|
+
this.daemons.set(id, entry);
|
|
285
|
+
return this.toInfo(entry);
|
|
286
|
+
}
|
|
287
|
+
list() {
|
|
288
|
+
return [...this.daemons.values()].map((entry) => this.toInfo(entry));
|
|
289
|
+
}
|
|
290
|
+
get(id) {
|
|
291
|
+
const entry = this.daemons.get(id);
|
|
292
|
+
return entry ? this.toInfo(entry) : void 0;
|
|
293
|
+
}
|
|
294
|
+
logs(id, options = {}) {
|
|
295
|
+
const entry = this.daemons.get(id);
|
|
296
|
+
if (!entry) return void 0;
|
|
297
|
+
const stream = options.stream ?? "all";
|
|
298
|
+
const filtered = stream === "all" ? entry.logs : entry.logs.filter((log) => log.stream === stream);
|
|
299
|
+
const limit = options.lines ?? 50;
|
|
300
|
+
const sliced = limit <= 0 ? filtered : filtered.slice(-limit);
|
|
301
|
+
const out = [];
|
|
302
|
+
let size = 0;
|
|
303
|
+
for (const log of [...sliced].reverse()) {
|
|
304
|
+
const line = `[${log.stream}] ${log.line}`;
|
|
305
|
+
if (size + line.length > MAX_LOGS_CHARS) {
|
|
306
|
+
out.push(`[garuda] \u2026 ${sliced.length - out.length} older line(s) left out`);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
size += line.length + 1;
|
|
310
|
+
out.push(line);
|
|
311
|
+
}
|
|
312
|
+
return out.reverse();
|
|
313
|
+
}
|
|
314
|
+
kill(id) {
|
|
315
|
+
const entry = this.daemons.get(id);
|
|
316
|
+
if (!entry) {
|
|
317
|
+
return { ok: false, message: `Daemon process not found: ${id}` };
|
|
318
|
+
}
|
|
319
|
+
if (entry.info.status !== "running") {
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
message: `Daemon process ${id} was already ${entry.info.status} (exit code: ${entry.info.exitCode ?? "none"}).`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
entry.stopRequested = true;
|
|
326
|
+
entry.process.stop();
|
|
327
|
+
return {
|
|
328
|
+
ok: true,
|
|
329
|
+
message: `Asked daemon process ${id} (pid: ${entry.info.pid ?? "unknown"}) to stop: SIGTERM now, SIGKILL after 2 s if it is still running.`
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
shutdown() {
|
|
333
|
+
for (const entry of this.daemons.values()) {
|
|
334
|
+
if (entry.info.status === "running") {
|
|
335
|
+
try {
|
|
336
|
+
entry.stopRequested = true;
|
|
337
|
+
entry.process.stop();
|
|
338
|
+
entry.info.status = "stopped";
|
|
339
|
+
entry.info.endTime = Date.now();
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
toInfo(entry) {
|
|
346
|
+
const { id, pid, command, status, startTime, endTime, exitCode, signal } = entry.info;
|
|
347
|
+
const uptimeMs = (endTime ?? Date.now()) - startTime;
|
|
348
|
+
return {
|
|
349
|
+
id,
|
|
350
|
+
pid,
|
|
351
|
+
command,
|
|
352
|
+
status,
|
|
353
|
+
startTime,
|
|
354
|
+
endTime,
|
|
355
|
+
uptimeMs,
|
|
356
|
+
exitCode,
|
|
357
|
+
signal
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/sandbox/process.ts
|
|
363
|
+
var KILL_GRACE_MS = 2e3;
|
|
364
|
+
var EXIT_DRAIN_MS = 2e3;
|
|
365
|
+
var ProcessExecutor = class {
|
|
366
|
+
/** Manages background daemon processes (0.14). */
|
|
367
|
+
daemons = new DaemonManager(this);
|
|
368
|
+
/** Process group ids of running commands. */
|
|
369
|
+
running = /* @__PURE__ */ new Set();
|
|
370
|
+
shutdown() {
|
|
371
|
+
this.daemons.shutdown();
|
|
372
|
+
for (const pid of this.running) signalGroup(pid, "SIGKILL");
|
|
373
|
+
this.running.clear();
|
|
374
|
+
}
|
|
375
|
+
start(argv, policy, env = {}) {
|
|
376
|
+
const { file, args } = this.launch(argv, policy);
|
|
377
|
+
const child = spawn(file, args, {
|
|
378
|
+
cwd: policy.root,
|
|
379
|
+
env: { ...allowedEnv(policy.envAllowlist), ...proxyEnv(policy), ...env },
|
|
380
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
381
|
+
detached: true
|
|
382
|
+
});
|
|
383
|
+
const pid = child.pid;
|
|
384
|
+
if (pid !== void 0) this.running.add(pid);
|
|
385
|
+
let killTimer;
|
|
386
|
+
child.on("exit", () => {
|
|
387
|
+
if (pid !== void 0) {
|
|
388
|
+
this.running.delete(pid);
|
|
389
|
+
signalGroup(pid, "SIGKILL");
|
|
390
|
+
}
|
|
391
|
+
if (killTimer !== void 0) clearTimeout(killTimer);
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
pid,
|
|
395
|
+
stdin: child.stdin,
|
|
396
|
+
stdout: child.stdout,
|
|
397
|
+
stderr: child.stderr,
|
|
398
|
+
stop: () => {
|
|
399
|
+
if (pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
|
|
400
|
+
signalGroup(pid, "SIGTERM");
|
|
401
|
+
killTimer ??= setTimeout(() => signalGroup(pid, "SIGKILL"), KILL_GRACE_MS);
|
|
402
|
+
killTimer.unref();
|
|
403
|
+
},
|
|
404
|
+
onExit: (listener) => {
|
|
405
|
+
child.on("exit", (code, signal) => listener(code, signal));
|
|
406
|
+
},
|
|
407
|
+
onError: (listener) => {
|
|
408
|
+
child.on("error", listener);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
run(command, policy, options = {}) {
|
|
413
|
+
const started = Date.now();
|
|
414
|
+
const stdout = new OutputCapture(policy.maxOutputBytes);
|
|
415
|
+
const stderr = new OutputCapture(policy.maxOutputBytes);
|
|
416
|
+
return new Promise((resolve2, reject) => {
|
|
417
|
+
if (options.signal?.aborted) {
|
|
418
|
+
reject(options.signal.reason);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const { file, args } = this.launch(["bash", "-c", command], policy);
|
|
422
|
+
const child = spawn(file, args, {
|
|
423
|
+
cwd: policy.root,
|
|
424
|
+
env: { ...allowedEnv(policy.envAllowlist), ...proxyEnv(policy), ...options.env },
|
|
425
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
426
|
+
detached: true
|
|
427
|
+
});
|
|
428
|
+
const pid = child.pid;
|
|
429
|
+
if (pid !== void 0) this.running.add(pid);
|
|
430
|
+
let timedOut = false;
|
|
431
|
+
let aborted = false;
|
|
432
|
+
let finished = false;
|
|
433
|
+
let killTimer;
|
|
434
|
+
let drainTimer;
|
|
435
|
+
const killTree = () => {
|
|
436
|
+
if (child.pid === void 0 || finished) return;
|
|
437
|
+
signalGroup(child.pid, "SIGTERM");
|
|
438
|
+
killTimer ??= setTimeout(() => {
|
|
439
|
+
if (child.pid !== void 0) signalGroup(child.pid, "SIGKILL");
|
|
440
|
+
}, KILL_GRACE_MS);
|
|
441
|
+
};
|
|
442
|
+
const timer = setTimeout(() => {
|
|
443
|
+
timedOut = true;
|
|
444
|
+
killTree();
|
|
445
|
+
}, policy.timeoutMs);
|
|
446
|
+
const onAbort = () => {
|
|
447
|
+
aborted = true;
|
|
448
|
+
killTree();
|
|
449
|
+
};
|
|
450
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
451
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
452
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
453
|
+
child.on("error", (error) => {
|
|
454
|
+
if (finished) return;
|
|
455
|
+
finished = true;
|
|
456
|
+
if (pid !== void 0) this.running.delete(pid);
|
|
457
|
+
clearTimeout(timer);
|
|
458
|
+
if (drainTimer !== void 0) clearTimeout(drainTimer);
|
|
459
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
460
|
+
reject(error);
|
|
461
|
+
});
|
|
462
|
+
const finish = (code, signal) => {
|
|
463
|
+
if (finished) return;
|
|
464
|
+
finished = true;
|
|
465
|
+
if (pid !== void 0) this.running.delete(pid);
|
|
466
|
+
clearTimeout(timer);
|
|
467
|
+
if (drainTimer !== void 0) clearTimeout(drainTimer);
|
|
468
|
+
if (killTimer !== void 0) clearTimeout(killTimer);
|
|
469
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
470
|
+
if (child.pid !== void 0) signalGroup(child.pid, "SIGKILL");
|
|
471
|
+
resolve2({
|
|
472
|
+
exitCode: code,
|
|
473
|
+
signal,
|
|
474
|
+
stdout: stdout.result(),
|
|
475
|
+
stderr: stderr.result(),
|
|
476
|
+
timedOut,
|
|
477
|
+
aborted,
|
|
478
|
+
durationMs: Date.now() - started
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
child.on("exit", (code, signal) => {
|
|
482
|
+
drainTimer = setTimeout(() => {
|
|
483
|
+
if (child.pid !== void 0) signalGroup(child.pid, "SIGKILL");
|
|
484
|
+
child.stdout.destroy();
|
|
485
|
+
child.stderr.destroy();
|
|
486
|
+
finish(code, signal);
|
|
487
|
+
}, EXIT_DRAIN_MS);
|
|
488
|
+
});
|
|
489
|
+
child.on("close", (code, signal) => finish(code, signal));
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
function signalGroup(pid, signal) {
|
|
494
|
+
try {
|
|
495
|
+
process.kill(-pid, signal);
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function allowedEnv(allowlist) {
|
|
500
|
+
const env = {};
|
|
501
|
+
for (const name of allowlist) {
|
|
502
|
+
const value = process.env[name];
|
|
503
|
+
if (value !== void 0) env[name] = value;
|
|
504
|
+
}
|
|
505
|
+
return env;
|
|
506
|
+
}
|
|
507
|
+
function proxyEnv(policy, base = allowedEnv(policy.envAllowlist)) {
|
|
508
|
+
if (!policy.sandbox || policy.network || policy.proxy === void 0) return {};
|
|
509
|
+
const url = `http://127.0.0.1:${policy.proxy.port}`;
|
|
510
|
+
const java = `-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=${policy.proxy.port} -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=${policy.proxy.port}`;
|
|
511
|
+
const add = (name) => `${base[name] === void 0 ? "" : `${base[name]} `}${java}`;
|
|
512
|
+
return {
|
|
513
|
+
HTTP_PROXY: url,
|
|
514
|
+
HTTPS_PROXY: url,
|
|
515
|
+
http_proxy: url,
|
|
516
|
+
https_proxy: url,
|
|
517
|
+
NO_PROXY: "localhost,127.0.0.1,::1",
|
|
518
|
+
no_proxy: "localhost,127.0.0.1,::1",
|
|
519
|
+
NODE_USE_ENV_PROXY: "1",
|
|
520
|
+
MAVEN_OPTS: add("MAVEN_OPTS"),
|
|
521
|
+
GRADLE_OPTS: add("GRADLE_OPTS")
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function plain(argv) {
|
|
525
|
+
const [file = "", ...args] = argv;
|
|
526
|
+
return { file, args };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/sandbox/host.ts
|
|
530
|
+
var HostExecutor = class extends ProcessExecutor {
|
|
531
|
+
name = "host";
|
|
532
|
+
isolation = "none";
|
|
533
|
+
launch(argv) {
|
|
534
|
+
return plain(argv);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/sandbox/index.ts
|
|
539
|
+
import { spawnSync } from "child_process";
|
|
540
|
+
import { existsSync as existsSync2 } from "fs";
|
|
541
|
+
import { delimiter, join as join2 } from "path";
|
|
542
|
+
|
|
543
|
+
// src/sandbox/bwrap.ts
|
|
544
|
+
import { existsSync, statSync } from "fs";
|
|
545
|
+
var BwrapExecutor = class extends ProcessExecutor {
|
|
546
|
+
constructor(bwrap) {
|
|
547
|
+
super();
|
|
548
|
+
this.bwrap = bwrap;
|
|
549
|
+
}
|
|
550
|
+
bwrap;
|
|
551
|
+
name = "bwrap";
|
|
552
|
+
isolation = "os";
|
|
553
|
+
launch(argv, policy) {
|
|
554
|
+
if (!policy.sandbox) return plain(argv);
|
|
555
|
+
return { file: this.bwrap, args: bwrapArgv(argv, policy) };
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
function bwrapArgv(argv, policy, kind = pathKind) {
|
|
559
|
+
const args = ["--ro-bind", "/", "/", "--dev", "/dev"];
|
|
560
|
+
for (const path of policy.writePaths)
|
|
561
|
+
if (kind(path) !== void 0) args.push("--bind", path, path);
|
|
562
|
+
for (const path of policy.denyWritePaths) {
|
|
563
|
+
if (kind(path) !== void 0) args.push("--ro-bind", path, path);
|
|
564
|
+
}
|
|
565
|
+
for (const path of policy.denyReadPaths) {
|
|
566
|
+
const k = kind(path);
|
|
567
|
+
if (k === "dir") args.push("--tmpfs", path, "--remount-ro", path);
|
|
568
|
+
else if (k === "file") args.push("--ro-bind", "/dev/null", path);
|
|
569
|
+
}
|
|
570
|
+
if (!policy.network) args.push("--unshare-net");
|
|
571
|
+
args.push("--unshare-pid", "--proc", "/proc");
|
|
572
|
+
const run = policy.sandbox && !policy.network ? withBridge(argv, policy) : argv;
|
|
573
|
+
args.push("--die-with-parent", "--chdir", policy.root, "--", ...run);
|
|
574
|
+
return args;
|
|
575
|
+
}
|
|
576
|
+
var BRIDGE_JS = 'const n=require("net");const[,s,p]=process.argv;n.createServer(c=>{const u=n.connect(s);c.pipe(u);u.pipe(c);c.on("error",()=>u.destroy());u.on("error",()=>c.destroy())}).listen(+p,"127.0.0.1")';
|
|
577
|
+
var BRIDGE_WRAP = '"$1" -e "$2" "$3" "$4" </dev/null >/dev/null 2>&1 & for i in $(seq 200); do (: </dev/tcp/127.0.0.1/$4) 2>/dev/null && break; sleep 0.01; done; shift 4; exec "$@"';
|
|
578
|
+
function withBridge(argv, policy) {
|
|
579
|
+
const proxy = policy.proxy;
|
|
580
|
+
if (proxy?.bridge === void 0) return argv;
|
|
581
|
+
return [
|
|
582
|
+
"bash",
|
|
583
|
+
"-c",
|
|
584
|
+
BRIDGE_WRAP,
|
|
585
|
+
"garuda-bridge",
|
|
586
|
+
proxy.bridge,
|
|
587
|
+
BRIDGE_JS,
|
|
588
|
+
proxy.socketPath,
|
|
589
|
+
String(proxy.port),
|
|
590
|
+
...argv
|
|
591
|
+
];
|
|
592
|
+
}
|
|
593
|
+
function pathKind(path) {
|
|
594
|
+
if (!existsSync(path)) return void 0;
|
|
595
|
+
return statSync(path).isDirectory() ? "dir" : "file";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/sandbox/seatbelt.ts
|
|
599
|
+
var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
600
|
+
var SeatbeltExecutor = class extends ProcessExecutor {
|
|
601
|
+
name = "seatbelt";
|
|
602
|
+
isolation = "os";
|
|
603
|
+
launch(argv, policy) {
|
|
604
|
+
if (!policy.sandbox) return plain(argv);
|
|
605
|
+
return { file: SANDBOX_EXEC, args: ["-p", seatbeltProfile(policy), ...argv] };
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
function seatbeltProfile(policy) {
|
|
609
|
+
const paths = (list) => list.map((p) => `(subpath ${quote(p)})`).join(" ");
|
|
610
|
+
const lines = [
|
|
611
|
+
"(version 1)",
|
|
612
|
+
"(allow default)",
|
|
613
|
+
"(deny file-write*)",
|
|
614
|
+
`(allow file-write* ${paths(policy.writePaths)} (literal "/dev/null") (literal "/dev/zero") (literal "/dev/dtracehelper") (regex #"^/dev/tty") (regex #"^/dev/fd/"))`
|
|
615
|
+
];
|
|
616
|
+
if (policy.denyWritePaths.length > 0) {
|
|
617
|
+
lines.push(`(deny file-write* ${paths(policy.denyWritePaths)})`);
|
|
618
|
+
}
|
|
619
|
+
if (policy.denyReadPaths.length > 0) {
|
|
620
|
+
lines.push(`(deny file-read* ${paths(policy.denyReadPaths)})`);
|
|
621
|
+
}
|
|
622
|
+
lines.push("(deny lsopen)", "(deny appleevent-send)");
|
|
623
|
+
if (!policy.network) {
|
|
624
|
+
lines.push(
|
|
625
|
+
'(deny network-outbound (remote ip "*:*"))',
|
|
626
|
+
'(deny network-inbound (local ip "*:*"))',
|
|
627
|
+
'(deny network-bind (local ip "*:*"))',
|
|
628
|
+
'(allow network-outbound (remote ip "localhost:*"))',
|
|
629
|
+
'(allow network-inbound (local ip "localhost:*"))',
|
|
630
|
+
'(allow network-bind (local ip "localhost:*"))'
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return lines.join("\n");
|
|
634
|
+
}
|
|
635
|
+
function quote(path) {
|
|
636
|
+
return `"${path.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/sandbox/index.ts
|
|
640
|
+
var EXECUTOR_NAMES = ["auto", "os", "host"];
|
|
641
|
+
function createExecutor(name = "auto", find = findOsSandbox) {
|
|
642
|
+
if (name === "host") return { executor: new HostExecutor() };
|
|
643
|
+
const found = find();
|
|
644
|
+
if ("executor" in found) return { executor: found.executor };
|
|
645
|
+
if (name === "os") throw new Error(`No OS sandbox: ${found.problem} ${found.fix}`);
|
|
646
|
+
return {
|
|
647
|
+
executor: new HostExecutor(),
|
|
648
|
+
notice: `No OS sandbox: ${found.problem} Commands run on your machine, and each one asks for approval. ${found.fix}`
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
var cached;
|
|
652
|
+
function findOsSandbox() {
|
|
653
|
+
cached ??= detect(process.platform);
|
|
654
|
+
return "make" in cached ? { executor: cached.make() } : cached;
|
|
655
|
+
}
|
|
656
|
+
function detect(platform) {
|
|
657
|
+
const fix = 'Set "executor": "host" in .garuda/settings.json to hide this notice.';
|
|
658
|
+
if (platform === "darwin") {
|
|
659
|
+
if (!existsSync2(SANDBOX_EXEC)) {
|
|
660
|
+
return { problem: `${SANDBOX_EXEC} is missing.`, fix };
|
|
661
|
+
}
|
|
662
|
+
const probe = spawnSync(SANDBOX_EXEC, ["-p", "(version 1)(allow default)", "--", "true"], {
|
|
663
|
+
encoding: "utf8",
|
|
664
|
+
timeout: 5e3
|
|
665
|
+
});
|
|
666
|
+
if (probe.status === 0) return { make: () => new SeatbeltExecutor() };
|
|
667
|
+
const why = (probe.stderr || probe.error?.message || "unknown error").trim().split("\n")[0];
|
|
668
|
+
return { problem: `sandbox-exec cannot start a sandbox here (${why}).`, fix };
|
|
669
|
+
}
|
|
670
|
+
if (platform === "linux") {
|
|
671
|
+
const bwrap = onPath("bwrap");
|
|
672
|
+
if (bwrap === void 0) {
|
|
673
|
+
return {
|
|
674
|
+
problem: "bubblewrap (bwrap) is not installed.",
|
|
675
|
+
fix: 'Install it (for example: sudo apt install bubblewrap), or set "executor": "host" in .garuda/settings.json.'
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const probe = spawnSync(
|
|
679
|
+
bwrap,
|
|
680
|
+
["--ro-bind", "/", "/", "--dev", "/dev", "--unshare-net", "--die-with-parent", "--", "true"],
|
|
681
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
682
|
+
);
|
|
683
|
+
if (probe.status === 0) return { make: () => new BwrapExecutor(bwrap) };
|
|
684
|
+
const why = (probe.stderr || probe.error?.message || "unknown error").trim().split("\n")[0];
|
|
685
|
+
return { problem: `bubblewrap cannot start a sandbox here (${why}).`, fix };
|
|
686
|
+
}
|
|
687
|
+
return { problem: `Garuda has no OS sandbox for ${platform}.`, fix };
|
|
688
|
+
}
|
|
689
|
+
function onPath(program) {
|
|
690
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
691
|
+
const path = join2(dir, program);
|
|
692
|
+
if (dir !== "" && existsSync2(path)) return path;
|
|
693
|
+
}
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/permissions/settings.ts
|
|
698
|
+
import { readFile } from "fs/promises";
|
|
699
|
+
import { join as join3 } from "path";
|
|
700
|
+
import { z } from "zod";
|
|
701
|
+
|
|
702
|
+
// src/model/types.ts
|
|
703
|
+
var EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
704
|
+
var ZERO_USAGE = {
|
|
705
|
+
inputTokens: 0,
|
|
706
|
+
outputTokens: 0,
|
|
707
|
+
cacheReadTokens: 0,
|
|
708
|
+
cacheWriteTokens: 0
|
|
709
|
+
};
|
|
710
|
+
function addUsage(a, b) {
|
|
711
|
+
const searches = (a.webSearches ?? 0) + (b.webSearches ?? 0);
|
|
712
|
+
return {
|
|
713
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
714
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
715
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
716
|
+
cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
|
|
717
|
+
...searches === 0 ? {} : { webSearches: searches }
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/permissions/settings.ts
|
|
722
|
+
var SETTINGS_FILE = join3(".garuda", "settings.json");
|
|
723
|
+
var NOTIFY_CHOICES = ["auto", "osc9", "bell", "off"];
|
|
724
|
+
var schema = z.strictObject({
|
|
725
|
+
executor: z.enum(EXECUTOR_NAMES).optional(),
|
|
726
|
+
sandbox: z.strictObject({
|
|
727
|
+
writePaths: z.array(z.string()).optional(),
|
|
728
|
+
denyRead: z.array(z.string()).optional()
|
|
729
|
+
}).optional(),
|
|
730
|
+
/** Code index tools for the model: "off", "lookup" or "all" (true = "all", false = "off"). */
|
|
731
|
+
codeIndex: z.union([z.boolean(), z.enum(["off", "lookup", "all"])]).optional(),
|
|
732
|
+
permissions: z.strictObject({
|
|
733
|
+
allow: z.array(z.string()).optional(),
|
|
734
|
+
deny: z.array(z.string()).optional()
|
|
735
|
+
}).optional(),
|
|
736
|
+
env: z.strictObject({ allow: z.array(z.string()).optional() }).optional(),
|
|
737
|
+
/** The network allowlist for sandboxed commands (0.13): presets (npm, pypi …) or hosts. */
|
|
738
|
+
network: z.strictObject({ allow: z.array(z.string()).optional() }).optional(),
|
|
739
|
+
/** web_fetch. enabled: default true. allowLocalhost: default false (loopback only, never private ranges). */
|
|
740
|
+
web: z.strictObject({ enabled: z.boolean().optional(), allowLocalhost: z.boolean().optional() }).optional(),
|
|
741
|
+
limits: z.strictObject({
|
|
742
|
+
maxSteps: z.number().int().min(1).max(1e3).optional(),
|
|
743
|
+
tokenBudget: z.number().int().min(1e3).optional()
|
|
744
|
+
}).optional(),
|
|
745
|
+
/** The todo_write tool (0.4). enabled: default false. */
|
|
746
|
+
todo: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
747
|
+
/** Undo snapshots before each turn (0.4). enabled: default true. */
|
|
748
|
+
undo: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
749
|
+
/** Skills (0.5). enabled: default true (the skill tool appears only when skills exist). */
|
|
750
|
+
skills: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
751
|
+
/** Custom agents (0.5). enabled: default true (the agent tool appears only when agents exist). */
|
|
752
|
+
agents: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
753
|
+
/**
|
|
754
|
+
* Notifications in the chat (0.6): when an approval waits, and when a turn that ran at least
|
|
755
|
+
* `afterSeconds` (default 10) ends. channel: default "auto". GARUDA_NOTIFY overrides the channel.
|
|
756
|
+
*/
|
|
757
|
+
notifications: z.strictObject({
|
|
758
|
+
channel: z.enum(NOTIFY_CHOICES).optional(),
|
|
759
|
+
afterSeconds: z.number().int().min(0).max(86400).optional()
|
|
760
|
+
}).optional(),
|
|
761
|
+
/** Language server diagnostics after edits (0.4). enabled: default false. */
|
|
762
|
+
lsp: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
763
|
+
/**
|
|
764
|
+
* The project's formatter after edits (0.10). enabled: default false (A/B first). commands: by
|
|
765
|
+
* name, false turns a detected formatter off; a command adds one or replaces a detected one.
|
|
766
|
+
*/
|
|
767
|
+
formatters: z.strictObject({
|
|
768
|
+
enabled: z.boolean().optional(),
|
|
769
|
+
commands: z.record(
|
|
770
|
+
z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
771
|
+
z.union([
|
|
772
|
+
z.literal(false),
|
|
773
|
+
z.strictObject({
|
|
774
|
+
extensions: z.array(z.string().min(1)).min(1),
|
|
775
|
+
command: z.array(z.string().min(1)).min(1)
|
|
776
|
+
})
|
|
777
|
+
])
|
|
778
|
+
).optional()
|
|
779
|
+
}).optional(),
|
|
780
|
+
/**
|
|
781
|
+
* Claude's thinking (0.9). keepBlocks: send thinking blocks back (default true, as the API asks);
|
|
782
|
+
* false is for A/B runs only. enabled, effort, show: the start value of /thinking.
|
|
783
|
+
*/
|
|
784
|
+
thinking: z.strictObject({
|
|
785
|
+
keepBlocks: z.boolean().optional(),
|
|
786
|
+
enabled: z.boolean().optional(),
|
|
787
|
+
effort: z.enum(EFFORTS).optional(),
|
|
788
|
+
show: z.boolean().optional()
|
|
789
|
+
}).optional(),
|
|
790
|
+
/** The explore subagent (0.3). enabled: default false. Limits per explore run. */
|
|
791
|
+
subagents: z.strictObject({
|
|
792
|
+
enabled: z.boolean().optional(),
|
|
793
|
+
maxSteps: z.number().int().min(1).max(200).optional(),
|
|
794
|
+
tokenBudget: z.number().int().min(1e4).optional()
|
|
795
|
+
}).optional(),
|
|
796
|
+
/**
|
|
797
|
+
* Background processes (0.14): bash is_daemon and the process_manager tool. Off by default
|
|
798
|
+
* (merge gate): no measured gain yet, and they add tools and long-lived processes.
|
|
799
|
+
*/
|
|
800
|
+
daemons: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
801
|
+
/** Mixture-of-Experts language specialist subagent dispatch (0.17). */
|
|
802
|
+
moe: z.strictObject({
|
|
803
|
+
enabled: z.boolean().optional(),
|
|
804
|
+
maxSteps: z.number().int().min(1).max(200).optional(),
|
|
805
|
+
tokenBudget: z.number().int().min(1e4).optional()
|
|
806
|
+
}).optional(),
|
|
807
|
+
model: z.strictObject({
|
|
808
|
+
contextWindow: z.number().int().min(1e4).optional(),
|
|
809
|
+
/** USD per million tokens. */
|
|
810
|
+
price: z.strictObject({
|
|
811
|
+
input: z.number().min(0),
|
|
812
|
+
output: z.number().min(0),
|
|
813
|
+
cacheRead: z.number().min(0),
|
|
814
|
+
cacheWrite: z.number().min(0)
|
|
815
|
+
}).optional()
|
|
816
|
+
}).optional()
|
|
817
|
+
});
|
|
818
|
+
var DEFAULT_SETTINGS = Object.freeze({
|
|
819
|
+
executor: "auto",
|
|
820
|
+
allow: Object.freeze([]),
|
|
821
|
+
deny: Object.freeze([]),
|
|
822
|
+
envAllow: Object.freeze([])
|
|
823
|
+
});
|
|
824
|
+
function parseSettings(json, source = SETTINGS_FILE) {
|
|
825
|
+
const parsed = schema.safeParse(json);
|
|
826
|
+
if (!parsed.success) throw new Error(`${source}: ${z.prettifyError(parsed.error)}`);
|
|
827
|
+
const {
|
|
828
|
+
executor = "auto",
|
|
829
|
+
sandbox,
|
|
830
|
+
web,
|
|
831
|
+
permissions = {},
|
|
832
|
+
env = {},
|
|
833
|
+
limits = {},
|
|
834
|
+
model = {},
|
|
835
|
+
codeIndex,
|
|
836
|
+
subagents,
|
|
837
|
+
moe,
|
|
838
|
+
daemons,
|
|
839
|
+
todo,
|
|
840
|
+
lsp,
|
|
841
|
+
undo,
|
|
842
|
+
skills,
|
|
843
|
+
agents,
|
|
844
|
+
notifications,
|
|
845
|
+
thinking,
|
|
846
|
+
formatters,
|
|
847
|
+
network
|
|
848
|
+
} = parsed.data;
|
|
849
|
+
const networkProblems = expandAllowlist(network?.allow ?? []).problems;
|
|
850
|
+
if (networkProblems.length > 0) {
|
|
851
|
+
throw new Error(`${source}: network.allow: ${networkProblems.join(" ")}`);
|
|
852
|
+
}
|
|
853
|
+
const rules = (list = []) => list.map((text) => {
|
|
854
|
+
try {
|
|
855
|
+
return parseRule(text);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
throw new Error(`${source}: ${error.message}`);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
return {
|
|
861
|
+
executor,
|
|
862
|
+
allow: rules(permissions.allow),
|
|
863
|
+
deny: rules(permissions.deny),
|
|
864
|
+
envAllow: env.allow ?? [],
|
|
865
|
+
...network?.allow === void 0 || network.allow.length === 0 ? {} : { network: { allow: network.allow } },
|
|
866
|
+
...web === void 0 ? {} : { web: { enabled: web.enabled ?? true, allowLocalhost: web.allowLocalhost ?? false } },
|
|
867
|
+
...sandbox === void 0 ? {} : {
|
|
868
|
+
sandbox: {
|
|
869
|
+
...sandbox.writePaths === void 0 ? {} : { writePaths: sandbox.writePaths },
|
|
870
|
+
...sandbox.denyRead === void 0 ? {} : { denyRead: sandbox.denyRead }
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
...codeIndex === void 0 ? {} : { codeIndex: codeIndex === true ? "all" : codeIndex === false ? "off" : codeIndex },
|
|
874
|
+
...limits.maxSteps === void 0 ? {} : { maxSteps: limits.maxSteps },
|
|
875
|
+
...limits.tokenBudget === void 0 ? {} : { tokenBudget: limits.tokenBudget },
|
|
876
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
877
|
+
...model.price === void 0 ? {} : { price: model.price },
|
|
878
|
+
...todo?.enabled === void 0 ? {} : { todo: { enabled: todo.enabled } },
|
|
879
|
+
...lsp?.enabled === void 0 ? {} : { lsp: { enabled: lsp.enabled } },
|
|
880
|
+
...undo?.enabled === void 0 ? {} : { undo: { enabled: undo.enabled } },
|
|
881
|
+
...skills?.enabled === void 0 ? {} : { skills: { enabled: skills.enabled } },
|
|
882
|
+
...agents?.enabled === void 0 ? {} : { agents: { enabled: agents.enabled } },
|
|
883
|
+
...formatters === void 0 ? {} : {
|
|
884
|
+
formatters: {
|
|
885
|
+
enabled: formatters.enabled ?? false,
|
|
886
|
+
commands: formatters.commands ?? {}
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
...thinking === void 0 ? {} : {
|
|
890
|
+
thinking: {
|
|
891
|
+
...thinking.keepBlocks === void 0 ? {} : { keepBlocks: thinking.keepBlocks },
|
|
892
|
+
...thinking.enabled === void 0 ? {} : { enabled: thinking.enabled },
|
|
893
|
+
...thinking.effort === void 0 ? {} : { effort: thinking.effort },
|
|
894
|
+
...thinking.show === void 0 ? {} : { show: thinking.show }
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
...notifications === void 0 ? {} : {
|
|
898
|
+
notifications: {
|
|
899
|
+
...notifications.channel === void 0 ? {} : { channel: notifications.channel },
|
|
900
|
+
...notifications.afterSeconds === void 0 ? {} : { afterSeconds: notifications.afterSeconds }
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
...subagents === void 0 ? {} : {
|
|
904
|
+
subagents: {
|
|
905
|
+
...subagents.enabled === void 0 ? {} : { enabled: subagents.enabled },
|
|
906
|
+
...subagents.maxSteps === void 0 ? {} : { maxSteps: subagents.maxSteps },
|
|
907
|
+
...subagents.tokenBudget === void 0 ? {} : { tokenBudget: subagents.tokenBudget }
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
...daemons?.enabled === void 0 ? {} : { daemons: { enabled: daemons.enabled } },
|
|
911
|
+
...moe === void 0 ? {} : {
|
|
912
|
+
moe: {
|
|
913
|
+
...moe.enabled === void 0 ? {} : { enabled: moe.enabled },
|
|
914
|
+
...moe.maxSteps === void 0 ? {} : { maxSteps: moe.maxSteps },
|
|
915
|
+
...moe.tokenBudget === void 0 ? {} : { tokenBudget: moe.tokenBudget }
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
async function loadSettings(root) {
|
|
921
|
+
let text;
|
|
922
|
+
try {
|
|
923
|
+
text = await readFile(join3(root, SETTINGS_FILE), "utf8");
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (error.code === "ENOENT") return DEFAULT_SETTINGS;
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
928
|
+
let json;
|
|
929
|
+
try {
|
|
930
|
+
json = JSON.parse(text);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
throw new Error(`${SETTINGS_FILE}: invalid JSON: ${error.message}`);
|
|
933
|
+
}
|
|
934
|
+
return parseSettings(json);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/permissions/engine.ts
|
|
938
|
+
import { sep } from "path";
|
|
939
|
+
|
|
940
|
+
// src/permissions/sensitive.ts
|
|
941
|
+
var SENSITIVE_PATTERNS = [
|
|
942
|
+
".env",
|
|
943
|
+
".env.*",
|
|
944
|
+
".envrc",
|
|
945
|
+
"**/.ssh/**",
|
|
946
|
+
"**/.aws/**",
|
|
947
|
+
"**/.gnupg/**",
|
|
948
|
+
"**/.docker/config.json",
|
|
949
|
+
"**/.kube/config",
|
|
950
|
+
".npmrc",
|
|
951
|
+
".pypirc",
|
|
952
|
+
".netrc",
|
|
953
|
+
".git-credentials",
|
|
954
|
+
"credentials",
|
|
955
|
+
"credentials.json",
|
|
956
|
+
"*.pem",
|
|
957
|
+
"*.key",
|
|
958
|
+
"*.p12",
|
|
959
|
+
"*.pfx",
|
|
960
|
+
"*.keystore",
|
|
961
|
+
"id_rsa*",
|
|
962
|
+
"id_ecdsa*",
|
|
963
|
+
"id_ed25519*"
|
|
964
|
+
];
|
|
965
|
+
var PROTECTED_WRITE_PATTERNS = [".git/**"];
|
|
966
|
+
function isSensitive(path, ignoreCase) {
|
|
967
|
+
return SENSITIVE_PATTERNS.some((pattern) => pathMatches(pattern, path, ignoreCase));
|
|
968
|
+
}
|
|
969
|
+
function isProtectedFromWrites(path) {
|
|
970
|
+
return PROTECTED_WRITE_PATTERNS.some((pattern) => pathMatches(pattern, path));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/permissions/engine.ts
|
|
974
|
+
var DEFAULT_ENV_ALLOWLIST = [
|
|
975
|
+
"PATH",
|
|
976
|
+
"HOME",
|
|
977
|
+
"USER",
|
|
978
|
+
"LOGNAME",
|
|
979
|
+
"SHELL",
|
|
980
|
+
"TERM",
|
|
981
|
+
"LANG",
|
|
982
|
+
"LC_ALL",
|
|
983
|
+
"LC_CTYPE",
|
|
984
|
+
"TMPDIR",
|
|
985
|
+
"TZ"
|
|
986
|
+
];
|
|
987
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 3e4;
|
|
988
|
+
var JOB_DENIAL = "This call is not in the approved list of this scheduled job, and nobody can approve it now. Do not retry it. Continue without it if you can, and list it in your final answer as work left for the user.";
|
|
989
|
+
var PLAN_MODE_DENIAL = "Plan mode is on: this call would change something. Put the change in your plan instead; the user switches to build mode to carry it out.";
|
|
990
|
+
var PermissionEngine = class {
|
|
991
|
+
root;
|
|
992
|
+
approver;
|
|
993
|
+
settings;
|
|
994
|
+
policy;
|
|
995
|
+
auditLogger;
|
|
996
|
+
isolation;
|
|
997
|
+
access;
|
|
998
|
+
mode;
|
|
999
|
+
unattended;
|
|
1000
|
+
sessionRules = [];
|
|
1001
|
+
/** The network proxy for sandboxed commands (0.13), when it runs. */
|
|
1002
|
+
network;
|
|
1003
|
+
constructor(options) {
|
|
1004
|
+
this.root = options.root;
|
|
1005
|
+
this.approver = options.approver;
|
|
1006
|
+
this.settings = options.settings ?? DEFAULT_SETTINGS;
|
|
1007
|
+
this.policy = options.policy;
|
|
1008
|
+
this.auditLogger = options.auditLogger;
|
|
1009
|
+
this.isolation = options.isolation ?? "none";
|
|
1010
|
+
this.access = options.access ?? { writePaths: [], envAllow: [] };
|
|
1011
|
+
this.mode = options.mode ?? (() => "build");
|
|
1012
|
+
this.unattended = options.unattended;
|
|
1013
|
+
}
|
|
1014
|
+
async check(request, signal) {
|
|
1015
|
+
const { decision, userChoice } = await this.evaluateCheck(request, signal);
|
|
1016
|
+
await this.auditLogger?.logPermissionDecision({
|
|
1017
|
+
tool: request.tool,
|
|
1018
|
+
...request.info?.target === void 0 ? {} : { target: request.info.target },
|
|
1019
|
+
readOnly: request.readOnly,
|
|
1020
|
+
decision,
|
|
1021
|
+
...userChoice === void 0 ? {} : { userChoice }
|
|
1022
|
+
});
|
|
1023
|
+
return decision;
|
|
1024
|
+
}
|
|
1025
|
+
async evaluateCheck(request, signal) {
|
|
1026
|
+
const { tool, info } = request;
|
|
1027
|
+
const target = info?.target;
|
|
1028
|
+
const allowRule = this.settings.allow.find((r) => ruleMatches(r, tool, target, "allow"));
|
|
1029
|
+
if (target?.kind === "path") {
|
|
1030
|
+
const real2 = await realRelative(this.root, target.path);
|
|
1031
|
+
if (real2 !== void 0) {
|
|
1032
|
+
const denial = this.pathDenial(tool, { kind: "path", path: real2 }, request.readOnly);
|
|
1033
|
+
if (denial !== void 0) {
|
|
1034
|
+
return {
|
|
1035
|
+
decision: { ...denial, reason: `${target.path} leads to ${real2}. ${denial.reason}` }
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (this.policy !== void 0) {
|
|
1041
|
+
if (target?.kind === "command") {
|
|
1042
|
+
const sandboxViolation = isSandboxRequiredByPolicy(
|
|
1043
|
+
this.policy,
|
|
1044
|
+
target.outsideSandbox === true || this.isolation === "none"
|
|
1045
|
+
);
|
|
1046
|
+
if (sandboxViolation.disallowed) {
|
|
1047
|
+
return {
|
|
1048
|
+
decision: {
|
|
1049
|
+
allowed: false,
|
|
1050
|
+
by: "policy",
|
|
1051
|
+
reason: sandboxViolation.reason ?? "Running commands outside the sandbox is disallowed by team policy."
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
const cmdViolation = isCommandDisallowedByPolicy(this.policy, target.command);
|
|
1056
|
+
if (cmdViolation.disallowed) {
|
|
1057
|
+
return {
|
|
1058
|
+
decision: {
|
|
1059
|
+
allowed: false,
|
|
1060
|
+
by: "policy",
|
|
1061
|
+
reason: cmdViolation.reason ?? "Command disallowed by team policy."
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (target?.kind === "path") {
|
|
1067
|
+
const pathViolation = isPathDeniedByPolicy(this.policy, target.path);
|
|
1068
|
+
if (pathViolation.denied) {
|
|
1069
|
+
return {
|
|
1070
|
+
decision: {
|
|
1071
|
+
allowed: false,
|
|
1072
|
+
by: "policy",
|
|
1073
|
+
reason: pathViolation.reason ?? "Path disallowed by team policy."
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (target?.kind === "url") {
|
|
1079
|
+
const hostViolation = isHostBlockedByPolicy(this.policy, target.host);
|
|
1080
|
+
if (hostViolation.blocked) {
|
|
1081
|
+
return {
|
|
1082
|
+
decision: {
|
|
1083
|
+
allowed: false,
|
|
1084
|
+
by: "policy",
|
|
1085
|
+
reason: hostViolation.reason ?? "Host blocked by team policy."
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (this.policy.network?.strictAllowlist === true && allowRule === void 0) {
|
|
1090
|
+
return {
|
|
1091
|
+
decision: {
|
|
1092
|
+
allowed: false,
|
|
1093
|
+
by: "policy",
|
|
1094
|
+
reason: `Host "${target.host}" is not on the network allowlist and strict policy prohibits user overrides.`
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (target?.kind === "path" && isSensitive(target.path)) {
|
|
1101
|
+
const named = this.settings.allow.some(
|
|
1102
|
+
(r) => r.pattern !== void 0 && ruleMatches(r, tool, target, "allow")
|
|
1103
|
+
);
|
|
1104
|
+
if (!named) {
|
|
1105
|
+
return {
|
|
1106
|
+
decision: {
|
|
1107
|
+
allowed: false,
|
|
1108
|
+
by: "sensitive",
|
|
1109
|
+
reason: `${target.path} is a sensitive file. Add "${tool}(${target.path})" to permissions.allow in .garuda/settings.json to allow it.`
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (target?.kind === "path" && !request.readOnly && isProtectedFromWrites(target.path)) {
|
|
1115
|
+
return {
|
|
1116
|
+
decision: {
|
|
1117
|
+
allowed: false,
|
|
1118
|
+
by: "rule",
|
|
1119
|
+
reason: `${target.path} is inside .git/.`,
|
|
1120
|
+
kind: "protected"
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const denyRule = this.settings.deny.find((r) => ruleMatches(r, tool, target, "deny"));
|
|
1125
|
+
if (denyRule !== void 0) {
|
|
1126
|
+
return {
|
|
1127
|
+
decision: {
|
|
1128
|
+
allowed: false,
|
|
1129
|
+
by: "rule",
|
|
1130
|
+
reason: `A deny rule blocks this call: ${formatRule(denyRule)}.`
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
if (this.mode() === "plan" && !request.readOnly) {
|
|
1135
|
+
return { decision: this.planDecision(tool, target, allowRule !== void 0) };
|
|
1136
|
+
}
|
|
1137
|
+
if (request.readOnly) return { decision: { allowed: true, by: "read_only" } };
|
|
1138
|
+
if (target?.kind === "command" && !target.outsideSandbox && this.isolation !== "none") {
|
|
1139
|
+
return { decision: { allowed: true, by: "sandbox" } };
|
|
1140
|
+
}
|
|
1141
|
+
const mustAsk = target?.kind === "url" && target.alwaysAsk === true;
|
|
1142
|
+
if (allowRule !== void 0 && !mustAsk) return { decision: { allowed: true, by: "rule" } };
|
|
1143
|
+
if (!mustAsk && this.sessionRules.some((r) => ruleMatches(r, tool, target, "allow"))) {
|
|
1144
|
+
return { decision: { allowed: true, by: "session" } };
|
|
1145
|
+
}
|
|
1146
|
+
const asked = target ?? { kind: "input", json: "{}" };
|
|
1147
|
+
if (this.unattended !== void 0) {
|
|
1148
|
+
this.unattended.onDeny?.(tool, asked);
|
|
1149
|
+
return { decision: { allowed: false, by: "unattended", reason: this.unattended.reason } };
|
|
1150
|
+
}
|
|
1151
|
+
let accepted;
|
|
1152
|
+
const choice = await this.approver.ask(
|
|
1153
|
+
{
|
|
1154
|
+
tool,
|
|
1155
|
+
target: asked,
|
|
1156
|
+
preview: info?.preview ?? describeTarget(asked),
|
|
1157
|
+
isolation: this.isolation,
|
|
1158
|
+
...request.callId === void 0 ? {} : { callId: request.callId },
|
|
1159
|
+
...info?.change === void 0 ? {} : { change: info.change },
|
|
1160
|
+
...info?.title === void 0 ? {} : { title: info.title },
|
|
1161
|
+
...info?.hunks === true ? {
|
|
1162
|
+
selectHunks: (hunks) => {
|
|
1163
|
+
accepted = [...hunks];
|
|
1164
|
+
}
|
|
1165
|
+
} : {}
|
|
1166
|
+
},
|
|
1167
|
+
signal
|
|
1168
|
+
);
|
|
1169
|
+
if (choice === "deny") {
|
|
1170
|
+
return {
|
|
1171
|
+
decision: {
|
|
1172
|
+
allowed: false,
|
|
1173
|
+
by: "user",
|
|
1174
|
+
reason: "The user denied this call. Do not retry it. Ask the user what to do instead."
|
|
1175
|
+
},
|
|
1176
|
+
userChoice: choice
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
if (choice === "session") this.sessionRules.push(sessionRule(tool, asked));
|
|
1180
|
+
if (choice === "once" && accepted !== void 0) {
|
|
1181
|
+
return { decision: { allowed: true, by: "user", hunks: accepted }, userChoice: choice };
|
|
1182
|
+
}
|
|
1183
|
+
return { decision: { allowed: true, by: "user" }, userChoice: choice };
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Plan mode: file changes and memory are always denied; a command runs only in the OS sandbox
|
|
1187
|
+
* (which then cannot write the project, see execPolicy); other calls (web_fetch, MCP tools)
|
|
1188
|
+
* need an allow rule. Plan mode never asks: the plan is the place for changes.
|
|
1189
|
+
*/
|
|
1190
|
+
planDecision(tool, target, allowed) {
|
|
1191
|
+
const deny = {
|
|
1192
|
+
allowed: false,
|
|
1193
|
+
by: "rule",
|
|
1194
|
+
reason: PLAN_MODE_DENIAL,
|
|
1195
|
+
kind: "plan"
|
|
1196
|
+
};
|
|
1197
|
+
if (target?.kind === "command") {
|
|
1198
|
+
if (target.outsideSandbox || this.isolation === "none") return deny;
|
|
1199
|
+
return { allowed: true, by: "sandbox" };
|
|
1200
|
+
}
|
|
1201
|
+
if (target?.kind === "path" || tool === "remember") return deny;
|
|
1202
|
+
const mustAsk = target?.kind === "url" && target.alwaysAsk === true;
|
|
1203
|
+
return allowed && !mustAsk ? { allowed: true, by: "rule" } : deny;
|
|
1204
|
+
}
|
|
1205
|
+
/** The denials that depend only on the path: team policy, sensitive files, .git, deny rules. */
|
|
1206
|
+
pathDenial(tool, target, readOnly) {
|
|
1207
|
+
if (this.policy !== void 0) {
|
|
1208
|
+
const violation = isPathDeniedByPolicy(this.policy, target.path);
|
|
1209
|
+
if (violation.denied) {
|
|
1210
|
+
return {
|
|
1211
|
+
allowed: false,
|
|
1212
|
+
by: "policy",
|
|
1213
|
+
reason: violation.reason ?? "Denied by team policy."
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (isSensitive(target.path)) {
|
|
1218
|
+
const named = this.settings.allow.some(
|
|
1219
|
+
(r) => r.pattern !== void 0 && ruleMatches(r, tool, target, "allow")
|
|
1220
|
+
);
|
|
1221
|
+
if (!named) {
|
|
1222
|
+
return { allowed: false, by: "sensitive", reason: `${target.path} is a sensitive file.` };
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (!readOnly && isProtectedFromWrites(target.path)) {
|
|
1226
|
+
return {
|
|
1227
|
+
allowed: false,
|
|
1228
|
+
by: "rule",
|
|
1229
|
+
reason: `${target.path} is inside .git/.`,
|
|
1230
|
+
kind: "protected"
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const deny = this.settings.deny.find((r) => ruleMatches(r, tool, target, "deny"));
|
|
1234
|
+
if (deny !== void 0) {
|
|
1235
|
+
return {
|
|
1236
|
+
allowed: false,
|
|
1237
|
+
by: "rule",
|
|
1238
|
+
reason: `A deny rule blocks this call: ${formatRule(deny)}.`
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
return void 0;
|
|
1242
|
+
}
|
|
1243
|
+
commandPolicyDenial(command, outsideSandbox) {
|
|
1244
|
+
if (this.policy === void 0) return void 0;
|
|
1245
|
+
const sandbox = isSandboxRequiredByPolicy(
|
|
1246
|
+
this.policy,
|
|
1247
|
+
outsideSandbox || this.isolation === "none"
|
|
1248
|
+
);
|
|
1249
|
+
if (sandbox.disallowed) {
|
|
1250
|
+
return sandbox.reason ?? "Running commands outside the sandbox is disallowed by team policy.";
|
|
1251
|
+
}
|
|
1252
|
+
const command_ = isCommandDisallowedByPolicy(this.policy, command);
|
|
1253
|
+
if (command_.disallowed) return command_.reason ?? "Command disallowed by team policy.";
|
|
1254
|
+
return void 0;
|
|
1255
|
+
}
|
|
1256
|
+
deniedByPolicy(path) {
|
|
1257
|
+
return this.policy !== void 0 && isPathDeniedByPolicy(this.policy, path).denied;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* The policy for one command. `readOnlyRoot` (plan mode, language servers): the project is
|
|
1261
|
+
* read-only; temp folders and package caches stay writable.
|
|
1262
|
+
*/
|
|
1263
|
+
execPolicy(timeoutMs, {
|
|
1264
|
+
sandbox = true,
|
|
1265
|
+
readOnlyRoot = this.mode() === "plan"
|
|
1266
|
+
} = {}) {
|
|
1267
|
+
const paths = sandboxPaths(this.root, this.settings.sandbox);
|
|
1268
|
+
const denied = policyDeniedPaths(this.root, this.policy?.denyPaths ?? []);
|
|
1269
|
+
let writePaths = [.../* @__PURE__ */ new Set([...paths.writePaths, ...this.access.writePaths])];
|
|
1270
|
+
let denyWritePaths = [...paths.denyWritePaths, ...denied];
|
|
1271
|
+
if (readOnlyRoot) {
|
|
1272
|
+
const inRoot = (p) => p === this.root || p.startsWith(`${this.root}${sep}`);
|
|
1273
|
+
writePaths = writePaths.filter((p) => !inRoot(p));
|
|
1274
|
+
denyWritePaths = [this.root, ...denyWritePaths];
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
root: this.root,
|
|
1278
|
+
sandbox,
|
|
1279
|
+
...paths,
|
|
1280
|
+
denyReadPaths: [...paths.denyReadPaths, ...denied],
|
|
1281
|
+
writePaths,
|
|
1282
|
+
denyWritePaths,
|
|
1283
|
+
network: !sandbox,
|
|
1284
|
+
...sandbox && this.network !== void 0 ? { proxy: this.network.policy } : {},
|
|
1285
|
+
envAllowlist: [
|
|
1286
|
+
.../* @__PURE__ */ new Set([...DEFAULT_ENV_ALLOWLIST, ...this.access.envAllow, ...this.settings.envAllow])
|
|
1287
|
+
],
|
|
1288
|
+
timeoutMs,
|
|
1289
|
+
maxOutputBytes: DEFAULT_MAX_OUTPUT_BYTES
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/** The network allowlist (0.13): the runtime sets it when the proxy runs. */
|
|
1293
|
+
setNetwork(network) {
|
|
1294
|
+
this.network = network;
|
|
1295
|
+
}
|
|
1296
|
+
takeNetworkBlocks() {
|
|
1297
|
+
return this.network?.takeBlocked() ?? [];
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* The policy for a language server (0.4): in the sandbox, project read-only, no network, no
|
|
1301
|
+
* time limit (it runs for the session).
|
|
1302
|
+
*/
|
|
1303
|
+
serverPolicy() {
|
|
1304
|
+
const { proxy: _proxy, ...policy } = this.execPolicy(0, { readOnlyRoot: true });
|
|
1305
|
+
return { ...policy, maxOutputBytes: 0 };
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
function sessionRule(tool, target) {
|
|
1309
|
+
if (target.kind === "command") return { tool, pattern: target.command, exact: true };
|
|
1310
|
+
if (target.kind === "url") return { tool, pattern: target.host };
|
|
1311
|
+
return { tool };
|
|
1312
|
+
}
|
|
1313
|
+
function describeTarget(target) {
|
|
1314
|
+
if (target.kind === "path") return target.path;
|
|
1315
|
+
if (target.kind === "command") {
|
|
1316
|
+
return target.outsideSandbox ? `[outside sandbox] ${target.command}` : target.command;
|
|
1317
|
+
}
|
|
1318
|
+
if (target.kind === "url") return target.url;
|
|
1319
|
+
return target.json;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export {
|
|
1323
|
+
EFFORTS,
|
|
1324
|
+
ZERO_USAGE,
|
|
1325
|
+
addUsage,
|
|
1326
|
+
isSensitive,
|
|
1327
|
+
NETWORK_PORTS,
|
|
1328
|
+
expandAllowlist,
|
|
1329
|
+
hostAllowed,
|
|
1330
|
+
describeAllowlist,
|
|
1331
|
+
sandboxPaths,
|
|
1332
|
+
HostExecutor,
|
|
1333
|
+
EXECUTOR_NAMES,
|
|
1334
|
+
createExecutor,
|
|
1335
|
+
parseSettings,
|
|
1336
|
+
loadSettings,
|
|
1337
|
+
DEFAULT_ENV_ALLOWLIST,
|
|
1338
|
+
JOB_DENIAL,
|
|
1339
|
+
PermissionEngine
|
|
1340
|
+
};
|
|
1341
|
+
//# sourceMappingURL=chunk-SI2QJYCR.js.map
|