@h-rig/isolation-plugin 0.0.6-alpha.157 → 0.0.6-alpha.159
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/dist/src/embedded-native-assets.d.ts +7 -0
- package/dist/src/embedded-native-assets.js +6 -0
- package/dist/src/image-fingerprint-sidecar.d.ts +1 -0
- package/dist/src/image-fingerprint-sidecar.js +515 -0
- package/dist/src/image.d.ts +40 -0
- package/dist/src/image.js +1498 -0
- package/dist/src/index.js +4220 -20
- package/dist/src/isolation/binary-build-worker.d.ts +1 -0
- package/dist/src/isolation/binary-build-worker.js +323 -0
- package/dist/src/isolation/discovery.d.ts +7 -0
- package/dist/src/isolation/discovery.js +477 -0
- package/dist/src/isolation/git-native.d.ts +28 -0
- package/dist/src/isolation/git-native.js +598 -0
- package/dist/src/isolation/home.d.ts +25 -0
- package/dist/src/isolation/home.js +929 -0
- package/dist/src/isolation/index.d.ts +43 -0
- package/dist/src/isolation/index.js +4062 -0
- package/dist/src/isolation/provisioning-env.d.ts +1 -0
- package/dist/src/isolation/provisioning-env.js +6 -0
- package/dist/src/isolation/runner.d.ts +20 -0
- package/dist/src/isolation/runner.js +1881 -0
- package/dist/src/isolation/runtime-binary-build.d.ts +88 -0
- package/dist/src/isolation/runtime-binary-build.js +480 -0
- package/dist/src/isolation/shared.d.ts +29 -0
- package/dist/src/isolation/shared.js +283 -0
- package/dist/src/isolation/toolchain.d.ts +71 -0
- package/dist/src/isolation/toolchain.js +1348 -0
- package/dist/src/isolation/types.d.ts +15 -0
- package/dist/src/isolation/types.js +1 -0
- package/dist/src/isolation/worktree.d.ts +22 -0
- package/dist/src/isolation/worktree.js +353 -0
- package/dist/src/native-extract.d.ts +2 -0
- package/dist/src/native-extract.js +44 -0
- package/dist/src/plugin.d.ts +2 -2
- package/dist/src/plugin.js +4219 -19
- package/dist/src/runtime-config.d.ts +3 -0
- package/dist/src/runtime-config.js +215 -0
- package/dist/src/runtime-native-sidecar.d.ts +8 -0
- package/dist/src/runtime-native-sidecar.js +368 -0
- package/dist/src/runtime-native.d.ts +51 -0
- package/dist/src/runtime-native.js +485 -0
- package/dist/src/sandbox/backend-bwrap.d.ts +20 -0
- package/dist/src/sandbox/backend-bwrap.js +268 -0
- package/dist/src/sandbox/backend-none.d.ts +11 -0
- package/dist/src/sandbox/backend-none.js +20 -0
- package/dist/src/sandbox/backend-seatbelt.d.ts +13 -0
- package/dist/src/sandbox/backend-seatbelt.js +225 -0
- package/dist/src/sandbox/backend.d.ts +117 -0
- package/dist/src/sandbox/backend.js +864 -0
- package/dist/src/sandbox/orchestrator.d.ts +21 -0
- package/dist/src/sandbox/orchestrator.js +895 -0
- package/dist/src/sandbox/utils.d.ts +43 -0
- package/dist/src/sandbox/utils.js +94 -0
- package/dist/src/service.d.ts +10 -5
- package/dist/src/service.js +4145 -2
- package/dist/src/sidecar-arg.d.ts +7 -0
- package/dist/src/sidecar-arg.js +6 -0
- package/dist/src/sidecar-entrypoint.d.ts +9 -0
- package/dist/src/sidecar-entrypoint.js +401 -0
- package/dist/src/snapshot-sidecar.d.ts +2 -0
- package/dist/src/snapshot-sidecar.js +566 -0
- package/dist/src/snapshot.d.ts +64 -0
- package/dist/src/snapshot.js +515 -0
- package/dist/src/task-run-snapshot.d.ts +26 -0
- package/dist/src/task-run-snapshot.js +713 -0
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +4 -0
- package/native/darwin-arm64/runtime-native.dylib +0 -0
- package/native/darwin-x64/rig-git +0 -0
- package/native/darwin-x64/runtime-native.dylib +0 -0
- package/native/linux-arm64/rig-git +0 -0
- package/native/linux-arm64/runtime-native.so +0 -0
- package/native/linux-x64/rig-git +0 -0
- package/native/linux-x64/runtime-native.so +0 -0
- package/native/win32-x64/rig-git.exe +0 -0
- package/native/win32-x64/runtime-native.dll +0 -0
- package/package.json +45 -5
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/isolation-plugin/src/sandbox/backend-bwrap.ts
|
|
5
|
+
import { mkdirSync } from "fs";
|
|
6
|
+
import { resolve as resolve2 } from "path";
|
|
7
|
+
|
|
8
|
+
// packages/isolation-plugin/src/sandbox/utils.ts
|
|
9
|
+
import { existsSync, readdirSync, realpathSync } from "fs";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import { resolveMonorepoRoot } from "@rig/core/layout";
|
|
12
|
+
function toRealPath(path) {
|
|
13
|
+
if (!existsSync(path)) {
|
|
14
|
+
return resolve(path);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return realpathSync.native(path);
|
|
18
|
+
} catch {
|
|
19
|
+
return resolve(path);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function resolveHostGitMetadataPaths(projectRoot, workspaceDir) {
|
|
23
|
+
const candidates = new Set;
|
|
24
|
+
const addPath = (candidate) => {
|
|
25
|
+
if (existsSync(candidate)) {
|
|
26
|
+
candidates.add(toRealPath(candidate));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
addPath(resolve(projectRoot, ".git"));
|
|
30
|
+
addPath(resolve(workspaceDir, "..", "..", ".git"));
|
|
31
|
+
for (const repoRoot of resolveHostRepoRootPaths(projectRoot)) {
|
|
32
|
+
addPath(resolve(repoRoot, ".git"));
|
|
33
|
+
}
|
|
34
|
+
const workspaceGit = resolve(workspaceDir, ".git");
|
|
35
|
+
if (existsSync(workspaceGit)) {
|
|
36
|
+
addPath(workspaceGit);
|
|
37
|
+
}
|
|
38
|
+
return [...candidates];
|
|
39
|
+
}
|
|
40
|
+
function resolveHostRepoRootPaths(projectRoot) {
|
|
41
|
+
const candidates = new Set;
|
|
42
|
+
const addPath = (candidate) => {
|
|
43
|
+
if (existsSync(candidate)) {
|
|
44
|
+
candidates.add(toRealPath(candidate));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
49
|
+
if (toRealPath(monorepoRoot) !== toRealPath(projectRoot)) {
|
|
50
|
+
addPath(monorepoRoot);
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
const reposDir = resolve(projectRoot, "repos");
|
|
54
|
+
if (existsSync(reposDir)) {
|
|
55
|
+
for (const entry of readdirSync(reposDir, { withFileTypes: true })) {
|
|
56
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
57
|
+
addPath(resolve(reposDir, entry.name));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...candidates];
|
|
62
|
+
}
|
|
63
|
+
function resolveNetworkWithPolicy(sandboxConfig, envOverride) {
|
|
64
|
+
if (envOverride) {
|
|
65
|
+
const envValue = parseBooleanEnv(envOverride, sandboxConfig.network);
|
|
66
|
+
if (envValue !== sandboxConfig.network) {
|
|
67
|
+
console.warn(`[sandbox] RIG_RUNTIME_SANDBOX_NETWORK=${envOverride} overrides policy sandbox.network=${sandboxConfig.network}`);
|
|
68
|
+
}
|
|
69
|
+
return envValue;
|
|
70
|
+
}
|
|
71
|
+
return sandboxConfig.network;
|
|
72
|
+
}
|
|
73
|
+
function parseBooleanEnv(raw, fallback) {
|
|
74
|
+
if (!raw) {
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
const normalized = raw.trim().toLowerCase();
|
|
78
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
function uniq(values) {
|
|
87
|
+
return [...new Set(values)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// packages/isolation-plugin/src/sandbox/backend-bwrap.ts
|
|
91
|
+
class BwrapBackend {
|
|
92
|
+
kind = "linux-bwrap";
|
|
93
|
+
binaryPath;
|
|
94
|
+
config;
|
|
95
|
+
ctx;
|
|
96
|
+
resolvedPaths;
|
|
97
|
+
which;
|
|
98
|
+
constructor(binaryPath, config, ctx, resolvedPaths, which) {
|
|
99
|
+
this.binaryPath = binaryPath;
|
|
100
|
+
this.config = config;
|
|
101
|
+
this.ctx = ctx;
|
|
102
|
+
this.resolvedPaths = resolvedPaths;
|
|
103
|
+
this.which = which ?? ((bin) => Bun.which(bin));
|
|
104
|
+
}
|
|
105
|
+
wrap(options) {
|
|
106
|
+
const command = this.buildCommand(options);
|
|
107
|
+
return {
|
|
108
|
+
command,
|
|
109
|
+
enabled: true,
|
|
110
|
+
backend: "linux-bwrap"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
buildCommand(options) {
|
|
114
|
+
const { runtime, projectRoot, command } = options;
|
|
115
|
+
const { ctx, resolvedPaths, config } = this;
|
|
116
|
+
const workspaceReal = ctx.realPath(runtime.workspaceDir);
|
|
117
|
+
const runtimeRootReal = ctx.realPath(runtime.rootDir);
|
|
118
|
+
const homeReal = ctx.realPath(runtime.homeDir);
|
|
119
|
+
const tmpReal = ctx.realPath(runtime.tmpDir);
|
|
120
|
+
const cacheReal = ctx.realPath(runtime.cacheDir);
|
|
121
|
+
const hostGitDirs = resolveHostGitMetadataPaths(projectRoot, runtime.workspaceDir);
|
|
122
|
+
const hostRepoRoots = resolveHostRepoRootPaths(projectRoot).map((repoRoot) => ctx.realPath(repoRoot));
|
|
123
|
+
const bunDir = ctx.realPath(resolvedPaths.bunDir);
|
|
124
|
+
const claudeDir = resolvedPaths.claudeDir ? ctx.realPath(resolvedPaths.claudeDir) : null;
|
|
125
|
+
const allowNetwork = resolveNetworkWithPolicy(config, process.env.RIG_RUNTIME_SANDBOX_NETWORK);
|
|
126
|
+
const args = [
|
|
127
|
+
this.binaryPath,
|
|
128
|
+
"--die-with-parent",
|
|
129
|
+
"--new-session",
|
|
130
|
+
"--unshare-pid",
|
|
131
|
+
"--tmpfs",
|
|
132
|
+
"/",
|
|
133
|
+
"--ro-bind",
|
|
134
|
+
"/usr",
|
|
135
|
+
"/usr",
|
|
136
|
+
"--ro-bind",
|
|
137
|
+
"/bin",
|
|
138
|
+
"/bin",
|
|
139
|
+
"--ro-bind",
|
|
140
|
+
"/sbin",
|
|
141
|
+
"/sbin"
|
|
142
|
+
];
|
|
143
|
+
for (const libPath of ["/lib", "/lib64"]) {
|
|
144
|
+
if (ctx.pathExists(libPath)) {
|
|
145
|
+
args.push("--ro-bind", libPath, libPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const etcEntry of [
|
|
149
|
+
"/etc/ld.so.cache",
|
|
150
|
+
"/etc/ld.so.conf",
|
|
151
|
+
"/etc/ld.so.conf.d",
|
|
152
|
+
"/etc/resolv.conf",
|
|
153
|
+
"/etc/hosts",
|
|
154
|
+
"/etc/nsswitch.conf",
|
|
155
|
+
"/etc/gai.conf",
|
|
156
|
+
"/etc/host.conf",
|
|
157
|
+
"/etc/ssl",
|
|
158
|
+
"/etc/ca-certificates",
|
|
159
|
+
"/etc/pki",
|
|
160
|
+
"/etc/passwd",
|
|
161
|
+
"/etc/group",
|
|
162
|
+
"/etc/localtime",
|
|
163
|
+
"/etc/timezone",
|
|
164
|
+
"/etc/locale.conf",
|
|
165
|
+
"/etc/default/locale"
|
|
166
|
+
]) {
|
|
167
|
+
if (ctx.pathExists(etcEntry)) {
|
|
168
|
+
args.push("--ro-bind", etcEntry, etcEntry);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (ctx.pathExists("/run/systemd/resolve")) {
|
|
172
|
+
args.push("--ro-bind", "/run/systemd/resolve", "/run/systemd/resolve");
|
|
173
|
+
}
|
|
174
|
+
if (ctx.pathExists("/run/nscd")) {
|
|
175
|
+
args.push("--ro-bind", "/run/nscd", "/run/nscd");
|
|
176
|
+
}
|
|
177
|
+
args.push("--ro-bind", bunDir, bunDir);
|
|
178
|
+
if (claudeDir) {
|
|
179
|
+
args.push("--ro-bind", claudeDir, claudeDir);
|
|
180
|
+
}
|
|
181
|
+
if (resolvedPaths.nodeDir && ctx.pathExists(resolvedPaths.nodeDir)) {
|
|
182
|
+
args.push("--ro-bind", resolvedPaths.nodeDir, resolvedPaths.nodeDir);
|
|
183
|
+
}
|
|
184
|
+
for (const depPath of resolvedPaths.depRoots) {
|
|
185
|
+
if (ctx.pathExists(depPath)) {
|
|
186
|
+
args.push("--ro-bind", depPath, depPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const projectRootReal = ctx.realPath(projectRoot);
|
|
190
|
+
if (projectRootReal !== workspaceReal && !projectRootReal.startsWith(workspaceReal + "/") && ctx.pathExists(projectRootReal)) {
|
|
191
|
+
args.push("--ro-bind", projectRootReal, projectRootReal);
|
|
192
|
+
}
|
|
193
|
+
for (const repoRoot of hostRepoRoots) {
|
|
194
|
+
if (!ctx.pathExists(repoRoot) || repoRoot === workspaceReal || repoRoot.startsWith(workspaceReal + "/") || repoRoot === projectRootReal || repoRoot.startsWith(projectRootReal + "/")) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
args.push("--ro-bind", repoRoot, repoRoot);
|
|
198
|
+
}
|
|
199
|
+
args.push("--bind", workspaceReal, workspaceReal);
|
|
200
|
+
args.push("--bind", runtimeRootReal, runtimeRootReal);
|
|
201
|
+
for (const rwPath of uniq([homeReal, tmpReal, cacheReal])) {
|
|
202
|
+
if (rwPath !== runtimeRootReal && !rwPath.startsWith(runtimeRootReal + "/")) {
|
|
203
|
+
args.push("--bind", rwPath, rwPath);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const gitPath of hostGitDirs) {
|
|
207
|
+
if (!ctx.pathExists(gitPath))
|
|
208
|
+
continue;
|
|
209
|
+
if (gitPath === runtimeRootReal || gitPath.startsWith(runtimeRootReal + "/"))
|
|
210
|
+
continue;
|
|
211
|
+
if (gitPath === workspaceReal || gitPath.startsWith(workspaceReal + "/"))
|
|
212
|
+
continue;
|
|
213
|
+
args.push("--bind", gitPath, gitPath);
|
|
214
|
+
}
|
|
215
|
+
const realHome = process.env.HOME?.trim();
|
|
216
|
+
if (realHome) {
|
|
217
|
+
for (const binSubdir of [".local/bin", ".local/lib", ".cargo/bin"]) {
|
|
218
|
+
const binPath = ctx.realPath(resolve2(realHome, binSubdir));
|
|
219
|
+
if (ctx.pathExists(binPath)) {
|
|
220
|
+
args.push("--ro-bind", binPath, binPath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const agentSshDir = resolve2(homeReal, ".ssh");
|
|
224
|
+
if (ctx.pathExists(agentSshDir)) {
|
|
225
|
+
args.push("--ro-bind", agentSshDir, agentSshDir);
|
|
226
|
+
} else {
|
|
227
|
+
const hostSshDir = resolve2(realHome, ".ssh");
|
|
228
|
+
if (ctx.pathExists(hostSshDir)) {
|
|
229
|
+
mkdirSync(agentSshDir, { recursive: true });
|
|
230
|
+
args.push("--ro-bind", hostSshDir, agentSshDir);
|
|
231
|
+
args.push("--ro-bind", hostSshDir, hostSshDir);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
args.push("--proc", "/proc");
|
|
236
|
+
args.push("--dev", "/dev");
|
|
237
|
+
args.push("--tmpfs", "/tmp");
|
|
238
|
+
args.push("--unsetenv", "CLAUDECODE");
|
|
239
|
+
args.push("--setenv", "HOME", homeReal);
|
|
240
|
+
args.push("--setenv", "TMPDIR", "/tmp");
|
|
241
|
+
args.push("--chdir", workspaceReal);
|
|
242
|
+
if (!allowNetwork) {
|
|
243
|
+
args.push("--unshare-net");
|
|
244
|
+
}
|
|
245
|
+
args.push("--", ...this.resolveCommandBinary(command));
|
|
246
|
+
return args;
|
|
247
|
+
}
|
|
248
|
+
resolveCommandBinary(command) {
|
|
249
|
+
if (command.length === 0)
|
|
250
|
+
return command;
|
|
251
|
+
const [bin, ...rest] = command;
|
|
252
|
+
if (!bin)
|
|
253
|
+
return command;
|
|
254
|
+
const resolved = this.which(bin);
|
|
255
|
+
if (!resolved)
|
|
256
|
+
return command;
|
|
257
|
+
try {
|
|
258
|
+
const { realpathSync: realpathSync2 } = __require("fs");
|
|
259
|
+
const resolvedBinary = realpathSync2(resolved);
|
|
260
|
+
return [resolvedBinary, ...rest];
|
|
261
|
+
} catch {
|
|
262
|
+
return [resolved, ...rest];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
BwrapBackend
|
|
268
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoSandboxBackend — trivial passthrough that disables sandboxing.
|
|
3
|
+
* Implements SandboxWrapper with no-op wrapping.
|
|
4
|
+
*/
|
|
5
|
+
import type { SandboxWrapper, SandboxWrapOptions, RuntimeSandboxPlan, SandboxBackendKind } from "./backend";
|
|
6
|
+
export declare class NoSandboxBackend implements SandboxWrapper {
|
|
7
|
+
readonly kind: SandboxBackendKind;
|
|
8
|
+
private readonly reason;
|
|
9
|
+
constructor(reason?: string);
|
|
10
|
+
wrap(options: SandboxWrapOptions): RuntimeSandboxPlan;
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/isolation-plugin/src/sandbox/backend-none.ts
|
|
3
|
+
class NoSandboxBackend {
|
|
4
|
+
kind = "none";
|
|
5
|
+
reason;
|
|
6
|
+
constructor(reason) {
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
}
|
|
9
|
+
wrap(options) {
|
|
10
|
+
return {
|
|
11
|
+
command: options.command,
|
|
12
|
+
enabled: false,
|
|
13
|
+
backend: "none",
|
|
14
|
+
...this.reason !== undefined ? { reason: this.reason } : {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
NoSandboxBackend
|
|
20
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SandboxConfig } from "@rig/contracts";
|
|
2
|
+
import type { FilesystemContext, ResolvedPaths, RuntimeSandboxPlan, SandboxBackendKind, SandboxWrapOptions, SandboxWrapper } from "./backend";
|
|
3
|
+
export declare class SeatbeltBackend implements SandboxWrapper {
|
|
4
|
+
readonly kind: SandboxBackendKind;
|
|
5
|
+
private readonly binaryPath;
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly ctx;
|
|
8
|
+
private readonly resolvedPaths;
|
|
9
|
+
constructor(binaryPath: string, config: SandboxConfig, ctx: FilesystemContext, resolvedPaths: ResolvedPaths);
|
|
10
|
+
wrap(options: SandboxWrapOptions): RuntimeSandboxPlan;
|
|
11
|
+
private writeSeatbeltProfile;
|
|
12
|
+
private renderProfile;
|
|
13
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/isolation-plugin/src/sandbox/backend-seatbelt.ts
|
|
3
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
4
|
+
import { resolve as resolve2 } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/isolation-plugin/src/sandbox/utils.ts
|
|
7
|
+
import { existsSync, readdirSync, realpathSync } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { resolveMonorepoRoot } from "@rig/core/layout";
|
|
10
|
+
function toRealPath(path) {
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
return resolve(path);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return realpathSync.native(path);
|
|
16
|
+
} catch {
|
|
17
|
+
return resolve(path);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function resolveHostGitMetadataPaths(projectRoot, workspaceDir) {
|
|
21
|
+
const candidates = new Set;
|
|
22
|
+
const addPath = (candidate) => {
|
|
23
|
+
if (existsSync(candidate)) {
|
|
24
|
+
candidates.add(toRealPath(candidate));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
addPath(resolve(projectRoot, ".git"));
|
|
28
|
+
addPath(resolve(workspaceDir, "..", "..", ".git"));
|
|
29
|
+
for (const repoRoot of resolveHostRepoRootPaths(projectRoot)) {
|
|
30
|
+
addPath(resolve(repoRoot, ".git"));
|
|
31
|
+
}
|
|
32
|
+
const workspaceGit = resolve(workspaceDir, ".git");
|
|
33
|
+
if (existsSync(workspaceGit)) {
|
|
34
|
+
addPath(workspaceGit);
|
|
35
|
+
}
|
|
36
|
+
return [...candidates];
|
|
37
|
+
}
|
|
38
|
+
function resolveHostRepoRootPaths(projectRoot) {
|
|
39
|
+
const candidates = new Set;
|
|
40
|
+
const addPath = (candidate) => {
|
|
41
|
+
if (existsSync(candidate)) {
|
|
42
|
+
candidates.add(toRealPath(candidate));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
47
|
+
if (toRealPath(monorepoRoot) !== toRealPath(projectRoot)) {
|
|
48
|
+
addPath(monorepoRoot);
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
const reposDir = resolve(projectRoot, "repos");
|
|
52
|
+
if (existsSync(reposDir)) {
|
|
53
|
+
for (const entry of readdirSync(reposDir, { withFileTypes: true })) {
|
|
54
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
55
|
+
addPath(resolve(reposDir, entry.name));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...candidates];
|
|
60
|
+
}
|
|
61
|
+
function resolveNetworkWithPolicy(sandboxConfig, envOverride) {
|
|
62
|
+
if (envOverride) {
|
|
63
|
+
const envValue = parseBooleanEnv(envOverride, sandboxConfig.network);
|
|
64
|
+
if (envValue !== sandboxConfig.network) {
|
|
65
|
+
console.warn(`[sandbox] RIG_RUNTIME_SANDBOX_NETWORK=${envOverride} overrides policy sandbox.network=${sandboxConfig.network}`);
|
|
66
|
+
}
|
|
67
|
+
return envValue;
|
|
68
|
+
}
|
|
69
|
+
return sandboxConfig.network;
|
|
70
|
+
}
|
|
71
|
+
function parseBooleanEnv(raw, fallback) {
|
|
72
|
+
if (!raw) {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
const normalized = raw.trim().toLowerCase();
|
|
76
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
function uniq(values) {
|
|
85
|
+
return [...new Set(values)];
|
|
86
|
+
}
|
|
87
|
+
function seatbeltString(value) {
|
|
88
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// packages/isolation-plugin/src/sandbox/backend-seatbelt.ts
|
|
92
|
+
class SeatbeltBackend {
|
|
93
|
+
kind = "macos-seatbelt";
|
|
94
|
+
binaryPath;
|
|
95
|
+
config;
|
|
96
|
+
ctx;
|
|
97
|
+
resolvedPaths;
|
|
98
|
+
constructor(binaryPath, config, ctx, resolvedPaths) {
|
|
99
|
+
this.binaryPath = binaryPath;
|
|
100
|
+
this.config = config;
|
|
101
|
+
this.ctx = ctx;
|
|
102
|
+
this.resolvedPaths = resolvedPaths;
|
|
103
|
+
}
|
|
104
|
+
wrap(options) {
|
|
105
|
+
const profilePath = this.writeSeatbeltProfile(options);
|
|
106
|
+
return {
|
|
107
|
+
command: [this.binaryPath, "-f", profilePath, ...options.command],
|
|
108
|
+
enabled: true,
|
|
109
|
+
backend: "macos-seatbelt",
|
|
110
|
+
profilePath
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
writeSeatbeltProfile(options) {
|
|
114
|
+
const sandboxDir = resolve2(options.runtime.rootDir, "sandbox");
|
|
115
|
+
mkdirSync(sandboxDir, { recursive: true });
|
|
116
|
+
const profilePath = resolve2(sandboxDir, "seatbelt.sb");
|
|
117
|
+
const profile = this.renderProfile(options);
|
|
118
|
+
writeFileSync(profilePath, `${profile}
|
|
119
|
+
`, "utf-8");
|
|
120
|
+
return profilePath;
|
|
121
|
+
}
|
|
122
|
+
renderProfile(options) {
|
|
123
|
+
const { runtime, projectRoot } = options;
|
|
124
|
+
const { ctx, resolvedPaths, config } = this;
|
|
125
|
+
const workspaceReal = ctx.realPath(runtime.workspaceDir);
|
|
126
|
+
const runtimeRootReal = ctx.realPath(runtime.rootDir);
|
|
127
|
+
const homeReal = ctx.realPath(runtime.homeDir);
|
|
128
|
+
const tmpReal = ctx.realPath(runtime.tmpDir);
|
|
129
|
+
const cacheReal = ctx.realPath(runtime.cacheDir);
|
|
130
|
+
const hostGitDirs = resolveHostGitMetadataPaths(projectRoot, runtime.workspaceDir);
|
|
131
|
+
const hostRepoRoots = resolveHostRepoRootPaths(projectRoot).map((repoRoot) => ctx.realPath(repoRoot));
|
|
132
|
+
const bunDir = ctx.realPath(resolvedPaths.bunDir);
|
|
133
|
+
const claudeDir = resolvedPaths.claudeDir ? ctx.realPath(resolvedPaths.claudeDir) : null;
|
|
134
|
+
const allowNetwork = resolveNetworkWithPolicy(config, process.env.RIG_RUNTIME_SANDBOX_NETWORK);
|
|
135
|
+
const lines = [
|
|
136
|
+
"(version 1)",
|
|
137
|
+
"(deny default)",
|
|
138
|
+
'(import "system.sb")',
|
|
139
|
+
"(allow process*)",
|
|
140
|
+
"(allow process-info*)",
|
|
141
|
+
"(allow signal)",
|
|
142
|
+
"(allow sysctl-read)",
|
|
143
|
+
"(allow file-read-metadata)"
|
|
144
|
+
];
|
|
145
|
+
if (allowNetwork) {
|
|
146
|
+
lines.push("(allow network*)");
|
|
147
|
+
}
|
|
148
|
+
for (const sysPath of [
|
|
149
|
+
"/usr/lib",
|
|
150
|
+
"/usr/bin",
|
|
151
|
+
"/usr/sbin",
|
|
152
|
+
"/usr/share",
|
|
153
|
+
"/bin",
|
|
154
|
+
"/sbin",
|
|
155
|
+
"/System",
|
|
156
|
+
"/Library",
|
|
157
|
+
"/Library/Frameworks",
|
|
158
|
+
"/Library/Developer",
|
|
159
|
+
"/Library/Apple",
|
|
160
|
+
"/Applications",
|
|
161
|
+
"/private/var/db",
|
|
162
|
+
"/opt/homebrew"
|
|
163
|
+
]) {
|
|
164
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(sysPath)}))`);
|
|
165
|
+
}
|
|
166
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(bunDir)}))`);
|
|
167
|
+
if (claudeDir) {
|
|
168
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(claudeDir)}))`);
|
|
169
|
+
}
|
|
170
|
+
if (resolvedPaths.nodeDir) {
|
|
171
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(resolvedPaths.nodeDir)}))`);
|
|
172
|
+
}
|
|
173
|
+
for (const depPath of resolvedPaths.depRoots) {
|
|
174
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(depPath)}))`);
|
|
175
|
+
}
|
|
176
|
+
for (const rwPath of uniq([
|
|
177
|
+
workspaceReal,
|
|
178
|
+
runtimeRootReal,
|
|
179
|
+
homeReal,
|
|
180
|
+
tmpReal,
|
|
181
|
+
cacheReal
|
|
182
|
+
])) {
|
|
183
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(rwPath)}))`);
|
|
184
|
+
lines.push(`(allow file-write* (subpath ${seatbeltString(rwPath)}))`);
|
|
185
|
+
}
|
|
186
|
+
for (const gitPath of hostGitDirs) {
|
|
187
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(gitPath)}))`);
|
|
188
|
+
lines.push(`(allow file-write* (subpath ${seatbeltString(gitPath)}))`);
|
|
189
|
+
}
|
|
190
|
+
const projectRootReal = ctx.realPath(projectRoot);
|
|
191
|
+
if (projectRootReal !== workspaceReal && !projectRootReal.startsWith(workspaceReal + "/")) {
|
|
192
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(projectRootReal)}))`);
|
|
193
|
+
}
|
|
194
|
+
for (const repoRoot of hostRepoRoots) {
|
|
195
|
+
if (!ctx.pathExists(repoRoot) || repoRoot === workspaceReal || repoRoot.startsWith(workspaceReal + "/") || repoRoot === projectRootReal || repoRoot.startsWith(projectRootReal + "/")) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(repoRoot)}))`);
|
|
199
|
+
}
|
|
200
|
+
const realHome = process.env.HOME?.trim();
|
|
201
|
+
if (realHome) {
|
|
202
|
+
for (const binSubdir of [".local/bin", ".cargo/bin"]) {
|
|
203
|
+
const binPath = resolve2(realHome, binSubdir);
|
|
204
|
+
if (ctx.pathExists(binPath)) {
|
|
205
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(ctx.realPath(binPath))}))`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const tempPath of [
|
|
210
|
+
"/dev",
|
|
211
|
+
"/tmp",
|
|
212
|
+
"/private/tmp",
|
|
213
|
+
"/var/folders",
|
|
214
|
+
"/private/var/folders"
|
|
215
|
+
]) {
|
|
216
|
+
lines.push(`(allow file-read* (subpath ${seatbeltString(tempPath)}))`);
|
|
217
|
+
lines.push(`(allow file-write* (subpath ${seatbeltString(tempPath)}))`);
|
|
218
|
+
}
|
|
219
|
+
return lines.join(`
|
|
220
|
+
`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
SeatbeltBackend
|
|
225
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { SandboxConfig } from "@rig/contracts";
|
|
2
|
+
export type RuntimeMode = "worktree";
|
|
3
|
+
/** Describes the filesystem layout of an agent runtime environment. */
|
|
4
|
+
export type RuntimeDescriptor = {
|
|
5
|
+
id: string;
|
|
6
|
+
mode: RuntimeMode;
|
|
7
|
+
rootDir: string;
|
|
8
|
+
workspaceDir: string;
|
|
9
|
+
homeDir: string;
|
|
10
|
+
tmpDir: string;
|
|
11
|
+
cacheDir: string;
|
|
12
|
+
stateDir: string;
|
|
13
|
+
sessionDir: string;
|
|
14
|
+
claudeHomeDir: string;
|
|
15
|
+
binDir: string;
|
|
16
|
+
};
|
|
17
|
+
/** All possible sandbox backend identifiers. Open union -- do not exhaustively match. */
|
|
18
|
+
export type SandboxBackendKind = "none" | "macos-seatbelt" | "linux-bwrap" | "docker";
|
|
19
|
+
/** Options passed to SandboxWrapper.wrap(). Config is injected at construction, not here. */
|
|
20
|
+
export type SandboxWrapOptions = {
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
runtime: RuntimeDescriptor;
|
|
23
|
+
command: string[];
|
|
24
|
+
};
|
|
25
|
+
/** The result of wrapping a command for sandboxed execution. */
|
|
26
|
+
export type RuntimeSandboxPlan = {
|
|
27
|
+
command: string[];
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
backend: SandboxBackendKind;
|
|
30
|
+
profilePath?: string;
|
|
31
|
+
reason?: string;
|
|
32
|
+
/** Backend-specific details for observability (mount count, network policy, image tag, etc.). */
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
/** Injectable filesystem context for testability. */
|
|
36
|
+
export type FilesystemContext = {
|
|
37
|
+
pathExists: (path: string) => boolean;
|
|
38
|
+
realPath: (path: string) => string;
|
|
39
|
+
};
|
|
40
|
+
/** Pre-resolved tool paths. Factory resolves these, backends consume them. */
|
|
41
|
+
export type ResolvedPaths = {
|
|
42
|
+
bunDir: string;
|
|
43
|
+
claudeDir: string | null;
|
|
44
|
+
nodeDir: string | null;
|
|
45
|
+
depRoots: string[];
|
|
46
|
+
};
|
|
47
|
+
/** Result of executing a command inside a SandboxExecutor. */
|
|
48
|
+
export type ExecResult = {
|
|
49
|
+
exitCode: number;
|
|
50
|
+
stdout: string;
|
|
51
|
+
stderr: string;
|
|
52
|
+
};
|
|
53
|
+
/** Options for SandboxExecutor.exec(). */
|
|
54
|
+
export type ExecOpts = {
|
|
55
|
+
cwd?: string;
|
|
56
|
+
env?: Record<string, string>;
|
|
57
|
+
timeout?: number;
|
|
58
|
+
};
|
|
59
|
+
/** Configuration passed to SandboxExecutor.init(). */
|
|
60
|
+
export type SandboxInitConfig = {
|
|
61
|
+
runtime: RuntimeDescriptor;
|
|
62
|
+
sandboxConfig: SandboxConfig;
|
|
63
|
+
paths: ResolvedPaths;
|
|
64
|
+
fs: FilesystemContext;
|
|
65
|
+
};
|
|
66
|
+
/** Backend that wraps a command for sandboxed execution. Caller owns spawning. */
|
|
67
|
+
export interface SandboxWrapper {
|
|
68
|
+
readonly kind: SandboxBackendKind;
|
|
69
|
+
wrap(options: SandboxWrapOptions): RuntimeSandboxPlan;
|
|
70
|
+
}
|
|
71
|
+
/** Backend that owns execution entirely. Used by future Wasm/container backends. */
|
|
72
|
+
export interface SandboxExecutor {
|
|
73
|
+
readonly kind: SandboxBackendKind;
|
|
74
|
+
init(config?: SandboxInitConfig): Promise<void>;
|
|
75
|
+
exec(command: string[], opts?: ExecOpts): Promise<ExecResult>;
|
|
76
|
+
readFile(path: string): Promise<string>;
|
|
77
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
78
|
+
dispose(): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Union type. Callers discriminate via `'wrap' in backend` or `'exec' in backend`.
|
|
82
|
+
* SandboxBackendKind is an open union -- consumers must not rely on exhaustive matching.
|
|
83
|
+
*/
|
|
84
|
+
export type SandboxErrorCode = "backend-unavailable" | "backend-probe-failed" | "profile-write-failed" | "policy-violation";
|
|
85
|
+
export declare class SandboxError extends Error {
|
|
86
|
+
readonly code: SandboxErrorCode;
|
|
87
|
+
constructor(code: SandboxErrorCode, message: string);
|
|
88
|
+
}
|
|
89
|
+
/** Result of the backend resolution factory. */
|
|
90
|
+
export type BackendResolution = {
|
|
91
|
+
backend: SandboxWrapper;
|
|
92
|
+
selectedKind: SandboxBackendKind;
|
|
93
|
+
/** What tools/binaries were checked during probe-based detection. */
|
|
94
|
+
probed: string[];
|
|
95
|
+
/** Human-readable reason for the selection. */
|
|
96
|
+
reason: string;
|
|
97
|
+
};
|
|
98
|
+
type RuntimeSandboxMode = "off" | "auto" | "enforce";
|
|
99
|
+
/**
|
|
100
|
+
* Map policy sandbox config to internal sandbox mode, with optional env override.
|
|
101
|
+
* Policy "observe" maps to "auto" in sandbox terms.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveRuntimeSandboxModeWithPolicy(sandboxConfig: SandboxConfig, envOverride: string | undefined): RuntimeSandboxMode;
|
|
104
|
+
/**
|
|
105
|
+
* Resolve which sandbox backend to use for the current platform and policy.
|
|
106
|
+
*
|
|
107
|
+
* 1. Loads SandboxConfig from policy.json via guard.ts
|
|
108
|
+
* 2. Resolves mode (off/auto/enforce) with optional env override
|
|
109
|
+
* 3. Checks for explicit backend selection (RIG_SANDBOX_BACKEND env)
|
|
110
|
+
* 4. Probes platform: darwin -> SeatbeltBackend, linux -> BwrapBackend
|
|
111
|
+
* 5. Falls back to NoSandboxBackend or throws if mode=enforce
|
|
112
|
+
*/
|
|
113
|
+
export declare function resolveBackend(projectRoot: string, options?: {
|
|
114
|
+
envOverride?: string;
|
|
115
|
+
backendOverride?: string;
|
|
116
|
+
}): Promise<BackendResolution>;
|
|
117
|
+
export {};
|