botmux 3.2.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/dist/adapters/backend/sandbox.d.ts +47 -0
  2. package/dist/adapters/backend/sandbox.d.ts.map +1 -1
  3. package/dist/adapters/backend/sandbox.js +137 -0
  4. package/dist/adapters/backend/sandbox.js.map +1 -1
  5. package/dist/adapters/cli/pi.d.ts.map +1 -1
  6. package/dist/adapters/cli/pi.js +4 -0
  7. package/dist/adapters/cli/pi.js.map +1 -1
  8. package/dist/adapters/cli/read-isolation.d.ts +72 -3
  9. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  10. package/dist/adapters/cli/read-isolation.js +143 -6
  11. package/dist/adapters/cli/read-isolation.js.map +1 -1
  12. package/dist/adapters/cli/registry.d.ts.map +1 -1
  13. package/dist/adapters/cli/registry.js +8 -2
  14. package/dist/adapters/cli/registry.js.map +1 -1
  15. package/dist/adapters/cli/types.d.ts +8 -1
  16. package/dist/adapters/cli/types.d.ts.map +1 -1
  17. package/dist/cli/deferred-topic-send.d.ts +37 -0
  18. package/dist/cli/deferred-topic-send.d.ts.map +1 -0
  19. package/dist/cli/deferred-topic-send.js +69 -0
  20. package/dist/cli/deferred-topic-send.js.map +1 -0
  21. package/dist/cli/pm2-command.d.ts.map +1 -1
  22. package/dist/cli/pm2-command.js +6 -0
  23. package/dist/cli/pm2-command.js.map +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +173 -34
  26. package/dist/cli.js.map +1 -1
  27. package/dist/core/command-handler.d.ts.map +1 -1
  28. package/dist/core/command-handler.js +17 -12
  29. package/dist/core/command-handler.js.map +1 -1
  30. package/dist/core/dashboard-ipc-server.d.ts +4 -1
  31. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  32. package/dist/core/dashboard-ipc-server.js +99 -8
  33. package/dist/core/dashboard-ipc-server.js.map +1 -1
  34. package/dist/core/deferred-schedule-settlement.d.ts +20 -0
  35. package/dist/core/deferred-schedule-settlement.d.ts.map +1 -0
  36. package/dist/core/deferred-schedule-settlement.js +18 -0
  37. package/dist/core/deferred-schedule-settlement.js.map +1 -0
  38. package/dist/core/deferred-topic-binding.d.ts +19 -0
  39. package/dist/core/deferred-topic-binding.d.ts.map +1 -0
  40. package/dist/core/deferred-topic-binding.js +55 -0
  41. package/dist/core/deferred-topic-binding.js.map +1 -0
  42. package/dist/core/device-isolation-activation.d.ts +56 -0
  43. package/dist/core/device-isolation-activation.d.ts.map +1 -0
  44. package/dist/core/device-isolation-activation.js +110 -0
  45. package/dist/core/device-isolation-activation.js.map +1 -0
  46. package/dist/core/device-isolation-daemon.d.ts +84 -0
  47. package/dist/core/device-isolation-daemon.d.ts.map +1 -0
  48. package/dist/core/device-isolation-daemon.js +591 -0
  49. package/dist/core/device-isolation-daemon.js.map +1 -0
  50. package/dist/core/scheduler.d.ts +26 -20
  51. package/dist/core/scheduler.d.ts.map +1 -1
  52. package/dist/core/scheduler.js +114 -56
  53. package/dist/core/scheduler.js.map +1 -1
  54. package/dist/core/session-discovery.d.ts.map +1 -1
  55. package/dist/core/session-discovery.js +47 -15
  56. package/dist/core/session-discovery.js.map +1 -1
  57. package/dist/core/session-manager.d.ts +17 -1
  58. package/dist/core/session-manager.d.ts.map +1 -1
  59. package/dist/core/session-manager.js +178 -84
  60. package/dist/core/session-manager.js.map +1 -1
  61. package/dist/core/types.d.ts +13 -0
  62. package/dist/core/types.d.ts.map +1 -1
  63. package/dist/core/types.js +10 -0
  64. package/dist/core/types.js.map +1 -1
  65. package/dist/core/worker-pool.d.ts +7 -0
  66. package/dist/core/worker-pool.d.ts.map +1 -1
  67. package/dist/core/worker-pool.js +86 -1
  68. package/dist/core/worker-pool.js.map +1 -1
  69. package/dist/daemon.d.ts.map +1 -1
  70. package/dist/daemon.js +95 -5
  71. package/dist/daemon.js.map +1 -1
  72. package/dist/dashboard/schedule-card-model.d.ts +12 -3
  73. package/dist/dashboard/schedule-card-model.d.ts.map +1 -1
  74. package/dist/dashboard/schedule-card-model.js +32 -8
  75. package/dist/dashboard/schedule-card-model.js.map +1 -1
  76. package/dist/dashboard/web/dashboard-components.d.ts +7 -0
  77. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
  78. package/dist/dashboard/web/dashboard-components.js +11 -2
  79. package/dist/dashboard/web/dashboard-components.js.map +1 -1
  80. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  81. package/dist/dashboard/web/i18n.js +30 -6
  82. package/dist/dashboard/web/i18n.js.map +1 -1
  83. package/dist/dashboard/web/schedules-page.d.ts +2 -0
  84. package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
  85. package/dist/dashboard/web/schedules-page.js +74 -27
  86. package/dist/dashboard/web/schedules-page.js.map +1 -1
  87. package/dist/dashboard-web/app.js +1 -1
  88. package/dist/dashboard-web/chunks/{bot-defaults-page-O37SAQD5.js → bot-defaults-page-B5AY4B2J.js} +1 -1
  89. package/dist/dashboard-web/chunks/{chunk-6QHMLMIM.js → chunk-CUV65BIQ.js} +3 -3
  90. package/dist/dashboard-web/chunks/{chunk-O2DPQL7C.js → chunk-JNRSPKLS.js} +1 -1
  91. package/dist/dashboard-web/chunks/{chunk-FYALHCPG.js → chunk-P6GO5KT4.js} +1 -1
  92. package/dist/dashboard-web/chunks/chunk-QFMH6ITB.js +1 -0
  93. package/dist/dashboard-web/chunks/{chunk-B42PCL5P.js → chunk-UBOLHQCK.js} +1 -1
  94. package/dist/dashboard-web/chunks/{connectors-page-DZBQ42MH.js → connectors-page-KTC2QWNW.js} +1 -1
  95. package/dist/dashboard-web/chunks/{groups-page-3IEGRZKQ.js → groups-page-B3EKGOLY.js} +1 -1
  96. package/dist/dashboard-web/chunks/{insights-page-X3ADRON3.js → insights-page-X6RXNCWX.js} +1 -1
  97. package/dist/dashboard-web/chunks/{monitor-room-IV36RGQY.js → monitor-room-LLRA5LQW.js} +1 -1
  98. package/dist/dashboard-web/chunks/{monitoring-page-ADFKVON4.js → monitoring-page-7MT5FEOS.js} +1 -1
  99. package/dist/dashboard-web/chunks/{office-page-CALAOWNS.js → office-page-KD46D5TD.js} +1 -1
  100. package/dist/dashboard-web/chunks/{overview-page-SLMGLK3M.js → overview-page-JUWNH32O.js} +1 -1
  101. package/dist/dashboard-web/chunks/{roles-page-L7JJ4FFV.js → roles-page-P6F7NLOI.js} +1 -1
  102. package/dist/dashboard-web/chunks/schedules-page-XIHWZST4.js +1 -0
  103. package/dist/dashboard-web/chunks/{sessions-page-OKJ5YWUZ.js → sessions-page-2VECQ27T.js} +1 -1
  104. package/dist/dashboard-web/chunks/{settings-page-P6MI2ZUE.js → settings-page-HMIUE2JA.js} +1 -1
  105. package/dist/dashboard-web/chunks/{skills-page-FT7BVZ6X.js → skills-page-A46KVWBL.js} +1 -1
  106. package/dist/dashboard-web/chunks/{team-federation-page-DEYCPUFB.js → team-federation-page-SDUC56DF.js} +1 -1
  107. package/dist/dashboard-web/chunks/{v3-page-4IOWREZG.js → v3-page-J5JOTUSQ.js} +1 -1
  108. package/dist/dashboard-web/chunks/{whiteboards-page-2VMBETZH.js → whiteboards-page-U7LSOAWI.js} +1 -1
  109. package/dist/dashboard-web/style.css +34 -4
  110. package/dist/dashboard.js +16 -1
  111. package/dist/dashboard.js.map +1 -1
  112. package/dist/desktop/main/auto-start.d.ts +2 -0
  113. package/dist/desktop/main/auto-start.d.ts.map +1 -1
  114. package/dist/desktop/main/auto-start.js +10 -6
  115. package/dist/desktop/main/auto-start.js.map +1 -1
  116. package/dist/desktop/main/bundled-runtime.d.ts +12 -0
  117. package/dist/desktop/main/bundled-runtime.d.ts.map +1 -0
  118. package/dist/desktop/main/bundled-runtime.js +30 -0
  119. package/dist/desktop/main/bundled-runtime.js.map +1 -0
  120. package/dist/desktop/main/device-status.d.ts +15 -0
  121. package/dist/desktop/main/device-status.d.ts.map +1 -0
  122. package/dist/desktop/main/device-status.js +94 -0
  123. package/dist/desktop/main/device-status.js.map +1 -0
  124. package/dist/desktop/main/external-runtime.d.ts +9 -0
  125. package/dist/desktop/main/external-runtime.d.ts.map +1 -1
  126. package/dist/desktop/main/external-runtime.js +88 -18
  127. package/dist/desktop/main/external-runtime.js.map +1 -1
  128. package/dist/desktop/main/ipc.d.ts.map +1 -1
  129. package/dist/desktop/main/ipc.js +1 -0
  130. package/dist/desktop/main/ipc.js.map +1 -1
  131. package/dist/desktop/main/node-command.d.ts +21 -0
  132. package/dist/desktop/main/node-command.d.ts.map +1 -1
  133. package/dist/desktop/main/node-command.js +41 -0
  134. package/dist/desktop/main/node-command.js.map +1 -1
  135. package/dist/desktop/main/pm2-apps.d.ts +4 -2
  136. package/dist/desktop/main/pm2-apps.d.ts.map +1 -1
  137. package/dist/desktop/main/pm2-apps.js +11 -3
  138. package/dist/desktop/main/pm2-apps.js.map +1 -1
  139. package/dist/desktop/main/runtime-service.d.ts +21 -5
  140. package/dist/desktop/main/runtime-service.d.ts.map +1 -1
  141. package/dist/desktop/main/runtime-service.js +94 -10
  142. package/dist/desktop/main/runtime-service.js.map +1 -1
  143. package/dist/desktop/main/runtime-source.d.ts +2 -0
  144. package/dist/desktop/main/runtime-source.d.ts.map +1 -1
  145. package/dist/desktop/main/runtime-source.js +12 -6
  146. package/dist/desktop/main/runtime-source.js.map +1 -1
  147. package/dist/desktop/main/tray.d.ts +1 -0
  148. package/dist/desktop/main/tray.d.ts.map +1 -1
  149. package/dist/desktop/main/tray.js +3 -3
  150. package/dist/desktop/main/tray.js.map +1 -1
  151. package/dist/desktop/main.js +28 -8
  152. package/dist/desktop/main.js.map +1 -1
  153. package/dist/desktop/preload.js +3 -0
  154. package/dist/desktop/preload.js.map +1 -1
  155. package/dist/desktop/renderer/app.d.ts +2 -1
  156. package/dist/desktop/renderer/app.d.ts.map +1 -1
  157. package/dist/desktop/renderer/app.js +6 -1
  158. package/dist/desktop/renderer/app.js.map +1 -1
  159. package/dist/desktop/renderer/runtime-state.d.ts.map +1 -1
  160. package/dist/desktop/renderer/runtime-state.js +2 -0
  161. package/dist/desktop/renderer/runtime-state.js.map +1 -1
  162. package/dist/desktop/shared/shell-path-probes.d.ts +24 -0
  163. package/dist/desktop/shared/shell-path-probes.d.ts.map +1 -0
  164. package/dist/desktop/shared/shell-path-probes.js +38 -0
  165. package/dist/desktop/shared/shell-path-probes.js.map +1 -0
  166. package/dist/desktop/shared/types.d.ts +21 -1
  167. package/dist/desktop/shared/types.d.ts.map +1 -1
  168. package/dist/desktop/smoke.d.ts +1 -0
  169. package/dist/desktop/smoke.d.ts.map +1 -1
  170. package/dist/desktop/smoke.js +20 -5
  171. package/dist/desktop/smoke.js.map +1 -1
  172. package/dist/i18n/en.d.ts.map +1 -1
  173. package/dist/i18n/en.js +16 -7
  174. package/dist/i18n/en.js.map +1 -1
  175. package/dist/i18n/zh.d.ts.map +1 -1
  176. package/dist/i18n/zh.js +16 -7
  177. package/dist/i18n/zh.js.map +1 -1
  178. package/dist/im/lark/card-handler.d.ts.map +1 -1
  179. package/dist/im/lark/card-handler.js +3 -0
  180. package/dist/im/lark/card-handler.js.map +1 -1
  181. package/dist/im/lark/event-dispatcher.d.ts +1 -0
  182. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  183. package/dist/im/lark/event-dispatcher.js +1 -1
  184. package/dist/im/lark/event-dispatcher.js.map +1 -1
  185. package/dist/im/lark/schedules-card.d.ts +2 -2
  186. package/dist/im/lark/schedules-card.d.ts.map +1 -1
  187. package/dist/im/lark/schedules-card.js +50 -34
  188. package/dist/im/lark/schedules-card.js.map +1 -1
  189. package/dist/index-daemon.js +13 -1
  190. package/dist/index-daemon.js.map +1 -1
  191. package/dist/platform/bind.d.ts +4 -1
  192. package/dist/platform/bind.d.ts.map +1 -1
  193. package/dist/platform/bind.js +10 -1
  194. package/dist/platform/bind.js.map +1 -1
  195. package/dist/platform/binding.d.ts.map +1 -1
  196. package/dist/platform/binding.js +15 -9
  197. package/dist/platform/binding.js.map +1 -1
  198. package/dist/platform/device-command.d.ts +35 -0
  199. package/dist/platform/device-command.d.ts.map +1 -0
  200. package/dist/platform/device-command.js +187 -0
  201. package/dist/platform/device-command.js.map +1 -0
  202. package/dist/platform/device-enroll.d.ts +156 -0
  203. package/dist/platform/device-enroll.d.ts.map +1 -0
  204. package/dist/platform/device-enroll.js +781 -0
  205. package/dist/platform/device-enroll.js.map +1 -0
  206. package/dist/platform/device-isolation-activation-client.d.ts +28 -0
  207. package/dist/platform/device-isolation-activation-client.d.ts.map +1 -0
  208. package/dist/platform/device-isolation-activation-client.js +279 -0
  209. package/dist/platform/device-isolation-activation-client.js.map +1 -0
  210. package/dist/platform/device-isolation.d.ts +35 -0
  211. package/dist/platform/device-isolation.d.ts.map +1 -0
  212. package/dist/platform/device-isolation.js +90 -0
  213. package/dist/platform/device-isolation.js.map +1 -0
  214. package/dist/platform/device-paths.d.ts +9 -0
  215. package/dist/platform/device-paths.d.ts.map +1 -0
  216. package/dist/platform/device-paths.js +14 -0
  217. package/dist/platform/device-paths.js.map +1 -0
  218. package/dist/platform/device.d.ts +77 -0
  219. package/dist/platform/device.d.ts.map +1 -0
  220. package/dist/platform/device.js +212 -0
  221. package/dist/platform/device.js.map +1 -0
  222. package/dist/platform/host-command-context.d.ts +15 -0
  223. package/dist/platform/host-command-context.d.ts.map +1 -0
  224. package/dist/platform/host-command-context.js +26 -0
  225. package/dist/platform/host-command-context.js.map +1 -0
  226. package/dist/platform/secure-host-file.d.ts +12 -0
  227. package/dist/platform/secure-host-file.d.ts.map +1 -0
  228. package/dist/platform/secure-host-file.js +182 -0
  229. package/dist/platform/secure-host-file.js.map +1 -0
  230. package/dist/services/schedule-store.d.ts +6 -2
  231. package/dist/services/schedule-store.d.ts.map +1 -1
  232. package/dist/services/schedule-store.js +19 -4
  233. package/dist/services/schedule-store.js.map +1 -1
  234. package/dist/skills/definitions.d.ts.map +1 -1
  235. package/dist/skills/definitions.js +4 -5
  236. package/dist/skills/definitions.js.map +1 -1
  237. package/dist/types.d.ts +45 -8
  238. package/dist/types.d.ts.map +1 -1
  239. package/dist/utils/atomic-write.d.ts +13 -0
  240. package/dist/utils/atomic-write.d.ts.map +1 -1
  241. package/dist/utils/atomic-write.js +89 -9
  242. package/dist/utils/atomic-write.js.map +1 -1
  243. package/dist/utils/child-env.d.ts +40 -1
  244. package/dist/utils/child-env.d.ts.map +1 -1
  245. package/dist/utils/child-env.js +45 -0
  246. package/dist/utils/child-env.js.map +1 -1
  247. package/dist/utils/pending-input-queue.d.ts +10 -0
  248. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  249. package/dist/utils/pending-input-queue.js +15 -0
  250. package/dist/utils/pending-input-queue.js.map +1 -1
  251. package/dist/worker.js +391 -28
  252. package/dist/worker.js.map +1 -1
  253. package/dist/workflows/events/payloads.d.ts +4 -4
  254. package/dist/workflows/events/schema.d.ts +16 -16
  255. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +3 -1
  256. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
  257. package/dist/workflows/hostExecutors/botmux-schedule.js +14 -3
  258. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
  259. package/dist/workflows/v3/architect.js +1 -1
  260. package/dist/workflows/v3/architect.js.map +1 -1
  261. package/dist/workflows/v3/contract.d.ts +8 -0
  262. package/dist/workflows/v3/contract.d.ts.map +1 -1
  263. package/dist/workflows/v3/contract.js.map +1 -1
  264. package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
  265. package/dist/workflows/v3/daemon-run.js +2 -0
  266. package/dist/workflows/v3/daemon-run.js.map +1 -1
  267. package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -1
  268. package/dist/workflows/v3/ephemeral-pool.js +10 -2
  269. package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
  270. package/dist/workflows/v3/run-envelope.d.ts +24 -24
  271. package/dist/workflows/v3/runtime.d.ts +5 -0
  272. package/dist/workflows/v3/runtime.d.ts.map +1 -1
  273. package/dist/workflows/v3/runtime.js +1 -0
  274. package/dist/workflows/v3/runtime.js.map +1 -1
  275. package/package.json +6 -3
  276. package/dist/dashboard-web/chunks/chunk-I35DXGM4.js +0 -1
  277. package/dist/dashboard-web/chunks/schedules-page-Z3672POL.js +0 -1
package/dist/worker.js CHANGED
@@ -13,12 +13,12 @@
13
13
  * 7. On 'restart', kills CLI and re-spawns with --resume
14
14
  */
15
15
  import { randomBytes } from 'node:crypto';
16
- import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
16
+ import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
17
17
  import { atomicWriteFileSync } from './utils/atomic-write.js';
18
18
  import { join, basename, dirname } from 'node:path';
19
19
  import { homedir, tmpdir } from 'node:os';
20
20
  import { spawnSync } from 'node:child_process';
21
- import { evaluateReadIsolationGate, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildReadIsolationProtectedWriteRules, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, isolationPaneMarkerContent, } from './adapters/cli/read-isolation.js';
21
+ import { evaluateReadIsolationGate, evaluateCredentialOnlyIsolationGate, credentialIsolationRequired, deviceCredentialIsolationMarkerPath, isCredentialIsolationReservedBasename, buildCredentialIsolationRules, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildReadIsolationProtectedWriteRules, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, isolationPaneMarkerContent, } from './adapters/cli/read-isolation.js';
22
22
  import { killPersistentSession } from './core/persistent-backend.js';
23
23
  import { readProcessStartIdentity } from './core/session-marker.js';
24
24
  import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
@@ -29,7 +29,7 @@ import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirs
29
29
  import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
30
30
  import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
31
31
  import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
32
- import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, shouldDeferArgsBakedDurablePrompt, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
32
+ import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
33
33
  import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
34
34
  import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
35
35
  import { sanitizePerBotEnv } from './core/per-bot-env.js';
@@ -88,7 +88,8 @@ import { zellijEnv } from './setup/ensure-zellij.js';
88
88
  import { isObserveBackend } from './adapters/backend/types.js';
89
89
  import { selectSessionBackend, decideBackendGate, backendGateUserMessage } from './adapters/backend/session-backend-selector.js';
90
90
  import { deriveRiffReposFromDirs, deriveRiffRepoFromWorkingDir, isValidRiffBaseUrl, isValidRiffSandboxCluster, } from './adapters/backend/riff-backend.js';
91
- import { prepareSandbox, attachSandboxOutbox, startOutboxWatcher, sandboxEnabled, sandboxedClaudeDataDir, localSandboxApplies } from './adapters/backend/sandbox.js';
91
+ import { prepareSandbox, prepareCredentialOnlySandbox, credentialOnlySandboxAvailable, probeHostCredentialIsolationMechanism, attachSandboxOutbox, startOutboxWatcher, sandboxEnabled, sandboxedClaudeDataDir, localSandboxApplies, } from './adapters/backend/sandbox.js';
92
+ import { DEVICE_AUTHORITY_DIRECTORY, DEVICE_CREDENTIAL_FILE, } from './platform/device-paths.js';
92
93
  import { tmuxEnv, probeTmuxFunctionalWithRetry } from './setup/ensure-tmux.js';
93
94
  import { tmuxRestartJitterMs } from './core/tmux-recovery.js';
94
95
  import { IdleDetector } from './utils/idle-detector.js';
@@ -100,7 +101,7 @@ import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web
100
101
  import { parseWorkerRequestUrl } from './utils/worker-http.js';
101
102
  import { detectCliUsageLimit, usageLimitStateKey } from './utils/cli-usage-limit.js';
102
103
  import { uploadImageBuffer } from './utils/lark-upload.js';
103
- import { redactChildEnv } from './utils/child-env.js';
104
+ import { redactChildEnv, scrubSessionCliHomeEnv } from './utils/child-env.js';
104
105
  import { decideSubmitConfirmationAction } from './services/submit-confirmation.js';
105
106
  import { config, resolveChatBotDiscoveryConfig } from './config.js';
106
107
  import * as sessionStore from './services/session-store.js';
@@ -114,6 +115,22 @@ import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.
114
115
  import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
115
116
  import { hasMatchingManagedOriginCapability, managedOriginCapabilityPath, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, } from './core/managed-origin-capability.js';
116
117
  import { CodexRpcEngine } from './codex-rpc-engine.js';
118
+ // A worker must never trust an INHERITED session-level CLI home pointer
119
+ // (CLAUDE_CONFIG_DIR / CODEX_HOME): a stale pm2 dump can resurrect the daemon
120
+ // with a sibling bot's home baked in (see the index-daemon.ts boot scrub —
121
+ // this one covers any worker spawn path that bypasses it). Scrubbing
122
+ // process.env — not just the spawned child's env — matters because worker-side
123
+ // resolvers consult it dynamically (codex submit confirmation / resume
124
+ // fallback / transcript bridge via services/codex-paths.ts, slash-command
125
+ // discovery), and childEnv is seeded from process.env below: cleaning only one
126
+ // side would leave the worker watching a different data root than the CLI
127
+ // writes (the exact failure mode documented at the isolated-codex re-pin in
128
+ // spawnCli). Per-session values are re-pinned AFTER this scrub — isolation
129
+ // sets process.env.CODEX_HOME / childEnv, adapter spawnEnv re-injects fork
130
+ // dirs. See SESSION_CLI_HOME_ENV_KEYS for why deleting beats pinning a
131
+ // default (~/.claude relocates Claude's state file → onboarding rerun) and
132
+ // why GROK_HOME is exempt.
133
+ scrubSessionCliHomeEnv(process.env);
117
134
  // ─── State ───────────────────────────────────────────────────────────────────
118
135
  let cliAdapter = null;
119
136
  let backend = null;
@@ -673,6 +690,10 @@ let lastCliExitSignal = null;
673
690
  * of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
674
691
  let isPipeMode = false;
675
692
  let effectiveBackendType = 'pty';
693
+ /** Worker-owned statement about the confinement attached to the CURRENT CLI
694
+ * generation. The daemon receives this over private IPC; child-writable PID
695
+ * marker files remain diagnostics only. */
696
+ let currentCliCredentialIsolated = false;
676
697
  /** pty-under-zellij backend (BACKEND_TYPE=zellij). Behaves like the non-tmux
677
698
  * pty path for the worker (renderer screenshots, relay web terminal) but owns
678
699
  * a persistent zellij session that survives daemon restart. */
@@ -736,6 +757,8 @@ function ensureZellijAttachConfig() {
736
757
  }
737
758
  let sessionId = '';
738
759
  let lastInitConfig = null;
760
+ let deferredTopicOutputTail = '';
761
+ const reportedDeferredTopicRoots = new Set();
739
762
  const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', genius: 'Genius', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', kimi: 'Kimi', grok: 'Grok Build', 'kiro-cli': 'Kiro', riff: 'Riff' };
740
763
  function cliName() { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'CLI'; }
741
764
  let isPromptReady = false;
@@ -4370,12 +4393,52 @@ function maybeCaptureKiroSessionId(data) {
4370
4393
  function splitCodexAppControl(data) {
4371
4394
  return appRunnerControlDecoder.push(data, APP_RUNNER_OSC_CLI_IDS.has(lastInitConfig?.cliId ?? ''), handleCodexAppMarker);
4372
4395
  }
4396
+ /** Riff runs `botmux send` in a remote filesystem, so its local binding
4397
+ * sidecar is invisible to the host daemon. The CLI includes the claimed root
4398
+ * in its success JSON; harvest that trusted-session/turn tuple from terminal
4399
+ * output and forward it over worker IPC. The daemon accepts this relay only
4400
+ * for Riff and verifies the claimed root belongs to the target chat; local
4401
+ * backends use the host-visible sidecar directly. */
4402
+ function maybeReportDeferredTopicMaterialization(data) {
4403
+ const run = lastInitConfig?.deferredScheduleRun;
4404
+ if (!run)
4405
+ return;
4406
+ deferredTopicOutputTail = tailChars(deferredTopicOutputTail + stripAnsiForLog(data), 16_000);
4407
+ const lines = deferredTopicOutputTail.split(/\r?\n/);
4408
+ deferredTopicOutputTail = lines.pop() ?? '';
4409
+ for (const line of lines) {
4410
+ const start = line.indexOf('{"success":true');
4411
+ const end = line.lastIndexOf('}');
4412
+ if (start < 0 || end < start)
4413
+ continue;
4414
+ try {
4415
+ const parsed = JSON.parse(line.slice(start, end + 1));
4416
+ const root = parsed.deferredTopicRootMessageId;
4417
+ if (parsed.success !== true
4418
+ || parsed.sessionId !== sessionId
4419
+ || parsed.turnId !== run.turnId
4420
+ || typeof root !== 'string'
4421
+ || !root.startsWith('om_')
4422
+ || reportedDeferredTopicRoots.has(root))
4423
+ continue;
4424
+ reportedDeferredTopicRoots.add(root);
4425
+ send({
4426
+ type: 'deferred_topic_materialized',
4427
+ sessionId,
4428
+ turnId: run.turnId,
4429
+ rootMessageId: root,
4430
+ });
4431
+ }
4432
+ catch { /* not a botmux send JSON line */ }
4433
+ }
4434
+ }
4373
4435
  // ─── Prompt Detection ────────────────────────────────────────────────────────
4374
4436
  function onPtyData(data) {
4375
4437
  data = splitCodexAppControl(data);
4376
4438
  if (data.length === 0)
4377
4439
  return;
4378
4440
  lastPtyActivityAtMs = Date.now();
4441
+ maybeReportDeferredTopicMaterialization(data);
4379
4442
  maybeCaptureKiroSessionId(data);
4380
4443
  captureWorkflowTranscript(data);
4381
4444
  renderer?.write(data);
@@ -5523,6 +5586,40 @@ function scheduleBusyPatternIdleProbe(source) {
5523
5586
  }
5524
5587
  async function spawnCli(cfg, opts = {}) {
5525
5588
  clearSessionRenameInFlight();
5589
+ currentCliCredentialIsolated = false;
5590
+ // Enrollment writes the fixed marker before any device credential appears.
5591
+ // From that instant onward every NEW local CLI must carry a credential
5592
+ // boundary, regardless of adapter capability or optional sandbox toggles.
5593
+ // lstat (not existsSync) deliberately treats a hostile/broken symlink as a
5594
+ // present authority signal and therefore fails closed.
5595
+ const hostHomeDir = homedir();
5596
+ const defaultBotmuxHome = join(hostHomeDir, '.botmux');
5597
+ const configuredBotmuxHome = process.env.SESSION_DATA_DIR
5598
+ ? dirname(process.env.SESSION_DATA_DIR)
5599
+ : defaultBotmuxHome;
5600
+ const hostEntryExistsNoFollow = (path) => {
5601
+ try {
5602
+ lstatSync(path);
5603
+ return true;
5604
+ }
5605
+ catch {
5606
+ return false;
5607
+ }
5608
+ };
5609
+ const deviceIsolationMarkerExists = hostEntryExistsNoFollow(deviceCredentialIsolationMarkerPath(hostHomeDir));
5610
+ const deviceCredentialExists = [...new Set([defaultBotmuxHome, configuredBotmuxHome])]
5611
+ .some(root => hostEntryExistsNoFollow(join(root, DEVICE_AUTHORITY_DIRECTORY, DEVICE_CREDENTIAL_FILE))
5612
+ // Upgrade fail-safe: a pre-dedicated-directory credential still activates
5613
+ // mandatory confinement until the host explicitly removes/migrates it.
5614
+ || hostEntryExistsNoFollow(join(root, DEVICE_CREDENTIAL_FILE)));
5615
+ const mandatoryCredentialIsolation = credentialIsolationRequired({
5616
+ markerExists: deviceIsolationMarkerExists,
5617
+ deviceCredentialExists,
5618
+ });
5619
+ if (mandatoryCredentialIsolation && cfg.adoptMode) {
5620
+ throw new Error(`[device-credential-isolation] refusing adopt session ${cfg.sessionId}: `
5621
+ + 'an already-running external CLI cannot be retrofitted with the mandatory credential boundary');
5622
+ }
5526
5623
  // (startupCommands one-shot is re-armed below, AFTER the reattach-vs-fresh
5527
5624
  // prediction — only a genuinely fresh CLI process replays them; see
5528
5625
  // willReattachPersistent.)
@@ -5746,6 +5843,15 @@ async function spawnCli(cfg, opts = {}) {
5746
5843
  sessionEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
5747
5844
  if (cfg.turnId)
5748
5845
  sessionEnv.BOTMUX_TURN_ID = cfg.turnId;
5846
+ if (cfg.deferredScheduleRun) {
5847
+ sessionEnv.BOTMUX_DEFERRED_SCHEDULE_TASK_ID = cfg.deferredScheduleRun.taskId;
5848
+ sessionEnv.BOTMUX_DEFERRED_SCHEDULE_TURN_ID = cfg.deferredScheduleRun.turnId;
5849
+ sessionEnv.BOTMUX_DEFERRED_SCHEDULE_ROUTING_ANCHOR = cfg.deferredScheduleRun.routingAnchor;
5850
+ sessionEnv.BOTMUX_DEFERRED_SCHEDULE_CREATED_AT = cfg.deferredScheduleRun.createdAt;
5851
+ if (cfg.deferredScheduleRun.topicTitle) {
5852
+ sessionEnv.BOTMUX_DEFERRED_SCHEDULE_TOPIC_TITLE = cfg.deferredScheduleRun.topicTitle;
5853
+ }
5854
+ }
5749
5855
  // Turn sender so `--mention-back` can resolve an @ target in the sandbox.
5750
5856
  if (cfg.ownerOpenId)
5751
5857
  sessionEnv.BOTMUX_OWNER_OPEN_ID = cfg.ownerOpenId;
@@ -5873,6 +5979,56 @@ async function spawnCli(cfg, opts = {}) {
5873
5979
  // established (unsandboxable backend, no SESSION_DATA_DIR) is a hard error at the
5874
5980
  // spawn site below, never a silent unconfined run.
5875
5981
  const willWriteSandbox = process.platform === 'darwin' && wantsFileSandbox;
5982
+ if (willWriteSandbox && !process.env.SESSION_DATA_DIR) {
5983
+ throw new Error(`[file-sandbox] refusing to start session ${cfg.sessionId}: missing SESSION_DATA_DIR`);
5984
+ }
5985
+ const fullIsolationCoversCredentials = !riffRemoteBackend && ((process.platform === 'darwin' && (willReadIsolate || willWriteSandbox))
5986
+ || (process.platform === 'linux' && wantsFileSandbox));
5987
+ let credentialMechanismAvailable = true;
5988
+ let credentialMechanismExecutable;
5989
+ if (mandatoryCredentialIsolation && !riffRemoteBackend && !fullIsolationCoversCredentials) {
5990
+ if (process.platform === 'darwin') {
5991
+ const probe = probeHostCredentialIsolationMechanism();
5992
+ credentialMechanismAvailable = probe.supported;
5993
+ if (probe.supported)
5994
+ credentialMechanismExecutable = probe.executable;
5995
+ }
5996
+ else {
5997
+ credentialMechanismAvailable = process.platform === 'linux'
5998
+ ? credentialOnlySandboxAvailable()
5999
+ : false;
6000
+ }
6001
+ }
6002
+ const credentialIsolationGate = evaluateCredentialOnlyIsolationGate({
6003
+ markerExists: deviceIsolationMarkerExists,
6004
+ deviceCredentialExists,
6005
+ remoteBackend: riffRemoteBackend,
6006
+ platform: process.platform,
6007
+ mechanismAvailable: credentialMechanismAvailable,
6008
+ fullIsolationCoversCredentials,
6009
+ });
6010
+ if (credentialIsolationGate.mode === 'blocked') {
6011
+ throw new Error(`[device-credential-isolation] refusing to start session ${cfg.sessionId}: `
6012
+ + credentialIsolationGate.failClosedReason);
6013
+ }
6014
+ const credentialBoundaryActive = credentialIsolationGate.required
6015
+ && credentialIsolationGate.mode !== 'remote-bypass';
6016
+ const credentialOnlySeatbelt = credentialIsolationGate.mode === 'seatbelt';
6017
+ const credentialOnlyBwrap = credentialIsolationGate.mode === 'bwrap';
6018
+ const appliedIsolationCapabilities = [];
6019
+ if (credentialBoundaryActive
6020
+ || willReadIsolate
6021
+ || (process.platform === 'linux' && wantsFileSandbox)) {
6022
+ appliedIsolationCapabilities.push('credential');
6023
+ }
6024
+ if (willReadIsolate)
6025
+ appliedIsolationCapabilities.push('read');
6026
+ if (willWriteSandbox || (process.platform === 'linux' && wantsFileSandbox)) {
6027
+ appliedIsolationCapabilities.push('write');
6028
+ }
6029
+ currentCliCredentialIsolated = appliedIsolationCapabilities.includes('credential');
6030
+ const isolationRuntimeDataDir = process.env.SESSION_DATA_DIR
6031
+ ?? join(defaultBotmuxHome, 'data');
5876
6032
  // Every bot — isolated OR not — gets its own BOT_HOME dir as a ready-made private-
5877
6033
  // storage slot. An isolated sibling denies this path regardless of whether the owner
5878
6034
  // is isolated (deny uses the full bots.json), so a non-isolated bot can drop private
@@ -5954,7 +6110,7 @@ async function spawnCli(cfg, opts = {}) {
5954
6110
  // so the probe below sees no pane and we cold-spawn fresh isolated. A current-
5955
6111
  // policy pane survives daemon restarts and suspend→resume safely because the
5956
6112
  // confinement remains attached to the live process.
5957
- if ((willReadIsolate || willWriteSandbox) && persistentSessionName && effectiveBackendType !== 'pty') {
6113
+ if (appliedIsolationCapabilities.length > 0 && persistentSessionName && effectiveBackendType !== 'pty') {
5958
6114
  const paneLive = effectiveBackendType === 'tmux'
5959
6115
  ? TmuxBackend.hasSession(persistentSessionName)
5960
6116
  : effectiveBackendType === 'zellij'
@@ -5962,8 +6118,8 @@ async function spawnCli(cfg, opts = {}) {
5962
6118
  : HerdrBackend.hasSession(persistentSessionName);
5963
6119
  if (paneLive) {
5964
6120
  let marker = null;
5965
- marker = readRegularHostFileNoFollow(join(process.env.SESSION_DATA_DIR ?? '', 'read-isolation', `${cfg.sessionId}.boot`));
5966
- if (isolatedPaneReattachSafe(marker)) {
6121
+ marker = readRegularHostFileNoFollow(join(isolationRuntimeDataDir, 'read-isolation', `${cfg.sessionId}.boot`));
6122
+ if (isolatedPaneReattachSafe(marker, appliedIsolationCapabilities)) {
5967
6123
  // Pane was spawned under the current isolation policy → still confined
5968
6124
  // on the running process across daemon restarts; warm reattach preserves
5969
6125
  // resume/context + tmux idle-suspend.
@@ -6135,6 +6291,8 @@ async function spawnCli(cfg, opts = {}) {
6135
6291
  // Also defer on RESUME for adapters whose initial-prompt launch flag is
6136
6292
  // silently ignored when continuing a session (OpenCode `--prompt` + `-s`):
6137
6293
  // baking it into args would drop the message that triggered the resume.
6294
+ // Finally, defer adapter-declared over-limit prompts to avoid backend command
6295
+ // string limits (tmux "command too long") while preserving short argv prompts.
6138
6296
  const deferInitialPrompt = shouldDeferInitialPromptForStartup({
6139
6297
  hasStartupCommands: !!cfg.startupCommands?.length,
6140
6298
  adoptMode: cfg.adoptMode === true,
@@ -6143,7 +6301,12 @@ async function spawnCli(cfg, opts = {}) {
6143
6301
  passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
6144
6302
  adoptMode: cfg.adoptMode === true,
6145
6303
  dispatchAttempt: cfg.dispatchAttempt,
6146
- }) || (effectiveResume && cliAdapter.initialPromptArgsIgnoredOnResume === true);
6304
+ }) || (effectiveResume && cliAdapter.initialPromptArgsIgnoredOnResume === true)
6305
+ || shouldDeferInitialPromptForArgLimit({
6306
+ passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
6307
+ prompt: cfg.prompt,
6308
+ maxInitialPromptArgBytes: cliAdapter.maxInitialPromptArgBytes,
6309
+ });
6147
6310
  kiroSessionIdCaptureArmed = cfg.cliId === 'kiro-cli' && !effectiveCliSessionId && !willReattachPersistent;
6148
6311
  kiroSessionIdCaptureBuffer = '';
6149
6312
  // Per-bot local read isolation: assemble the Seatbelt profile context (the gate
@@ -6196,16 +6359,20 @@ async function spawnCli(cfg, opts = {}) {
6196
6359
  const sessionDataDir = process.env.SESSION_DATA_DIR;
6197
6360
  // Regex rules cannot be realpath'd after construction, so build their home
6198
6361
  // prefix from the canonical path up front (Seatbelt matches canonical paths).
6199
- const sandboxHome = (() => { try {
6200
- return realpathSync(homedir());
6201
- }
6202
- catch {
6203
- return homedir();
6204
- } })();
6362
+ const canonicalSandboxPath = (path) => {
6363
+ try {
6364
+ return realpathSync(path);
6365
+ }
6366
+ catch {
6367
+ return path;
6368
+ }
6369
+ };
6370
+ const sandboxHome = canonicalSandboxPath(homedir());
6205
6371
  writeSandboxRules = buildWriteSandboxRules({
6206
6372
  homeDir: sandboxHome,
6207
- botmuxHome: dirname(sessionDataDir),
6208
- sessionDataDir,
6373
+ botmuxHome: canonicalSandboxPath(dirname(sessionDataDir)),
6374
+ defaultBotmuxHome: canonicalSandboxPath(join(homedir(), '.botmux')),
6375
+ sessionDataDir: canonicalSandboxPath(sessionDataDir),
6209
6376
  workingDir: cfg.workingDir,
6210
6377
  currentAppId: cfg.larkAppId,
6211
6378
  // TMPDIR on macOS resolves under /private/var/folders (already allowed), but
@@ -6324,6 +6491,15 @@ async function spawnCli(cfg, opts = {}) {
6324
6491
  delete childEnv.BOTMUX_CHAT_TYPE;
6325
6492
  childEnv.BOTMUX_LARK_APP_ID = cfg.larkAppId;
6326
6493
  childEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
6494
+ // Session owner under the standard BOTMUX_* name (the riff backend already
6495
+ // exposes it; `__OWNER_OPEN_ID` stays for back-compat). Custom CLI wrappers
6496
+ // use it for per-user permission isolation. Advisory identity only — botmux's
6497
+ // own authz paths (v3 command authority, host.ts) ignore env owner values and
6498
+ // resolve the current-turn caller from turn provenance instead.
6499
+ if (cfg.ownerOpenId)
6500
+ childEnv.BOTMUX_OWNER_OPEN_ID = cfg.ownerOpenId;
6501
+ else
6502
+ delete childEnv.BOTMUX_OWNER_OPEN_ID;
6327
6503
  // NOTE: under read isolation `botmux send` gets this bot's secret from the worker-
6328
6504
  // written cred FILE in its BOT_HOME (send-cred.json, see sendCredFilePath) located
6329
6505
  // via the BOTMUX_LARK_APP_ID above — NOT from the env. The secret is deliberately kept OUT
@@ -6353,11 +6529,16 @@ async function spawnCli(cfg, opts = {}) {
6353
6529
  // them past the server's global env.
6354
6530
  if (cliAdapter.spawnEnv)
6355
6531
  Object.assign(childEnv, cliAdapter.spawnEnv);
6356
- // v2 read isolation: point the CLI at its PER-BOT config dir (set AFTER spawnEnv so
6357
- // it overrides any adapter default). claude → CLAUDE_CONFIG_DIR, codex → CODEX_HOME.
6358
- // Both are in BOTMUX_INJECTED_ENV_KEYS so the tmux backend forwards them into the
6359
- // pane; without this the CLI falls back to the global ~/.claude|~/.codex which the
6360
- // Seatbelt wrapper denies → it can't read its own data and won't start.
6532
+ // v2 read isolation: point the CLI at its PER-BOT config dir (set AFTER spawnEnv
6533
+ // so it overrides any adapter default). claude → CLAUDE_CONFIG_DIR, codex →
6534
+ // CODEX_HOME. Both are in BOTMUX_INJECTED_ENV_KEYS so the tmux backend forwards
6535
+ // them into the pane; without this the CLI falls back to the global
6536
+ // ~/.claude|~/.codex which the Seatbelt wrapper denies → it can't read its own
6537
+ // data and won't start. Non-isolated sessions get NO explicit value: the
6538
+ // worker-boot scrubSessionCliHomeEnv (module top) plus the pane wrapper's
6539
+ // unset clause (PANE_ENV_UNSET_CLAUSE covers BOTMUX_INJECTED_ENV_KEYS)
6540
+ // already guarantee the pane starts clean, and the CLI's built-in default
6541
+ // preserves stock semantics.
6361
6542
  if (isolationBotHome) {
6362
6543
  if (claudeDataDir)
6363
6544
  childEnv.CLAUDE_CONFIG_DIR = claudeDataDir; // = <BOT_HOME>/claude
@@ -6448,6 +6629,31 @@ async function spawnCli(cfg, opts = {}) {
6448
6629
  sessionDataDirs: [readIsolationCtx.sessionDataDir],
6449
6630
  });
6450
6631
  }
6632
+ if (credentialBoundaryActive) {
6633
+ const credentialRules = buildCredentialIsolationRules({
6634
+ homeDir: canonical(hostHomeDir),
6635
+ botmuxHome: canonical(configuredBotmuxHome),
6636
+ defaultBotmuxHome: canonical(defaultBotmuxHome),
6637
+ });
6638
+ denyPaths = [...new Set([...denyPaths, ...credentialRules.denyPaths.map(canonical)])];
6639
+ denyRegexes = [...new Set([...denyRegexes, ...credentialRules.denyRegexes])];
6640
+ protectedWrites = {
6641
+ denyWritePaths: [...new Set([
6642
+ ...(protectedWrites?.denyWritePaths ?? []),
6643
+ ...credentialRules.denyWritePaths.map(canonical),
6644
+ ])],
6645
+ denyWriteRegexes: [...new Set([
6646
+ ...(protectedWrites?.denyWriteRegexes ?? []),
6647
+ ...credentialRules.denyWriteRegexes,
6648
+ ])],
6649
+ denyWriteLiterals: [...new Set([
6650
+ ...(protectedWrites?.denyWriteLiterals ?? []),
6651
+ ...credentialRules.denyWriteLiterals.map(canonical),
6652
+ defaultBotmuxHome,
6653
+ configuredBotmuxHome,
6654
+ ])],
6655
+ };
6656
+ }
6451
6657
  if (mcpRuntimeManifest) {
6452
6658
  finalDenyPaths.push(...sessionMcpRuntimeHostOnlyPaths(mcpRuntimeManifest, config.session.dataDir).map(canonical));
6453
6659
  }
@@ -6473,29 +6679,30 @@ async function spawnCli(cfg, opts = {}) {
6473
6679
  allowWritePaths: writeSandboxRules.allowWritePaths.map(canonical),
6474
6680
  allowWriteRegexes: writeSandboxRules.allowWriteRegexes,
6475
6681
  denyWritePaths: writeSandboxRules.denyWritePaths.map(canonical),
6682
+ denyWriteRegexes: writeSandboxRules.denyWriteRegexes,
6476
6683
  }
6477
6684
  : undefined;
6478
6685
  if (!locateOnPath('sandbox-exec')) {
6479
6686
  throw new Error(`[file-sandbox] refusing to start session ${cfg.sessionId}: sandbox-exec not found`);
6480
6687
  }
6481
- const profileDir = join(process.env.SESSION_DATA_DIR, 'read-isolation');
6688
+ const profileDir = join(isolationRuntimeDataDir, 'read-isolation');
6482
6689
  mkdirSync(profileDir, { recursive: true });
6483
6690
  const profilePath = join(profileDir, `${cfg.sessionId}.sb`);
6484
6691
  replaceManagedOriginCapabilityFile(profilePath, buildSeatbeltProfile(denyPaths, allowPaths, finalDenyPaths, traverseDirs, denyRegexes, writeRules, protectedWrites));
6485
6692
  seatbeltProfilePath = profilePath;
6486
6693
  spawnArgs = ['-f', profilePath, spawnBin, ...spawnArgs];
6487
- spawnBin = 'sandbox-exec';
6694
+ spawnBin = credentialMechanismExecutable ?? '/usr/bin/sandbox-exec';
6488
6695
  log(`[file-sandbox] wrapping ${cliAdapter.id} in Seatbelt (read-isolation=${!!readIsolationCtx}, write-sandbox=${!!writeRules}): sandbox-exec -f ${profilePath}`);
6489
6696
  }
6490
6697
  // Fresh sandboxed spawn on a persistent backend: stamp the pane with this daemon's
6491
6698
  // policy version + boot id so a later suspend→resume reattach can be trusted (see the stale-pane
6492
6699
  // guard above). Applies to read-isolation AND write-sandbox panes (both carry the
6493
6700
  // Seatbelt confinement on the live process). pty needs no marker (never reattached).
6494
- if ((readIsolationCtx || writeSandboxRules) && persistentSessionName && !willReattachPersistent) {
6701
+ if (appliedIsolationCapabilities.length > 0 && persistentSessionName && !willReattachPersistent) {
6495
6702
  try {
6496
- const markerDir = join(process.env.SESSION_DATA_DIR, 'read-isolation');
6703
+ const markerDir = join(isolationRuntimeDataDir, 'read-isolation');
6497
6704
  mkdirSync(markerDir, { recursive: true });
6498
- replaceManagedOriginCapabilityFile(join(markerDir, `${cfg.sessionId}.boot`), isolationPaneMarkerContent(cfg.daemonBootId ?? ''));
6705
+ replaceManagedOriginCapabilityFile(join(markerDir, `${cfg.sessionId}.boot`), isolationPaneMarkerContent(cfg.daemonBootId ?? '', appliedIsolationCapabilities));
6499
6706
  }
6500
6707
  catch { /* non-fatal: worst case a same-lifetime reattach cold-spawns instead */ }
6501
6708
  }
@@ -6748,6 +6955,143 @@ async function spawnCli(cfg, opts = {}) {
6748
6955
  }
6749
6956
  }
6750
6957
  }
6958
+ // Mandatory credential-only confinement is the OUTERMOST launch wrapper so
6959
+ // wrapperCli and every descendant it starts inherit the boundary. Full
6960
+ // Seatbelt/bwrap sessions were already wrapped above and never enter these
6961
+ // branches (gate mode `covered`), avoiding nested/double sandboxes.
6962
+ if (!willReattachPersistent && credentialOnlySeatbelt) {
6963
+ const canonical = (path) => {
6964
+ try {
6965
+ return realpathSync(path);
6966
+ }
6967
+ catch {
6968
+ return path;
6969
+ }
6970
+ };
6971
+ const rules = buildCredentialIsolationRules({
6972
+ homeDir: canonical(hostHomeDir),
6973
+ botmuxHome: canonical(configuredBotmuxHome),
6974
+ defaultBotmuxHome: canonical(defaultBotmuxHome),
6975
+ });
6976
+ const profileDir = join(isolationRuntimeDataDir, 'read-isolation');
6977
+ mkdirSync(profileDir, { recursive: true });
6978
+ const profilePath = join(profileDir, `${cfg.sessionId}.sb`);
6979
+ replaceManagedOriginCapabilityFile(profilePath, buildSeatbeltProfile(rules.denyPaths.map(canonical), [], [], [], rules.denyRegexes, undefined, {
6980
+ denyWritePaths: [...new Set([
6981
+ ...rules.denyWritePaths.map(canonical),
6982
+ canonical(profileDir),
6983
+ ])],
6984
+ denyWriteRegexes: rules.denyWriteRegexes,
6985
+ denyWriteLiterals: [...new Set([
6986
+ ...rules.denyWriteLiterals.map(canonical),
6987
+ canonical(profileDir),
6988
+ defaultBotmuxHome,
6989
+ configuredBotmuxHome,
6990
+ ])],
6991
+ }));
6992
+ seatbeltProfilePath = profilePath;
6993
+ spawnArgs = ['-f', profilePath, spawnBin, ...spawnArgs];
6994
+ // Absolute path only: a pre-activation unconfined CLI could plant a fake
6995
+ // sandbox-exec earlier on PATH. Prefer the probe result; fall back to the
6996
+ // well-known system location used by full Seatbelt wrapping above.
6997
+ spawnBin = credentialMechanismExecutable ?? '/usr/bin/sandbox-exec';
6998
+ log(`[device-credential-isolation] wrapping ${cliAdapter.id} in credential-only Seatbelt: ${spawnBin} -f ${profilePath}`);
6999
+ }
7000
+ if (!willReattachPersistent && credentialOnlyBwrap) {
7001
+ const panePolicyDir = join(isolationRuntimeDataDir, 'read-isolation');
7002
+ mkdirSync(panePolicyDir, { recursive: true });
7003
+ const hideDirectories = new Set();
7004
+ const hideFiles = new Set();
7005
+ const processedRoots = new Set();
7006
+ const isDashboardAuthorityBasename = (name) => name === '.dashboard-secret'
7007
+ || name.startsWith('.dashboard-secret.')
7008
+ || name === '.dashboard-token'
7009
+ || name.startsWith('.dashboard-token.');
7010
+ for (const rawRoot of [defaultBotmuxHome, configuredBotmuxHome]) {
7011
+ let root = rawRoot;
7012
+ try {
7013
+ root = realpathSync(rawRoot);
7014
+ }
7015
+ catch { /* gate authority root is checked below */ }
7016
+ if (processedRoots.has(root))
7017
+ continue;
7018
+ let rootStat;
7019
+ try {
7020
+ rootStat = lstatSync(root);
7021
+ }
7022
+ catch {
7023
+ // An unrelated/default root can be absent when the activation signal
7024
+ // came from a custom BOTMUX_HOME. At least one signalled root must
7025
+ // survive; the builder below rejects an empty root set.
7026
+ continue;
7027
+ }
7028
+ if (!rootStat.isDirectory()) {
7029
+ throw new Error(`[device-credential-isolation] authority root is not a directory: ${rawRoot}`);
7030
+ }
7031
+ processedRoots.add(root);
7032
+ const authorityDirectory = join(root, DEVICE_AUTHORITY_DIRECTORY);
7033
+ try {
7034
+ const authorityStat = lstatSync(authorityDirectory);
7035
+ if (!authorityStat.isDirectory()) {
7036
+ throw new Error(`[device-credential-isolation] device authority path is not a directory: ${authorityDirectory}`);
7037
+ }
7038
+ }
7039
+ catch (error) {
7040
+ if (error.code !== 'ENOENT')
7041
+ throw error;
7042
+ // A fixed empty mount target lets the child mask the entire authority
7043
+ // namespace while leaving BOTMUX_HOME itself live and writable.
7044
+ mkdirSync(authorityDirectory, { mode: 0o700 });
7045
+ }
7046
+ hideDirectories.add(realpathSync(authorityDirectory));
7047
+ for (const name of readdirSync(root)) {
7048
+ if (name === DEVICE_AUTHORITY_DIRECTORY)
7049
+ continue;
7050
+ if (!isCredentialIsolationReservedBasename(name)
7051
+ && !isDashboardAuthorityBasename(name))
7052
+ continue;
7053
+ const entry = join(root, name);
7054
+ try {
7055
+ const stat = lstatSync(entry);
7056
+ if (!stat.isFile()) {
7057
+ // Never follow a host-authority symlink or special file. Exact mask
7058
+ // destinations must be stable regular files or the launch fails
7059
+ // closed before an untrusted CLI starts.
7060
+ throw new Error(`[device-credential-isolation] authority entry is not a regular file: ${entry}`);
7061
+ }
7062
+ hideFiles.add(entry);
7063
+ }
7064
+ catch (error) {
7065
+ if (error.code !== 'ENOENT')
7066
+ throw error;
7067
+ // A racing removal needs no mask; a later exact credential rotation
7068
+ // is host-only and the long-lived device secret namespace itself is
7069
+ // protected wholesale above.
7070
+ }
7071
+ }
7072
+ }
7073
+ let credentialCliBin = spawnBin;
7074
+ try {
7075
+ credentialCliBin = realpathSync(spawnBin);
7076
+ }
7077
+ catch { /* spawn will fail closed if unresolved */ }
7078
+ const credentialSandbox = prepareCredentialOnlySandbox({
7079
+ hideDirectories: [...hideDirectories],
7080
+ hideFiles: [...hideFiles],
7081
+ readonlyPaths: [realpathSync(panePolicyDir)],
7082
+ workingDir: spawnCwd,
7083
+ cliBin: credentialCliBin,
7084
+ cliArgs: spawnArgs,
7085
+ });
7086
+ if (!credentialSandbox) {
7087
+ throw new Error(`[device-credential-isolation] refusing to start session ${cfg.sessionId}: `
7088
+ + 'credential-only bubblewrap could not be established');
7089
+ }
7090
+ spawnBin = credentialSandbox.bin;
7091
+ spawnArgs = credentialSandbox.args;
7092
+ log(`[device-credential-isolation] wrapping ${cliAdapter.id} in credential-only bwrap `
7093
+ + `(${hideDirectories.size} authority dir(s), ${hideFiles.size} exact file(s))`);
7094
+ }
6751
7095
  backend.spawn(spawnBin, spawnArgs, {
6752
7096
  cwd: spawnCwd,
6753
7097
  cols: PTY_COLS,
@@ -6760,6 +7104,7 @@ async function spawnCli(cfg, opts = {}) {
6760
7104
  // can verify they were spawned inside a botmux session by walking the
6761
7105
  // process tree and looking for a matching pid file in this directory.
6762
7106
  const cliPid = backend.getChildPid?.();
7107
+ publishLocalProcessAttestation(cliPid ?? undefined);
6763
7108
  if (cliPid && process.env.SESSION_DATA_DIR) {
6764
7109
  const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
6765
7110
  try {
@@ -6802,6 +7147,7 @@ async function spawnCli(cfg, opts = {}) {
6802
7147
  // Per-tick maybeFollowSessionRotationViaPid (bridge 1s poller) reads the
6803
7148
  // module-level bridgeCliPid and re-points to the real CLI's jsonl.
6804
7149
  bridgeCliPid = realPid;
7150
+ publishLocalProcessAttestation(realPid);
6805
7151
  },
6806
7152
  schedule: (fn, ms) => { setTimeout(fn, ms); },
6807
7153
  });
@@ -6836,6 +7182,7 @@ async function spawnCli(cfg, opts = {}) {
6836
7182
  return;
6837
7183
  const pid = backend.getChildPid?.();
6838
7184
  if (pid) {
7185
+ publishLocalProcessAttestation(pid);
6839
7186
  if (process.env.SESSION_DATA_DIR && !cliPidMarker) {
6840
7187
  try {
6841
7188
  const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
@@ -7241,6 +7588,7 @@ async function spawnCli(cfg, opts = {}) {
7241
7588
  }
7242
7589
  }
7243
7590
  function killCli(opts = {}) {
7591
+ currentCliCredentialIsolated = false;
7244
7592
  stopSessionMcpGatewayHost();
7245
7593
  stopCodexRpcEngine();
7246
7594
  destroyCrashDiagnosticTerminal('killCli');
@@ -8551,6 +8899,16 @@ function send(msg) {
8551
8899
  }
8552
8900
  process.send?.(payload);
8553
8901
  }
8902
+ function publishLocalProcessAttestation(cliPid) {
8903
+ const cliProcStart = cliPid ? readProcessStartIdentity(cliPid) : undefined;
8904
+ send({
8905
+ type: 'local_process_attestation',
8906
+ backendType: effectiveBackendType,
8907
+ credentialIsolated: currentCliCredentialIsolated,
8908
+ ...(cliPid ? { cliPid } : {}),
8909
+ ...(cliProcStart ? { cliProcStart } : {}),
8910
+ });
8911
+ }
8554
8912
  /** Deliver a terminal IPC message before exiting the worker. `process.send()`
8555
8913
  * only queues asynchronously; calling process.exit() on the next line can drop
8556
8914
  * the exact startup diagnostic the user needs and recreate the silent-bot bug.
@@ -8711,7 +9069,12 @@ process.on('message', async (raw) => {
8711
9069
  passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
8712
9070
  adoptMode: msg.adoptMode === true,
8713
9071
  dispatchAttempt: msg.dispatchAttempt,
8714
- }) || (msg.adoptMode !== true && lastSpawnEffectiveResume && cliAdapter?.initialPromptArgsIgnoredOnResume === true);
9072
+ }) || (msg.adoptMode !== true && lastSpawnEffectiveResume && cliAdapter?.initialPromptArgsIgnoredOnResume === true)
9073
+ || shouldDeferInitialPromptForArgLimit({
9074
+ passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
9075
+ prompt: msg.prompt,
9076
+ maxInitialPromptArgBytes: cliAdapter?.maxInitialPromptArgBytes,
9077
+ });
8715
9078
  if (msg.prompt && cliAdapter?.passesInitialPromptViaArgs && !deferInitialPrompt && codexBridgeFallbackActive()) {
8716
9079
  // Args-baked first prompts (notably Pi) never pass through the normal
8717
9080
  // 'message' IPC path, so the structured bridge would otherwise see the