@ravefox/space 0.2.1 → 0.4.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.
Files changed (167) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +104 -31
  3. package/bin/space.js +113 -0
  4. package/lib/agents.js +181 -0
  5. package/lib/boardTools.js +89 -0
  6. package/lib/credentials.js +95 -0
  7. package/lib/daemon.js +294 -0
  8. package/lib/harness.js +166 -0
  9. package/lib/http.js +101 -0
  10. package/lib/login.js +68 -0
  11. package/lib/mcp.js +39 -0
  12. package/lib/runtime.js +253 -0
  13. package/lib/store.js +96 -0
  14. package/lib/tools.js +93 -0
  15. package/lib/workspaceSandbox.js +77 -0
  16. package/lib/workspaceTools.js +160 -0
  17. package/lib/workspaceWorker.js +17 -0
  18. package/package.json +27 -46
  19. package/NOTICE +0 -55
  20. package/assets/webhook-sdk/python/bot.py +0 -63
  21. package/assets/webhook-sdk/python/commonly.py +0 -232
  22. package/node_modules/@commonlyai/seats/README.md +0 -25
  23. package/node_modules/@commonlyai/seats/package.json +0 -22
  24. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  25. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  26. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  27. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  28. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  29. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  30. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  31. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  32. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  33. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  34. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  35. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  36. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  37. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  38. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  39. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  40. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  41. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  42. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  43. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  44. package/skills/commonly/SKILL.md +0 -233
  45. package/src/commands/agent.js +0 -6334
  46. package/src/commands/daemon.js +0 -933
  47. package/src/commands/dev.js +0 -153
  48. package/src/commands/doctor.js +0 -141
  49. package/src/commands/inbox.js +0 -330
  50. package/src/commands/login.js +0 -194
  51. package/src/commands/mcp.js +0 -56
  52. package/src/commands/pod.js +0 -120
  53. package/src/commands/task.js +0 -279
  54. package/src/commands/verify.js +0 -48
  55. package/src/index.js +0 -99
  56. package/src/lib/adapter-login.js +0 -76
  57. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  58. package/src/lib/adapters/claude.js +0 -2207
  59. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  60. package/src/lib/adapters/codex.js +0 -1898
  61. package/src/lib/adapters/dsh.js +0 -513
  62. package/src/lib/adapters/goose.js +0 -510
  63. package/src/lib/adapters/hermes.js +0 -18
  64. package/src/lib/adapters/index.js +0 -68
  65. package/src/lib/adapters/opencode.js +0 -1681
  66. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  67. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  68. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  69. package/src/lib/adapters/pi.js +0 -1025
  70. package/src/lib/adapters/qwen.js +0 -523
  71. package/src/lib/adapters/seat-model.js +0 -8
  72. package/src/lib/adapters/stub.js +0 -1
  73. package/src/lib/adapters/tested-version.js +0 -106
  74. package/src/lib/api.js +0 -205
  75. package/src/lib/claim-outcome.js +0 -128
  76. package/src/lib/claimed-work-gate.js +0 -73
  77. package/src/lib/cli-hint.js +0 -33
  78. package/src/lib/commit-attribution.js +0 -391
  79. package/src/lib/config.js +0 -155
  80. package/src/lib/content-refusal.js +0 -94
  81. package/src/lib/credential-file.js +0 -208
  82. package/src/lib/daemon-agent-states.js +0 -95
  83. package/src/lib/daemon-hint.js +0 -18
  84. package/src/lib/daemon-logs.js +0 -43
  85. package/src/lib/daemon-service-windows.js +0 -263
  86. package/src/lib/daemon-service.js +0 -486
  87. package/src/lib/daemon-state.js +0 -84
  88. package/src/lib/daemon-store.js +0 -119
  89. package/src/lib/daemon-supervisor.js +0 -629
  90. package/src/lib/daemon-watch.js +0 -115
  91. package/src/lib/declared-mcp-guard.js +0 -284
  92. package/src/lib/default-environment.js +0 -207
  93. package/src/lib/device-login.js +0 -126
  94. package/src/lib/digest-frame.js +0 -151
  95. package/src/lib/doctor.js +0 -291
  96. package/src/lib/egress-proxy.js +0 -160
  97. package/src/lib/enforcement.js +0 -946
  98. package/src/lib/environment.js +0 -681
  99. package/src/lib/first-output-watchdog.js +0 -191
  100. package/src/lib/git-readonly-server.js +0 -138
  101. package/src/lib/git-readonly.js +0 -463
  102. package/src/lib/git-remote-commonly.mjs +0 -100
  103. package/src/lib/grant-broker-guard.js +0 -345
  104. package/src/lib/hooks-config.js +0 -238
  105. package/src/lib/host-gh-broker.js +0 -152
  106. package/src/lib/host-gh-shim.mjs +0 -98
  107. package/src/lib/inbox.js +0 -483
  108. package/src/lib/instance-proxy.js +0 -76
  109. package/src/lib/machine-host.js +0 -29
  110. package/src/lib/mcp-credential-delivery.js +0 -121
  111. package/src/lib/mcp-first-run.js +0 -50
  112. package/src/lib/mcp-home.js +0 -644
  113. package/src/lib/mcp-ready.js +0 -68
  114. package/src/lib/mcp-server-version.js +0 -123
  115. package/src/lib/mcp-token.js +0 -36
  116. package/src/lib/mcp-warm-child.mjs +0 -412
  117. package/src/lib/memory-bridge.js +0 -191
  118. package/src/lib/memory-import.js +0 -156
  119. package/src/lib/missing-harness.js +0 -31
  120. package/src/lib/model-unavailable.js +0 -154
  121. package/src/lib/native-runtime-inventory.js +0 -40
  122. package/src/lib/outsider-grant-mcp.js +0 -82
  123. package/src/lib/own-key-launch.js +0 -42
  124. package/src/lib/pod-catch-up.js +0 -55
  125. package/src/lib/pod-focus.js +0 -224
  126. package/src/lib/pod-tail.js +0 -68
  127. package/src/lib/poll-liveness.js +0 -103
  128. package/src/lib/poll-retry.js +0 -81
  129. package/src/lib/poller.js +0 -200
  130. package/src/lib/provider-proxy.js +0 -42
  131. package/src/lib/public-seat-mcp-flag.js +0 -36
  132. package/src/lib/public-tool-error.js +0 -60
  133. package/src/lib/public-withheld-tools.js +0 -48
  134. package/src/lib/reply-receipt.js +0 -55
  135. package/src/lib/resolve-on-path.js +0 -22
  136. package/src/lib/runtime-inventory.js +0 -56
  137. package/src/lib/runtime-observation.js +0 -37
  138. package/src/lib/sandbox/bwrap.js +0 -275
  139. package/src/lib/sandbox/codex-perimeter.js +0 -16
  140. package/src/lib/sandbox/host-check.js +0 -268
  141. package/src/lib/sandbox/mode.js +0 -49
  142. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  143. package/src/lib/sandbox/seatbelt.js +0 -684
  144. package/src/lib/sandbox/turn-dir.js +0 -56
  145. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  146. package/src/lib/sandbox/workspace-git-config.js +0 -243
  147. package/src/lib/sandbox/workspace-pin.js +0 -158
  148. package/src/lib/seat-runtime-controls.js +0 -61
  149. package/src/lib/seat-stop.js +0 -97
  150. package/src/lib/server-skills.js +0 -32
  151. package/src/lib/session-recycle.js +0 -219
  152. package/src/lib/session-store.js +0 -215
  153. package/src/lib/skill-prompt.js +0 -74
  154. package/src/lib/skills-import.js +0 -113
  155. package/src/lib/spawn-credential.js +0 -402
  156. package/src/lib/spawn-retry.js +0 -311
  157. package/src/lib/stable-install.js +0 -176
  158. package/src/lib/supervised-turn.js +0 -352
  159. package/src/lib/task.js +0 -235
  160. package/src/lib/turn-failure-notice.js +0 -177
  161. package/src/lib/unconfined-seat-guard.js +0 -94
  162. package/src/lib/update-check.js +0 -192
  163. package/src/lib/upstream-refusal.js +0 -1
  164. package/src/lib/webhook-server.js +0 -109
  165. package/src/lib/windows-command.js +0 -393
  166. package/src/lib/workspace-git.js +0 -87
  167. 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;