janissary 0.5.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 +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- package/package.json +138 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Static Seatbelt (`sandbox-exec`) profile text, built from the table-driven path/env lists in
|
|
2
|
+
// sandbox-paths.ts. Dynamic paths (the workspace, its temp dir, `$HOME`, the parent repo's git
|
|
3
|
+
// objects dir) are never string-interpolated into the profile — they're substituted at spawn time
|
|
4
|
+
// via `-D KEY=value` params (see `sandbox.ts`), so this module has no injection surface and stays
|
|
5
|
+
// a plain constant.
|
|
6
|
+
//
|
|
7
|
+
// Rule ordering follows Seatbelt's "last matching rule wins" semantic: default deny → broad
|
|
8
|
+
// read/write allow → `$HOME` read-deny → carve-in allows → secret denies last (so a secret path
|
|
9
|
+
// stays denied even inside a carve-in).
|
|
10
|
+
import { WRITE_CARVEOUT_PARAMS, READ_CARVEIN_PARAMS, SECRET_DENY_PARAMS, LISTING_DIR_PARAMS, WRITE_PREFIX_PARAMS, clausesFor, literalClausesFor, prefixClausesFor, } from './paths.js';
|
|
11
|
+
const writeCarveClauses = clausesFor(WRITE_CARVEOUT_PARAMS);
|
|
12
|
+
const readCarveClauses = clausesFor(READ_CARVEIN_PARAMS);
|
|
13
|
+
const secretDenyClauses = clausesFor(SECRET_DENY_PARAMS);
|
|
14
|
+
const listingClauses = literalClausesFor(LISTING_DIR_PARAMS);
|
|
15
|
+
const writePrefixClauses = prefixClausesFor(WRITE_PREFIX_PARAMS);
|
|
16
|
+
function buildProfile(networkClause) {
|
|
17
|
+
return String.raw `(version 1)
|
|
18
|
+
(deny default)
|
|
19
|
+
(allow process-fork)
|
|
20
|
+
(allow process-exec)
|
|
21
|
+
(deny process-exec
|
|
22
|
+
(subpath "/tmp")
|
|
23
|
+
(subpath "/private/tmp"))
|
|
24
|
+
|
|
25
|
+
; Writes: denied by default (the top-level deny above). Allowed only inside the workspace, its
|
|
26
|
+
; private temp dir, /dev/null and tty/pty devices, and the narrow harness-state carve-outs.
|
|
27
|
+
; DARWIN_USER_CACHE_DIR (the real per-user /var/folders/<hash>/C/ macOS confstr(3) hands out for
|
|
28
|
+
; system caches — NOT its T/ temp sibling, which stays denied) is carved in for writes here even
|
|
29
|
+
; though it's outside $HOME entirely: TMPDIR above is overridden to a workspace-local path, but
|
|
30
|
+
; frameworks that look this path up directly via confstr bypass that override and write lock/cache
|
|
31
|
+
; files into the real one regardless (e.g. Security.framework's legacy MDS subsystem locks
|
|
32
|
+
; .../C/mds/mds.lock on every SecItemCopyMatching call — denied, the call silently fails rather
|
|
33
|
+
; than erroring, so a sandboxed harness reads back "not logged in" even with a valid Keychain
|
|
34
|
+
; item). Reads there already work via the broad file-read* allow below; only writes are
|
|
35
|
+
; default-denied. CLAUDE_SCRATCH_DIR (/private/tmp/claude-<uid>/) is the harness CLI's own
|
|
36
|
+
; per-user scratch tree, where it creates a per-project/session scratchpad directory before
|
|
37
|
+
; running any tool — a fixed path outside $HOME and outside our TMPDIR override, so without this
|
|
38
|
+
; carve-in every tool invocation inside a sandboxed harness session fails at that housekeeping
|
|
39
|
+
; step, before the tool's own command ever runs.
|
|
40
|
+
(allow file-write*
|
|
41
|
+
(subpath (param "WORKSPACE"))
|
|
42
|
+
(subpath (param "TMPDIR"))
|
|
43
|
+
(subpath (param "DARWIN_USER_CACHE_DIR"))
|
|
44
|
+
(subpath (param "CLAUDE_SCRATCH_DIR"))
|
|
45
|
+
${writeCarveClauses}
|
|
46
|
+
${writePrefixClauses})
|
|
47
|
+
(allow file-read-data file-write-data
|
|
48
|
+
(literal "/dev/null")
|
|
49
|
+
(regex #"^/dev/tty")
|
|
50
|
+
(regex #"^/dev/pty"))
|
|
51
|
+
; Terminal ioctls (raw-mode/echo termios, window size) are a separate Seatbelt operation from
|
|
52
|
+
; file-read*/file-write* — without this, a PTY-backed harness tab can't disable canonical mode
|
|
53
|
+
; or query window size, so keystrokes and mouse-tracking escapes leak through unparsed instead
|
|
54
|
+
; of being consumed by the TUI (e.g. Enter never submits).
|
|
55
|
+
(allow file-ioctl
|
|
56
|
+
(literal "/dev/null")
|
|
57
|
+
(regex #"^/dev/tty")
|
|
58
|
+
(regex #"^/dev/pty"))
|
|
59
|
+
|
|
60
|
+
; Reads: allowed everywhere by default (system paths, harness binaries, /usr, node, homebrew
|
|
61
|
+
; all stay readable), then $HOME's *contents* are denied (data/xattr — metadata/stat stays
|
|
62
|
+
; allowed everywhere, see below), then the workspace/temp dir/parent-repo-objects/harness-state/
|
|
63
|
+
; .gitconfig/.gitexcludes are carved back in, then secrets are denied last (full file-read*,
|
|
64
|
+
; including metadata, so their existence isn't observable either) so they lose even inside a
|
|
65
|
+
; carve-in.
|
|
66
|
+
(allow file-read*)
|
|
67
|
+
; Directory metadata (stat/lstat) stays allowed through all of $HOME, not just the carve-ins:
|
|
68
|
+
; resolving a path — realpath, a pre-exec chdir, git's ancestor-ownership walk — requires
|
|
69
|
+
; traversing every ancestor directory between $HOME and the workspace, and Seatbelt checks each
|
|
70
|
+
; component individually rather than just the final target. Only actual file contents (data/xattr)
|
|
71
|
+
; are denied outside the carve-ins below; metadata alone doesn't leak file contents.
|
|
72
|
+
(allow file-read-metadata (subpath (param "HOME")))
|
|
73
|
+
; Plain deny (EPERM), not errno ENOENT: an ENOENT swap here was tried and reverted after it broke
|
|
74
|
+
; esbuild's own config resolution (see git history) — esbuild opens ancestor *directories* (not
|
|
75
|
+
; just candidate files) while resolving an entry point, e.g. to list $HOME itself while walking up
|
|
76
|
+
; from the workspace looking for a tsconfig/package boundary. Metadata says that directory exists
|
|
77
|
+
; (allowed above), so telling the content-read ENOENT instead of EPERM makes the directory-open
|
|
78
|
+
; lie about its own existence — esbuild's resolver treats "an ancestor directory doesn't exist" as
|
|
79
|
+
; fatal for the whole resolution, unlike a merely-missing individual file. EPERM doesn't carry that
|
|
80
|
+
; false signal, so directory-listing-based resolvers degrade the way they do outside a sandbox
|
|
81
|
+
; (permission error on an ancestor they don't actually need, harmlessly ignored) instead of
|
|
82
|
+
; concluding the target itself can't exist. Narrower ENOENT carve-ins for specific *files* that
|
|
83
|
+
; upward-walking config discovery (cosmiconfig, Node's package-scope resolution) actually needs are
|
|
84
|
+
; still safe — see the package.json carve-in below — since a single missing config file at one
|
|
85
|
+
; level is exactly what those walks expect to see constantly, with no directory-existence lie
|
|
86
|
+
; involved.
|
|
87
|
+
(deny file-read-data file-read-xattr (subpath (param "HOME")))
|
|
88
|
+
; A process reading its own executable (and the directory it lives in) is always safe to allow —
|
|
89
|
+
; frameworks the process links against may reopen its own binary for introspection (e.g. Keychain's
|
|
90
|
+
; SecItemCopyMatching calls CFBundleGetMainBundle, which does exactly this to determine code identity
|
|
91
|
+
; for ACL matching). Harness binaries installed under $HOME (nvm, ~/.opencode/bin, …) would otherwise
|
|
92
|
+
; fail that self-read and the harness would appear logged out even with a valid Keychain item.
|
|
93
|
+
; SERVER_NODE_DIR_L/R (the janissary server's own process.execPath directory) is carved in for the
|
|
94
|
+
; same self-read reasoning, and so a script running inside the sandbox can invoke the known-good
|
|
95
|
+
; node at JANISSARY_NODE (see sandbox.ts) instead of relying on PATH resolution inside the
|
|
96
|
+
; sandboxed process, which doesn't always find a working node first.
|
|
97
|
+
(allow file-read-data file-read-xattr
|
|
98
|
+
(subpath (param "WORKSPACE"))
|
|
99
|
+
(subpath (param "TMPDIR"))
|
|
100
|
+
(subpath (param "GIT_OBJECTS"))
|
|
101
|
+
(subpath (param "SELF_DIR_L"))
|
|
102
|
+
(subpath (param "SELF_DIR_R"))
|
|
103
|
+
(subpath (param "SERVER_NODE_DIR_L"))
|
|
104
|
+
(subpath (param "SERVER_NODE_DIR_R"))
|
|
105
|
+
${readCarveClauses}
|
|
106
|
+
${listingClauses})
|
|
107
|
+
; Any package.json or tsconfig.json anywhere under $HOME, at any depth, stays readable. The
|
|
108
|
+
; workspace nests inside the parent repo (or repos, however many levels up), and config/module-
|
|
109
|
+
; resolution walks — Node's package-scope resolution, cosmiconfig (stylelint, eslint, prettier,
|
|
110
|
+
; postcss all use it), and TypeScript-adjacent tooling (jiti/get-tsconfig, tsc itself) — probe
|
|
111
|
+
; upward for these files indefinitely, not just one level. This is a plain file read (unlike the
|
|
112
|
+
; ancestor-directory-listing problem the $HOME-deny comment above describes), so there's no
|
|
113
|
+
; directory-existence lie to worry about — granting real read access is both simpler and more
|
|
114
|
+
; correct than an ENOENT trick would be, since tools that actually want the content (e.g.
|
|
115
|
+
; cosmiconfig checking for a "stylelint" key, or jiti resolving compilerOptions) get real data
|
|
116
|
+
; instead of a fake absence. One manifest per level leaks no secrets; everything else under $HOME
|
|
117
|
+
; stays denied.
|
|
118
|
+
(allow file-read-data file-read-xattr
|
|
119
|
+
(require-all (subpath (param "HOME")) (regex #"/(package|tsconfig)\.json$")))
|
|
120
|
+
; errno ENOENT here too (see the $HOME-deny comment above): a secret path reads as genuinely
|
|
121
|
+
; absent rather than access-denied, which is both friendlier to tools that treat EPERM as fatal
|
|
122
|
+
; and better secrecy — EPERM already confirms the path exists, ENOENT reveals nothing. Unlike the
|
|
123
|
+
; $HOME-wide deny, this is safe: secret paths are individual files/dirs a resolver doesn't treat as
|
|
124
|
+
; a required ancestor of anything else, so there's no directory-existence lie in play. Uses the
|
|
125
|
+
; exact same operation set (file-read-data file-read-xattr) as the $HOME-wide deny above rather
|
|
126
|
+
; than the broader file-read* alias — empirically, the errno qualifier only takes effect when a
|
|
127
|
+
; later deny's operation set exactly matches an earlier deny matching the same path; file-read*
|
|
128
|
+
; here silently lost to the earlier plain file-read-data/file-read-xattr deny and produced EPERM
|
|
129
|
+
; instead of ENOENT. Metadata (stat) stays allowed throughout $HOME regardless, same as everywhere
|
|
130
|
+
; else — existence was never hidden, only content.
|
|
131
|
+
(deny file-read-data file-read-xattr
|
|
132
|
+
(with errno ENOENT)
|
|
133
|
+
${secretDenyClauses})
|
|
134
|
+
|
|
135
|
+
; IPC: mach-lookup is allowed broadly — notably securityd/Keychain, which OAuth-based harness
|
|
136
|
+
; auth needs (e.g. Claude Code's own credentials live in the Keychain, not a file) — except no
|
|
137
|
+
; controlling other apps and no clipboard reads, denied last so they lose even though the
|
|
138
|
+
; broad allow above matches them too.
|
|
139
|
+
(deny appleevent-send)
|
|
140
|
+
(allow mach-lookup)
|
|
141
|
+
(deny mach-lookup (global-name-regex #"^com\.apple\.pboard"))
|
|
142
|
+
|
|
143
|
+
; Read-only system info (CPU/memory/OS-version queries) — no user data, but JS engines that JIT
|
|
144
|
+
; (Bun, which compiles the claude/opencode CLIs) probe these during startup via sysctlbyname and
|
|
145
|
+
; hit a hard trap (SIGTRAP) rather than an error if denied, instead of falling back gracefully.
|
|
146
|
+
(allow sysctl-read)
|
|
147
|
+
|
|
148
|
+
; signal has no rule above, so it falls to the top-level default deny — every kill(2) call fails
|
|
149
|
+
; EPERM, including a process terminating its own children (e.g. ShellManager killing the
|
|
150
|
+
; persistent shell it spawned for a tab, or a package manager killing a build step it started).
|
|
151
|
+
; target children allows exactly that self-inflicted case — signaling a descendant of the
|
|
152
|
+
; sandboxed process tree — without opening the door to signaling arbitrary other processes on the
|
|
153
|
+
; host.
|
|
154
|
+
(allow signal (target children))
|
|
155
|
+
|
|
156
|
+
${networkClause}
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
// Network allowed by default; `--offline` swaps in the deny-network variant.
|
|
160
|
+
export const SANDBOX_PROFILE = buildProfile('(allow network*)');
|
|
161
|
+
export const SANDBOX_PROFILE_OFFLINE = buildProfile('(deny network*)');
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Table-driven path/env lists the Seatbelt profile in sandbox-profile.ts is built from.
|
|
2
|
+
// Extending any restriction is a one-line table change here.
|
|
3
|
+
// Narrow write carve-outs: harness auth/state directories and package-manager cache subpaths —
|
|
4
|
+
// never the whole `~/.claude`, `~/.cache`, or `~/.npm` (broad cache write access would let a
|
|
5
|
+
// sandboxed agent poison packages/plugins that other, non-sandboxed processes later consume).
|
|
6
|
+
// The `.claude/` entries are the session-state paths a sandboxed `claude` writes while running:
|
|
7
|
+
// transcripts, session env, task/todo/job state, shell snapshots, edit history, debug logs,
|
|
8
|
+
// plans, paste cache, and its stats/statsig telemetry caches. Deliberately absent: `.claude/plugins`
|
|
9
|
+
// (write access would let a sandboxed agent replace plugin code the unsandboxed CLI later runs —
|
|
10
|
+
// reads are carved in below), `daemon`/`ide`/`backups`, and the auto-updater's state files.
|
|
11
|
+
export const HOME_WRITE_CARVEOUTS = [
|
|
12
|
+
'.claude/projects',
|
|
13
|
+
'.claude/session-env',
|
|
14
|
+
'.claude/sessions',
|
|
15
|
+
'.claude/tasks',
|
|
16
|
+
'.claude/todos',
|
|
17
|
+
'.claude/jobs',
|
|
18
|
+
'.claude/shell-snapshots',
|
|
19
|
+
'.claude/file-history',
|
|
20
|
+
'.claude/history.jsonl',
|
|
21
|
+
'.claude/debug',
|
|
22
|
+
'.claude/plans',
|
|
23
|
+
'.claude/paste-cache',
|
|
24
|
+
'.claude/cache',
|
|
25
|
+
'.claude/statsig',
|
|
26
|
+
'.claude/stats-cache.json',
|
|
27
|
+
'.claude/mcp-needs-auth-cache.json',
|
|
28
|
+
'.claude.json',
|
|
29
|
+
'.codex',
|
|
30
|
+
'.config/opencode',
|
|
31
|
+
'.local/share/opencode',
|
|
32
|
+
'.local/state/opencode',
|
|
33
|
+
'.npm/_cacache',
|
|
34
|
+
'.npm/_logs',
|
|
35
|
+
'.npm/_npx',
|
|
36
|
+
'.cache/pip',
|
|
37
|
+
'.cache/yarn',
|
|
38
|
+
];
|
|
39
|
+
// Read carve-ins: the write carve-outs (a harness needs to read its own state), plus read-only
|
|
40
|
+
// extras — `.claude/settings.json` and the customization dirs a sandboxed `claude` loads on
|
|
41
|
+
// startup but must not modify (`plugins`, `skills`, `agents`, `commands`, `keybindings.json`),
|
|
42
|
+
// `.gitconfig` and `.gitexcludes` (the latter is whatever `core.excludesfile`
|
|
43
|
+
// in the former points to; every git invocation reads both), `.config/gh/config.yml` (`gh`'s
|
|
44
|
+
// general settings — git_protocol, editor, prompt — as opposed to `hosts.yml`, which can hold a
|
|
45
|
+
// plaintext OAuth token and stays denied via `SECRET_DENY_PATHS`; a workspaced `gh` authenticates
|
|
46
|
+
// via the injected `GH_TOKEN` env var instead, see `github-token.ts`), `Library/Keychains` (see
|
|
47
|
+
// the comment on `SECRET_DENY_PATHS` — read access is needed for any Keychain lookup to work at all
|
|
48
|
+
// on this OS, including harness login), `.nvm` (nvm's own `nvm.sh`/`bash_completion` loader
|
|
49
|
+
// scripts plus every installed Node version's binaries and libs under `versions/` — broader than
|
|
50
|
+
// SERVER_NODE_DIR_L/R in sandbox.ts, which only carves in the one version the janissary server
|
|
51
|
+
// itself runs on; a sandboxed process that sources `nvm.sh` or shells out to a bare `node`/`npm`/
|
|
52
|
+
// `npx`, or to a harness installed under a different nvm-managed version, needs to read the whole
|
|
53
|
+
// tree. Execute access for these binaries doesn't need a separate carve-in — `process-exec` is
|
|
54
|
+
// allowed everywhere except `/tmp`/`/private/tmp`, so this read carve-in is the only gap), `.rvm`
|
|
55
|
+
// (same reasoning as `.nvm`, for Ruby: `rvm`'s own loader script plus every installed Ruby
|
|
56
|
+
// version's binaries and gems under `rubies/`/`gems/`), and `.bash_profile`/`.bashrc` (a
|
|
57
|
+
// login/interactive bash shell sources these on startup; without the carve-in, spawning
|
|
58
|
+
// `bash -lc` inside the sandbox fails that read silently and the shell starts with none of the
|
|
59
|
+
// user's aliases/functions/PATH additions).
|
|
60
|
+
export const HOME_READ_CARVEINS = [
|
|
61
|
+
...HOME_WRITE_CARVEOUTS,
|
|
62
|
+
'.claude/settings.json', '.claude/plugins', '.claude/skills', '.claude/agents', '.claude/commands', '.claude/keybindings.json',
|
|
63
|
+
'.gitconfig', '.gitexcludes', '.config/gh/config.yml', 'Library/Keychains', '.nvm', '.rvm',
|
|
64
|
+
'.bash_profile', '.bashrc',
|
|
65
|
+
];
|
|
66
|
+
// Directories where `opendir`/`readdir` must work even though most of their *contents* aren't
|
|
67
|
+
// separately carved in. `.claude` itself: a settings read-modify-write (e.g. `claude --effort`, or
|
|
68
|
+
// any tool following this project's own "read raw settings before writing" convention) opens the
|
|
69
|
+
// directory node itself to enumerate settings files before touching `settings.json` — a plain
|
|
70
|
+
// `subpath` carve-in of the file alone only covers `open()`ing that one path, not the parent
|
|
71
|
+
// directory's `file-read-data` (Seatbelt's listing operation, distinct from the `file-read-metadata`
|
|
72
|
+
// stat access already allowed HOME-wide — see the deny-then-carve-in comment in sandbox-profile.ts).
|
|
73
|
+
// These use `literal`, not `subpath`: the directory node itself becomes listable, but files inside
|
|
74
|
+
// it stay gated by the specific carve-ins in HOME_READ_CARVEINS above (or stay denied).
|
|
75
|
+
export const HOME_READ_LISTING_DIRS = ['.claude'];
|
|
76
|
+
// Write carve-outs matched by Seatbelt `prefix` (string prefix on the full path) rather than
|
|
77
|
+
// `subpath` (path-component match) or `literal` (exact match). `.claude/settings.json` itself is
|
|
78
|
+
// deliberately absent from HOME_WRITE_CARVEOUTS above — but a settings read-modify-write (e.g.
|
|
79
|
+
// `claude --effort`) writes it via an atomic-rename pattern: create a sibling temp file
|
|
80
|
+
// (`settings.json.tmp.<pid>.<random>`, a fresh, unpredictable name every write), fsync it, then
|
|
81
|
+
// rename it over the target. `subpath`/`literal` can't carve in a name that doesn't exist yet and
|
|
82
|
+
// varies per write; `prefix` matches both the base filename and every `<base>.tmp.*` sibling
|
|
83
|
+
// because the base string is a literal character-prefix of each. Narrower than carving in the
|
|
84
|
+
// whole `.claude` directory for writes (still deliberately avoided, per the comment on
|
|
85
|
+
// HOME_WRITE_CARVEOUTS above) — this only ever matches paths starting with the exact
|
|
86
|
+
// `settings.json` string.
|
|
87
|
+
export const HOME_WRITE_PREFIX_CARVEOUTS = ['.claude/settings.json'];
|
|
88
|
+
// Secret paths denied last, even inside a carve-in.
|
|
89
|
+
//
|
|
90
|
+
// `Library/Keychains` is deliberately NOT here (writes stay denied by the top-level deny-default —
|
|
91
|
+
// only reads matter). Even "modern" Keychain Services calls (SecItemCopyMatching) fall through to
|
|
92
|
+
// a legacy CDSA/MDS implementation on this OS that reads the keychain DB file directly rather than
|
|
93
|
+
// only talking to securityd over IPC; denying that read blocks every keychain lookup a sandboxed
|
|
94
|
+
// harness makes, including its own OAuth credential, and it shows up as "not logged in" rather
|
|
95
|
+
// than a permission error. The DB stays encrypted and per-item ACL-enforced by securityd regardless
|
|
96
|
+
// of raw file readability, so this doesn't hand out plaintext secrets — it's a materially larger
|
|
97
|
+
// read surface than the other entries here, but the alternative breaks harness login outright.
|
|
98
|
+
//
|
|
99
|
+
// `.config/gh/hosts.yml`'s deny below now reads as ENOENT too (see the errno qualifier on the
|
|
100
|
+
// final deny rule in buildProfile), which is exactly what `gh` needs — but `sandbox.ts` still
|
|
101
|
+
// separately points `GH_CONFIG_DIR` at an empty, workspace-writable directory whenever a scoped
|
|
102
|
+
// token is injected, so `gh` finds a real, uncomplicated absent `hosts.yml` there rather than
|
|
103
|
+
// depending on this deny rule's errno behavior. Kept as defense in depth: `gh`'s Go config loader
|
|
104
|
+
// treats any read error there as fatal, so relying on a single mechanism to keep that read from
|
|
105
|
+
// ever mattering felt worth avoiding.
|
|
106
|
+
export const SECRET_DENY_PATHS = [
|
|
107
|
+
// On installs without Keychain access (Linux, some containers) this file holds the harness
|
|
108
|
+
// OAuth token in plaintext. Already outside every carve-in above, but denied explicitly so a
|
|
109
|
+
// future widening of the `.claude/` entries can't silently expose it.
|
|
110
|
+
'.claude/.credentials.json',
|
|
111
|
+
'.ssh', '.aws', '.gnupg', '.kube', '.netrc', '.config/gh/hosts.yml', '.docker',
|
|
112
|
+
'.config/gcloud', '.azure', '.cargo/credentials', '.cargo/credentials.toml',
|
|
113
|
+
'.pypirc', '.m2/settings.xml', '.terraform.d',
|
|
114
|
+
'.bash_history', '.zsh_history', '.python_history', '.node_repl_history',
|
|
115
|
+
'Library/Application Support/Google/Chrome',
|
|
116
|
+
'Library/Application Support/Firefox',
|
|
117
|
+
'Library/Application Support/BraveSoftware',
|
|
118
|
+
'Library/Safari',
|
|
119
|
+
];
|
|
120
|
+
// Env vars scrubbed from a workspaced process's environment: credential-shaped vars and
|
|
121
|
+
// agent-socket / credential-helper escape vectors that bypass the file-read denies above (e.g.
|
|
122
|
+
// `SSH_AUTH_SOCK` lets a process use the user's SSH keys without ever reading `~/.ssh`). LLM
|
|
123
|
+
// provider keys (`ANTHROPIC_*`, `OPENAI_*`, `GEMINI_*`/`GOOGLE_*`) are deliberately NOT matched —
|
|
124
|
+
// the harnesses and the ACP agent need their own credentials to function.
|
|
125
|
+
export const ENV_SCRUB_PATTERNS = [
|
|
126
|
+
/^AWS_/, /^GITHUB_TOKEN$/, /^GH_TOKEN$/, /^NPM_TOKEN$/, /^DOCKER_/, /^KUBECONFIG$/,
|
|
127
|
+
/_SECRET$/, /_PASSWORD$/,
|
|
128
|
+
/^SSH_AUTH_SOCK$/, /^GPG_AGENT_INFO$/, /^GNUPGHOME$/,
|
|
129
|
+
/^GIT_ASKPASS$/, /^GIT_CREDENTIAL_HELPER$/, /^KRB5CCNAME$/,
|
|
130
|
+
];
|
|
131
|
+
function paramName(prefix, index) {
|
|
132
|
+
return `${prefix}${index}`;
|
|
133
|
+
}
|
|
134
|
+
// Two `-D` param names per table entry, in the same order — `sandbox.ts` pairs these with the
|
|
135
|
+
// literal (`~/…`, unresolved beyond `$HOME` itself) and fully realpath-resolved forms of the same
|
|
136
|
+
// path. Both are needed: many dotfiles (`.gitconfig`, `.npmrc`, …) are themselves symlinks (e.g.
|
|
137
|
+
// managed by a dotfile manager), and Seatbelt evaluates an `lstat`/`readlink` of the symlink node
|
|
138
|
+
// against its literal path but a `read`/`open` that follows it against the resolved target —
|
|
139
|
+
// carving in only one leaves the other operation denied.
|
|
140
|
+
export function dualParams(prefix, count) {
|
|
141
|
+
return {
|
|
142
|
+
literal: Array.from({ length: count }, (_, i) => paramName(`${prefix}L`, i)),
|
|
143
|
+
real: Array.from({ length: count }, (_, i) => paramName(`${prefix}R`, i)),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export const WRITE_CARVEOUT_PARAMS = dualParams('W', HOME_WRITE_CARVEOUTS.length);
|
|
147
|
+
export const READ_CARVEIN_PARAMS = dualParams('R', HOME_READ_CARVEINS.length);
|
|
148
|
+
export const SECRET_DENY_PARAMS = dualParams('S', SECRET_DENY_PATHS.length);
|
|
149
|
+
export const LISTING_DIR_PARAMS = dualParams('D', HOME_READ_LISTING_DIRS.length);
|
|
150
|
+
export const WRITE_PREFIX_PARAMS = dualParams('P', HOME_WRITE_PREFIX_CARVEOUTS.length);
|
|
151
|
+
export const clausesFor = (params) => [...params.literal, ...params.real].map((p) => ` (subpath (param "${p}"))`).join('\n');
|
|
152
|
+
// Same shape as `clausesFor`, but matches only the exact path (Seatbelt `literal`) — used for
|
|
153
|
+
// HOME_READ_LISTING_DIRS, where a directory node must be listable without granting recursive
|
|
154
|
+
// access to everything under it (that's what `subpath` would do).
|
|
155
|
+
export const literalClausesFor = (params) => [...params.literal, ...params.real].map((p) => ` (literal (param "${p}"))`).join('\n');
|
|
156
|
+
// Same shape again, but Seatbelt `prefix` (string-prefix match) — used for
|
|
157
|
+
// HOME_WRITE_PREFIX_CARVEOUTS, where the writable path's exact name isn't known ahead of time
|
|
158
|
+
// (a randomly-named atomic-write temp sibling of a fixed base filename).
|
|
159
|
+
export const prefixClausesFor = (params) => [...params.literal, ...params.real].map((p) => ` (prefix (param "${p}"))`).join('\n');
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Static Seatbelt (`sandbox-exec`) profile text, built from the table-driven path/env lists in
|
|
2
|
+
// sandbox-paths.ts. Dynamic paths (the workspace, its temp dir, `$HOME`, the parent repo's git
|
|
3
|
+
// objects dir) are never string-interpolated into the profile — they're substituted at spawn time
|
|
4
|
+
// via `-D KEY=value` params (see `sandbox.ts`), so this module has no injection surface and stays
|
|
5
|
+
// a plain constant.
|
|
6
|
+
//
|
|
7
|
+
// Rule ordering follows Seatbelt's "last matching rule wins" semantic: default deny → broad
|
|
8
|
+
// read/write allow → `$HOME` read-deny → carve-in allows → secret denies last (so a secret path
|
|
9
|
+
// stays denied even inside a carve-in).
|
|
10
|
+
import { WRITE_CARVEOUT_PARAMS, READ_CARVEIN_PARAMS, SECRET_DENY_PARAMS, LISTING_DIR_PARAMS, WRITE_PREFIX_PARAMS, clausesFor, literalClausesFor, prefixClausesFor, } from './sandbox-paths.js';
|
|
11
|
+
const writeCarveClauses = clausesFor(WRITE_CARVEOUT_PARAMS);
|
|
12
|
+
const readCarveClauses = clausesFor(READ_CARVEIN_PARAMS);
|
|
13
|
+
const secretDenyClauses = clausesFor(SECRET_DENY_PARAMS);
|
|
14
|
+
const listingClauses = literalClausesFor(LISTING_DIR_PARAMS);
|
|
15
|
+
const writePrefixClauses = prefixClausesFor(WRITE_PREFIX_PARAMS);
|
|
16
|
+
function buildProfile(networkClause) {
|
|
17
|
+
return String.raw `(version 1)
|
|
18
|
+
(deny default)
|
|
19
|
+
(allow process-fork)
|
|
20
|
+
(allow process-exec)
|
|
21
|
+
(deny process-exec
|
|
22
|
+
(subpath "/tmp")
|
|
23
|
+
(subpath "/private/tmp"))
|
|
24
|
+
|
|
25
|
+
; Writes: denied by default (the top-level deny above). Allowed only inside the workspace, its
|
|
26
|
+
; private temp dir, /dev/null and tty/pty devices, and the narrow harness-state carve-outs.
|
|
27
|
+
; DARWIN_USER_CACHE_DIR (the real per-user /var/folders/<hash>/C/ macOS confstr(3) hands out for
|
|
28
|
+
; system caches — NOT its T/ temp sibling, which stays denied) is carved in for writes here even
|
|
29
|
+
; though it's outside $HOME entirely: TMPDIR above is overridden to a workspace-local path, but
|
|
30
|
+
; frameworks that look this path up directly via confstr bypass that override and write lock/cache
|
|
31
|
+
; files into the real one regardless (e.g. Security.framework's legacy MDS subsystem locks
|
|
32
|
+
; .../C/mds/mds.lock on every SecItemCopyMatching call — denied, the call silently fails rather
|
|
33
|
+
; than erroring, so a sandboxed harness reads back "not logged in" even with a valid Keychain
|
|
34
|
+
; item). Reads there already work via the broad file-read* allow below; only writes are
|
|
35
|
+
; default-denied. CLAUDE_SCRATCH_DIR (/private/tmp/claude-<uid>/) is the harness CLI's own
|
|
36
|
+
; per-user scratch tree, where it creates a per-project/session scratchpad directory before
|
|
37
|
+
; running any tool — a fixed path outside $HOME and outside our TMPDIR override, so without this
|
|
38
|
+
; carve-in every tool invocation inside a sandboxed harness session fails at that housekeeping
|
|
39
|
+
; step, before the tool's own command ever runs.
|
|
40
|
+
(allow file-write*
|
|
41
|
+
(subpath (param "WORKSPACE"))
|
|
42
|
+
(subpath (param "TMPDIR"))
|
|
43
|
+
(subpath (param "DARWIN_USER_CACHE_DIR"))
|
|
44
|
+
(subpath (param "CLAUDE_SCRATCH_DIR"))
|
|
45
|
+
${writeCarveClauses}
|
|
46
|
+
${writePrefixClauses})
|
|
47
|
+
(allow file-read-data file-write-data
|
|
48
|
+
(literal "/dev/null")
|
|
49
|
+
(regex #"^/dev/tty")
|
|
50
|
+
(regex #"^/dev/pty"))
|
|
51
|
+
; Terminal ioctls (raw-mode/echo termios, window size) are a separate Seatbelt operation from
|
|
52
|
+
; file-read*/file-write* — without this, a PTY-backed harness tab can't disable canonical mode
|
|
53
|
+
; or query window size, so keystrokes and mouse-tracking escapes leak through unparsed instead
|
|
54
|
+
; of being consumed by the TUI (e.g. Enter never submits).
|
|
55
|
+
(allow file-ioctl
|
|
56
|
+
(literal "/dev/null")
|
|
57
|
+
(regex #"^/dev/tty")
|
|
58
|
+
(regex #"^/dev/pty"))
|
|
59
|
+
|
|
60
|
+
; Reads: allowed everywhere by default (system paths, harness binaries, /usr, node, homebrew
|
|
61
|
+
; all stay readable), then $HOME's *contents* are denied (data/xattr — metadata/stat stays
|
|
62
|
+
; allowed everywhere, see below), then the workspace/temp dir/parent-repo-objects/harness-state/
|
|
63
|
+
; .gitconfig/.gitexcludes are carved back in, then secrets are denied last (full file-read*,
|
|
64
|
+
; including metadata, so their existence isn't observable either) so they lose even inside a
|
|
65
|
+
; carve-in.
|
|
66
|
+
(allow file-read*)
|
|
67
|
+
; Directory metadata (stat/lstat) stays allowed through all of $HOME, not just the carve-ins:
|
|
68
|
+
; resolving a path — realpath, a pre-exec chdir, git's ancestor-ownership walk — requires
|
|
69
|
+
; traversing every ancestor directory between $HOME and the workspace, and Seatbelt checks each
|
|
70
|
+
; component individually rather than just the final target. Only actual file contents (data/xattr)
|
|
71
|
+
; are denied outside the carve-ins below; metadata alone doesn't leak file contents.
|
|
72
|
+
(allow file-read-metadata (subpath (param "HOME")))
|
|
73
|
+
; Plain deny (EPERM), not errno ENOENT: an ENOENT swap here was tried and reverted after it broke
|
|
74
|
+
; esbuild's own config resolution (see git history) — esbuild opens ancestor *directories* (not
|
|
75
|
+
; just candidate files) while resolving an entry point, e.g. to list $HOME itself while walking up
|
|
76
|
+
; from the workspace looking for a tsconfig/package boundary. Metadata says that directory exists
|
|
77
|
+
; (allowed above), so telling the content-read ENOENT instead of EPERM makes the directory-open
|
|
78
|
+
; lie about its own existence — esbuild's resolver treats "an ancestor directory doesn't exist" as
|
|
79
|
+
; fatal for the whole resolution, unlike a merely-missing individual file. EPERM doesn't carry that
|
|
80
|
+
; false signal, so directory-listing-based resolvers degrade the way they do outside a sandbox
|
|
81
|
+
; (permission error on an ancestor they don't actually need, harmlessly ignored) instead of
|
|
82
|
+
; concluding the target itself can't exist. Narrower ENOENT carve-ins for specific *files* that
|
|
83
|
+
; upward-walking config discovery (cosmiconfig, Node's package-scope resolution) actually needs are
|
|
84
|
+
; still safe — see the package.json carve-in below — since a single missing config file at one
|
|
85
|
+
; level is exactly what those walks expect to see constantly, with no directory-existence lie
|
|
86
|
+
; involved.
|
|
87
|
+
(deny file-read-data file-read-xattr (subpath (param "HOME")))
|
|
88
|
+
; A process reading its own executable (and the directory it lives in) is always safe to allow —
|
|
89
|
+
; frameworks the process links against may reopen its own binary for introspection (e.g. Keychain's
|
|
90
|
+
; SecItemCopyMatching calls CFBundleGetMainBundle, which does exactly this to determine code identity
|
|
91
|
+
; for ACL matching). Harness binaries installed under $HOME (nvm, ~/.opencode/bin, …) would otherwise
|
|
92
|
+
; fail that self-read and the harness would appear logged out even with a valid Keychain item.
|
|
93
|
+
; SERVER_NODE_DIR_L/R (the janissary server's own process.execPath directory) is carved in for the
|
|
94
|
+
; same self-read reasoning, and so a script running inside the sandbox can invoke the known-good
|
|
95
|
+
; node at JANISSARY_NODE (see sandbox.ts) instead of relying on PATH resolution inside the
|
|
96
|
+
; sandboxed process, which doesn't always find a working node first.
|
|
97
|
+
(allow file-read-data file-read-xattr
|
|
98
|
+
(subpath (param "WORKSPACE"))
|
|
99
|
+
(subpath (param "TMPDIR"))
|
|
100
|
+
(subpath (param "GIT_OBJECTS"))
|
|
101
|
+
(subpath (param "SELF_DIR_L"))
|
|
102
|
+
(subpath (param "SELF_DIR_R"))
|
|
103
|
+
(subpath (param "SERVER_NODE_DIR_L"))
|
|
104
|
+
(subpath (param "SERVER_NODE_DIR_R"))
|
|
105
|
+
${readCarveClauses}
|
|
106
|
+
${listingClauses})
|
|
107
|
+
; Any package.json or tsconfig.json anywhere under $HOME, at any depth, stays readable. The
|
|
108
|
+
; workspace nests inside the parent repo (or repos, however many levels up), and config/module-
|
|
109
|
+
; resolution walks — Node's package-scope resolution, cosmiconfig (stylelint, eslint, prettier,
|
|
110
|
+
; postcss all use it), and TypeScript-adjacent tooling (jiti/get-tsconfig, tsc itself) — probe
|
|
111
|
+
; upward for these files indefinitely, not just one level. This is a plain file read (unlike the
|
|
112
|
+
; ancestor-directory-listing problem the $HOME-deny comment above describes), so there's no
|
|
113
|
+
; directory-existence lie to worry about — granting real read access is both simpler and more
|
|
114
|
+
; correct than an ENOENT trick would be, since tools that actually want the content (e.g.
|
|
115
|
+
; cosmiconfig checking for a "stylelint" key, or jiti resolving compilerOptions) get real data
|
|
116
|
+
; instead of a fake absence. One manifest per level leaks no secrets; everything else under $HOME
|
|
117
|
+
; stays denied.
|
|
118
|
+
(allow file-read-data file-read-xattr
|
|
119
|
+
(require-all (subpath (param "HOME")) (regex #"/(package|tsconfig)\.json$")))
|
|
120
|
+
; errno ENOENT here too (see the $HOME-deny comment above): a secret path reads as genuinely
|
|
121
|
+
; absent rather than access-denied, which is both friendlier to tools that treat EPERM as fatal
|
|
122
|
+
; and better secrecy — EPERM already confirms the path exists, ENOENT reveals nothing. Unlike the
|
|
123
|
+
; $HOME-wide deny, this is safe: secret paths are individual files/dirs a resolver doesn't treat as
|
|
124
|
+
; a required ancestor of anything else, so there's no directory-existence lie in play. Uses the
|
|
125
|
+
; exact same operation set (file-read-data file-read-xattr) as the $HOME-wide deny above rather
|
|
126
|
+
; than the broader file-read* alias — empirically, the errno qualifier only takes effect when a
|
|
127
|
+
; later deny's operation set exactly matches an earlier deny matching the same path; file-read*
|
|
128
|
+
; here silently lost to the earlier plain file-read-data/file-read-xattr deny and produced EPERM
|
|
129
|
+
; instead of ENOENT. Metadata (stat) stays allowed throughout $HOME regardless, same as everywhere
|
|
130
|
+
; else — existence was never hidden, only content.
|
|
131
|
+
(deny file-read-data file-read-xattr
|
|
132
|
+
(with errno ENOENT)
|
|
133
|
+
${secretDenyClauses})
|
|
134
|
+
|
|
135
|
+
; IPC: mach-lookup is allowed broadly — notably securityd/Keychain, which OAuth-based harness
|
|
136
|
+
; auth needs (e.g. Claude Code's own credentials live in the Keychain, not a file) — except no
|
|
137
|
+
; controlling other apps and no clipboard reads, denied last so they lose even though the
|
|
138
|
+
; broad allow above matches them too.
|
|
139
|
+
(deny appleevent-send)
|
|
140
|
+
(allow mach-lookup)
|
|
141
|
+
(deny mach-lookup (global-name-regex #"^com\.apple\.pboard"))
|
|
142
|
+
|
|
143
|
+
; Read-only system info (CPU/memory/OS-version queries) — no user data, but JS engines that JIT
|
|
144
|
+
; (Bun, which compiles the claude/opencode CLIs) probe these during startup via sysctlbyname and
|
|
145
|
+
; hit a hard trap (SIGTRAP) rather than an error if denied, instead of falling back gracefully.
|
|
146
|
+
(allow sysctl-read)
|
|
147
|
+
|
|
148
|
+
; signal has no rule above, so it falls to the top-level default deny — every kill(2) call fails
|
|
149
|
+
; EPERM, including a process terminating its own children (e.g. ShellManager killing the
|
|
150
|
+
; persistent shell it spawned for a tab, or a package manager killing a build step it started).
|
|
151
|
+
; target children allows exactly that self-inflicted case — signaling a descendant of the
|
|
152
|
+
; sandboxed process tree — without opening the door to signaling arbitrary other processes on the
|
|
153
|
+
; host.
|
|
154
|
+
(allow signal (target children))
|
|
155
|
+
|
|
156
|
+
${networkClause}
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
// Network allowed by default; `--offline` swaps in the deny-network variant.
|
|
160
|
+
export const SANDBOX_PROFILE = buildProfile('(allow network*)');
|
|
161
|
+
export const SANDBOX_PROFILE_OFFLINE = buildProfile('(deny network*)');
|