@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
|
@@ -1,684 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* macOS Seatbelt wrapper for public local-CLI agents.
|
|
3
|
-
*
|
|
4
|
-
* `sandbox-exec` is deprecated as a public API, but remains the host-native
|
|
5
|
-
* kernel boundary available to local CLI wrappers on current macOS. This
|
|
6
|
-
* profile starts from deny-by-default, admits only the system runtime the CLI
|
|
7
|
-
* needs, and then grants explicit access to:
|
|
8
|
-
* - the selected CLI executable,
|
|
9
|
-
* - one workspace (read/write or read-only),
|
|
10
|
-
* - one isolated state directory, and
|
|
11
|
-
* - the adapter-owned transient MCP config directory (read-only).
|
|
12
|
-
*
|
|
13
|
-
* The static platform baseline is adapted from OpenAI Codex's Apache-2.0
|
|
14
|
-
* Seatbelt policy:
|
|
15
|
-
* https://github.com/openai/codex/tree/main/codex-rs/sandboxing/src
|
|
16
|
-
*
|
|
17
|
-
* Public seats still need outbound network access for their provider API and
|
|
18
|
-
* declared MCP transports. Host confidentiality therefore comes from the
|
|
19
|
-
* filesystem boundary, not a claim that all network syscalls are disabled.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { spawnSync } from 'child_process';
|
|
23
|
-
import { lstatSync, realpathSync } from 'fs';
|
|
24
|
-
import { homedir } from 'os';
|
|
25
|
-
import {
|
|
26
|
-
basename, dirname, isAbsolute, join, resolve,
|
|
27
|
-
} from 'path';
|
|
28
|
-
|
|
29
|
-
const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
|
|
30
|
-
const DARWIN_MESSAGE = 'Seatbelt sandboxing is available only on macOS';
|
|
31
|
-
|
|
32
|
-
// Keep this baseline static and auditable. Dynamic paths are appended by
|
|
33
|
-
// buildSeatbeltProfile after strict absolute-path validation + SBPL escaping.
|
|
34
|
-
const PLATFORM_BASELINE = String.raw`
|
|
35
|
-
(version 1)
|
|
36
|
-
(deny default)
|
|
37
|
-
|
|
38
|
-
; Children inherit the same sandbox. Process inspection is limited to this
|
|
39
|
-
; sandbox, so a model cannot inspect unrelated operator processes.
|
|
40
|
-
(allow process-exec)
|
|
41
|
-
(allow process-fork)
|
|
42
|
-
(allow signal (target same-sandbox))
|
|
43
|
-
(allow process-info* (target same-sandbox))
|
|
44
|
-
|
|
45
|
-
(allow file-write-data
|
|
46
|
-
(require-all
|
|
47
|
-
(path "/dev/null")
|
|
48
|
-
(vnode-type CHARACTER-DEVICE)))
|
|
49
|
-
|
|
50
|
-
; Loader + standard system resources. Node opens this one macOS OpenSSL config
|
|
51
|
-
; file at startup; the macOS adapter job's Node was denied it without this rule.
|
|
52
|
-
(allow file-read* file-test-existence
|
|
53
|
-
(subpath "/Library/Apple")
|
|
54
|
-
(subpath "/Library/Filesystems/NetFSPlugins")
|
|
55
|
-
(subpath "/Library/Preferences/Logging")
|
|
56
|
-
(subpath "/private/var/db/DarwinDirectory/local/recordStore.data")
|
|
57
|
-
(subpath "/private/var/db/timezone")
|
|
58
|
-
(subpath "/usr/lib")
|
|
59
|
-
(subpath "/usr/share")
|
|
60
|
-
(literal "/System/Library/OpenSSL/openssl.cnf")
|
|
61
|
-
(subpath "/Library/Preferences")
|
|
62
|
-
(subpath "/var/db")
|
|
63
|
-
(subpath "/private/var/db"))
|
|
64
|
-
(allow file-read* file-test-existence
|
|
65
|
-
(subpath "/System/Volumes/Preboot/Cryptexes/OS"))
|
|
66
|
-
|
|
67
|
-
(allow file-map-executable
|
|
68
|
-
(subpath "/Library/Apple/System/Library/Frameworks")
|
|
69
|
-
(subpath "/Library/Apple/System/Library/PrivateFrameworks")
|
|
70
|
-
(subpath "/Library/Apple/usr/lib")
|
|
71
|
-
(subpath "/System/Library/Extensions")
|
|
72
|
-
(subpath "/System/Library/Frameworks")
|
|
73
|
-
(subpath "/System/Library/PrivateFrameworks")
|
|
74
|
-
(subpath "/System/Library/SubFrameworks")
|
|
75
|
-
(subpath "/System/iOSSupport/System/Library/Frameworks")
|
|
76
|
-
(subpath "/System/iOSSupport/System/Library/PrivateFrameworks")
|
|
77
|
-
(subpath "/System/iOSSupport/System/Library/SubFrameworks")
|
|
78
|
-
(subpath "/usr/lib"))
|
|
79
|
-
|
|
80
|
-
(allow file-read* file-test-existence
|
|
81
|
-
(subpath "/Library/Apple/System/Library/Frameworks")
|
|
82
|
-
(subpath "/Library/Apple/System/Library/PrivateFrameworks")
|
|
83
|
-
(subpath "/Library/Apple/usr/lib")
|
|
84
|
-
(subpath "/System/Library/Frameworks")
|
|
85
|
-
(subpath "/System/Library/PrivateFrameworks")
|
|
86
|
-
(subpath "/System/Library/SubFrameworks")
|
|
87
|
-
(subpath "/System/iOSSupport/System/Library/Frameworks")
|
|
88
|
-
(subpath "/System/iOSSupport/System/Library/PrivateFrameworks")
|
|
89
|
-
(subpath "/System/iOSSupport/System/Library/SubFrameworks")
|
|
90
|
-
(subpath "/usr/lib"))
|
|
91
|
-
|
|
92
|
-
(allow system-mac-syscall (mac-policy-name "vnguard"))
|
|
93
|
-
(allow system-mac-syscall
|
|
94
|
-
(require-all
|
|
95
|
-
(mac-policy-name "Sandbox")
|
|
96
|
-
(mac-syscall-number 67)))
|
|
97
|
-
|
|
98
|
-
(allow file-read-metadata file-test-existence
|
|
99
|
-
(literal "/etc")
|
|
100
|
-
(literal "/tmp")
|
|
101
|
-
(literal "/var")
|
|
102
|
-
(literal "/private/etc/localtime")
|
|
103
|
-
(path-ancestors "/System/Volumes/Data/private"))
|
|
104
|
-
(allow file-read* file-test-existence (literal "/"))
|
|
105
|
-
|
|
106
|
-
(allow file-read* file-test-existence
|
|
107
|
-
(literal "/dev/autofs_nowait")
|
|
108
|
-
(literal "/dev/random")
|
|
109
|
-
(literal "/dev/urandom")
|
|
110
|
-
(literal "/private/etc/master.passwd")
|
|
111
|
-
(literal "/private/etc/passwd")
|
|
112
|
-
(literal "/private/etc/protocols")
|
|
113
|
-
(literal "/private/etc/services"))
|
|
114
|
-
(allow file-read* file-test-existence file-write-data file-ioctl
|
|
115
|
-
(literal "/dev/dtracehelper"))
|
|
116
|
-
(allow file-read* file-test-existence file-write-data file-ioctl
|
|
117
|
-
(literal "/dev/null")
|
|
118
|
-
(literal "/dev/zero"))
|
|
119
|
-
(allow file-read-data file-test-existence file-write-data (subpath "/dev/fd"))
|
|
120
|
-
|
|
121
|
-
; System configuration is readable; user-home configuration is not.
|
|
122
|
-
(allow file-read* (subpath "/etc"))
|
|
123
|
-
(allow file-read* (subpath "/private/etc"))
|
|
124
|
-
(allow file-read* file-test-existence
|
|
125
|
-
(literal "/System/Library/CoreServices")
|
|
126
|
-
(literal "/System/Library/CoreServices/.SystemVersionPlatform.plist")
|
|
127
|
-
(literal "/System/Library/CoreServices/SystemVersion.plist"))
|
|
128
|
-
(allow file-read-metadata file-test-existence
|
|
129
|
-
(literal "/Library")
|
|
130
|
-
(literal "/Library/Application Support")
|
|
131
|
-
(literal "/System/Library")
|
|
132
|
-
(literal "/System/Cryptexes")
|
|
133
|
-
(literal "/System/Cryptexes/App")
|
|
134
|
-
(literal "/System/Cryptexes/OS"))
|
|
135
|
-
(allow file-read-metadata (subpath "/var"))
|
|
136
|
-
(allow file-read-metadata (subpath "/private/var"))
|
|
137
|
-
|
|
138
|
-
; The wrapper and declared MCP launchers may execute children. Claude
|
|
139
|
-
; bootstraps stdio MCP servers through /bin/sh, whose interpreter is /bin/bash
|
|
140
|
-
; on current macOS. An executable-level shell deny would therefore disable
|
|
141
|
-
; declared MCP capability too. Each adapter denies its built-in shell tool in
|
|
142
|
-
; its own config; any declared child inherits this Seatbelt profile.
|
|
143
|
-
|
|
144
|
-
(allow file-read-data file-read-metadata (subpath "/bin"))
|
|
145
|
-
(allow file-read-data file-read-metadata (subpath "/sbin"))
|
|
146
|
-
(allow file-read-data file-read-metadata (subpath "/usr/bin"))
|
|
147
|
-
(allow file-read-data file-read-metadata (subpath "/usr/sbin"))
|
|
148
|
-
(allow file-read-data file-read-metadata (subpath "/usr/libexec"))
|
|
149
|
-
(allow file-read* file-test-existence (subpath "/opt/homebrew/bin"))
|
|
150
|
-
(allow file-read* file-test-existence file-map-executable
|
|
151
|
-
(subpath "/opt/homebrew/Cellar"))
|
|
152
|
-
(allow file-read* file-test-existence
|
|
153
|
-
(subpath "/opt/homebrew/etc/openssl@3"))
|
|
154
|
-
; openssl@3/cert.pem resolves to this Homebrew CA bundle. Seatbelt checks the
|
|
155
|
-
; target path, so the openssl@3 directory grant alone does not admit TLS roots.
|
|
156
|
-
(allow file-read* file-test-existence
|
|
157
|
-
(literal "/opt/homebrew/etc/ca-certificates/cert.pem"))
|
|
158
|
-
(allow file-read* (subpath "/opt/homebrew/lib"))
|
|
159
|
-
(allow file-read* file-test-existence file-map-executable
|
|
160
|
-
(subpath "/opt/homebrew/opt"))
|
|
161
|
-
(allow file-read* file-test-existence (subpath "/usr/local/bin"))
|
|
162
|
-
(allow file-read* (subpath "/usr/local/lib"))
|
|
163
|
-
|
|
164
|
-
; Standard device handles and PTY support.
|
|
165
|
-
(allow pseudo-tty)
|
|
166
|
-
(allow file-read* file-write* file-ioctl (literal "/dev/ptmx"))
|
|
167
|
-
(allow file-read* file-write*
|
|
168
|
-
(require-all
|
|
169
|
-
(regex #"^/dev/ttys[0-9]+")
|
|
170
|
-
(extension "com.apple.sandbox.pty")))
|
|
171
|
-
(allow file-ioctl (regex #"^/dev/ttys[0-9]+"))
|
|
172
|
-
(allow file-read* (regex "^/dev/fd/(0|1|2)$"))
|
|
173
|
-
(allow file-write* (regex "^/dev/fd/(1|2)$"))
|
|
174
|
-
(allow file-read* file-write* (literal "/dev/tty"))
|
|
175
|
-
(allow file-read-metadata (literal "/dev"))
|
|
176
|
-
(allow file-read-metadata (regex "^/dev/.*$"))
|
|
177
|
-
(allow file-read-metadata (literal "/dev/stdin"))
|
|
178
|
-
(allow file-read-metadata (literal "/dev/stdout"))
|
|
179
|
-
(allow file-read-metadata (literal "/dev/stderr"))
|
|
180
|
-
|
|
181
|
-
; Runtime queries used by native binaries and Node/Bun.
|
|
182
|
-
(allow sysctl-read
|
|
183
|
-
(sysctl-name "hw.activecpu")
|
|
184
|
-
(sysctl-name "hw.busfrequency_compat")
|
|
185
|
-
(sysctl-name "hw.byteorder")
|
|
186
|
-
(sysctl-name "hw.cacheconfig")
|
|
187
|
-
(sysctl-name "hw.cachelinesize_compat")
|
|
188
|
-
(sysctl-name "hw.cpufamily")
|
|
189
|
-
(sysctl-name "hw.cpufrequency")
|
|
190
|
-
(sysctl-name "hw.cpufrequency_compat")
|
|
191
|
-
(sysctl-name "hw.cputype")
|
|
192
|
-
(sysctl-name "hw.ephemeral_storage")
|
|
193
|
-
(sysctl-name "hw.l1dcachesize_compat")
|
|
194
|
-
(sysctl-name "hw.l1icachesize_compat")
|
|
195
|
-
(sysctl-name "hw.l2cachesize_compat")
|
|
196
|
-
(sysctl-name "hw.l3cachesize_compat")
|
|
197
|
-
(sysctl-name "hw.logicalcpu")
|
|
198
|
-
(sysctl-name "hw.logicalcpu_max")
|
|
199
|
-
(sysctl-name "hw.machine")
|
|
200
|
-
(sysctl-name "hw.memsize")
|
|
201
|
-
(sysctl-name "hw.model")
|
|
202
|
-
(sysctl-name "hw.ncpu")
|
|
203
|
-
(sysctl-name "hw.packages")
|
|
204
|
-
(sysctl-name "hw.pagesize")
|
|
205
|
-
(sysctl-name "hw.pagesize_compat")
|
|
206
|
-
(sysctl-name "hw.physicalcpu")
|
|
207
|
-
(sysctl-name "hw.physicalcpu_max")
|
|
208
|
-
(sysctl-name "hw.tbfrequency_compat")
|
|
209
|
-
(sysctl-name "hw.vectorunit")
|
|
210
|
-
(sysctl-name-prefix "hw.optional.arm.")
|
|
211
|
-
(sysctl-name-prefix "hw.optional.armv8_")
|
|
212
|
-
(sysctl-name-prefix "hw.perflevel")
|
|
213
|
-
(sysctl-name "kern.argmax")
|
|
214
|
-
(sysctl-name "kern.bootargs")
|
|
215
|
-
(sysctl-name "kern.hostname")
|
|
216
|
-
(sysctl-name "kern.iossupportversion")
|
|
217
|
-
(sysctl-name "kern.maxfilesperproc")
|
|
218
|
-
(sysctl-name "kern.maxproc")
|
|
219
|
-
(sysctl-name "kern.osproductversion")
|
|
220
|
-
(sysctl-name "kern.osrelease")
|
|
221
|
-
(sysctl-name "kern.ostype")
|
|
222
|
-
(sysctl-name "kern.osvariant_status")
|
|
223
|
-
(sysctl-name "kern.osversion")
|
|
224
|
-
(sysctl-name "kern.secure_kernel")
|
|
225
|
-
(sysctl-name "kern.usrstack64")
|
|
226
|
-
(sysctl-name "kern.version")
|
|
227
|
-
(sysctl-name "machdep.cpu.brand_string")
|
|
228
|
-
(sysctl-name "security.mac.lockdown_mode_state")
|
|
229
|
-
(sysctl-name "sysctl.proc_cputype")
|
|
230
|
-
(sysctl-name "vm.loadavg")
|
|
231
|
-
(sysctl-name-prefix "kern.proc.pgrp.")
|
|
232
|
-
(sysctl-name-prefix "kern.proc.pid.")
|
|
233
|
-
(sysctl-name-prefix "net.routetable."))
|
|
234
|
-
(allow sysctl-write (sysctl-name "kern.grade_cputype"))
|
|
235
|
-
|
|
236
|
-
(allow iokit-open (iokit-registry-entry-class "RootDomainUserClient"))
|
|
237
|
-
(allow ipc-posix-sem)
|
|
238
|
-
(allow ipc-posix-shm-read-data ipc-posix-shm-write-create ipc-posix-shm-write-data
|
|
239
|
-
(ipc-posix-name "com.apple.AppleDatabaseChanged"))
|
|
240
|
-
(allow ipc-posix-shm-read-data
|
|
241
|
-
ipc-posix-shm-write-create
|
|
242
|
-
ipc-posix-shm-write-unlink
|
|
243
|
-
(ipc-posix-name-regex #"^/__KMP_REGISTERED_LIB_[0-9]+$"))
|
|
244
|
-
(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs."))
|
|
245
|
-
(allow ipc-posix-shm-read*
|
|
246
|
-
(ipc-posix-name "apple.shm.notification_center"))
|
|
247
|
-
(allow mach-lookup
|
|
248
|
-
(global-name "com.apple.PowerManagement.control")
|
|
249
|
-
(global-name "com.apple.analyticsd")
|
|
250
|
-
(global-name "com.apple.analyticsd.messagetracer")
|
|
251
|
-
(global-name "com.apple.appsleep")
|
|
252
|
-
(global-name "com.apple.bsd.dirhelper")
|
|
253
|
-
(global-name "com.apple.cfprefsd.agent")
|
|
254
|
-
(global-name "com.apple.cfprefsd.daemon")
|
|
255
|
-
(global-name "com.apple.diagnosticd")
|
|
256
|
-
(global-name "com.apple.FSEvents")
|
|
257
|
-
(global-name "com.apple.logd")
|
|
258
|
-
(global-name "com.apple.logd.events")
|
|
259
|
-
(global-name "com.apple.runningboard")
|
|
260
|
-
(global-name "com.apple.secinitd")
|
|
261
|
-
(global-name "com.apple.system.DirectoryService.libinfo_v1")
|
|
262
|
-
(global-name "com.apple.system.logger")
|
|
263
|
-
(global-name "com.apple.system.notification_center")
|
|
264
|
-
(global-name "com.apple.system.opendirectoryd.libinfo")
|
|
265
|
-
(global-name "com.apple.system.opendirectoryd.membership")
|
|
266
|
-
(global-name "com.apple.trustd")
|
|
267
|
-
(global-name "com.apple.trustd.agent")
|
|
268
|
-
(global-name "com.apple.xpc.activity.unmanaged")
|
|
269
|
-
(local-name "com.apple.cfprefsd.agent"))
|
|
270
|
-
; cfprefsd serves a sandboxed client a preference domain only when this
|
|
271
|
-
; profile admits user-preference-read for that domain (or file-read-data on
|
|
272
|
-
; its plist, which HOME never gets). An unfiltered rule hands a confined agent
|
|
273
|
-
; every app's domain in ~/Library/Preferences. Admit only the global domain
|
|
274
|
-
; (.GlobalPreferences: locale, languages, units). It is the one domain a
|
|
275
|
-
; sandboxed tool was measured requesting: /usr/bin/security, which Claude
|
|
276
|
-
; runs to read its Keychain credential. Node, OpenCode and Qwen request none.
|
|
277
|
-
; Apple's App Sandbox grants every app this same domain.
|
|
278
|
-
(allow user-preference-read
|
|
279
|
-
(preference-domain "kCFPreferencesAnyApplication"))
|
|
280
|
-
`;
|
|
281
|
-
|
|
282
|
-
// Open network, the tail of the default baseline. A `network: 'loopback'`
|
|
283
|
-
// profile leaves it out: no outbound except the loopback ports it lists, and
|
|
284
|
-
// none of the Security framework services the keychain is reached through.
|
|
285
|
-
const OPEN_NETWORK = String.raw`
|
|
286
|
-
; The selected provider and declared MCP transports need outbound network. The model's
|
|
287
|
-
; WebSearch/WebFetch/Bash tools are denied independently in the adapter argv.
|
|
288
|
-
(allow network-outbound)
|
|
289
|
-
(allow system-socket
|
|
290
|
-
(require-all
|
|
291
|
-
(socket-domain AF_SYSTEM)
|
|
292
|
-
(socket-protocol 2)))
|
|
293
|
-
(allow mach-lookup
|
|
294
|
-
(global-name "com.apple.SecurityServer")
|
|
295
|
-
(global-name "com.apple.SystemConfiguration.configd")
|
|
296
|
-
(global-name "com.apple.networkd")
|
|
297
|
-
(global-name "com.apple.ocspd")
|
|
298
|
-
(global-name "com.apple.securityd")
|
|
299
|
-
(global-name "com.apple.securityd.xpc"))
|
|
300
|
-
`;
|
|
301
|
-
|
|
302
|
-
// The static paths the baseline lets a jail READ (file-read* or file-read-data),
|
|
303
|
-
// as `subpaths` (whole trees) and `literals` (one file or directory entry).
|
|
304
|
-
// Read from PLATFORM_BASELINE itself, so a caller keeping a secret out of the
|
|
305
|
-
// jail (a public seat's provider key file) checks what the profile actually
|
|
306
|
-
// admits rather than a copy that drifts. Metadata-only grants are not reads;
|
|
307
|
-
// regex grants cover /dev only and are left out. Dynamic per-turn paths
|
|
308
|
-
// (workspace, state, executables, read-only directories) are the caller's.
|
|
309
|
-
const READ_OPERATIONS = ['file-read*', 'file-read-data'];
|
|
310
|
-
export const parseSeatbeltReadRoots = (profile) => {
|
|
311
|
-
const subpaths = new Set();
|
|
312
|
-
const literals = new Set();
|
|
313
|
-
const text = profile.replace(/;[^\n]*/g, '');
|
|
314
|
-
let at = text.indexOf('(allow ');
|
|
315
|
-
while (at !== -1) {
|
|
316
|
-
// The rule's balanced s-expression; parentheses inside a string don't count.
|
|
317
|
-
let depth = 0;
|
|
318
|
-
let end = at;
|
|
319
|
-
let inString = false;
|
|
320
|
-
for (; end < text.length; end += 1) {
|
|
321
|
-
const ch = text[end];
|
|
322
|
-
if (inString) { if (ch === '\\') end += 1; else if (ch === '"') inString = false; continue; }
|
|
323
|
-
if (ch === '"') inString = true;
|
|
324
|
-
else if (ch === '(') depth += 1;
|
|
325
|
-
else if (ch === ')') { depth -= 1; if (depth === 0) break; }
|
|
326
|
-
}
|
|
327
|
-
const rule = text.slice(at, end + 1);
|
|
328
|
-
const operations = rule.slice('(allow '.length).split('(')[0].trim().split(/\s+/);
|
|
329
|
-
if (operations.some((op) => READ_OPERATIONS.includes(op))) {
|
|
330
|
-
for (const match of rule.matchAll(/\((subpath|literal) "([^"]+)"\)/g)) {
|
|
331
|
-
(match[1] === 'subpath' ? subpaths : literals).add(match[2]);
|
|
332
|
-
}
|
|
333
|
-
// Fail closed: a read filter this cannot turn into a root would be a
|
|
334
|
-
// readable path the key-file check never sees. Regex grants are known
|
|
335
|
-
// to cover /dev only, and are held to that.
|
|
336
|
-
for (const filter of rule.matchAll(/\((path|prefix|regex|home-subpath|home-literal|home-prefix)\s+(#?"[^"]*")/g)) {
|
|
337
|
-
if (filter[1] !== 'regex' || !/^#?"\^\/dev\//.test(filter[2])) {
|
|
338
|
-
throw new Error(`seatbeltSystemReadRoots: a read rule uses a filter it cannot check: (${filter[1]} ${filter[2]})`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
at = text.indexOf('(allow ', end + 1);
|
|
343
|
-
}
|
|
344
|
-
return Object.freeze({
|
|
345
|
-
subpaths: Object.freeze([...subpaths]),
|
|
346
|
-
literals: Object.freeze([...literals]),
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
let systemReadRoots = null;
|
|
350
|
-
export const seatbeltSystemReadRoots = () => {
|
|
351
|
-
if (!systemReadRoots) systemReadRoots = parseSeatbeltReadRoots(PLATFORM_BASELINE);
|
|
352
|
-
return systemReadRoots;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
// Both network modes. On macOS, c-ares (node's dns.Resolver and dns.resolve*)
|
|
356
|
-
// reads the resolver list from configd's DNSConfiguration service while the
|
|
357
|
-
// Resolver is constructed. Denied, that lookup does not always fail: at
|
|
358
|
-
// times it blocks the main thread forever. dsh builds a Resolver as it loads,
|
|
359
|
-
// so confined turns ran to their 900 s timeout without sending a request
|
|
360
|
-
// Admitted, resolution fails fast, because mDNSResponder and port 53
|
|
361
|
-
// stay unreachable in a loopback profile. The service returns configuration
|
|
362
|
-
// and opens no channel out.
|
|
363
|
-
const RESOLVER_CONFIG = String.raw`
|
|
364
|
-
(allow mach-lookup (global-name "com.apple.SystemConfiguration.DNSConfiguration"))
|
|
365
|
-
`;
|
|
366
|
-
|
|
367
|
-
const escapeSbplString = (value) => String(value)
|
|
368
|
-
.replaceAll('\\', '\\\\')
|
|
369
|
-
.replaceAll('"', '\\"')
|
|
370
|
-
.replaceAll('\n', '\\n')
|
|
371
|
-
.replaceAll('\r', '\\r');
|
|
372
|
-
|
|
373
|
-
const literal = (path) => `(literal "${escapeSbplString(path)}")`;
|
|
374
|
-
const subpath = (path) => `(subpath "${escapeSbplString(path)}")`;
|
|
375
|
-
// Every .env and .env.* file under the workspace, at any depth, and everything
|
|
376
|
-
// inside a directory of that name: a real project keeps one beside each
|
|
377
|
-
// package, not only at its root (TASK-294). An SBPL regex literal takes no
|
|
378
|
-
// escapes, so a path it cannot hold is refused.
|
|
379
|
-
const regexUnder = (workspace, tail) => {
|
|
380
|
-
if (/["\r\n]/.test(workspace)) {
|
|
381
|
-
throw new Error('Seatbelt workspacePath must not contain a quote or a line break');
|
|
382
|
-
}
|
|
383
|
-
const pattern = workspace.replace(/[\\^$.|?*+()[\]{}]/g, '\\$&');
|
|
384
|
-
return `(regex #"^${pattern}/(.*/)?${tail}")`;
|
|
385
|
-
};
|
|
386
|
-
const envFilesUnder = (workspace) => regexUnder(workspace, '\\.env([./].*)?$');
|
|
387
|
-
// Every .npmrc and .yarnrc* (.yarnrc, .yarnrc.yml) under the workspace, at any
|
|
388
|
-
// depth, and everything inside a directory of that name: they hold registry
|
|
389
|
-
// tokens. Opt-in, since a seat that installs packages reads its project's.
|
|
390
|
-
const registryConfigUnder = (workspace) => regexUnder(workspace, '(\\.npmrc|\\.yarnrc[^/]*)(/.*)?$');
|
|
391
|
-
|
|
392
|
-
const assertAbsolutePath = (path, label) => {
|
|
393
|
-
if (!path || !isAbsolute(path)) {
|
|
394
|
-
throw new Error(`Seatbelt ${label} must be an absolute path`);
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const realpathOrSelf = (path) => {
|
|
399
|
-
try {
|
|
400
|
-
return realpathSync(path);
|
|
401
|
-
} catch {
|
|
402
|
-
return path;
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
const isSymlink = (path) => {
|
|
407
|
-
try {
|
|
408
|
-
return lstatSync(path).isSymbolicLink();
|
|
409
|
-
} catch {
|
|
410
|
-
return false;
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
// A folder this profile lets the jail write is one the jail can also replace:
|
|
415
|
-
// a write rule on a folder covers the folder's own name, so a process from an
|
|
416
|
-
// earlier turn can move it aside and leave a link there. The profile grants
|
|
417
|
-
// the folder's resolved path, so a link at that name would carry the next
|
|
418
|
-
// turn's write grant to wherever it points. Each such folder must therefore
|
|
419
|
-
// resolve to itself: its realpath must be its parent's realpath plus its own
|
|
420
|
-
// name. A folder that does not exist yet has nothing to resolve and is
|
|
421
|
-
// granted by name, as before.
|
|
422
|
-
//
|
|
423
|
-
// Returns the path to grant. It is the same realpath the check compared, read
|
|
424
|
-
// once, so the grant and the check cannot disagree. Seatbelt matches the
|
|
425
|
-
// resolved path of each access, not the name used, so a link planted after
|
|
426
|
-
// this read leads outside the granted path and is denied there: a swap after
|
|
427
|
-
// the check breaks that turn's own writes, it does not widen them. That is why
|
|
428
|
-
// the check is not repeated just before spawn. The time-of-check gap that
|
|
429
|
-
// remains is inside this function, between the two realpath reads, and a
|
|
430
|
-
// swap there is caught by the comparison or yields a grant on the real
|
|
431
|
-
// folder.
|
|
432
|
-
const pinnedRealpath = (given, label) => {
|
|
433
|
-
// Normalized first, so a trailing `.`, `..` or slash cannot make the name
|
|
434
|
-
// compared differ from the folder the jail is granted.
|
|
435
|
-
const path = resolve(given);
|
|
436
|
-
let stat;
|
|
437
|
-
try {
|
|
438
|
-
stat = lstatSync(path);
|
|
439
|
-
} catch {
|
|
440
|
-
return path;
|
|
441
|
-
}
|
|
442
|
-
const refuse = () => new Error(
|
|
443
|
-
`refusing a confined turn: its writable ${label} ${path} is a symbolic link or resolves `
|
|
444
|
-
+ 'somewhere other than its own name. A confined turn may write only to a real folder at '
|
|
445
|
-
+ 'the path it was given. Remove the link (the agent recreates its own folders) or point the '
|
|
446
|
-
+ 'agent at a real folder, and run the turn again.',
|
|
447
|
-
);
|
|
448
|
-
if (stat.isSymbolicLink()) throw refuse();
|
|
449
|
-
let resolved;
|
|
450
|
-
let resolvedParent;
|
|
451
|
-
try {
|
|
452
|
-
resolved = realpathSync(path);
|
|
453
|
-
resolvedParent = realpathSync(dirname(path));
|
|
454
|
-
} catch {
|
|
455
|
-
throw refuse();
|
|
456
|
-
}
|
|
457
|
-
if (resolved !== join(resolvedParent, basename(path))) throw refuse();
|
|
458
|
-
return resolved;
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
export const detectSeatbelt = () => {
|
|
462
|
-
if (process.platform !== 'darwin') {
|
|
463
|
-
return { available: false, error: DARWIN_MESSAGE };
|
|
464
|
-
}
|
|
465
|
-
try {
|
|
466
|
-
const result = spawnSync(
|
|
467
|
-
SANDBOX_EXEC,
|
|
468
|
-
['-p', '(version 1)(allow default)', '/usr/bin/true'],
|
|
469
|
-
{ encoding: 'utf8' },
|
|
470
|
-
);
|
|
471
|
-
if (result.error || result.status !== 0) {
|
|
472
|
-
return {
|
|
473
|
-
available: false,
|
|
474
|
-
error: `sandbox-exec preflight failed: ${result.error?.message || result.stderr?.trim() || `exit ${result.status}`}`,
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
return { available: true, path: SANDBOX_EXEC };
|
|
478
|
-
} catch (err) {
|
|
479
|
-
return { available: false, error: `sandbox-exec detection failed: ${err.message}` };
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
export const buildSeatbeltProfile = ({
|
|
484
|
-
workspacePath,
|
|
485
|
-
workspaceAccess = 'write',
|
|
486
|
-
executablePath,
|
|
487
|
-
statePath,
|
|
488
|
-
mcpConfigDir = null,
|
|
489
|
-
executablePaths = [],
|
|
490
|
-
readOnlyPaths = [],
|
|
491
|
-
readOnlyDirectories = [],
|
|
492
|
-
loopbackNetworkPorts = [],
|
|
493
|
-
allowClaudeRuntimeAccess = false,
|
|
494
|
-
// 'open' (the default) or 'loopback': only the listed loopback ports.
|
|
495
|
-
network = 'open',
|
|
496
|
-
// Names directly inside a writable workspace that stay read-only. A
|
|
497
|
-
// workspace seat granted write keeps its project's .git read-only, so every
|
|
498
|
-
// change it makes is one `git status` shows the person (TASK-294).
|
|
499
|
-
readOnlyWorkspaceEntries = [],
|
|
500
|
-
// Whose process information a jailed process may read: 'same-sandbox' (the
|
|
501
|
-
// baseline) or 'self'. 'self' stops a child from inspecting its jailed parent
|
|
502
|
-
// (lsof/proc_pidinfo: open files, cwd), which matters when that parent holds
|
|
503
|
-
// a seat credential in memory, as a public pi does. Signals are not narrowed:
|
|
504
|
-
// a parent must still stop the children it started.
|
|
505
|
-
processInfo = 'same-sandbox',
|
|
506
|
-
// Pin a writable workspace to its own name (see pinnedRealpath). Set it for
|
|
507
|
-
// a public seat. Its workspace is usually the scratch folder the CLI made;
|
|
508
|
-
// when it is an `environment.workspace.path` the operator gave and that path
|
|
509
|
-
// is a link, the pin refuses the turn (fail-closed) rather than follow it.
|
|
510
|
-
// Other seats work in a folder the operator chose, which may be a link on
|
|
511
|
-
// purpose, and leave this unset.
|
|
512
|
-
pinWorkspace = false,
|
|
513
|
-
// The realpath the workspace must still resolve to (F200): a workspace
|
|
514
|
-
// seat's folder as recorded on its first turn (workspace-pin.js). The grant
|
|
515
|
-
// is refused, not moved, when the folder now resolves elsewhere.
|
|
516
|
-
expectedWorkspaceRealpath = null,
|
|
517
|
-
// Folders no rule here may let the jail write, however a grant above is
|
|
518
|
-
// shaped: a workspace seat's own record of its folder (F200).
|
|
519
|
-
denyWritePaths = [],
|
|
520
|
-
// Deny .npmrc and .yarnrc* under the workspace, as .env files are: for a
|
|
521
|
-
// process that only reads the workspace and installs nothing, such as a
|
|
522
|
-
// public Codex seat's git tool.
|
|
523
|
-
denyRegistryConfig = false,
|
|
524
|
-
}) => {
|
|
525
|
-
for (const [path, label] of [
|
|
526
|
-
[workspacePath, 'workspacePath'],
|
|
527
|
-
[executablePath, 'executablePath'],
|
|
528
|
-
[statePath, 'statePath'],
|
|
529
|
-
]) {
|
|
530
|
-
assertAbsolutePath(path, label);
|
|
531
|
-
}
|
|
532
|
-
if (mcpConfigDir) assertAbsolutePath(mcpConfigDir, 'mcpConfigDir');
|
|
533
|
-
for (const path of denyWritePaths) assertAbsolutePath(path, 'denyWritePaths entry');
|
|
534
|
-
for (const path of readOnlyPaths) assertAbsolutePath(path, 'readOnlyPaths entry');
|
|
535
|
-
for (const path of readOnlyDirectories) assertAbsolutePath(path, 'readOnlyDirectories entry');
|
|
536
|
-
for (const port of loopbackNetworkPorts) {
|
|
537
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
538
|
-
throw new Error('Seatbelt loopbackNetworkPorts entries must be valid TCP ports');
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (!['same-sandbox', 'self'].includes(processInfo)) {
|
|
542
|
-
throw new Error('Seatbelt processInfo must be same-sandbox or self');
|
|
543
|
-
}
|
|
544
|
-
if (!['open', 'loopback'].includes(network)) {
|
|
545
|
-
throw new Error('Seatbelt network must be open or loopback');
|
|
546
|
-
}
|
|
547
|
-
if (!['read', 'write'].includes(workspaceAccess)) {
|
|
548
|
-
throw new Error('Seatbelt workspaceAccess must be read or write');
|
|
549
|
-
}
|
|
550
|
-
for (const entry of readOnlyWorkspaceEntries) {
|
|
551
|
-
if (typeof entry !== 'string' || !entry || entry.includes('/') || entry === '.' || entry === '..') {
|
|
552
|
-
throw new Error('Seatbelt readOnlyWorkspaceEntries entries must be names inside the workspace');
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Two folders here get a write grant: the state folder, and the workspace
|
|
557
|
-
// when it is writable. The state folder is always pinned (see
|
|
558
|
-
// pinnedRealpath): the CLI creates it for every seat, so it is never a link
|
|
559
|
-
// on purpose. The writable workspace is pinned only when the caller asks
|
|
560
|
-
// (pinWorkspace), which is a public seat. Its workspace is usually a
|
|
561
|
-
// scratch folder the CLI creates; a public seat given an
|
|
562
|
-
// `environment.workspace.path` that is a link is refused (fail-closed).
|
|
563
|
-
// Any other seat's workspace is a folder the operator chose, and a link
|
|
564
|
-
// there may be the operator's own, which the profile cannot tell from one a
|
|
565
|
-
// seat left; it keeps the link-following grant on its target, as before. A
|
|
566
|
-
// read-only workspace also follows links. The other write rules in this
|
|
567
|
-
// profile name fixed device paths (/dev/null, /dev/zero, /dev/dtracehelper,
|
|
568
|
-
// /dev/fd, /dev/ptmx, /dev/ttys*, /dev/tty), which no seat can replace.
|
|
569
|
-
//
|
|
570
|
-
// The turn reaper is best-effort; this pin is the boundary.
|
|
571
|
-
const resolvedWorkspace = workspaceAccess === 'write' && pinWorkspace
|
|
572
|
-
? pinnedRealpath(workspacePath, 'workspace')
|
|
573
|
-
: realpathOrSelf(workspacePath);
|
|
574
|
-
if (expectedWorkspaceRealpath && resolvedWorkspace !== expectedWorkspaceRealpath) {
|
|
575
|
-
throw new Error(
|
|
576
|
-
`refusing a workspace turn: ${workspacePath} now resolves to ${resolvedWorkspace}, `
|
|
577
|
-
+ `not ${expectedWorkspaceRealpath} as it did when the turn was checked. The folder was `
|
|
578
|
-
+ 'replaced during the turn\'s preparation; run the turn again.',
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
const resolvedExecutable = realpathOrSelf(executablePath);
|
|
582
|
-
const resolvedState = pinnedRealpath(statePath, 'state folder');
|
|
583
|
-
const resolvedMcpConfig = mcpConfigDir ? realpathOrSelf(mcpConfigDir) : null;
|
|
584
|
-
const resolvedReadOnlyPaths = [...new Set(readOnlyPaths.map(realpathOrSelf))];
|
|
585
|
-
const resolvedReadOnlyDirectories = [...new Set(readOnlyDirectories.map(realpathOrSelf))];
|
|
586
|
-
const executables = [...new Set(
|
|
587
|
-
[resolvedExecutable, process.execPath, ...executablePaths]
|
|
588
|
-
.filter(Boolean)
|
|
589
|
-
.map(realpathOrSelf),
|
|
590
|
-
)];
|
|
591
|
-
for (const executable of executables) {
|
|
592
|
-
assertAbsolutePath(executable, 'executable path');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const workspaceRule = workspaceAccess === 'write'
|
|
596
|
-
? `(allow file-read* file-test-existence file-write* ${subpath(resolvedWorkspace)})`
|
|
597
|
-
: `(allow file-read* file-test-existence ${subpath(resolvedWorkspace)})`;
|
|
598
|
-
const executableRules = executables.map((executable) => [
|
|
599
|
-
`(allow file-read* file-test-existence file-map-executable ${literal(executable)})`,
|
|
600
|
-
`(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(executable)}"))`,
|
|
601
|
-
].join('\n')).join('\n');
|
|
602
|
-
const executableSymlinkRules = isSymlink(executablePath) ? [
|
|
603
|
-
`(allow file-read* file-test-existence ${literal(executablePath)})`,
|
|
604
|
-
`(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(executablePath)}"))`,
|
|
605
|
-
].join('\n') : '';
|
|
606
|
-
const mcpRule = resolvedMcpConfig
|
|
607
|
-
? `(allow file-read* file-test-existence ${subpath(resolvedMcpConfig)})`
|
|
608
|
-
: '';
|
|
609
|
-
const readOnlyRules = resolvedReadOnlyPaths.map((path) => (
|
|
610
|
-
`(allow file-read* file-test-existence ${literal(path)})`
|
|
611
|
-
)).concat(resolvedReadOnlyDirectories.map((path) => [
|
|
612
|
-
`(allow file-read* file-test-existence ${subpath(path)})`,
|
|
613
|
-
`(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(path)}"))`,
|
|
614
|
-
].join('\n'))).join('\n');
|
|
615
|
-
// A symlinked entry is held at both its name and its target, so the write
|
|
616
|
-
// cannot land through whichever path the link resolves to.
|
|
617
|
-
const readOnlyWorkspaceRules = [...new Set(readOnlyWorkspaceEntries.flatMap((entry) => {
|
|
618
|
-
const entryPath = join(resolvedWorkspace, entry);
|
|
619
|
-
return [entryPath, realpathOrSelf(entryPath)];
|
|
620
|
-
}))].map((path) => `(deny file-write* ${subpath(path)})`).join('\n');
|
|
621
|
-
const loopbackNetworkRules = [...new Set(loopbackNetworkPorts)].map((port) => (
|
|
622
|
-
`(allow network-outbound (remote tcp "localhost:${port}"))`
|
|
623
|
-
)).join('\n');
|
|
624
|
-
const claudeRuntimeRules = allowClaudeRuntimeAccess ? (() => {
|
|
625
|
-
// Claude's temp root is the seat's own: the adapter sets CLAUDE_CODE_TMPDIR
|
|
626
|
-
// under statePath, which the state rule above already admits. The
|
|
627
|
-
// uid-wide /private/tmp/claude-<uid> holds every other Claude session's
|
|
628
|
-
// temp files on the machine, so it is not admitted.
|
|
629
|
-
// Claude's security helper needs the encrypted login-keychain database and
|
|
630
|
-
// this non-secret locale file: that one file, not the Keychains folder.
|
|
631
|
-
const loginKeychain = join(homedir(), 'Library', 'Keychains', 'login.keychain-db');
|
|
632
|
-
const textEncoding = join(homedir(), '.CFUserTextEncoding');
|
|
633
|
-
return [
|
|
634
|
-
`(allow file-read* file-test-existence ${literal(loginKeychain)} ${literal(textEncoding)})`,
|
|
635
|
-
`(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(loginKeychain)}"))`,
|
|
636
|
-
];
|
|
637
|
-
})() : [];
|
|
638
|
-
|
|
639
|
-
return [
|
|
640
|
-
`${PLATFORM_BASELINE}${RESOLVER_CONFIG}${network === 'open' ? OPEN_NETWORK : ''}`.trim(),
|
|
641
|
-
executableRules,
|
|
642
|
-
executableSymlinkRules,
|
|
643
|
-
`(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(resolvedWorkspace)}"))`,
|
|
644
|
-
`(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(resolvedState)}"))`,
|
|
645
|
-
workspaceRule,
|
|
646
|
-
`(allow file-read* file-test-existence file-write* ${subpath(resolvedState)})`,
|
|
647
|
-
loopbackNetworkRules,
|
|
648
|
-
mcpRule,
|
|
649
|
-
readOnlyRules,
|
|
650
|
-
// The workspace is intentionally readable, but credential-like nested
|
|
651
|
-
// paths are not. Explicit deny wins over the broad workspace allow.
|
|
652
|
-
`(deny file-read* file-write* ${subpath(join(resolvedWorkspace, '.commonly'))})`,
|
|
653
|
-
`(deny file-read* file-write* ${subpath(join(resolvedWorkspace, '.codex'))})`,
|
|
654
|
-
`(deny file-read* file-write* ${envFilesUnder(resolvedWorkspace)})`,
|
|
655
|
-
denyRegistryConfig ? `(deny file-read* file-write* ${registryConfigUnder(resolvedWorkspace)})` : '',
|
|
656
|
-
readOnlyWorkspaceRules,
|
|
657
|
-
// After every allow above, so a workspace that contained one of these
|
|
658
|
-
// folders still could not write it (later rules win).
|
|
659
|
-
[...new Set(denyWritePaths.flatMap((path) => [path, realpathOrSelf(path)]))]
|
|
660
|
-
.map((path) => `(deny file-write* ${subpath(path)})`).join('\n'),
|
|
661
|
-
...claudeRuntimeRules,
|
|
662
|
-
// Later rules win, so this narrows the baseline's same-sandbox allow.
|
|
663
|
-
processInfo === 'self'
|
|
664
|
-
? '(deny process-info* (target same-sandbox))\n(allow process-info* (target self))'
|
|
665
|
-
: '',
|
|
666
|
-
].filter(Boolean).join('\n\n');
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
export const wrapArgvWithSeatbelt = (innerArgv, opts = {}) => {
|
|
670
|
-
if (process.platform !== 'darwin') {
|
|
671
|
-
throw new Error(DARWIN_MESSAGE);
|
|
672
|
-
}
|
|
673
|
-
if (!Array.isArray(innerArgv) || innerArgv.length === 0) {
|
|
674
|
-
throw new Error('wrapArgvWithSeatbelt requires a non-empty innerArgv');
|
|
675
|
-
}
|
|
676
|
-
const profile = buildSeatbeltProfile(opts);
|
|
677
|
-
return [SANDBOX_EXEC, '-p', profile, ...innerArgv];
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
export const publicClaudeStateRoot = (agentKey) => (
|
|
681
|
-
join(homedir(), '.commonly', 'claude-homes', agentKey)
|
|
682
|
-
);
|
|
683
|
-
|
|
684
|
-
export const SEATBELT_DARWIN_MESSAGE = DARWIN_MESSAGE;
|