@garuda-agent/garuda 0.16.1 → 0.18.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 +319 -0
- package/README.md +56 -1074
- package/SECURITY.md +9 -2
- package/dist/{acpCommand-OTAYW7BS.js → acpCommand-IUZPLPLI.js} +25 -23
- package/dist/{anthropic-NQDBCHJU.js → anthropic-NQIZLD64.js} +12 -6
- package/dist/{chunk-NP6RIC25.js → chunk-2FIV2VZP.js} +11 -7
- package/dist/{chunk-YQTGMCU6.js → chunk-3MRQJKUO.js} +29 -5
- package/dist/{chunk-HE2K657U.js → chunk-4BVCYCQT.js} +47 -16
- package/dist/{chunk-WMOLXTBU.js → chunk-4LFJFDCF.js} +2 -1
- package/dist/{chunk-44QN5SWZ.js → chunk-55CFEEIN.js} +22 -2
- package/dist/{chunk-USCUXCAU.js → chunk-5B5HDTUR.js} +3 -3
- package/dist/chunk-65NUEOI7.js +143 -0
- package/dist/{chunk-TUUEBHG2.js → chunk-6XHON3Q6.js} +2 -2
- package/dist/chunk-7KBOIPUT.js +157 -0
- package/dist/{chunk-B6FA4I7Q.js → chunk-7TH3NHCQ.js} +2 -2
- package/dist/{chunk-OUCV3FCT.js → chunk-AOGFYUKG.js} +3 -3
- package/dist/{chunk-GXZACJUI.js → chunk-B4SJY7KT.js} +322 -39
- package/dist/{chunk-3EQU2GUX.js → chunk-BHZ6G65V.js} +5 -1
- package/dist/chunk-C57GHLKP.js +9 -0
- package/dist/chunk-DOCDCQVM.js +484 -0
- package/dist/chunk-FOPBOB7W.js +332 -0
- package/dist/{chunk-TILFPYWO.js → chunk-NHWHG3SW.js} +1930 -992
- package/dist/{chunk-BQTUOO4G.js → chunk-OQ26W5L4.js} +81 -32
- package/dist/{chunk-HUIK3WXO.js → chunk-P6XWWIEK.js} +20 -3
- package/dist/chunk-Q4P3W657.js +6372 -0
- package/dist/{chunk-ZAE25JM2.js → chunk-QANRZW7W.js} +8 -1
- package/dist/chunk-UIRJOQQS.js +118 -0
- package/dist/chunk-UKIE4AAM.js +472 -0
- package/dist/{chunk-KKB5W47S.js → chunk-VTMD5IHD.js} +26 -2
- package/dist/cli/index.js +148 -57
- package/dist/config-WV2H64TF.js +23 -0
- package/dist/{create-VXIH2GUZ.js → create-W6BV75QX.js} +18 -12
- package/dist/{deadline-F5RJJ2QW.js → deadline-TP7ZGLT2.js} +4 -2
- package/dist/evalCommand-SKJQ6CW2.js +686 -0
- package/dist/evalTune-CZRCPSZ3.js +295 -0
- package/dist/{export-HN3CT7AV.js → export-SJ3DUP6W.js} +2 -2
- package/dist/{inkChat-WEDGQMCY.js → inkChat-WYYAFZXD.js} +20 -19
- package/dist/install-TKWN5AYH.js +19 -0
- package/dist/{job-3NYDMP2M.js → job-7JOI4YIQ.js} +6 -2
- package/dist/jobCommand-2ODQLHB3.js +33 -0
- package/dist/{launchd-SSX362VG.js → launchd-3A3ONEW5.js} +9 -8
- package/dist/{lspCommand-JRXKRSL7.js → lspCommand-ZZ6R4PA2.js} +7 -6
- package/dist/{manager-773PUDFE.js → manager-TCYTA5XU.js} +15 -14
- package/dist/{nightCommand-65MUEV32.js → nightCommand-WJU6VZA6.js} +26 -20
- package/dist/{openaiCompatible-CJWVJP5M.js → openaiCompatible-6TLDDW5U.js} +29 -15
- package/dist/{policy-EJPLEEXP.js → policy-ZJ7RMOFN.js} +10 -4
- package/dist/{remove-6GDSTAIO.js → remove-42WQNVPX.js} +8 -7
- package/dist/{run-ML6GSPN4.js → run-PTMRQCIU.js} +5 -5
- package/dist/{server-NGKVIQEV.js → server-VEYIJHEU.js} +2 -2
- package/dist/{setupCommand-VA7D5MZK.js → setupCommand-YMF6I3MD.js} +45 -13
- package/dist/text-LS35WSRL.js +17 -0
- package/dist/tune-YVI2KWQF.js +31 -0
- package/native/bin/garuda-reap +0 -0
- package/native/garuda-reap.c +113 -0
- package/package.json +39 -13
- package/dist/chunk-EIXUSVFX.js +0 -116
- package/dist/chunk-R7I2NBPX.js +0 -71
- package/dist/chunk-ULAJPMY7.js +0 -107
- package/dist/chunk-VBZCNMUD.js +0 -9
- package/dist/evalCommand-RGMZRYDR.js +0 -2812
- package/dist/install-EWDSPFFZ.js +0 -18
- package/dist/jobCommand-ACSGSBQ4.js +0 -32
- package/dist/text-J2GD647J.js +0 -11
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
realRelative
|
|
4
|
-
} from "./chunk-WMOLXTBU.js";
|
|
5
2
|
import {
|
|
6
3
|
EFFORTS
|
|
7
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BHZ6G65V.js";
|
|
8
5
|
import {
|
|
6
|
+
PROFILES,
|
|
9
7
|
isCommandDisallowedByPolicy,
|
|
10
8
|
isHostBlockedByPolicy,
|
|
11
9
|
isPathDeniedByPolicy,
|
|
12
10
|
isSandboxRequiredByPolicy
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-P6XWWIEK.js";
|
|
12
|
+
import {
|
|
13
|
+
prepareWriteScope,
|
|
14
|
+
scopeDenial,
|
|
15
|
+
scopePaths
|
|
16
|
+
} from "./chunk-UIRJOQQS.js";
|
|
17
|
+
import {
|
|
18
|
+
realRelative
|
|
19
|
+
} from "./chunk-4LFJFDCF.js";
|
|
14
20
|
import {
|
|
15
21
|
CASE_INSENSITIVE_FS,
|
|
16
22
|
formatRule,
|
|
@@ -131,6 +137,14 @@ function unique(paths) {
|
|
|
131
137
|
return [...new Set(paths.filter((p) => isAbsolute(p)))];
|
|
132
138
|
}
|
|
133
139
|
var MAX_POLICY_DENIED_PATHS = 1e3;
|
|
140
|
+
var PolicyPathsOverflow = class extends Error {
|
|
141
|
+
name = "PolicyPathsOverflow";
|
|
142
|
+
constructor(found) {
|
|
143
|
+
super(
|
|
144
|
+
`The team policy's denyPaths match ${found} paths in this project, more paths than the OS sandbox can protect (at most ${MAX_POLICY_DENIED_PATHS}). The command does not run. Narrow the patterns, or deny a folder instead of its files.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
134
148
|
function policyDeniedPaths(root, patterns, ignoreCase = CASE_INSENSITIVE_FS) {
|
|
135
149
|
if (patterns.length === 0) return [];
|
|
136
150
|
const found = globbySync(patterns.map(fullGlob), {
|
|
@@ -140,10 +154,10 @@ function policyDeniedPaths(root, patterns, ignoreCase = CASE_INSENSITIVE_FS) {
|
|
|
140
154
|
onlyFiles: false,
|
|
141
155
|
followSymbolicLinks: false,
|
|
142
156
|
gitignore: false,
|
|
143
|
-
caseSensitiveMatch: !ignoreCase
|
|
144
|
-
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
157
|
+
caseSensitiveMatch: !ignoreCase
|
|
145
158
|
});
|
|
146
|
-
|
|
159
|
+
if (found.length > MAX_POLICY_DENIED_PATHS) throw new PolicyPathsOverflow(found.length);
|
|
160
|
+
return found;
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
// src/sandbox/process.ts
|
|
@@ -372,13 +386,61 @@ var ProcessExecutor = class {
|
|
|
372
386
|
daemons = new DaemonManager(this);
|
|
373
387
|
/** Process group ids of running commands. */
|
|
374
388
|
running = /* @__PURE__ */ new Set();
|
|
389
|
+
/** Markers of commands and processes that still run (0.17, IPI-04): shutdown() reaps them. */
|
|
390
|
+
trackers = /* @__PURE__ */ new Set();
|
|
391
|
+
/**
|
|
392
|
+
* Markers for one command, when this executor can find the processes that leave its process
|
|
393
|
+
* group (Seatbelt with the helper). Others need none: bubblewrap's PID namespace ends them all.
|
|
394
|
+
*/
|
|
395
|
+
track(_policy) {
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
/** Remove what tracking keeps (the markers folder), at shutdown. */
|
|
399
|
+
closeTracking() {
|
|
400
|
+
}
|
|
375
401
|
shutdown() {
|
|
376
402
|
this.daemons.shutdown();
|
|
377
403
|
for (const pid of this.running) signalGroup(pid, "SIGKILL");
|
|
378
404
|
this.running.clear();
|
|
405
|
+
for (const tracker of this.trackers) {
|
|
406
|
+
try {
|
|
407
|
+
tracker.reapSync();
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.trackers.clear();
|
|
412
|
+
this.closeTracking();
|
|
413
|
+
}
|
|
414
|
+
/** The launch, with markers; the markers go away when the launch fails. */
|
|
415
|
+
launchTracked(argv, policy) {
|
|
416
|
+
const tracker = this.track(policy);
|
|
417
|
+
try {
|
|
418
|
+
const launch = this.launch(argv, policy, tracker);
|
|
419
|
+
if (tracker === void 0) return launch;
|
|
420
|
+
this.trackers.add(tracker);
|
|
421
|
+
return { ...launch, tracker };
|
|
422
|
+
} catch (error) {
|
|
423
|
+
tracker?.dispose();
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/** Kill the tracked processes that still run (the command's own group is already gone). */
|
|
428
|
+
async reap(tracker) {
|
|
429
|
+
try {
|
|
430
|
+
const n = await tracker.reap();
|
|
431
|
+
return n > 0 ? { reaped: n } : {};
|
|
432
|
+
} catch (error) {
|
|
433
|
+
return { reapProblem: error.message };
|
|
434
|
+
} finally {
|
|
435
|
+
this.trackers.delete(tracker);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
dropTracker(tracker) {
|
|
439
|
+
if (tracker === void 0 || !this.trackers.delete(tracker)) return;
|
|
440
|
+
tracker.dispose();
|
|
379
441
|
}
|
|
380
442
|
start(argv, policy, env = {}) {
|
|
381
|
-
const { file, args } = this.
|
|
443
|
+
const { file, args, tracker } = this.launchTracked(argv, policy);
|
|
382
444
|
const child = spawn(file, args, {
|
|
383
445
|
cwd: policy.root,
|
|
384
446
|
env: { ...allowedEnv(policy.envAllowlist), ...proxyEnv(policy), ...env },
|
|
@@ -394,7 +456,9 @@ var ProcessExecutor = class {
|
|
|
394
456
|
signalGroup(pid, "SIGKILL");
|
|
395
457
|
}
|
|
396
458
|
if (killTimer !== void 0) clearTimeout(killTimer);
|
|
459
|
+
if (tracker !== void 0) void this.reap(tracker);
|
|
397
460
|
});
|
|
461
|
+
child.on("error", () => this.dropTracker(tracker));
|
|
398
462
|
return {
|
|
399
463
|
pid,
|
|
400
464
|
stdin: child.stdin,
|
|
@@ -423,7 +487,7 @@ var ProcessExecutor = class {
|
|
|
423
487
|
reject(options.signal.reason);
|
|
424
488
|
return;
|
|
425
489
|
}
|
|
426
|
-
const { file, args } = this.
|
|
490
|
+
const { file, args, tracker } = this.launchTracked(["bash", "-c", command], policy);
|
|
427
491
|
const child = spawn(file, args, {
|
|
428
492
|
cwd: policy.root,
|
|
429
493
|
env: { ...allowedEnv(policy.envAllowlist), ...proxyEnv(policy), ...options.env },
|
|
@@ -458,6 +522,7 @@ var ProcessExecutor = class {
|
|
|
458
522
|
child.on("error", (error) => {
|
|
459
523
|
if (finished) return;
|
|
460
524
|
finished = true;
|
|
525
|
+
this.dropTracker(tracker);
|
|
461
526
|
if (pid !== void 0) this.running.delete(pid);
|
|
462
527
|
clearTimeout(timer);
|
|
463
528
|
if (drainTimer !== void 0) clearTimeout(drainTimer);
|
|
@@ -473,7 +538,7 @@ var ProcessExecutor = class {
|
|
|
473
538
|
if (killTimer !== void 0) clearTimeout(killTimer);
|
|
474
539
|
options.signal?.removeEventListener("abort", onAbort);
|
|
475
540
|
if (child.pid !== void 0) signalGroup(child.pid, "SIGKILL");
|
|
476
|
-
|
|
541
|
+
const result = {
|
|
477
542
|
exitCode: code,
|
|
478
543
|
signal,
|
|
479
544
|
stdout: stdout.result(),
|
|
@@ -481,7 +546,12 @@ var ProcessExecutor = class {
|
|
|
481
546
|
timedOut,
|
|
482
547
|
aborted,
|
|
483
548
|
durationMs: Date.now() - started
|
|
484
|
-
}
|
|
549
|
+
};
|
|
550
|
+
if (tracker === void 0) {
|
|
551
|
+
resolve2(result);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
void this.reap(tracker).then((reaped) => resolve2({ ...result, ...reaped }));
|
|
485
555
|
};
|
|
486
556
|
child.on("exit", (code, signal) => {
|
|
487
557
|
drainTimer = setTimeout(() => {
|
|
@@ -541,9 +611,9 @@ var HostExecutor = class extends ProcessExecutor {
|
|
|
541
611
|
};
|
|
542
612
|
|
|
543
613
|
// src/sandbox/index.ts
|
|
544
|
-
import { spawnSync } from "child_process";
|
|
545
|
-
import { existsSync as
|
|
546
|
-
import { delimiter, join as
|
|
614
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
615
|
+
import { existsSync as existsSync3 } from "fs";
|
|
616
|
+
import { delimiter, join as join3 } from "path";
|
|
547
617
|
|
|
548
618
|
// src/sandbox/bwrap.ts
|
|
549
619
|
import { existsSync, statSync } from "fs";
|
|
@@ -564,6 +634,12 @@ function bwrapArgv(argv, policy, kind = pathKind) {
|
|
|
564
634
|
const args = ["--ro-bind", "/", "/", "--dev", "/dev"];
|
|
565
635
|
for (const path of policy.writePaths)
|
|
566
636
|
if (kind(path) !== void 0) args.push("--bind", path, path);
|
|
637
|
+
for (const mount of policy.mounts ?? []) args.push("--bind", mount.source, mount.target);
|
|
638
|
+
if (policy.writeScope !== void 0) {
|
|
639
|
+
args.push("--ro-bind", policy.writeScope.root, policy.writeScope.root);
|
|
640
|
+
for (const path of policy.writeScope.paths)
|
|
641
|
+
if (kind(path) !== void 0) args.push("--bind", path, path);
|
|
642
|
+
}
|
|
567
643
|
for (const path of policy.denyWritePaths) {
|
|
568
644
|
if (kind(path) !== void 0) args.push("--ro-bind", path, path);
|
|
569
645
|
}
|
|
@@ -600,24 +676,146 @@ function pathKind(path) {
|
|
|
600
676
|
return statSync(path).isDirectory() ? "dir" : "file";
|
|
601
677
|
}
|
|
602
678
|
|
|
679
|
+
// src/sandbox/reaper.ts
|
|
680
|
+
import { execFile, spawnSync } from "child_process";
|
|
681
|
+
import { randomBytes } from "crypto";
|
|
682
|
+
import { existsSync as existsSync2, mkdtempSync, realpathSync as realpathSync2, rmSync, writeFileSync } from "fs";
|
|
683
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
684
|
+
import { dirname, join as join2 } from "path";
|
|
685
|
+
import { fileURLToPath } from "url";
|
|
686
|
+
var HELPER_TIMEOUT_MS = 1e4;
|
|
687
|
+
var ReapError = class extends Error {
|
|
688
|
+
name = "ReapError";
|
|
689
|
+
};
|
|
690
|
+
var Reaper = class {
|
|
691
|
+
constructor(helper, parent = tmpdir2()) {
|
|
692
|
+
this.helper = helper;
|
|
693
|
+
this.parent = parent;
|
|
694
|
+
}
|
|
695
|
+
helper;
|
|
696
|
+
parent;
|
|
697
|
+
folder;
|
|
698
|
+
/** The markers folder, made at the first command. Seatbelt matches real paths (/private/var). */
|
|
699
|
+
dir() {
|
|
700
|
+
this.folder ??= realpathSync2(mkdtempSync(join2(this.parent, "garuda-markers-")));
|
|
701
|
+
return this.folder;
|
|
702
|
+
}
|
|
703
|
+
/** Remove the markers folder (at shutdown). */
|
|
704
|
+
close() {
|
|
705
|
+
if (this.folder !== void 0) rmSync(this.folder, { recursive: true, force: true });
|
|
706
|
+
this.folder = void 0;
|
|
707
|
+
}
|
|
708
|
+
/** New markers for one command. */
|
|
709
|
+
track() {
|
|
710
|
+
const dir = this.dir();
|
|
711
|
+
const id = randomBytes(8).toString("hex");
|
|
712
|
+
const a = join2(dir, `${id}-a`);
|
|
713
|
+
const b = join2(dir, `${id}-b`);
|
|
714
|
+
writeFileSync(a, "", { mode: 384 });
|
|
715
|
+
writeFileSync(b, "", { mode: 384 });
|
|
716
|
+
const remove = () => {
|
|
717
|
+
rmSync(a, { force: true });
|
|
718
|
+
rmSync(b, { force: true });
|
|
719
|
+
};
|
|
720
|
+
const helper = this.helper;
|
|
721
|
+
return {
|
|
722
|
+
rules: [
|
|
723
|
+
`(deny file-read* (literal ${quote(a)}))`,
|
|
724
|
+
`(allow file-read* (literal ${quote(b)}))`,
|
|
725
|
+
`(deny file-write* (subpath ${quote(dir)}))`
|
|
726
|
+
],
|
|
727
|
+
reap: () => new Promise((resolve2, reject) => {
|
|
728
|
+
execFile(
|
|
729
|
+
helper,
|
|
730
|
+
[a, b],
|
|
731
|
+
{ timeout: HELPER_TIMEOUT_MS, encoding: "utf8" },
|
|
732
|
+
(error, stdout, stderr) => {
|
|
733
|
+
remove();
|
|
734
|
+
if (error) reject(new ReapError(why(error.message, stderr)));
|
|
735
|
+
else resolve2(killed(stdout));
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
}),
|
|
739
|
+
reapSync: () => {
|
|
740
|
+
const r = spawnSync(helper, [a, b], { timeout: HELPER_TIMEOUT_MS, encoding: "utf8" });
|
|
741
|
+
remove();
|
|
742
|
+
if (r.status !== 0) throw new ReapError(why(r.error?.message ?? "", r.stderr ?? ""));
|
|
743
|
+
return killed(r.stdout ?? "");
|
|
744
|
+
},
|
|
745
|
+
dispose: remove
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
function locateHelper() {
|
|
750
|
+
let dir;
|
|
751
|
+
try {
|
|
752
|
+
dir = dirname(fileURLToPath(import.meta.url));
|
|
753
|
+
} catch {
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
for (let i = 0; i < 5; i++) {
|
|
757
|
+
const candidate = join2(dir, "native", "bin", "garuda-reap");
|
|
758
|
+
if (existsSync2(candidate)) return candidate;
|
|
759
|
+
const up = dirname(dir);
|
|
760
|
+
if (up === dir) break;
|
|
761
|
+
dir = up;
|
|
762
|
+
}
|
|
763
|
+
return void 0;
|
|
764
|
+
}
|
|
765
|
+
function killed(stdout) {
|
|
766
|
+
const match = /killed (\d+)/.exec(stdout);
|
|
767
|
+
return match === null ? 0 : Number(match[1]);
|
|
768
|
+
}
|
|
769
|
+
function why(message, stderr) {
|
|
770
|
+
return `garuda-reap failed: ${(stderr.trim() || message).split("\n")[0]}`;
|
|
771
|
+
}
|
|
772
|
+
function quote(path) {
|
|
773
|
+
return `"${path.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
774
|
+
}
|
|
775
|
+
|
|
603
776
|
// src/sandbox/seatbelt.ts
|
|
604
777
|
var SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
605
778
|
var SeatbeltExecutor = class extends ProcessExecutor {
|
|
779
|
+
/**
|
|
780
|
+
* `reaper` (0.17, IPI-04): finds and kills the processes that leave a command's process group.
|
|
781
|
+
* Without it, such a process outlives the command (index.ts warns, or strict refuses).
|
|
782
|
+
*/
|
|
783
|
+
constructor(reaper) {
|
|
784
|
+
super();
|
|
785
|
+
this.reaper = reaper;
|
|
786
|
+
}
|
|
787
|
+
reaper;
|
|
606
788
|
name = "seatbelt";
|
|
607
789
|
isolation = "os";
|
|
608
|
-
|
|
790
|
+
track(policy) {
|
|
791
|
+
return policy.sandbox && this.reaper !== void 0 ? this.reaper.track() : void 0;
|
|
792
|
+
}
|
|
793
|
+
closeTracking() {
|
|
794
|
+
this.reaper?.close();
|
|
795
|
+
}
|
|
796
|
+
launch(argv, policy, tracker) {
|
|
609
797
|
if (!policy.sandbox) return plain(argv);
|
|
610
|
-
|
|
798
|
+
if ((policy.mounts ?? []).length > 0) throw new Error("Seatbelt cannot mount folders.");
|
|
799
|
+
return {
|
|
800
|
+
file: SANDBOX_EXEC,
|
|
801
|
+
args: ["-p", seatbeltProfile(policy, tracker?.rules), ...argv]
|
|
802
|
+
};
|
|
611
803
|
}
|
|
612
804
|
};
|
|
613
|
-
function seatbeltProfile(policy) {
|
|
614
|
-
const paths = (list) => list.map((p) => `(subpath ${
|
|
805
|
+
function seatbeltProfile(policy, extra = []) {
|
|
806
|
+
const paths = (list) => list.map((p) => `(subpath ${quote2(p)})`).join(" ");
|
|
615
807
|
const lines = [
|
|
616
808
|
"(version 1)",
|
|
617
809
|
"(allow default)",
|
|
618
810
|
"(deny file-write*)",
|
|
619
811
|
`(allow file-write* ${paths(policy.writePaths)} (literal "/dev/null") (literal "/dev/zero") (literal "/dev/dtracehelper") (regex #"^/dev/tty") (regex #"^/dev/fd/"))`
|
|
620
812
|
];
|
|
813
|
+
if (policy.writeScope !== void 0) {
|
|
814
|
+
lines.push(`(deny file-write* ${paths([policy.writeScope.root])})`);
|
|
815
|
+
if (policy.writeScope.paths.length > 0) {
|
|
816
|
+
lines.push(`(allow file-write* ${paths(policy.writeScope.paths)})`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
621
819
|
if (policy.denyWritePaths.length > 0) {
|
|
622
820
|
lines.push(`(deny file-write* ${paths(policy.denyWritePaths)})`);
|
|
623
821
|
}
|
|
@@ -635,42 +833,75 @@ function seatbeltProfile(policy) {
|
|
|
635
833
|
'(allow network-bind (local ip "localhost:*"))'
|
|
636
834
|
);
|
|
637
835
|
}
|
|
836
|
+
lines.push(...extra);
|
|
638
837
|
return lines.join("\n");
|
|
639
838
|
}
|
|
640
|
-
function
|
|
839
|
+
function quote2(path) {
|
|
641
840
|
return `"${path.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
642
841
|
}
|
|
643
842
|
|
|
644
843
|
// src/sandbox/index.ts
|
|
645
844
|
var EXECUTOR_NAMES = ["auto", "os", "host"];
|
|
646
|
-
|
|
845
|
+
var StrictProfileError = class extends Error {
|
|
846
|
+
};
|
|
847
|
+
function strictFix(platform) {
|
|
848
|
+
if (platform === "linux")
|
|
849
|
+
return "Install bubblewrap (for example: sudo apt install bubblewrap) and allow user namespaces, or remove the strict profile.";
|
|
850
|
+
return 'Run Garuda where the OS sandbox works, or remove the strict profile ("profile": "strict").';
|
|
851
|
+
}
|
|
852
|
+
function createExecutor(name = "auto", find = findOsSandbox, strict = false, platform = process.platform) {
|
|
853
|
+
if (strict && name === "host") {
|
|
854
|
+
throw new StrictProfileError(
|
|
855
|
+
'The strict profile requires the OS sandbox, but "executor" is "host". Remove "executor": "host" from .garuda/settings.json, or remove the strict profile.'
|
|
856
|
+
);
|
|
857
|
+
}
|
|
647
858
|
if (name === "host") return { executor: new HostExecutor() };
|
|
648
859
|
const found = find();
|
|
649
|
-
if ("executor" in found)
|
|
860
|
+
if ("executor" in found) {
|
|
861
|
+
if (found.warning === void 0) return { executor: found.executor };
|
|
862
|
+
if (strict) {
|
|
863
|
+
throw new StrictProfileError(
|
|
864
|
+
`The strict profile requires the macOS helper garuda-reap. ${found.warning}`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return { executor: found.executor, notice: found.warning };
|
|
868
|
+
}
|
|
869
|
+
if (strict) {
|
|
870
|
+
throw new StrictProfileError(
|
|
871
|
+
`The strict profile requires the OS sandbox, and this machine has none: ${found.problem} Garuda does not run commands on the host in this profile. ${strictFix(platform)}`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
650
874
|
if (name === "os") throw new Error(`No OS sandbox: ${found.problem} ${found.fix}`);
|
|
651
875
|
return {
|
|
652
876
|
executor: new HostExecutor(),
|
|
653
|
-
notice: `No OS sandbox: ${found.problem} Commands run on your machine, and each one asks for approval. ${found.fix}`
|
|
877
|
+
notice: `No OS sandbox: ${found.problem} Commands run on your machine, and each one asks for approval, unless an allow rule covers it. ${found.fix}`
|
|
654
878
|
};
|
|
655
879
|
}
|
|
880
|
+
var HELPER_MISSING = "The macOS helper garuda-reap is missing, so a process that leaves a command's process group (for example with setsid) can keep running after the command, inside the sandbox. Reinstall Garuda; in a source checkout, run `pnpm build` (it needs the Command Line Tools).";
|
|
656
881
|
var cached;
|
|
657
882
|
function findOsSandbox() {
|
|
658
883
|
cached ??= detect(process.platform);
|
|
659
|
-
|
|
884
|
+
if (!("make" in cached)) return cached;
|
|
885
|
+
return cached.warning === void 0 ? { executor: cached.make() } : { executor: cached.make(), warning: cached.warning };
|
|
660
886
|
}
|
|
661
887
|
function detect(platform) {
|
|
662
888
|
const fix = 'Set "executor": "host" in .garuda/settings.json to hide this notice.';
|
|
663
889
|
if (platform === "darwin") {
|
|
664
|
-
if (!
|
|
890
|
+
if (!existsSync3(SANDBOX_EXEC)) {
|
|
665
891
|
return { problem: `${SANDBOX_EXEC} is missing.`, fix };
|
|
666
892
|
}
|
|
667
|
-
const probe =
|
|
893
|
+
const probe = spawnSync2(SANDBOX_EXEC, ["-p", "(version 1)(allow default)", "--", "true"], {
|
|
668
894
|
encoding: "utf8",
|
|
669
895
|
timeout: 5e3
|
|
670
896
|
});
|
|
671
|
-
if (probe.status === 0)
|
|
672
|
-
|
|
673
|
-
|
|
897
|
+
if (probe.status === 0) {
|
|
898
|
+
const helper = locateHelper();
|
|
899
|
+
if (helper === void 0)
|
|
900
|
+
return { make: () => new SeatbeltExecutor(), warning: HELPER_MISSING };
|
|
901
|
+
return { make: () => new SeatbeltExecutor(new Reaper(helper)) };
|
|
902
|
+
}
|
|
903
|
+
const why2 = (probe.stderr || probe.error?.message || "unknown error").trim().split("\n")[0];
|
|
904
|
+
return { problem: `sandbox-exec cannot start a sandbox here (${why2}).`, fix };
|
|
674
905
|
}
|
|
675
906
|
if (platform === "linux") {
|
|
676
907
|
const bwrap = onPath("bwrap");
|
|
@@ -680,33 +911,34 @@ function detect(platform) {
|
|
|
680
911
|
fix: 'Install it (for example: sudo apt install bubblewrap), or set "executor": "host" in .garuda/settings.json.'
|
|
681
912
|
};
|
|
682
913
|
}
|
|
683
|
-
const probe =
|
|
914
|
+
const probe = spawnSync2(
|
|
684
915
|
bwrap,
|
|
685
916
|
["--ro-bind", "/", "/", "--dev", "/dev", "--unshare-net", "--die-with-parent", "--", "true"],
|
|
686
917
|
{ encoding: "utf8", timeout: 5e3 }
|
|
687
918
|
);
|
|
688
919
|
if (probe.status === 0) return { make: () => new BwrapExecutor(bwrap) };
|
|
689
|
-
const
|
|
690
|
-
return { problem: `bubblewrap cannot start a sandbox here (${
|
|
920
|
+
const why2 = (probe.stderr || probe.error?.message || "unknown error").trim().split("\n")[0];
|
|
921
|
+
return { problem: `bubblewrap cannot start a sandbox here (${why2}).`, fix };
|
|
691
922
|
}
|
|
692
923
|
return { problem: `Garuda has no OS sandbox for ${platform}.`, fix };
|
|
693
924
|
}
|
|
694
925
|
function onPath(program) {
|
|
695
926
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
696
|
-
const path =
|
|
697
|
-
if (dir !== "" &&
|
|
927
|
+
const path = join3(dir, program);
|
|
928
|
+
if (dir !== "" && existsSync3(path)) return path;
|
|
698
929
|
}
|
|
699
930
|
return void 0;
|
|
700
931
|
}
|
|
701
932
|
|
|
702
933
|
// src/permissions/settings.ts
|
|
703
934
|
import { readFile } from "fs/promises";
|
|
704
|
-
import { join as
|
|
935
|
+
import { join as join4 } from "path";
|
|
705
936
|
import { z } from "zod";
|
|
706
|
-
var SETTINGS_FILE =
|
|
937
|
+
var SETTINGS_FILE = join4(".garuda", "settings.json");
|
|
707
938
|
var NOTIFY_CHOICES = ["auto", "osc9", "bell", "off"];
|
|
708
939
|
var schema = z.strictObject({
|
|
709
940
|
executor: z.enum(EXECUTOR_NAMES).optional(),
|
|
941
|
+
profile: z.enum(PROFILES).optional(),
|
|
710
942
|
sandbox: z.strictObject({
|
|
711
943
|
writePaths: z.array(z.string()).optional(),
|
|
712
944
|
denyRead: z.array(z.string()).optional()
|
|
@@ -782,6 +1014,11 @@ var schema = z.strictObject({
|
|
|
782
1014
|
* (merge gate): no measured gain yet, and they add tools and long-lived processes.
|
|
783
1015
|
*/
|
|
784
1016
|
daemons: z.strictObject({ enabled: z.boolean().optional() }).optional(),
|
|
1017
|
+
/**
|
|
1018
|
+
* Staged commands (0.17, review T5): "staged" runs each sandboxed command in a staging copy and
|
|
1019
|
+
* asks before its changes reach the project. Default "direct".
|
|
1020
|
+
*/
|
|
1021
|
+
commands: z.strictObject({ mode: z.enum(["direct", "staged"]).optional() }).optional(),
|
|
785
1022
|
/** Mixture-of-Experts language specialist subagent dispatch (0.17). */
|
|
786
1023
|
moe: z.strictObject({
|
|
787
1024
|
enabled: z.boolean().optional(),
|
|
@@ -810,6 +1047,7 @@ function parseSettings(json, source = SETTINGS_FILE) {
|
|
|
810
1047
|
if (!parsed.success) throw new Error(`${source}: ${z.prettifyError(parsed.error)}`);
|
|
811
1048
|
const {
|
|
812
1049
|
executor = "auto",
|
|
1050
|
+
profile,
|
|
813
1051
|
sandbox,
|
|
814
1052
|
web,
|
|
815
1053
|
permissions = {},
|
|
@@ -820,6 +1058,7 @@ function parseSettings(json, source = SETTINGS_FILE) {
|
|
|
820
1058
|
subagents,
|
|
821
1059
|
moe,
|
|
822
1060
|
daemons,
|
|
1061
|
+
commands,
|
|
823
1062
|
todo,
|
|
824
1063
|
lsp,
|
|
825
1064
|
undo,
|
|
@@ -843,6 +1082,7 @@ function parseSettings(json, source = SETTINGS_FILE) {
|
|
|
843
1082
|
});
|
|
844
1083
|
return {
|
|
845
1084
|
executor,
|
|
1085
|
+
...profile === void 0 ? {} : { profile },
|
|
846
1086
|
allow: rules(permissions.allow),
|
|
847
1087
|
deny: rules(permissions.deny),
|
|
848
1088
|
envAllow: env.allow ?? [],
|
|
@@ -859,6 +1099,7 @@ function parseSettings(json, source = SETTINGS_FILE) {
|
|
|
859
1099
|
...limits.tokenBudget === void 0 ? {} : { tokenBudget: limits.tokenBudget },
|
|
860
1100
|
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
861
1101
|
...model.price === void 0 ? {} : { price: model.price },
|
|
1102
|
+
...commands?.mode === void 0 ? {} : { commands: { mode: commands.mode } },
|
|
862
1103
|
...todo?.enabled === void 0 ? {} : { todo: { enabled: todo.enabled } },
|
|
863
1104
|
...lsp?.enabled === void 0 ? {} : { lsp: { enabled: lsp.enabled } },
|
|
864
1105
|
...undo?.enabled === void 0 ? {} : { undo: { enabled: undo.enabled } },
|
|
@@ -904,7 +1145,7 @@ function parseSettings(json, source = SETTINGS_FILE) {
|
|
|
904
1145
|
async function loadSettings(root) {
|
|
905
1146
|
let text;
|
|
906
1147
|
try {
|
|
907
|
-
text = await readFile(
|
|
1148
|
+
text = await readFile(join4(root, SETTINGS_FILE), "utf8");
|
|
908
1149
|
} catch (error) {
|
|
909
1150
|
if (error.code === "ENOENT") return DEFAULT_SETTINGS;
|
|
910
1151
|
throw error;
|
|
@@ -1119,6 +1360,31 @@ var PermissionEngine = class {
|
|
|
1119
1360
|
return { decision: this.planDecision(tool, target, allowRule !== void 0) };
|
|
1120
1361
|
}
|
|
1121
1362
|
if (request.readOnly) return { decision: { allowed: true, by: "read_only" } };
|
|
1363
|
+
if (target?.kind === "command" && target.outsideSandbox === true && (this.unattended !== void 0 || this.settings.writeScope !== void 0)) {
|
|
1364
|
+
this.unattended?.onDeny?.(tool, target);
|
|
1365
|
+
return {
|
|
1366
|
+
decision: {
|
|
1367
|
+
allowed: false,
|
|
1368
|
+
by: "rule",
|
|
1369
|
+
reason: "A scheduled job runs commands only in the OS sandbox: outside_sandbox is not allowed. Do not retry it; list it in your final answer as work left for the user."
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
const scope = this.settings.writeScope;
|
|
1374
|
+
const ruled = allowRule !== void 0 && !(target?.kind === "url" && target.alwaysAsk === true) || this.sessionRules.some((r) => ruleMatches(r, tool, target, "allow"));
|
|
1375
|
+
if (scope !== void 0 && target?.kind === "path" && (ruled || this.unattended === void 0)) {
|
|
1376
|
+
const outside = await scopeDenial(this.root, target.path, scope);
|
|
1377
|
+
if (outside !== void 0) {
|
|
1378
|
+
this.unattended?.onDeny?.(tool, target);
|
|
1379
|
+
return {
|
|
1380
|
+
decision: {
|
|
1381
|
+
allowed: false,
|
|
1382
|
+
by: "rule",
|
|
1383
|
+
reason: `${outside}. Do not retry it; list it in your final answer as work left for the user.`
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1122
1388
|
if (target?.kind === "command" && !target.outsideSandbox && this.isolation !== "none") {
|
|
1123
1389
|
return { decision: { allowed: true, by: "sandbox" } };
|
|
1124
1390
|
}
|
|
@@ -1252,17 +1518,25 @@ var PermissionEngine = class {
|
|
|
1252
1518
|
const denied = policyDeniedPaths(this.root, this.policy?.denyPaths ?? []);
|
|
1253
1519
|
let writePaths = [.../* @__PURE__ */ new Set([...paths.writePaths, ...this.access.writePaths])];
|
|
1254
1520
|
let denyWritePaths = [...paths.denyWritePaths, ...denied];
|
|
1521
|
+
const inRoot = (p) => p === this.root || p.startsWith(`${this.root}${sep}`);
|
|
1255
1522
|
if (readOnlyRoot) {
|
|
1256
|
-
const inRoot = (p) => p === this.root || p.startsWith(`${this.root}${sep}`);
|
|
1257
1523
|
writePaths = writePaths.filter((p) => !inRoot(p));
|
|
1258
1524
|
denyWritePaths = [this.root, ...denyWritePaths];
|
|
1259
1525
|
}
|
|
1526
|
+
const scope = !readOnlyRoot && sandbox && this.settings.writeScope !== void 0 ? { root: this.root, paths: scopePaths(this.root, this.settings.writeScope) } : void 0;
|
|
1527
|
+
if (scope !== void 0) {
|
|
1528
|
+
writePaths = writePaths.filter((p) => !inRoot(p));
|
|
1529
|
+
const problem = prepareWriteScope(this.root, this.settings.writeScope ?? []);
|
|
1530
|
+
if (problem !== void 0)
|
|
1531
|
+
throw new Error(`The job's write scope cannot be used: ${problem}.`);
|
|
1532
|
+
}
|
|
1260
1533
|
return {
|
|
1261
1534
|
root: this.root,
|
|
1262
1535
|
sandbox,
|
|
1263
1536
|
...paths,
|
|
1264
1537
|
denyReadPaths: [...paths.denyReadPaths, ...denied],
|
|
1265
1538
|
writePaths,
|
|
1539
|
+
...scope === void 0 ? {} : { writeScope: scope },
|
|
1266
1540
|
denyWritePaths,
|
|
1267
1541
|
network: !sandbox,
|
|
1268
1542
|
...sandbox && this.network !== void 0 ? { proxy: this.network.policy } : {},
|
|
@@ -1273,6 +1547,15 @@ var PermissionEngine = class {
|
|
|
1273
1547
|
maxOutputBytes: DEFAULT_MAX_OUTPUT_BYTES
|
|
1274
1548
|
};
|
|
1275
1549
|
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Why a write to this path is outside the job's write scope, or undefined (no scope, or inside
|
|
1552
|
+
* it). Write tools call it again just before they write (second review of 0.17): a link can
|
|
1553
|
+
* change between the permission check and the write.
|
|
1554
|
+
*/
|
|
1555
|
+
async writeScopeDenial(path) {
|
|
1556
|
+
const scope = this.settings.writeScope;
|
|
1557
|
+
return scope === void 0 ? void 0 : scopeDenial(this.root, path, scope);
|
|
1558
|
+
}
|
|
1276
1559
|
/** The network allowlist (0.13): the runtime sets it when the proxy runs. */
|
|
1277
1560
|
setNetwork(network) {
|
|
1278
1561
|
this.network = network;
|
|
@@ -1319,4 +1602,4 @@ export {
|
|
|
1319
1602
|
JOB_DENIAL,
|
|
1320
1603
|
PermissionEngine
|
|
1321
1604
|
};
|
|
1322
|
-
//# sourceMappingURL=chunk-
|
|
1605
|
+
//# sourceMappingURL=chunk-B4SJY7KT.js.map
|
|
@@ -8,6 +8,9 @@ var ZERO_USAGE = {
|
|
|
8
8
|
cacheReadTokens: 0,
|
|
9
9
|
cacheWriteTokens: 0
|
|
10
10
|
};
|
|
11
|
+
function reportedUsage(response) {
|
|
12
|
+
return response.usageReported === false ? ZERO_USAGE : response.usage;
|
|
13
|
+
}
|
|
11
14
|
function addUsage(a, b) {
|
|
12
15
|
const searches = (a.webSearches ?? 0) + (b.webSearches ?? 0);
|
|
13
16
|
return {
|
|
@@ -22,6 +25,7 @@ function addUsage(a, b) {
|
|
|
22
25
|
export {
|
|
23
26
|
EFFORTS,
|
|
24
27
|
ZERO_USAGE,
|
|
28
|
+
reportedUsage,
|
|
25
29
|
addUsage
|
|
26
30
|
};
|
|
27
|
-
//# sourceMappingURL=chunk-
|
|
31
|
+
//# sourceMappingURL=chunk-BHZ6G65V.js.map
|