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.
Files changed (268) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +28 -0
  3. package/agent-names.json +52 -0
  4. package/bin/janus.mjs +27 -0
  5. package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
  6. package/chrome-extension/content-script.js +62 -0
  7. package/chrome-extension/manifest.json +25 -0
  8. package/chrome-extension/rules.json +16 -0
  9. package/dist/acp/index.js +93 -0
  10. package/dist/acp/loop.js +75 -0
  11. package/dist/acp/manager.js +126 -0
  12. package/dist/acp/rate-limit.js +8 -0
  13. package/dist/acp/runner.js +18 -0
  14. package/dist/acp/tools.js +27 -0
  15. package/dist/acp-loop.js +75 -0
  16. package/dist/acp-manager.js +109 -0
  17. package/dist/acp-runner.js +18 -0
  18. package/dist/acp.js +91 -0
  19. package/dist/agent/commands.js +29 -0
  20. package/dist/agent/communication-manager.js +71 -0
  21. package/dist/agent/state.js +58 -0
  22. package/dist/agent-commands.js +29 -0
  23. package/dist/agent-communication-manager.js +71 -0
  24. package/dist/agent-state.js +58 -0
  25. package/dist/app-themes.js +13 -0
  26. package/dist/browser/command.js +104 -0
  27. package/dist/browser/index.js +124 -0
  28. package/dist/browser/tab-helpers.js +51 -0
  29. package/dist/browser/tab.js +99 -0
  30. package/dist/browser-command.js +104 -0
  31. package/dist/browser-tab-helpers.js +51 -0
  32. package/dist/browser-tab.js +99 -0
  33. package/dist/browser.js +124 -0
  34. package/dist/buffer.js +42 -0
  35. package/dist/bus.js +52 -0
  36. package/dist/capture/manager.js +48 -0
  37. package/dist/capture/router.js +18 -0
  38. package/dist/capture-manager.js +48 -0
  39. package/dist/capture-router.js +20 -0
  40. package/dist/chrome-extension-loader.js +62 -0
  41. package/dist/cli-args.js +80 -0
  42. package/dist/command-manager.js +126 -0
  43. package/dist/command-router.js +14 -0
  44. package/dist/commands/acp-reset.js +13 -0
  45. package/dist/commands/acp.js +5 -0
  46. package/dist/commands/agent.js +5 -0
  47. package/dist/commands/broadcast.js +22 -0
  48. package/dist/commands/browser.js +5 -0
  49. package/dist/commands/clear.js +5 -0
  50. package/dist/commands/close.js +42 -0
  51. package/dist/commands/connection.js +5 -0
  52. package/dist/commands/db.js +5 -0
  53. package/dist/commands/edit.js +18 -0
  54. package/dist/commands/files.js +13 -0
  55. package/dist/commands/hist.js +7 -0
  56. package/dist/commands/index.js +62 -0
  57. package/dist/commands/monitor.js +61 -0
  58. package/dist/commands/msg.js +17 -0
  59. package/dist/commands/next.js +7 -0
  60. package/dist/commands/notifications.js +16 -0
  61. package/dist/commands/notify.js +20 -0
  62. package/dist/commands/open.js +41 -0
  63. package/dist/commands/profile.js +5 -0
  64. package/dist/commands/queue.js +37 -0
  65. package/dist/commands/quit.js +6 -0
  66. package/dist/commands/rename.js +12 -0
  67. package/dist/commands/resolve-target.js +12 -0
  68. package/dist/commands/schedule.js +74 -0
  69. package/dist/commands/search.js +26 -0
  70. package/dist/commands/send.js +51 -0
  71. package/dist/commands/state.js +9 -0
  72. package/dist/commands/syntax.js +36 -0
  73. package/dist/commands/tasks.js +7 -0
  74. package/dist/commands/theme.js +41 -0
  75. package/dist/commands/types.js +1 -0
  76. package/dist/commands.js +70 -0
  77. package/dist/completion/browser.js +24 -0
  78. package/dist/completion/fs.js +26 -0
  79. package/dist/completion/handlers.js +81 -0
  80. package/dist/completion/helpers.js +43 -0
  81. package/dist/completion/index.js +43 -0
  82. package/dist/completion-browser.js +24 -0
  83. package/dist/completion-fs.js +26 -0
  84. package/dist/completion-handlers.js +69 -0
  85. package/dist/completion-helpers.js +43 -0
  86. package/dist/completion.js +42 -0
  87. package/dist/config.js +66 -0
  88. package/dist/connection/close.js +41 -0
  89. package/dist/connection/manager.js +93 -0
  90. package/dist/connection/parsing.js +28 -0
  91. package/dist/connection-close.js +41 -0
  92. package/dist/connection-manager.js +93 -0
  93. package/dist/connection-parsing.js +28 -0
  94. package/dist/connections.js +75 -0
  95. package/dist/controller-events.js +34 -0
  96. package/dist/controller-file-tree.js +36 -0
  97. package/dist/controller.js +201 -0
  98. package/dist/database/index.js +83 -0
  99. package/dist/database/manager.js +67 -0
  100. package/dist/database/parsing.js +54 -0
  101. package/dist/database/query.js +41 -0
  102. package/dist/database-manager.js +67 -0
  103. package/dist/database-parsing.js +54 -0
  104. package/dist/database-query.js +41 -0
  105. package/dist/database.js +83 -0
  106. package/dist/datetime.js +15 -0
  107. package/dist/editor/save.js +27 -0
  108. package/dist/editor/sync.js +10 -0
  109. package/dist/editor/watch-manager.js +84 -0
  110. package/dist/editor-save.js +24 -0
  111. package/dist/editor-watch-manager.js +84 -0
  112. package/dist/file-tree-args.js +36 -0
  113. package/dist/file-tree-git-refresh.js +33 -0
  114. package/dist/file-tree-manager.js +252 -0
  115. package/dist/file-tree-moves.js +26 -0
  116. package/dist/file-tree-open.js +56 -0
  117. package/dist/file-tree-rebuild.js +22 -0
  118. package/dist/file-tree-watch.js +27 -0
  119. package/dist/file-tree.js +82 -0
  120. package/dist/git-status.js +64 -0
  121. package/dist/github-token.js +19 -0
  122. package/dist/global-history.js +48 -0
  123. package/dist/harness/auto-approve.js +79 -0
  124. package/dist/harness/busy-status.js +91 -0
  125. package/dist/harness/capture-file.js +28 -0
  126. package/dist/harness/index.js +87 -0
  127. package/dist/harness/manager.js +182 -0
  128. package/dist/harness/models.js +8 -0
  129. package/dist/harness/recorder.js +89 -0
  130. package/dist/harness/recording-file.js +25 -0
  131. package/dist/harness/screen.js +79 -0
  132. package/dist/harness-capture-file.js +28 -0
  133. package/dist/harness-manager.js +112 -0
  134. package/dist/harness-models.js +8 -0
  135. package/dist/harness-screen.js +72 -0
  136. package/dist/harness.js +54 -0
  137. package/dist/index.js +163 -0
  138. package/dist/instance-lock.js +34 -0
  139. package/dist/interactive.js +33 -0
  140. package/dist/main.js +203 -0
  141. package/dist/managers.js +1 -0
  142. package/dist/message-handler.js +134 -0
  143. package/dist/messaging.js +47 -0
  144. package/dist/monitor/acp.js +24 -0
  145. package/dist/monitor/ask.js +31 -0
  146. package/dist/monitor/context.js +27 -0
  147. package/dist/monitor/editor-feed.js +49 -0
  148. package/dist/monitor/feed-diff.js +22 -0
  149. package/dist/monitor/feeds.js +19 -0
  150. package/dist/monitor/framing.js +29 -0
  151. package/dist/monitor/harness-feed.js +21 -0
  152. package/dist/monitor/info.js +29 -0
  153. package/dist/monitor/manager.js +221 -0
  154. package/dist/monitor/page-feed.js +21 -0
  155. package/dist/monitor/parsing.js +82 -0
  156. package/dist/monitor/reply.js +11 -0
  157. package/dist/monitor/session.js +36 -0
  158. package/dist/monitor/stop.js +33 -0
  159. package/dist/monitor/suggestion.js +12 -0
  160. package/dist/monitor/targets.js +69 -0
  161. package/dist/monitor/window.js +109 -0
  162. package/dist/monitor-acp.js +22 -0
  163. package/dist/monitor-info.js +18 -0
  164. package/dist/monitor-manager.js +233 -0
  165. package/dist/monitor-parsing.js +68 -0
  166. package/dist/monitor-session.js +19 -0
  167. package/dist/monitor-targets.js +37 -0
  168. package/dist/monitor-window.js +81 -0
  169. package/dist/notifications-tab.js +32 -0
  170. package/dist/notifications.js +87 -0
  171. package/dist/open-file-manager.js +100 -0
  172. package/dist/openers/editor.js +41 -0
  173. package/dist/openers/external-viewer.js +11 -0
  174. package/dist/openers/image.js +27 -0
  175. package/dist/openers/index.js +16 -0
  176. package/dist/openers/markdown.js +20 -0
  177. package/dist/openers/os-open.js +24 -0
  178. package/dist/openers/page.js +51 -0
  179. package/dist/openers/size.js +13 -0
  180. package/dist/openers/types.js +1 -0
  181. package/dist/page/sync.js +10 -0
  182. package/dist/paths.js +45 -0
  183. package/dist/persona-parsing.js +49 -0
  184. package/dist/personas.js +44 -0
  185. package/dist/profile/agent-opener.js +154 -0
  186. package/dist/profile/files.js +17 -0
  187. package/dist/profile/manager.js +72 -0
  188. package/dist/profile/monitors.js +23 -0
  189. package/dist/profile/notifications.js +11 -0
  190. package/dist/profile-agent-opener.js +136 -0
  191. package/dist/profile-manager.js +72 -0
  192. package/dist/profiles.js +143 -0
  193. package/dist/protocol.js +1 -0
  194. package/dist/pseudoterminal-manager.js +106 -0
  195. package/dist/pty.js +41 -0
  196. package/dist/recognizers/acp.js +38 -0
  197. package/dist/recognizers/analyze.js +49 -0
  198. package/dist/recognizers/bash.js +51 -0
  199. package/dist/recognizers/db.js +26 -0
  200. package/dist/recognizers/index.js +1 -0
  201. package/dist/recognizers/lexicon.js +11 -0
  202. package/dist/recognizers/types.js +5 -0
  203. package/dist/resolve.js +39 -0
  204. package/dist/route-choice.js +11 -0
  205. package/dist/sandbox/index.js +217 -0
  206. package/dist/sandbox/paths.js +178 -0
  207. package/dist/sandbox/profile.js +161 -0
  208. package/dist/sandbox-paths.js +159 -0
  209. package/dist/sandbox-profile.js +161 -0
  210. package/dist/sandbox.js +217 -0
  211. package/dist/schedule/display.js +19 -0
  212. package/dist/schedule/helpers.js +76 -0
  213. package/dist/schedule/index.js +76 -0
  214. package/dist/schedule/manager.js +101 -0
  215. package/dist/schedule/parsing.js +63 -0
  216. package/dist/schedule/time.js +34 -0
  217. package/dist/schedule-display.js +19 -0
  218. package/dist/schedule-helpers.js +76 -0
  219. package/dist/schedule-manager.js +98 -0
  220. package/dist/schedule-parsing.js +63 -0
  221. package/dist/schedule-time.js +34 -0
  222. package/dist/schedule.js +76 -0
  223. package/dist/search-matches.js +40 -0
  224. package/dist/security.js +44 -0
  225. package/dist/shell-manager.js +139 -0
  226. package/dist/shell.js +59 -0
  227. package/dist/ssh-manager.js +42 -0
  228. package/dist/ssh.js +39 -0
  229. package/dist/startup-errors.js +21 -0
  230. package/dist/state-event.js +14 -0
  231. package/dist/state-format.js +38 -0
  232. package/dist/syntax-themes.js +14 -0
  233. package/dist/tab/agent-state.js +19 -0
  234. package/dist/tab/cleanup.js +33 -0
  235. package/dist/tab/colors.js +43 -0
  236. package/dist/tab/creators.js +95 -0
  237. package/dist/tab/dock.js +35 -0
  238. package/dist/tab/focus-history.js +41 -0
  239. package/dist/tab/formatting-handlers.js +77 -0
  240. package/dist/tab/formatting.js +21 -0
  241. package/dist/tab/history.js +14 -0
  242. package/dist/tab/index.js +65 -0
  243. package/dist/tab/manager.js +329 -0
  244. package/dist/tab/openers.js +35 -0
  245. package/dist/tab/queue.js +33 -0
  246. package/dist/tab/rehydrate.js +15 -0
  247. package/dist/tab/reorder.js +15 -0
  248. package/dist/tab/transcript.js +15 -0
  249. package/dist/tab/utils.js +55 -0
  250. package/dist/tab/view.js +42 -0
  251. package/dist/tab-cleanup.js +27 -0
  252. package/dist/tab-colors.js +43 -0
  253. package/dist/tab-creators.js +85 -0
  254. package/dist/tab-formatting-handlers.js +77 -0
  255. package/dist/tab-formatting.js +21 -0
  256. package/dist/tab-manager.js +418 -0
  257. package/dist/tab-utils.js +55 -0
  258. package/dist/tab.js +57 -0
  259. package/dist/tasks.js +32 -0
  260. package/dist/transcript/logger.js +36 -0
  261. package/dist/transcript/store.js +66 -0
  262. package/dist/types.js +6 -0
  263. package/dist/user-agent.js +65 -0
  264. package/dist/word-wrapping.js +42 -0
  265. package/dist/workspace-manager.js +40 -0
  266. package/dist/workspace.js +120 -0
  267. package/help.md +97 -0
  268. 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*)');