@commonlyai/cli 0.1.2 → 0.1.4

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.
@@ -0,0 +1,417 @@
1
+ /**
2
+ * macOS Seatbelt wrapper for public Claude Code 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 Claude
7
+ * needs, and then grants explicit access to:
8
+ * - the selected Claude executable,
9
+ * - one workspace (read/write or read-only),
10
+ * - one isolated, token-free Claude 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 Claude still needs outbound network access for the Anthropic 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 { realpathSync } from 'fs';
24
+ import { homedir } from 'os';
25
+ import { isAbsolute, join } from 'path';
26
+
27
+ const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
28
+ const DARWIN_MESSAGE = 'Seatbelt sandboxing is available only on macOS';
29
+
30
+ // Keep this baseline static and auditable. Dynamic paths are appended by
31
+ // buildSeatbeltProfile after strict absolute-path validation + SBPL escaping.
32
+ const PLATFORM_BASELINE = String.raw`
33
+ (version 1)
34
+ (deny default)
35
+
36
+ ; Children inherit the same sandbox. Process inspection is limited to this
37
+ ; sandbox, so a model cannot inspect unrelated operator processes.
38
+ (allow process-exec)
39
+ (allow process-fork)
40
+ (allow signal (target same-sandbox))
41
+ (allow process-info* (target same-sandbox))
42
+
43
+ (allow file-write-data
44
+ (require-all
45
+ (path "/dev/null")
46
+ (vnode-type CHARACTER-DEVICE)))
47
+
48
+ ; Loader + standard system resources.
49
+ (allow file-read* file-test-existence
50
+ (subpath "/Library/Apple")
51
+ (subpath "/Library/Filesystems/NetFSPlugins")
52
+ (subpath "/Library/Preferences/Logging")
53
+ (subpath "/private/var/db/DarwinDirectory/local/recordStore.data")
54
+ (subpath "/private/var/db/timezone")
55
+ (subpath "/usr/lib")
56
+ (subpath "/usr/share")
57
+ (subpath "/Library/Preferences")
58
+ (subpath "/Library/Keychains")
59
+ (subpath "/var/db")
60
+ (subpath "/private/var/db"))
61
+ (allow file-read* file-test-existence
62
+ (subpath "/System/Volumes/Preboot/Cryptexes/OS"))
63
+
64
+ (allow file-map-executable
65
+ (subpath "/Library/Apple/System/Library/Frameworks")
66
+ (subpath "/Library/Apple/System/Library/PrivateFrameworks")
67
+ (subpath "/Library/Apple/usr/lib")
68
+ (subpath "/System/Library/Extensions")
69
+ (subpath "/System/Library/Frameworks")
70
+ (subpath "/System/Library/PrivateFrameworks")
71
+ (subpath "/System/Library/SubFrameworks")
72
+ (subpath "/System/iOSSupport/System/Library/Frameworks")
73
+ (subpath "/System/iOSSupport/System/Library/PrivateFrameworks")
74
+ (subpath "/System/iOSSupport/System/Library/SubFrameworks")
75
+ (subpath "/usr/lib"))
76
+
77
+ (allow file-read* file-test-existence
78
+ (subpath "/Library/Apple/System/Library/Frameworks")
79
+ (subpath "/Library/Apple/System/Library/PrivateFrameworks")
80
+ (subpath "/Library/Apple/usr/lib")
81
+ (subpath "/System/Library/Frameworks")
82
+ (subpath "/System/Library/PrivateFrameworks")
83
+ (subpath "/System/Library/SubFrameworks")
84
+ (subpath "/System/iOSSupport/System/Library/Frameworks")
85
+ (subpath "/System/iOSSupport/System/Library/PrivateFrameworks")
86
+ (subpath "/System/iOSSupport/System/Library/SubFrameworks")
87
+ (subpath "/usr/lib"))
88
+
89
+ (allow system-mac-syscall (mac-policy-name "vnguard"))
90
+ (allow system-mac-syscall
91
+ (require-all
92
+ (mac-policy-name "Sandbox")
93
+ (mac-syscall-number 67)))
94
+
95
+ (allow file-read-metadata file-test-existence
96
+ (literal "/etc")
97
+ (literal "/tmp")
98
+ (literal "/var")
99
+ (literal "/private/etc/localtime")
100
+ (path-ancestors "/System/Volumes/Data/private"))
101
+ (allow file-read* file-test-existence (literal "/"))
102
+
103
+ (allow file-read* file-test-existence
104
+ (literal "/dev/autofs_nowait")
105
+ (literal "/dev/random")
106
+ (literal "/dev/urandom")
107
+ (literal "/private/etc/master.passwd")
108
+ (literal "/private/etc/passwd")
109
+ (literal "/private/etc/protocols")
110
+ (literal "/private/etc/services"))
111
+ (allow file-read* file-test-existence file-write-data file-ioctl
112
+ (literal "/dev/dtracehelper"))
113
+ (allow file-read* file-test-existence file-write-data file-ioctl
114
+ (literal "/dev/null")
115
+ (literal "/dev/zero"))
116
+ (allow file-read-data file-test-existence file-write-data (subpath "/dev/fd"))
117
+
118
+ ; System configuration is readable; user-home configuration is not.
119
+ (allow file-read* (subpath "/etc"))
120
+ (allow file-read* (subpath "/private/etc"))
121
+ (allow file-read* file-test-existence
122
+ (literal "/System/Library/CoreServices")
123
+ (literal "/System/Library/CoreServices/.SystemVersionPlatform.plist")
124
+ (literal "/System/Library/CoreServices/SystemVersion.plist"))
125
+ (allow file-read-metadata file-test-existence
126
+ (literal "/Library")
127
+ (literal "/Library/Application Support")
128
+ (literal "/System/Library")
129
+ (literal "/System/Cryptexes")
130
+ (literal "/System/Cryptexes/App")
131
+ (literal "/System/Cryptexes/OS"))
132
+ (allow file-read-metadata (subpath "/var"))
133
+ (allow file-read-metadata (subpath "/private/var"))
134
+
135
+ ; The native Claude binary and declared MCP launchers may execute children.
136
+ ; Claude bootstraps stdio MCP servers through /bin/sh, whose interpreter is
137
+ ; /bin/bash on current macOS. An executable-level shell deny would therefore
138
+ ; disable the declared capability too. The built-in Bash tool is denied in
139
+ ; Claude argv; any trusted child that does run inherits this same Seatbelt
140
+ ; profile, so host-secret reads and out-of-workspace writes remain kernel
141
+ ; denied.
142
+
143
+ (allow file-read-data file-read-metadata (subpath "/bin"))
144
+ (allow file-read-data file-read-metadata (subpath "/sbin"))
145
+ (allow file-read-data file-read-metadata (subpath "/usr/bin"))
146
+ (allow file-read-data file-read-metadata (subpath "/usr/sbin"))
147
+ (allow file-read-data file-read-metadata (subpath "/usr/libexec"))
148
+ (allow file-read* file-test-existence (subpath "/opt/homebrew/bin"))
149
+ (allow file-read* file-test-existence file-map-executable
150
+ (subpath "/opt/homebrew/Cellar"))
151
+ (allow file-read* file-test-existence
152
+ (subpath "/opt/homebrew/etc/openssl@3"))
153
+ (allow file-read* (subpath "/opt/homebrew/lib"))
154
+ (allow file-read* file-test-existence file-map-executable
155
+ (subpath "/opt/homebrew/opt"))
156
+ (allow file-read* file-test-existence (subpath "/usr/local/bin"))
157
+ (allow file-read* (subpath "/usr/local/lib"))
158
+
159
+ ; Standard device handles and PTY support.
160
+ (allow pseudo-tty)
161
+ (allow file-read* file-write* file-ioctl (literal "/dev/ptmx"))
162
+ (allow file-read* file-write*
163
+ (require-all
164
+ (regex #"^/dev/ttys[0-9]+")
165
+ (extension "com.apple.sandbox.pty")))
166
+ (allow file-ioctl (regex #"^/dev/ttys[0-9]+"))
167
+ (allow file-read* (regex "^/dev/fd/(0|1|2)$"))
168
+ (allow file-write* (regex "^/dev/fd/(1|2)$"))
169
+ (allow file-read* file-write* (literal "/dev/tty"))
170
+ (allow file-read-metadata (literal "/dev"))
171
+ (allow file-read-metadata (regex "^/dev/.*$"))
172
+ (allow file-read-metadata (literal "/dev/stdin"))
173
+ (allow file-read-metadata (literal "/dev/stdout"))
174
+ (allow file-read-metadata (literal "/dev/stderr"))
175
+
176
+ ; Runtime queries used by native binaries and Node/Bun.
177
+ (allow sysctl-read
178
+ (sysctl-name "hw.activecpu")
179
+ (sysctl-name "hw.busfrequency_compat")
180
+ (sysctl-name "hw.byteorder")
181
+ (sysctl-name "hw.cacheconfig")
182
+ (sysctl-name "hw.cachelinesize_compat")
183
+ (sysctl-name "hw.cpufamily")
184
+ (sysctl-name "hw.cpufrequency")
185
+ (sysctl-name "hw.cpufrequency_compat")
186
+ (sysctl-name "hw.cputype")
187
+ (sysctl-name "hw.ephemeral_storage")
188
+ (sysctl-name "hw.l1dcachesize_compat")
189
+ (sysctl-name "hw.l1icachesize_compat")
190
+ (sysctl-name "hw.l2cachesize_compat")
191
+ (sysctl-name "hw.l3cachesize_compat")
192
+ (sysctl-name "hw.logicalcpu")
193
+ (sysctl-name "hw.logicalcpu_max")
194
+ (sysctl-name "hw.machine")
195
+ (sysctl-name "hw.memsize")
196
+ (sysctl-name "hw.model")
197
+ (sysctl-name "hw.ncpu")
198
+ (sysctl-name "hw.packages")
199
+ (sysctl-name "hw.pagesize")
200
+ (sysctl-name "hw.pagesize_compat")
201
+ (sysctl-name "hw.physicalcpu")
202
+ (sysctl-name "hw.physicalcpu_max")
203
+ (sysctl-name "hw.tbfrequency_compat")
204
+ (sysctl-name "hw.vectorunit")
205
+ (sysctl-name-prefix "hw.optional.arm.")
206
+ (sysctl-name-prefix "hw.optional.armv8_")
207
+ (sysctl-name-prefix "hw.perflevel")
208
+ (sysctl-name "kern.argmax")
209
+ (sysctl-name "kern.bootargs")
210
+ (sysctl-name "kern.hostname")
211
+ (sysctl-name "kern.iossupportversion")
212
+ (sysctl-name "kern.maxfilesperproc")
213
+ (sysctl-name "kern.maxproc")
214
+ (sysctl-name "kern.osproductversion")
215
+ (sysctl-name "kern.osrelease")
216
+ (sysctl-name "kern.ostype")
217
+ (sysctl-name "kern.osvariant_status")
218
+ (sysctl-name "kern.osversion")
219
+ (sysctl-name "kern.secure_kernel")
220
+ (sysctl-name "kern.usrstack64")
221
+ (sysctl-name "kern.version")
222
+ (sysctl-name "machdep.cpu.brand_string")
223
+ (sysctl-name "security.mac.lockdown_mode_state")
224
+ (sysctl-name "sysctl.proc_cputype")
225
+ (sysctl-name "vm.loadavg")
226
+ (sysctl-name-prefix "kern.proc.pgrp.")
227
+ (sysctl-name-prefix "kern.proc.pid.")
228
+ (sysctl-name-prefix "net.routetable."))
229
+ (allow sysctl-write (sysctl-name "kern.grade_cputype"))
230
+
231
+ (allow iokit-open (iokit-registry-entry-class "RootDomainUserClient"))
232
+ (allow ipc-posix-sem)
233
+ (allow ipc-posix-shm-read-data ipc-posix-shm-write-create ipc-posix-shm-write-data
234
+ (ipc-posix-name "com.apple.AppleDatabaseChanged"))
235
+ (allow ipc-posix-shm-read-data
236
+ ipc-posix-shm-write-create
237
+ ipc-posix-shm-write-unlink
238
+ (ipc-posix-name-regex #"^/__KMP_REGISTERED_LIB_[0-9]+$"))
239
+ (allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs."))
240
+ (allow ipc-posix-shm-read*
241
+ (ipc-posix-name "apple.shm.notification_center"))
242
+ (allow mach-lookup
243
+ (global-name "com.apple.PowerManagement.control")
244
+ (global-name "com.apple.analyticsd")
245
+ (global-name "com.apple.analyticsd.messagetracer")
246
+ (global-name "com.apple.appsleep")
247
+ (global-name "com.apple.bsd.dirhelper")
248
+ (global-name "com.apple.cfprefsd.agent")
249
+ (global-name "com.apple.cfprefsd.daemon")
250
+ (global-name "com.apple.diagnosticd")
251
+ (global-name "com.apple.FSEvents")
252
+ (global-name "com.apple.logd")
253
+ (global-name "com.apple.logd.events")
254
+ (global-name "com.apple.runningboard")
255
+ (global-name "com.apple.secinitd")
256
+ (global-name "com.apple.system.DirectoryService.libinfo_v1")
257
+ (global-name "com.apple.system.logger")
258
+ (global-name "com.apple.system.notification_center")
259
+ (global-name "com.apple.system.opendirectoryd.libinfo")
260
+ (global-name "com.apple.system.opendirectoryd.membership")
261
+ (global-name "com.apple.trustd")
262
+ (global-name "com.apple.trustd.agent")
263
+ (global-name "com.apple.xpc.activity.unmanaged")
264
+ (local-name "com.apple.cfprefsd.agent"))
265
+ (allow user-preference-read)
266
+
267
+ ; Claude itself and declared MCP transports need outbound network. The model's
268
+ ; WebSearch/WebFetch/Bash tools are denied independently in the adapter argv.
269
+ (allow network-outbound)
270
+ (allow system-socket
271
+ (require-all
272
+ (socket-domain AF_SYSTEM)
273
+ (socket-protocol 2)))
274
+ (allow mach-lookup
275
+ (global-name "com.apple.SecurityServer")
276
+ (global-name "com.apple.SystemConfiguration.DNSConfiguration")
277
+ (global-name "com.apple.SystemConfiguration.configd")
278
+ (global-name "com.apple.networkd")
279
+ (global-name "com.apple.ocspd")
280
+ (global-name "com.apple.securityd")
281
+ (global-name "com.apple.securityd.xpc"))
282
+ `;
283
+
284
+ const escapeSbplString = (value) => String(value)
285
+ .replaceAll('\\', '\\\\')
286
+ .replaceAll('"', '\\"')
287
+ .replaceAll('\n', '\\n')
288
+ .replaceAll('\r', '\\r');
289
+
290
+ const literal = (path) => `(literal "${escapeSbplString(path)}")`;
291
+ const subpath = (path) => `(subpath "${escapeSbplString(path)}")`;
292
+
293
+ const assertAbsolutePath = (path, label) => {
294
+ if (!path || !isAbsolute(path)) {
295
+ throw new Error(`Seatbelt ${label} must be an absolute path`);
296
+ }
297
+ };
298
+
299
+ const realpathOrSelf = (path) => {
300
+ try {
301
+ return realpathSync(path);
302
+ } catch {
303
+ return path;
304
+ }
305
+ };
306
+
307
+ export const detectSeatbelt = () => {
308
+ if (process.platform !== 'darwin') {
309
+ return { available: false, error: DARWIN_MESSAGE };
310
+ }
311
+ try {
312
+ const result = spawnSync(
313
+ SANDBOX_EXEC,
314
+ ['-p', '(version 1)(allow default)', '/usr/bin/true'],
315
+ { encoding: 'utf8' },
316
+ );
317
+ if (result.error || result.status !== 0) {
318
+ return {
319
+ available: false,
320
+ error: `sandbox-exec preflight failed: ${result.error?.message || result.stderr?.trim() || `exit ${result.status}`}`,
321
+ };
322
+ }
323
+ return { available: true, path: SANDBOX_EXEC };
324
+ } catch (err) {
325
+ return { available: false, error: `sandbox-exec detection failed: ${err.message}` };
326
+ }
327
+ };
328
+
329
+ export const buildSeatbeltProfile = ({
330
+ workspacePath,
331
+ workspaceAccess = 'write',
332
+ claudePath,
333
+ statePath,
334
+ mcpConfigDir = null,
335
+ executablePaths = [],
336
+ }) => {
337
+ for (const [path, label] of [
338
+ [workspacePath, 'workspacePath'],
339
+ [claudePath, 'claudePath'],
340
+ [statePath, 'statePath'],
341
+ ]) {
342
+ assertAbsolutePath(path, label);
343
+ }
344
+ if (mcpConfigDir) assertAbsolutePath(mcpConfigDir, 'mcpConfigDir');
345
+ if (!['read', 'write'].includes(workspaceAccess)) {
346
+ throw new Error('Seatbelt workspaceAccess must be read or write');
347
+ }
348
+
349
+ const resolvedWorkspace = realpathOrSelf(workspacePath);
350
+ const resolvedClaude = realpathOrSelf(claudePath);
351
+ const resolvedState = realpathOrSelf(statePath);
352
+ const resolvedMcpConfig = mcpConfigDir ? realpathOrSelf(mcpConfigDir) : null;
353
+ const executables = [...new Set(
354
+ [resolvedClaude, process.execPath, ...executablePaths]
355
+ .filter(Boolean)
356
+ .map(realpathOrSelf),
357
+ )];
358
+ for (const executable of executables) {
359
+ assertAbsolutePath(executable, 'executable path');
360
+ }
361
+
362
+ const workspaceRule = workspaceAccess === 'write'
363
+ ? `(allow file-read* file-test-existence file-write* ${subpath(resolvedWorkspace)})`
364
+ : `(allow file-read* file-test-existence ${subpath(resolvedWorkspace)})`;
365
+ const executableRules = executables.map((executable) => [
366
+ `(allow file-read* file-test-existence file-map-executable ${literal(executable)})`,
367
+ `(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(executable)}"))`,
368
+ ].join('\n')).join('\n');
369
+ const mcpRule = resolvedMcpConfig
370
+ ? `(allow file-read* file-test-existence ${subpath(resolvedMcpConfig)})`
371
+ : '';
372
+ const claudeTmp = `/private/tmp/claude-${typeof process.getuid === 'function' ? process.getuid() : '0'}`;
373
+ const userKeychains = join(homedir(), 'Library', 'Keychains');
374
+ const textEncoding = join(homedir(), '.CFUserTextEncoding');
375
+
376
+ return [
377
+ PLATFORM_BASELINE.trim(),
378
+ executableRules,
379
+ `(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(resolvedWorkspace)}"))`,
380
+ `(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(resolvedState)}"))`,
381
+ workspaceRule,
382
+ `(allow file-read* file-test-existence file-write* ${subpath(resolvedState)})`,
383
+ mcpRule,
384
+ // The workspace is intentionally readable, but credential-like nested
385
+ // paths are not. Explicit deny wins over the broad workspace allow.
386
+ `(deny file-read* file-write* ${subpath(join(resolvedWorkspace, '.commonly'))})`,
387
+ `(deny file-read* file-write* ${subpath(join(resolvedWorkspace, '.codex'))})`,
388
+ `(deny file-read* file-write* ${literal(join(resolvedWorkspace, '.env'))})`,
389
+ // Native Claude uses a uid-scoped /private/tmp directory even when
390
+ // TMPDIR points elsewhere. Admit that exact runtime directory; every
391
+ // sibling under /private/tmp remains denied by the default policy.
392
+ `(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(claudeTmp)}"))`,
393
+ `(allow file-read* file-test-existence file-write* ${literal(claudeTmp)} ${subpath(claudeTmp)})`,
394
+ // OAuth remains in macOS Keychain rather than a bearer-token file or
395
+ // child environment variable. Claude's `security` helper needs the
396
+ // encrypted login-keychain database plus this non-secret locale file.
397
+ `(allow file-read* file-test-existence ${subpath(userKeychains)} ${literal(textEncoding)})`,
398
+ `(allow file-read-metadata file-test-existence (path-ancestors "${escapeSbplString(userKeychains)}"))`,
399
+ ].filter(Boolean).join('\n\n');
400
+ };
401
+
402
+ export const wrapArgvWithSeatbelt = (innerArgv, opts = {}) => {
403
+ if (process.platform !== 'darwin') {
404
+ throw new Error(DARWIN_MESSAGE);
405
+ }
406
+ if (!Array.isArray(innerArgv) || innerArgv.length === 0) {
407
+ throw new Error('wrapArgvWithSeatbelt requires a non-empty innerArgv');
408
+ }
409
+ const profile = buildSeatbeltProfile(opts);
410
+ return [SANDBOX_EXEC, '-p', profile, ...innerArgv];
411
+ };
412
+
413
+ export const publicClaudeStateRoot = (agentKey) => (
414
+ join(homedir(), '.commonly', 'claude-homes', agentKey)
415
+ );
416
+
417
+ export const SEATBELT_DARWIN_MESSAGE = DARWIN_MESSAGE;