@ravefox/space 0.2.0 → 0.3.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/LICENSE +21 -203
- package/README.md +89 -26
- package/bin/space.js +106 -0
- package/lib/agents.js +119 -0
- package/lib/credentials.js +95 -0
- package/lib/daemon.js +294 -0
- package/lib/harness.js +166 -0
- package/lib/http.js +101 -0
- package/lib/login.js +68 -0
- package/lib/mcp.js +34 -0
- package/lib/runtime.js +241 -0
- package/lib/store.js +96 -0
- package/lib/tools.js +93 -0
- package/package.json +28 -40
- package/NOTICE +0 -44
- package/assets/webhook-sdk/python/bot.py +0 -63
- package/assets/webhook-sdk/python/commonly.py +0 -232
- package/node_modules/@commonlyai/seats/README.md +0 -25
- package/node_modules/@commonlyai/seats/package.json +0 -22
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
- package/node_modules/@commonlyai/seats/src/contract.js +0 -48
- package/node_modules/@commonlyai/seats/src/failures.js +0 -44
- package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
- package/node_modules/@commonlyai/seats/src/index.js +0 -22
- package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
- package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
- package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
- package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
- package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
- package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
- package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
- package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
- package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
- package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
- package/skills/commonly/SKILL.md +0 -233
- package/src/commands/agent.js +0 -6334
- package/src/commands/daemon.js +0 -933
- package/src/commands/dev.js +0 -153
- package/src/commands/doctor.js +0 -141
- package/src/commands/inbox.js +0 -330
- package/src/commands/login.js +0 -194
- package/src/commands/mcp.js +0 -56
- package/src/commands/pod.js +0 -120
- package/src/commands/task.js +0 -279
- package/src/commands/verify.js +0 -48
- package/src/index.js +0 -99
- package/src/lib/adapter-login.js +0 -76
- package/src/lib/adapters/broker-bridge-entry.js +0 -76
- package/src/lib/adapters/claude.js +0 -2207
- package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
- package/src/lib/adapters/codex.js +0 -1898
- package/src/lib/adapters/dsh.js +0 -513
- package/src/lib/adapters/goose.js +0 -510
- package/src/lib/adapters/hermes.js +0 -18
- package/src/lib/adapters/index.js +0 -68
- package/src/lib/adapters/opencode.js +0 -1681
- package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
- package/src/lib/adapters/pi-mcp-client.mjs +0 -617
- package/src/lib/adapters/pi-shell-env.mjs +0 -41
- package/src/lib/adapters/pi.js +0 -1025
- package/src/lib/adapters/qwen.js +0 -523
- package/src/lib/adapters/seat-model.js +0 -8
- package/src/lib/adapters/stub.js +0 -1
- package/src/lib/adapters/tested-version.js +0 -106
- package/src/lib/api.js +0 -205
- package/src/lib/claim-outcome.js +0 -128
- package/src/lib/claimed-work-gate.js +0 -73
- package/src/lib/cli-hint.js +0 -33
- package/src/lib/commit-attribution.js +0 -391
- package/src/lib/config.js +0 -155
- package/src/lib/content-refusal.js +0 -94
- package/src/lib/credential-file.js +0 -208
- package/src/lib/daemon-agent-states.js +0 -95
- package/src/lib/daemon-hint.js +0 -18
- package/src/lib/daemon-logs.js +0 -43
- package/src/lib/daemon-service-windows.js +0 -263
- package/src/lib/daemon-service.js +0 -486
- package/src/lib/daemon-state.js +0 -84
- package/src/lib/daemon-store.js +0 -119
- package/src/lib/daemon-supervisor.js +0 -629
- package/src/lib/daemon-watch.js +0 -115
- package/src/lib/declared-mcp-guard.js +0 -284
- package/src/lib/default-environment.js +0 -207
- package/src/lib/device-login.js +0 -126
- package/src/lib/digest-frame.js +0 -151
- package/src/lib/doctor.js +0 -291
- package/src/lib/egress-proxy.js +0 -160
- package/src/lib/enforcement.js +0 -946
- package/src/lib/environment.js +0 -681
- package/src/lib/first-output-watchdog.js +0 -191
- package/src/lib/git-readonly-server.js +0 -138
- package/src/lib/git-readonly.js +0 -463
- package/src/lib/git-remote-commonly.mjs +0 -100
- package/src/lib/grant-broker-guard.js +0 -345
- package/src/lib/hooks-config.js +0 -238
- package/src/lib/host-gh-broker.js +0 -152
- package/src/lib/host-gh-shim.mjs +0 -98
- package/src/lib/inbox.js +0 -483
- package/src/lib/instance-proxy.js +0 -76
- package/src/lib/machine-host.js +0 -29
- package/src/lib/mcp-credential-delivery.js +0 -121
- package/src/lib/mcp-first-run.js +0 -50
- package/src/lib/mcp-home.js +0 -644
- package/src/lib/mcp-ready.js +0 -68
- package/src/lib/mcp-server-version.js +0 -123
- package/src/lib/mcp-token.js +0 -36
- package/src/lib/mcp-warm-child.mjs +0 -412
- package/src/lib/memory-bridge.js +0 -191
- package/src/lib/memory-import.js +0 -156
- package/src/lib/missing-harness.js +0 -31
- package/src/lib/model-unavailable.js +0 -154
- package/src/lib/native-runtime-inventory.js +0 -40
- package/src/lib/outsider-grant-mcp.js +0 -82
- package/src/lib/own-key-launch.js +0 -42
- package/src/lib/pod-catch-up.js +0 -55
- package/src/lib/pod-focus.js +0 -224
- package/src/lib/pod-tail.js +0 -68
- package/src/lib/poll-liveness.js +0 -103
- package/src/lib/poll-retry.js +0 -81
- package/src/lib/poller.js +0 -200
- package/src/lib/provider-proxy.js +0 -42
- package/src/lib/public-seat-mcp-flag.js +0 -36
- package/src/lib/public-tool-error.js +0 -60
- package/src/lib/public-withheld-tools.js +0 -48
- package/src/lib/reply-receipt.js +0 -55
- package/src/lib/resolve-on-path.js +0 -22
- package/src/lib/runtime-inventory.js +0 -56
- package/src/lib/runtime-observation.js +0 -37
- package/src/lib/sandbox/bwrap.js +0 -275
- package/src/lib/sandbox/codex-perimeter.js +0 -16
- package/src/lib/sandbox/host-check.js +0 -268
- package/src/lib/sandbox/mode.js +0 -49
- package/src/lib/sandbox/opencode-loopback.mjs +0 -68
- package/src/lib/sandbox/seatbelt.js +0 -684
- package/src/lib/sandbox/turn-dir.js +0 -56
- package/src/lib/sandbox/windows-unsandboxed.js +0 -183
- package/src/lib/sandbox/workspace-git-config.js +0 -243
- package/src/lib/sandbox/workspace-pin.js +0 -158
- package/src/lib/seat-runtime-controls.js +0 -61
- package/src/lib/seat-stop.js +0 -97
- package/src/lib/server-skills.js +0 -32
- package/src/lib/session-recycle.js +0 -219
- package/src/lib/session-store.js +0 -215
- package/src/lib/skill-prompt.js +0 -74
- package/src/lib/skills-import.js +0 -113
- package/src/lib/spawn-credential.js +0 -402
- package/src/lib/spawn-retry.js +0 -311
- package/src/lib/stable-install.js +0 -176
- package/src/lib/supervised-turn.js +0 -352
- package/src/lib/task.js +0 -235
- package/src/lib/turn-failure-notice.js +0 -177
- package/src/lib/unconfined-seat-guard.js +0 -94
- package/src/lib/update-check.js +0 -192
- package/src/lib/upstream-refusal.js +0 -1
- package/src/lib/webhook-server.js +0 -109
- package/src/lib/windows-command.js +0 -393
- package/src/lib/workspace-git.js +0 -87
- package/src/lib/workspace-trust.js +0 -162
package/src/lib/sandbox/bwrap.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bubblewrap sandbox adapter — ADR-008 Phase 1.
|
|
3
|
-
*
|
|
4
|
-
* Wraps an inner adapter argv (e.g. claude -p ...) in a bwrap invocation that
|
|
5
|
-
* pins the spawn into a workspace, isolates the network namespace, and
|
|
6
|
-
* read-only-binds a small allowlist of host paths the adapter needs to start
|
|
7
|
-
* (TLS certs, libc, the binary itself).
|
|
8
|
-
*
|
|
9
|
-
* Linux-only by design: bwrap is a setuid Linux helper. macOS callers get a
|
|
10
|
-
* loud, specific error at attach time so the failure shape never reaches run.
|
|
11
|
-
*
|
|
12
|
-
* Detection idiom mirrors `claude.js#detect`: spawnSync with --version, treat
|
|
13
|
-
* non-zero or spawn error as "not available."
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { spawnSync } from 'child_process';
|
|
17
|
-
import { existsSync, readFileSync } from 'fs';
|
|
18
|
-
import { isAbsolute, join } from 'path';
|
|
19
|
-
import { homedir } from 'os';
|
|
20
|
-
|
|
21
|
-
// Tilde-expansion lives here (and not in environment.js's expandHome) because
|
|
22
|
-
// bwrap reads OS-level paths verbatim — `~` is a shell construct, not a
|
|
23
|
-
// kernel one, so any `~/foo` entry in `read-outside` / `write-outside` would
|
|
24
|
-
// be passed literally to `--ro-bind-try` and silently skipped (the `-try`
|
|
25
|
-
// variant tolerates missing paths). Surfaced live during the 2026-04-17
|
|
26
|
-
// demo validation: `~/.local/bin` was declared but `claude` still wasn't
|
|
27
|
-
// reachable inside the sandbox.
|
|
28
|
-
const expandHome = (p) => {
|
|
29
|
-
if (!p || typeof p !== 'string') return p;
|
|
30
|
-
if (p === '~') return homedir();
|
|
31
|
-
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
32
|
-
return p;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Never advise `sandbox.mode: none` here: a public seat refuses it, and the
|
|
36
|
-
// mechanisms that do exist are named instead.
|
|
37
|
-
const MACOS_MESSAGE = 'bwrap is Linux-only. On macOS a public seat runs under Seatbelt '
|
|
38
|
-
+ '(sandbox.mode workspace or read-only).';
|
|
39
|
-
// D245: native Windows has no sandbox for agents. WSL runs the Linux one,
|
|
40
|
-
// Claude Code included since F96, and host-check.js offers it; this message
|
|
41
|
-
// names the native path, which is the user's own opt-in
|
|
42
|
-
// (sandbox/windows-unsandboxed.js).
|
|
43
|
-
const WINDOWS_MESSAGE = 'bwrap is Linux-only, and native Windows has no sandbox for agents yet. '
|
|
44
|
-
+ 'To run this agent without one, with your own access to files and commands, add --unsandboxed.';
|
|
45
|
-
const nonLinuxMessage = (platform) => (platform === 'win32' ? WINDOWS_MESSAGE : MACOS_MESSAGE);
|
|
46
|
-
|
|
47
|
-
const DEFAULT_RO_BINDS = [
|
|
48
|
-
'/etc/ssl/certs',
|
|
49
|
-
'/etc/resolv.conf',
|
|
50
|
-
'/usr/lib',
|
|
51
|
-
'/usr/lib64',
|
|
52
|
-
'/usr/bin',
|
|
53
|
-
'/bin',
|
|
54
|
-
'/lib',
|
|
55
|
-
'/lib64',
|
|
56
|
-
];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* What a bwrap sandbox binds for reading, as roots (F453): the default binds
|
|
60
|
-
* plus the seat's declared read-outside and write-outside, tilde-expanded the
|
|
61
|
-
* way wrapArgvWithBwrap expands them. A caller keeping a secret out of the
|
|
62
|
-
* sandbox (a public seat's provider key file) checks against this rather than
|
|
63
|
-
* a copy of the bind list. Per-turn paths (workspace, state, adapter inputs)
|
|
64
|
-
* are the caller's.
|
|
65
|
-
*/
|
|
66
|
-
export const bwrapReadRoots = (env) => {
|
|
67
|
-
const declared = (key) => (Array.isArray(env?.sandbox?.filesystem?.[key])
|
|
68
|
-
? env.sandbox.filesystem[key].map(expandHome) : []);
|
|
69
|
-
return {
|
|
70
|
-
subpaths: [...new Set([...DEFAULT_RO_BINDS, ...declared('read-outside'), ...declared('write-outside')])],
|
|
71
|
-
literals: [],
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// `bwrap --version` only proves the binary is there. On Ubuntu 24.04 and later
|
|
76
|
-
// AppArmor restricts unprivileged user namespaces, so bwrap is installed and
|
|
77
|
-
// every sandbox it starts fails (`setting up uid map: Permission denied`,
|
|
78
|
-
// ubuntu run 38982219348). Starting one empty namespace is the real check.
|
|
79
|
-
const NAMESPACE_PROBE = ['--unshare-all', '--ro-bind', '/', '/', 'true'];
|
|
80
|
-
|
|
81
|
-
const APPARMOR_USERNS_SYSCTL = '/proc/sys/kernel/apparmor_restrict_unprivileged_userns';
|
|
82
|
-
|
|
83
|
-
// Inside a container the sysctl reports the host, and what blocks bwrap is the
|
|
84
|
-
// container's own policy, so the host's AppArmor is not the person's way out.
|
|
85
|
-
// Docker leaves /.dockerenv, Podman /run/.containerenv, and systemd writes
|
|
86
|
-
// /run/systemd/container inside a container (nspawn, LXC), never on a host.
|
|
87
|
-
// The filesystem is injectable for the tests.
|
|
88
|
-
const CONTAINER_MARKERS = ['/.dockerenv', '/run/.containerenv', '/run/systemd/container'];
|
|
89
|
-
export const readApparmorRestriction = ({ read = readFileSync, exists = existsSync } = {}) => {
|
|
90
|
-
if (CONTAINER_MARKERS.some((marker) => exists(marker))) return false;
|
|
91
|
-
try {
|
|
92
|
-
return read(APPARMOR_USERNS_SYSCTL, 'utf8').trim() === '1';
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// os-release values may be quoted, so `ID="ubuntu"` reads the same as `ID=ubuntu`.
|
|
99
|
-
// /etc/os-release wins; /usr/lib/os-release is the file it normally links to,
|
|
100
|
-
// read only when the first is missing.
|
|
101
|
-
const readOsRelease = (read) => {
|
|
102
|
-
try {
|
|
103
|
-
return read('/etc/os-release', 'utf8');
|
|
104
|
-
} catch {
|
|
105
|
-
return read('/usr/lib/os-release', 'utf8');
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
export const readUbuntuRelease = ({ read = readFileSync } = {}) => {
|
|
109
|
-
try {
|
|
110
|
-
const release = readOsRelease(read);
|
|
111
|
-
if (!/^ID=(["']?)ubuntu\1$/m.test(release)) return null;
|
|
112
|
-
// The last assignment wins, as it does when a shell sources the file.
|
|
113
|
-
const versions = [...release.matchAll(/^VERSION_ID=(["']?)([\d.]+)\1$/gm)];
|
|
114
|
-
return versions.at(-1)?.[2] ?? null;
|
|
115
|
-
} catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export const detectBwrap = ({
|
|
121
|
-
spawn = spawnSync,
|
|
122
|
-
platform = process.platform,
|
|
123
|
-
readRestriction = readApparmorRestriction,
|
|
124
|
-
readRelease = readUbuntuRelease,
|
|
125
|
-
} = {}) => {
|
|
126
|
-
if (platform !== 'linux') {
|
|
127
|
-
return { available: false, error: nonLinuxMessage(platform) };
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
const res = spawn('bwrap', ['--version'], { encoding: 'utf8' });
|
|
131
|
-
if (res.error || res.status !== 0) {
|
|
132
|
-
return {
|
|
133
|
-
available: false,
|
|
134
|
-
error: 'bwrap not found on PATH. Install bubblewrap: `sudo apt install bubblewrap` (Debian/Ubuntu, WSL), '
|
|
135
|
-
+ '`sudo dnf install bubblewrap` (Fedora) or `sudo pacman -S bubblewrap` (Arch).',
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
const probe = spawn('bwrap', NAMESPACE_PROBE, { encoding: 'utf8', timeout: 10_000 });
|
|
139
|
-
if (probe.error || probe.status !== 0) {
|
|
140
|
-
const reason = (probe.stderr || probe.error?.message || `exit ${probe.status}`).trim().split('\n').pop();
|
|
141
|
-
return {
|
|
142
|
-
available: false,
|
|
143
|
-
// Only when bwrap ran and exited refusing: a probe that timed out or
|
|
144
|
-
// couldn't spawn says nothing about this host.
|
|
145
|
-
namespaceRefused: !probe.error && typeof probe.status === 'number',
|
|
146
|
-
reason,
|
|
147
|
-
apparmorRestricted: readRestriction(),
|
|
148
|
-
ubuntuRelease: readRelease(),
|
|
149
|
-
error: `bubblewrap is installed but cannot start a sandbox on this machine (${reason}). `
|
|
150
|
-
+ 'On Ubuntu 24.04 and later, AppArmor restricts the unprivileged user namespaces it needs.',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return { available: true, path: 'bwrap' };
|
|
154
|
-
} catch (err) {
|
|
155
|
-
return { available: false, error: `bwrap detection failed: ${err.message}` };
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Build the bwrap-wrapped argv. Caller spawns argv[0] with argv.slice(1).
|
|
161
|
-
*
|
|
162
|
-
* Network policy honesty (ADR-008 §invariant #4 informs this):
|
|
163
|
-
* - unrestricted → --share-net (host networking inside the namespace)
|
|
164
|
-
* - restricted → --unshare-net (no external network). OpenCode may opt
|
|
165
|
-
* into a private loopback bridge to its per-spawn Unix-socket proxy.
|
|
166
|
-
* Host-allowlist filtering is not supplied by bwrap itself.
|
|
167
|
-
*
|
|
168
|
-
* The expansion order matches `bwrap`'s left-to-right argument application:
|
|
169
|
-
* isolation flags first, then binds (later binds win on collision), then the
|
|
170
|
-
* isolated HOME, then `--` and the inner argv. The caller passes the filtered
|
|
171
|
-
* child environment as the bwrap process environment; values must not be
|
|
172
|
-
* serialized into argv, where they would be visible in the process list.
|
|
173
|
-
*/
|
|
174
|
-
export const wrapArgvWithBwrap = (innerArgv, env, opts = {}) => {
|
|
175
|
-
if (process.platform !== 'linux') {
|
|
176
|
-
throw new Error(nonLinuxMessage(process.platform));
|
|
177
|
-
}
|
|
178
|
-
if (!Array.isArray(innerArgv) || innerArgv.length === 0) {
|
|
179
|
-
throw new Error('wrapArgvWithBwrap requires a non-empty innerArgv');
|
|
180
|
-
}
|
|
181
|
-
if (!opts.workspacePath || !isAbsolute(opts.workspacePath)) {
|
|
182
|
-
throw new Error('wrapArgvWithBwrap requires opts.workspacePath as an absolute path');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const policy = env?.sandbox?.network?.policy || 'unrestricted';
|
|
186
|
-
const networkFlags = policy === 'restricted'
|
|
187
|
-
? ['--unshare-net']
|
|
188
|
-
: ['--share-net'];
|
|
189
|
-
if (opts.enableLoopback && policy !== 'restricted') {
|
|
190
|
-
throw new Error('bwrap loopback bridge requires an isolated network namespace');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const flags = [
|
|
194
|
-
'--unshare-all',
|
|
195
|
-
...networkFlags,
|
|
196
|
-
...(opts.enableLoopback ? [
|
|
197
|
-
'--uid', '0', '--gid', '0',
|
|
198
|
-
'--cap-add', 'CAP_NET_ADMIN', '--cap-add', 'CAP_SETPCAP',
|
|
199
|
-
] : []),
|
|
200
|
-
'--die-with-parent',
|
|
201
|
-
'--new-session',
|
|
202
|
-
'--proc', '/proc',
|
|
203
|
-
'--dev', '/dev',
|
|
204
|
-
'--tmpfs', '/tmp',
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
const userReadOutside = Array.isArray(env?.sandbox?.filesystem?.['read-outside'])
|
|
208
|
-
? env.sandbox.filesystem['read-outside'].map(expandHome)
|
|
209
|
-
: [];
|
|
210
|
-
const roBinds = [...new Set([...DEFAULT_RO_BINDS, ...userReadOutside])];
|
|
211
|
-
for (const p of roBinds) {
|
|
212
|
-
flags.push('--ro-bind-try', p, p);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const userWriteOutside = Array.isArray(env?.sandbox?.filesystem?.['write-outside'])
|
|
216
|
-
? env.sandbox.filesystem['write-outside'].map(expandHome)
|
|
217
|
-
: [];
|
|
218
|
-
for (const p of userWriteOutside) {
|
|
219
|
-
flags.push('--bind-try', p, p);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Adapter-owned transient inputs (for example Claude's per-spawn MCP
|
|
223
|
-
// config) live outside the workspace so model-visible Read(./**) cannot
|
|
224
|
-
// reach them. Bind only the exact directories the adapter prepared; they
|
|
225
|
-
// remain read-only inside the namespace and disappear after the spawn.
|
|
226
|
-
for (const p of opts.readOnlyPaths || []) {
|
|
227
|
-
if (!p || !isAbsolute(p)) {
|
|
228
|
-
throw new Error('wrapArgvWithBwrap opts.readOnlyPaths entries must be absolute paths');
|
|
229
|
-
}
|
|
230
|
-
flags.push('--ro-bind', p, p);
|
|
231
|
-
}
|
|
232
|
-
for (const p of opts.optionalReadOnlyPaths || []) {
|
|
233
|
-
if (!p || !isAbsolute(p)) {
|
|
234
|
-
throw new Error('wrapArgvWithBwrap opts.optionalReadOnlyPaths entries must be absolute paths');
|
|
235
|
-
}
|
|
236
|
-
flags.push('--ro-bind-try', p, p);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// A HOME the adapter keeps outside the workspace (a workspace seat's state)
|
|
240
|
-
// is bound writable on its own, so the child's transcripts and login stay
|
|
241
|
-
// out of the person's project.
|
|
242
|
-
for (const p of opts.writablePaths || []) {
|
|
243
|
-
if (!p || !isAbsolute(p)) {
|
|
244
|
-
throw new Error('wrapArgvWithBwrap opts.writablePaths entries must be absolute paths');
|
|
245
|
-
}
|
|
246
|
-
flags.push('--bind', p, p);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// The folder mounted at workspacePath: workspaceSource when the caller has
|
|
250
|
-
// already resolved and checked it, so the mount cannot follow a link that
|
|
251
|
-
// replaced the folder after that check.
|
|
252
|
-
const workspaceSource = opts.workspaceSource || opts.workspacePath;
|
|
253
|
-
if (!isAbsolute(workspaceSource)) {
|
|
254
|
-
throw new Error('wrapArgvWithBwrap opts.workspaceSource must be an absolute path');
|
|
255
|
-
}
|
|
256
|
-
flags.push(opts.workspaceReadOnly ? '--ro-bind' : '--bind', workspaceSource, opts.workspacePath);
|
|
257
|
-
// Entries held read-only inside a writable workspace (its .git). Bound after
|
|
258
|
-
// the workspace so they land on top of it. In a writable workspace a missing
|
|
259
|
-
// entry cannot be protected — the child could create it, and a planted
|
|
260
|
-
// .git/hooks runs on the person's next git command — so the bind is strict
|
|
261
|
-
// there and bwrap fails rather than starts.
|
|
262
|
-
for (const entry of opts.readOnlyWorkspaceEntries || []) {
|
|
263
|
-
if (!entry || isAbsolute(entry) || entry.split(/[\\/]/).includes('..')) {
|
|
264
|
-
throw new Error('wrapArgvWithBwrap opts.readOnlyWorkspaceEntries entries must be relative names inside the workspace');
|
|
265
|
-
}
|
|
266
|
-
flags.push(opts.workspaceReadOnly ? '--ro-bind-try' : '--ro-bind',
|
|
267
|
-
join(workspaceSource, entry), join(opts.workspacePath, entry));
|
|
268
|
-
}
|
|
269
|
-
flags.push('--chdir', opts.workspacePath);
|
|
270
|
-
flags.push('--setenv', 'HOME', opts.homePath || opts.workspacePath);
|
|
271
|
-
|
|
272
|
-
return ['bwrap', ...flags, '--', ...innerArgv];
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
export const BWRAP_MACOS_MESSAGE = MACOS_MESSAGE;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* F387: a cloud-codex k8s pod declares itself with this env var, set only by
|
|
3
|
-
* k8s/helm/commonly/templates/agents/cloud-codex-deployment.yaml. bwrap can't
|
|
4
|
-
* run in a standard k8s container and the pod is the boundary, so a Codex seat
|
|
5
|
-
* there keeps the pod's execution policy. Anywhere else a private Codex seat
|
|
6
|
-
* runs under the private permission profile (adapters/codex.js); on Linux it
|
|
7
|
-
* is refused by default until that profile is walked there (host-check.js).
|
|
8
|
-
*
|
|
9
|
-
* Only the exact value "1" counts, so a missing or mistyped value lands on the
|
|
10
|
-
* profile.
|
|
11
|
-
*/
|
|
12
|
-
export const CODEX_TRUSTED_PERIMETER_ENV = 'COMMONLY_CODEX_TRUSTED_PERIMETER';
|
|
13
|
-
|
|
14
|
-
export const codexTrustedPerimeter = (env = process.env) => (
|
|
15
|
-
String(env?.[CODEX_TRUSTED_PERIMETER_ENV] ?? '').trim() === '1'
|
|
16
|
-
);
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Can THIS host confine the seat it is about to run? Asked once, before the
|
|
3
|
-
* first poll.
|
|
4
|
-
*
|
|
5
|
-
* A public seat's sandbox is resolved per host (mode.js): Seatbelt on macOS,
|
|
6
|
-
* bubblewrap elsewhere. `agent attach --env` checks that mechanism up front;
|
|
7
|
-
* `agent run` did not, so a public seat on a host without it (native Windows,
|
|
8
|
-
* or Linux without bubblewrap) would start, poll, and fail every turn while
|
|
9
|
-
* the pod saw nothing.
|
|
10
|
-
*
|
|
11
|
-
* Only the adapters that run inside these mechanisms are checked. Codex
|
|
12
|
-
* confines with its own permission profiles (mode.js), except on native
|
|
13
|
-
* Windows: there a public Codex seat is refused, and any other runs only by the
|
|
14
|
-
* person's own opt-in (CODEX_WINDOWS_MESSAGE below). pi confines an
|
|
15
|
-
* explicitly public seat with Seatbelt and refuses one it cannot confine at
|
|
16
|
-
* attach (validateEnvironment) and at spawn (adapters/pi.js); a Connect-page pi
|
|
17
|
-
* seat declares no sandbox and is not checked here.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { normalizeSandboxTrust } from '../environment.js';
|
|
21
|
-
import { PUBLIC_SANDBOX_MODES, resolvePublicSandboxMode } from './mode.js';
|
|
22
|
-
import { detectBwrap as defaultDetectBwrap } from './bwrap.js';
|
|
23
|
-
import { detectSeatbelt as defaultDetectSeatbelt } from './seatbelt.js';
|
|
24
|
-
import { codexTrustedPerimeter } from './codex-perimeter.js';
|
|
25
|
-
import { CLI } from '../cli-hint.js';
|
|
26
|
-
|
|
27
|
-
// D245: the Windows opt-in (windows-unsandboxed.js re-exports it; defined here to avoid an import cycle).
|
|
28
|
-
export const UNSANDBOXED_FLAG = '--unsandboxed';
|
|
29
|
-
|
|
30
|
-
// The Seatbelt modes come from mode.js, as grant-broker-guard's do, so a mode
|
|
31
|
-
// added there is not silently missing here.
|
|
32
|
-
const SEATBELT_MODES = PUBLIC_SANDBOX_MODES;
|
|
33
|
-
|
|
34
|
-
const ADAPTER_LABELS = { claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', qwen: 'Qwen Code' };
|
|
35
|
-
|
|
36
|
-
const HOSTED_LINE = ' - A hosted agent: choose "Run it here" on the Connect page. Nothing runs on this machine.';
|
|
37
|
-
|
|
38
|
-
const CODEX_WSL_LINE = ' - WSL 2 (Ubuntu): install Node and Codex inside WSL, run `sudo apt install bubblewrap`,'
|
|
39
|
-
+ ' then run these same commands there.';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Codex's unelevated Windows sandbox cannot enforce the split read denies a
|
|
43
|
-
* public seat's profile carries ("refusing to run unsandboxed"), and Codex has
|
|
44
|
-
* no tool policy beneath that profile, so an unconfined Codex is a full shell
|
|
45
|
-
* driven by pod content. That is why a public Codex seat on native Windows
|
|
46
|
-
* always refuses, and why even the opt-in reaches only a seat whose every pod
|
|
47
|
-
* is confirmed private (windows-unsandboxed.js), re-read before every turn.
|
|
48
|
-
* Codex's elevated Windows sandbox is the hardened path; Commonly doesn't drive
|
|
49
|
-
* it yet.
|
|
50
|
-
*/
|
|
51
|
-
const CODEX_WINDOWS_MESSAGE = [
|
|
52
|
-
"Codex can't enforce a sandbox on Windows without its elevated setup,",
|
|
53
|
-
'so by default this agent cannot listen for @mentions from native Windows. You can:',
|
|
54
|
-
` - In invite-only pods, run it without a sandbox: add ${UNSANDBOXED_FLAG}`
|
|
55
|
-
+ ` (${CLI} agent run <name> ${UNSANDBOXED_FLAG}, or ${CLI} daemon install ${UNSANDBOXED_FLAG}).`
|
|
56
|
-
+ ' Nothing confines it: it runs with your own access to files and commands, anyone in the pod,'
|
|
57
|
-
+ ' people you invite later included, can direct it, and it gets no connector grants.',
|
|
58
|
-
CODEX_WSL_LINE,
|
|
59
|
-
HOSTED_LINE,
|
|
60
|
-
].join('\n');
|
|
61
|
-
|
|
62
|
-
// The seat can't take the flag: its pods rule it out, or it declares itself
|
|
63
|
-
// public. Naming the flag would only send the person round in a circle.
|
|
64
|
-
const CODEX_WINDOWS_PUBLIC_MESSAGE = [
|
|
65
|
-
'This agent is public: its settings say so, or a pod it answers in is publicly readable or open to join,',
|
|
66
|
-
"or did not report whether it is. A public agent needs a sandbox, which Codex can't enforce on Windows"
|
|
67
|
-
+ ' without its elevated setup. You can:',
|
|
68
|
-
CODEX_WSL_LINE,
|
|
69
|
-
HOSTED_LINE,
|
|
70
|
-
].join('\n');
|
|
71
|
-
|
|
72
|
-
const CLAUDE_MCP_LINE = ' - Use Space from your own Claude Code sessions: the Connect page shows a `claude mcp add` command.';
|
|
73
|
-
|
|
74
|
-
// Claude Code and Codex each answered on prod under this profile, on a fresh
|
|
75
|
-
// Ubuntu 24.04 runner (byo-linux.yml runs 42398237284, 42398763392), so only
|
|
76
|
-
// they are offered it. Printed for the person to run; nothing here runs it. It
|
|
77
|
-
// stays the last and optional route, and the sysctl behind the restriction is
|
|
78
|
-
// never named as a way out. The lines take the one file out of the package:
|
|
79
|
-
// installing apparmor-profiles enforces 21 more profiles at once, among them
|
|
80
|
-
// ping, dnsmasq and smbd (run 42400201955), which nobody here agreed to.
|
|
81
|
-
// The subshell keeps the person in their project folder, which a first run on a
|
|
82
|
-
// private pod binds to, and stops at the first failure. byo-linux.yml runs these
|
|
83
|
-
// exact lines.
|
|
84
|
-
export const UBUNTU_BWRAP_PROFILE_COMMANDS = [
|
|
85
|
-
'sudo apt-get update',
|
|
86
|
-
'(cd "$(mktemp -d)" && apt-get download apparmor-profiles && dpkg-deb -x apparmor-profiles_*.deb .'
|
|
87
|
-
+ ' && sudo cp usr/share/apparmor/extra-profiles/bwrap-userns-restrict /etc/apparmor.d/)',
|
|
88
|
-
'sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict',
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
export const UBUNTU_BWRAP_PROFILE_UNDO = [
|
|
92
|
-
'sudo apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict',
|
|
93
|
-
'sudo rm /etc/apparmor.d/bwrap-userns-restrict',
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
const UBUNTU_BWRAP_PROFILE_LINES = [
|
|
97
|
-
" - Optional, if you'd rather run it on this machine: load Ubuntu's own AppArmor profile for bubblewrap.",
|
|
98
|
-
' It lets any program here use bubblewrap to create user namespaces; what bubblewrap starts gets no special',
|
|
99
|
-
" capabilities. It's your call, it needs sudo, and Space never runs it for you. We tested it on a fresh",
|
|
100
|
-
" Ubuntu 24.04 server image, not on every machine. These lines load that one profile and nothing else from",
|
|
101
|
-
" Ubuntu's package:",
|
|
102
|
-
...UBUNTU_BWRAP_PROFILE_COMMANDS.map((line) => ` ${line}`),
|
|
103
|
-
' Then run the same command again. To undo it:',
|
|
104
|
-
...UBUNTU_BWRAP_PROFILE_UNDO.map((line) => ` ${line}`),
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
/** bubblewrap is installed and this host refused it a sandbox. */
|
|
108
|
-
export const bwrapRefusedMessage = (adapterName, { reason, apparmorRestricted, ubuntuRelease }) => {
|
|
109
|
-
const label = ADAPTER_LABELS[adapterName] || adapterName;
|
|
110
|
-
// 24.04 only: the file moves or is absent on other releases.
|
|
111
|
-
const offersProfile = apparmorRestricted === true && ubuntuRelease === '24.04'
|
|
112
|
-
&& (adapterName === 'claude' || adapterName === 'codex');
|
|
113
|
-
return [
|
|
114
|
-
`A connected ${label} runs inside a bubblewrap sandbox, and this machine won't let bubblewrap start one (${reason}).`,
|
|
115
|
-
...(apparmorRestricted === true
|
|
116
|
-
? ['AppArmor on Ubuntu 24.04 and later restricts the user namespaces bubblewrap needs.']
|
|
117
|
-
: []),
|
|
118
|
-
'So this agent cannot listen for @mentions here yet. You can:',
|
|
119
|
-
HOSTED_LINE,
|
|
120
|
-
...(adapterName === 'claude' ? [CLAUDE_MCP_LINE] : []),
|
|
121
|
-
...(offersProfile ? UBUNTU_BWRAP_PROFILE_LINES : []),
|
|
122
|
-
].join('\n');
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const CODEX_UNSANDBOXED_RUN_LINE = ` - In invite-only pods, run it without a sandbox: ${CLI} agent run <name> ${UNSANDBOXED_FLAG}`
|
|
126
|
-
+ ' (in the foreground; the daemon takes this only on Windows).'
|
|
127
|
-
+ ' Nothing confines it: it runs with your own access to files and commands, anyone in the pod,'
|
|
128
|
-
+ ' people you invite later included, can direct it, and it gets no connector grants.';
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* F387, F444: a private Codex seat on Linux runs under Codex's own sandbox,
|
|
132
|
-
* which uses bubblewrap there. Where bubblewrap can't start one, the seat is
|
|
133
|
-
* refused before its first poll, with the fix for this host. The person can
|
|
134
|
-
* run it unconfined by their own choice, as on native Windows; it never falls
|
|
135
|
-
* back to that without being asked.
|
|
136
|
-
*/
|
|
137
|
-
export const codexPrivateLinuxMessage = (bwrap = {}, { optedIn = false } = {}) => {
|
|
138
|
-
const refused = bwrap.namespaceRefused === true;
|
|
139
|
-
const offersProfile = refused && bwrap.apparmorRestricted === true && bwrap.ubuntuRelease === '24.04';
|
|
140
|
-
return [
|
|
141
|
-
'A private Codex agent runs inside Codex\'s own sandbox, which keeps your other credentials out of its reach'
|
|
142
|
-
+ ` and uses bubblewrap on Linux. ${refused
|
|
143
|
-
? `This machine won't let bubblewrap start one (${bwrap.reason}).`
|
|
144
|
-
: (bwrap.error || 'bubblewrap was not found.')}`,
|
|
145
|
-
...(refused && bwrap.apparmorRestricted === true
|
|
146
|
-
? ['AppArmor on Ubuntu 24.04 and later restricts the user namespaces bubblewrap needs.']
|
|
147
|
-
: []),
|
|
148
|
-
...(optedIn
|
|
149
|
-
? ['A pod this agent answers in is publicly readable or open to join, or did not report whether it is,'
|
|
150
|
-
+ ' so it cannot run without a sandbox either.']
|
|
151
|
-
: []),
|
|
152
|
-
'So this agent cannot listen for @mentions here yet. You can:',
|
|
153
|
-
...(refused ? [] : [' - Install bubblewrap (`sudo apt install bubblewrap`) and run the same command again.']),
|
|
154
|
-
...(optedIn ? [] : [CODEX_UNSANDBOXED_RUN_LINE]),
|
|
155
|
-
HOSTED_LINE,
|
|
156
|
-
...(offersProfile ? UBUNTU_BWRAP_PROFILE_LINES : []),
|
|
157
|
-
].join('\n');
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// The person opted in, and a pod rules it out, so the turn has no sandbox to
|
|
161
|
-
// fall back to on any platform. Naming the flag again would only send them
|
|
162
|
-
// round in a circle.
|
|
163
|
-
export const CODEX_OPTED_IN_PUBLIC_POD_MESSAGE = [
|
|
164
|
-
'A pod this agent answers in is publicly readable or open to join, or did not report whether it is,',
|
|
165
|
-
'so it cannot run without a sandbox. You can:',
|
|
166
|
-
HOSTED_LINE,
|
|
167
|
-
].join('\n');
|
|
168
|
-
|
|
169
|
-
/** What a person on native Windows can do instead. */
|
|
170
|
-
export const windowsSandboxMessage = (adapterName) => {
|
|
171
|
-
if (adapterName === 'codex') return CODEX_WINDOWS_MESSAGE;
|
|
172
|
-
const label = ADAPTER_LABELS[adapterName] || adapterName;
|
|
173
|
-
// WSL runs the Linux sandbox, which carries every adapter, Claude Code
|
|
174
|
-
// included (F96). Claude Code also has the MCP command, which needs none.
|
|
175
|
-
return [
|
|
176
|
-
`Space runs a connected ${label} inside a sandbox, and has no sandbox for Windows yet,`,
|
|
177
|
-
'so by default this agent cannot listen for @mentions from native Windows. You can:',
|
|
178
|
-
` - WSL 2 (Ubuntu): install Node and ${label} inside WSL, run \`sudo apt install bubblewrap\`,`
|
|
179
|
-
+ ' then run these same commands there.',
|
|
180
|
-
// D245: the user's own choice, never a default (sandbox/windows-unsandboxed.js).
|
|
181
|
-
...(adapterName === 'claude'
|
|
182
|
-
? [` - Run it without an OS sandbox: add ${UNSANDBOXED_FLAG}`
|
|
183
|
-
+ ` (${CLI} agent run <name> ${UNSANDBOXED_FLAG}, or ${CLI} daemon install ${UNSANDBOXED_FLAG}).`
|
|
184
|
-
+ " It keeps Space's restricted tool settings and gets no shell, but nothing at the OS level confines it, and it gets no connector grants."]
|
|
185
|
-
: []),
|
|
186
|
-
...(adapterName === 'claude'
|
|
187
|
-
? [' - Use Space from your own Claude Code sessions: the Connect page shows a PowerShell `claude mcp add` command.']
|
|
188
|
-
: []),
|
|
189
|
-
HOSTED_LINE,
|
|
190
|
-
].join('\n');
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/** Why this host cannot confine the seat, or null when it can (or the seat declares no sandbox). */
|
|
194
|
-
export const hostSandboxRefusal = ({
|
|
195
|
-
environment,
|
|
196
|
-
adapterName,
|
|
197
|
-
platform = process.platform,
|
|
198
|
-
// D245: the user's own opt-in for this run (windows-unsandboxed.js
|
|
199
|
-
// hostUnconfinedForRun). Never derived from the environment.
|
|
200
|
-
hostUnconfined = false,
|
|
201
|
-
// The person asked for the opt-in, whether or not the seat qualified. Only
|
|
202
|
-
// chooses Codex's words: it never lets a seat through.
|
|
203
|
-
optedIn = false,
|
|
204
|
-
detectBwrap = defaultDetectBwrap,
|
|
205
|
-
detectSeatbelt = defaultDetectSeatbelt,
|
|
206
|
-
// F387: a cloud-codex pod declares itself here (codex-perimeter.js).
|
|
207
|
-
env = process.env,
|
|
208
|
-
} = {}) => {
|
|
209
|
-
if (adapterName === 'codex') {
|
|
210
|
-
// Keyed on trust, not mode: Codex resolves no Seatbelt or bwrap mode of its
|
|
211
|
-
// own, and a legacy `internal` normalizes to public. Workspace trust is
|
|
212
|
-
// left to Codex's own refusal, at attach and at spawn on every platform,
|
|
213
|
-
// so WSL advice would only send that seat to a second refusal.
|
|
214
|
-
const trust = normalizeSandboxTrust(environment?.sandbox)?.trust;
|
|
215
|
-
if (platform === 'linux' && trust === 'public') {
|
|
216
|
-
// Codex's own sandbox uses bubblewrap here. Refused a namespace, every
|
|
217
|
-
// turn dies at session start and the pod hears nothing (run 42398478814).
|
|
218
|
-
// A missing bwrap stays Codex's own to report.
|
|
219
|
-
const bwrap = detectBwrap();
|
|
220
|
-
return bwrap.namespaceRefused === true ? bwrapRefusedMessage(adapterName, bwrap) : null;
|
|
221
|
-
}
|
|
222
|
-
// F387, F444: a private seat outside a cloud-codex pod runs under the
|
|
223
|
-
// private permission profile, which Codex enforces with bubblewrap on Linux
|
|
224
|
-
// (walked on byo-linux.yml, walk=private-codex). Where bubblewrap can't
|
|
225
|
-
// start, every turn would fail at session start, so the seat is refused
|
|
226
|
-
// before its first poll, unless the person ran it unconfined by their own
|
|
227
|
-
// --unsandboxed (D245, as on Windows: only with every pod confirmed
|
|
228
|
-
// private). Never unconfined without being asked.
|
|
229
|
-
if (platform === 'linux' && trust !== 'workspace' && !codexTrustedPerimeter(env)) {
|
|
230
|
-
if (hostUnconfined === true) return null;
|
|
231
|
-
const bwrap = detectBwrap();
|
|
232
|
-
if (bwrap.available) return null;
|
|
233
|
-
return codexPrivateLinuxMessage(bwrap, { optedIn: optedIn === true });
|
|
234
|
-
}
|
|
235
|
-
if (platform !== 'win32') return null;
|
|
236
|
-
if (trust === 'workspace') return null;
|
|
237
|
-
if (trust === 'public') return optedIn === true ? CODEX_WINDOWS_PUBLIC_MESSAGE : CODEX_WINDOWS_MESSAGE;
|
|
238
|
-
// No public trust: `agent run` removed the block for this run after
|
|
239
|
-
// confirming every pod private, or the seat never had one. Either way it
|
|
240
|
-
// would run unconfined, which only the person's own opt-in allows.
|
|
241
|
-
if (hostUnconfined === true) return null;
|
|
242
|
-
return optedIn === true ? CODEX_WINDOWS_PUBLIC_MESSAGE : CODEX_WINDOWS_MESSAGE;
|
|
243
|
-
}
|
|
244
|
-
if (!Object.prototype.hasOwnProperty.call(ADAPTER_LABELS, adapterName)) return null;
|
|
245
|
-
const sandbox = normalizeSandboxTrust(environment?.sandbox);
|
|
246
|
-
if (!sandbox || typeof sandbox !== 'object') return null;
|
|
247
|
-
// Every host-confined trust (public, and workspace since TASK-294) derives its
|
|
248
|
-
// mode here; any other trust gets back only the mode it declared.
|
|
249
|
-
const mode = sandbox.mode ?? resolvePublicSandboxMode(sandbox, platform);
|
|
250
|
-
if (mode !== 'bwrap' && !SEATBELT_MODES.has(mode)) return null;
|
|
251
|
-
|
|
252
|
-
if (platform === 'win32') {
|
|
253
|
-
return hostUnconfined === true && adapterName === 'claude' ? null : windowsSandboxMessage(adapterName);
|
|
254
|
-
}
|
|
255
|
-
if (mode === 'bwrap') {
|
|
256
|
-
const bwrap = detectBwrap();
|
|
257
|
-
if (bwrap.available) return null;
|
|
258
|
-
return bwrap.namespaceRefused === true
|
|
259
|
-
? bwrapRefusedMessage(adapterName, bwrap)
|
|
260
|
-
: `This seat runs in a bubblewrap sandbox, which is not available here. ${bwrap.error}`;
|
|
261
|
-
}
|
|
262
|
-
const seatbelt = detectSeatbelt();
|
|
263
|
-
return seatbelt.available
|
|
264
|
-
? null
|
|
265
|
-
: `This seat runs in a macOS Seatbelt sandbox (sandbox.mode=${mode}), which is not available here. ${seatbelt.error}`;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
export default { hostSandboxRefusal, windowsSandboxMessage };
|
package/src/lib/sandbox/mode.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which host mechanism enforces a public seat.
|
|
3
|
-
*
|
|
4
|
-
* A stored environment is platform-independent and the host is not. The daemon
|
|
5
|
-
* derives `sandbox: { trust: 'public' }` with NO mode (lib/default-environment.js)
|
|
6
|
-
* because `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux,
|
|
7
|
-
* while `mode: 'bwrap'` is meaningless on macOS — writing either into the row
|
|
8
|
-
* moves a host fact into the database and breaks the day the seat is re-homed.
|
|
9
|
-
*
|
|
10
|
-
* So the mode is resolved here, at the point that knows the host:
|
|
11
|
-
* darwin → 'workspace' (the claude adapter maps it to Seatbelt)
|
|
12
|
-
* else → 'bwrap' (bubblewrap; attach checks it is installed)
|
|
13
|
-
*
|
|
14
|
-
* Resolution is PUBLIC-only on purpose, and PUBLIC IS THE EFFECTIVE TRUST: a
|
|
15
|
-
* legacy `internal` declaration reads as `public` (environment.js),
|
|
16
|
-
* so the mapping is applied before this comparison. Comparing the raw value
|
|
17
|
-
* made `{ trust: 'internal' }` resolve to no mode at all — the mapping was
|
|
18
|
-
* applied in `confinementReason` and not here, which is half a rule, and the
|
|
19
|
-
* half that failed was the one that WITHHOLDS: the grant broker was taken from
|
|
20
|
-
* a seat this daemon would have confined. A non-public declaration keeps
|
|
21
|
-
* whatever mode it wrote, and an adapter that cannot enforce it is expected to
|
|
22
|
-
* refuse rather than quietly spawn unconfined. An explicit mode on a public
|
|
23
|
-
* declaration still wins — including a wrong one, which the caller then refuses
|
|
24
|
-
* (an unresolvable public mode must not fall through to a bare,
|
|
25
|
-
* unconfined spawn).
|
|
26
|
-
*
|
|
27
|
-
* NOT used by the codex adapter. Codex's `mode` is read-vs-write access, not a
|
|
28
|
-
* host mechanism — its permission profiles run on both platforms — so its
|
|
29
|
-
* derived default is 'workspace' everywhere.
|
|
30
|
-
*
|
|
31
|
-
* A `workspace` trust (TASK-294) resolves the same way: it is confined by the
|
|
32
|
-
* same host mechanism and differs only in what the policy inside allows. The
|
|
33
|
-
* grant-broker guard still reads the trust itself, not this mode, so resolving
|
|
34
|
-
* it here does not make a workspace seat eligible for a room grant.
|
|
35
|
-
*/
|
|
36
|
-
import { effectiveSandboxTrust } from '../environment.js';
|
|
37
|
-
|
|
38
|
-
export const PUBLIC_SANDBOX_MODES = new Set(['workspace', 'read-only']);
|
|
39
|
-
|
|
40
|
-
const HOST_CONFINED_TRUSTS = new Set(['public', 'workspace']);
|
|
41
|
-
|
|
42
|
-
export const resolvePublicSandboxMode = (sandbox, platform = process.platform) => {
|
|
43
|
-
const declared = sandbox?.mode;
|
|
44
|
-
if (!HOST_CONFINED_TRUSTS.has(effectiveSandboxTrust(sandbox?.trust))) return declared;
|
|
45
|
-
if (declared !== undefined && declared !== null) return declared;
|
|
46
|
-
return platform === 'darwin' ? 'workspace' : 'bwrap';
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export default { PUBLIC_SANDBOX_MODES, resolvePublicSandboxMode };
|