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.
- package/dist/adapters/backend/sandbox.d.ts +47 -0
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +137 -0
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +4 -0
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts +72 -3
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +143 -6
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +8 -2
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +8 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/cli/deferred-topic-send.d.ts +37 -0
- package/dist/cli/deferred-topic-send.d.ts.map +1 -0
- package/dist/cli/deferred-topic-send.js +69 -0
- package/dist/cli/deferred-topic-send.js.map +1 -0
- package/dist/cli/pm2-command.d.ts.map +1 -1
- package/dist/cli/pm2-command.js +6 -0
- package/dist/cli/pm2-command.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +173 -34
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +17 -12
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +4 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +99 -8
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/deferred-schedule-settlement.d.ts +20 -0
- package/dist/core/deferred-schedule-settlement.d.ts.map +1 -0
- package/dist/core/deferred-schedule-settlement.js +18 -0
- package/dist/core/deferred-schedule-settlement.js.map +1 -0
- package/dist/core/deferred-topic-binding.d.ts +19 -0
- package/dist/core/deferred-topic-binding.d.ts.map +1 -0
- package/dist/core/deferred-topic-binding.js +55 -0
- package/dist/core/deferred-topic-binding.js.map +1 -0
- package/dist/core/device-isolation-activation.d.ts +56 -0
- package/dist/core/device-isolation-activation.d.ts.map +1 -0
- package/dist/core/device-isolation-activation.js +110 -0
- package/dist/core/device-isolation-activation.js.map +1 -0
- package/dist/core/device-isolation-daemon.d.ts +84 -0
- package/dist/core/device-isolation-daemon.d.ts.map +1 -0
- package/dist/core/device-isolation-daemon.js +591 -0
- package/dist/core/device-isolation-daemon.js.map +1 -0
- package/dist/core/scheduler.d.ts +26 -20
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +114 -56
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +47 -15
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +17 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +178 -84
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +7 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +86 -1
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +95 -5
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/schedule-card-model.d.ts +12 -3
- package/dist/dashboard/schedule-card-model.d.ts.map +1 -1
- package/dist/dashboard/schedule-card-model.js +32 -8
- package/dist/dashboard/schedule-card-model.js.map +1 -1
- package/dist/dashboard/web/dashboard-components.d.ts +7 -0
- package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-components.js +11 -2
- package/dist/dashboard/web/dashboard-components.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +30 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/schedules-page.d.ts +2 -0
- package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
- package/dist/dashboard/web/schedules-page.js +74 -27
- package/dist/dashboard/web/schedules-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/{bot-defaults-page-O37SAQD5.js → bot-defaults-page-B5AY4B2J.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-6QHMLMIM.js → chunk-CUV65BIQ.js} +3 -3
- package/dist/dashboard-web/chunks/{chunk-O2DPQL7C.js → chunk-JNRSPKLS.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FYALHCPG.js → chunk-P6GO5KT4.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-QFMH6ITB.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-B42PCL5P.js → chunk-UBOLHQCK.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-DZBQ42MH.js → connectors-page-KTC2QWNW.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-3IEGRZKQ.js → groups-page-B3EKGOLY.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-X3ADRON3.js → insights-page-X6RXNCWX.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-IV36RGQY.js → monitor-room-LLRA5LQW.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-ADFKVON4.js → monitoring-page-7MT5FEOS.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-CALAOWNS.js → office-page-KD46D5TD.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-SLMGLK3M.js → overview-page-JUWNH32O.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-L7JJ4FFV.js → roles-page-P6F7NLOI.js} +1 -1
- package/dist/dashboard-web/chunks/schedules-page-XIHWZST4.js +1 -0
- package/dist/dashboard-web/chunks/{sessions-page-OKJ5YWUZ.js → sessions-page-2VECQ27T.js} +1 -1
- package/dist/dashboard-web/chunks/{settings-page-P6MI2ZUE.js → settings-page-HMIUE2JA.js} +1 -1
- package/dist/dashboard-web/chunks/{skills-page-FT7BVZ6X.js → skills-page-A46KVWBL.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-DEYCPUFB.js → team-federation-page-SDUC56DF.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-4IOWREZG.js → v3-page-J5JOTUSQ.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-2VMBETZH.js → whiteboards-page-U7LSOAWI.js} +1 -1
- package/dist/dashboard-web/style.css +34 -4
- package/dist/dashboard.js +16 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/desktop/main/auto-start.d.ts +2 -0
- package/dist/desktop/main/auto-start.d.ts.map +1 -1
- package/dist/desktop/main/auto-start.js +10 -6
- package/dist/desktop/main/auto-start.js.map +1 -1
- package/dist/desktop/main/bundled-runtime.d.ts +12 -0
- package/dist/desktop/main/bundled-runtime.d.ts.map +1 -0
- package/dist/desktop/main/bundled-runtime.js +30 -0
- package/dist/desktop/main/bundled-runtime.js.map +1 -0
- package/dist/desktop/main/device-status.d.ts +15 -0
- package/dist/desktop/main/device-status.d.ts.map +1 -0
- package/dist/desktop/main/device-status.js +94 -0
- package/dist/desktop/main/device-status.js.map +1 -0
- package/dist/desktop/main/external-runtime.d.ts +9 -0
- package/dist/desktop/main/external-runtime.d.ts.map +1 -1
- package/dist/desktop/main/external-runtime.js +88 -18
- package/dist/desktop/main/external-runtime.js.map +1 -1
- package/dist/desktop/main/ipc.d.ts.map +1 -1
- package/dist/desktop/main/ipc.js +1 -0
- package/dist/desktop/main/ipc.js.map +1 -1
- package/dist/desktop/main/node-command.d.ts +21 -0
- package/dist/desktop/main/node-command.d.ts.map +1 -1
- package/dist/desktop/main/node-command.js +41 -0
- package/dist/desktop/main/node-command.js.map +1 -1
- package/dist/desktop/main/pm2-apps.d.ts +4 -2
- package/dist/desktop/main/pm2-apps.d.ts.map +1 -1
- package/dist/desktop/main/pm2-apps.js +11 -3
- package/dist/desktop/main/pm2-apps.js.map +1 -1
- package/dist/desktop/main/runtime-service.d.ts +21 -5
- package/dist/desktop/main/runtime-service.d.ts.map +1 -1
- package/dist/desktop/main/runtime-service.js +94 -10
- package/dist/desktop/main/runtime-service.js.map +1 -1
- package/dist/desktop/main/runtime-source.d.ts +2 -0
- package/dist/desktop/main/runtime-source.d.ts.map +1 -1
- package/dist/desktop/main/runtime-source.js +12 -6
- package/dist/desktop/main/runtime-source.js.map +1 -1
- package/dist/desktop/main/tray.d.ts +1 -0
- package/dist/desktop/main/tray.d.ts.map +1 -1
- package/dist/desktop/main/tray.js +3 -3
- package/dist/desktop/main/tray.js.map +1 -1
- package/dist/desktop/main.js +28 -8
- package/dist/desktop/main.js.map +1 -1
- package/dist/desktop/preload.js +3 -0
- package/dist/desktop/preload.js.map +1 -1
- package/dist/desktop/renderer/app.d.ts +2 -1
- package/dist/desktop/renderer/app.d.ts.map +1 -1
- package/dist/desktop/renderer/app.js +6 -1
- package/dist/desktop/renderer/app.js.map +1 -1
- package/dist/desktop/renderer/runtime-state.d.ts.map +1 -1
- package/dist/desktop/renderer/runtime-state.js +2 -0
- package/dist/desktop/renderer/runtime-state.js.map +1 -1
- package/dist/desktop/shared/shell-path-probes.d.ts +24 -0
- package/dist/desktop/shared/shell-path-probes.d.ts.map +1 -0
- package/dist/desktop/shared/shell-path-probes.js +38 -0
- package/dist/desktop/shared/shell-path-probes.js.map +1 -0
- package/dist/desktop/shared/types.d.ts +21 -1
- package/dist/desktop/shared/types.d.ts.map +1 -1
- package/dist/desktop/smoke.d.ts +1 -0
- package/dist/desktop/smoke.d.ts.map +1 -1
- package/dist/desktop/smoke.js +20 -5
- package/dist/desktop/smoke.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -7
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +16 -7
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +3 -0
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +1 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +1 -1
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/schedules-card.d.ts +2 -2
- package/dist/im/lark/schedules-card.d.ts.map +1 -1
- package/dist/im/lark/schedules-card.js +50 -34
- package/dist/im/lark/schedules-card.js.map +1 -1
- package/dist/index-daemon.js +13 -1
- package/dist/index-daemon.js.map +1 -1
- package/dist/platform/bind.d.ts +4 -1
- package/dist/platform/bind.d.ts.map +1 -1
- package/dist/platform/bind.js +10 -1
- package/dist/platform/bind.js.map +1 -1
- package/dist/platform/binding.d.ts.map +1 -1
- package/dist/platform/binding.js +15 -9
- package/dist/platform/binding.js.map +1 -1
- package/dist/platform/device-command.d.ts +35 -0
- package/dist/platform/device-command.d.ts.map +1 -0
- package/dist/platform/device-command.js +187 -0
- package/dist/platform/device-command.js.map +1 -0
- package/dist/platform/device-enroll.d.ts +156 -0
- package/dist/platform/device-enroll.d.ts.map +1 -0
- package/dist/platform/device-enroll.js +781 -0
- package/dist/platform/device-enroll.js.map +1 -0
- package/dist/platform/device-isolation-activation-client.d.ts +28 -0
- package/dist/platform/device-isolation-activation-client.d.ts.map +1 -0
- package/dist/platform/device-isolation-activation-client.js +279 -0
- package/dist/platform/device-isolation-activation-client.js.map +1 -0
- package/dist/platform/device-isolation.d.ts +35 -0
- package/dist/platform/device-isolation.d.ts.map +1 -0
- package/dist/platform/device-isolation.js +90 -0
- package/dist/platform/device-isolation.js.map +1 -0
- package/dist/platform/device-paths.d.ts +9 -0
- package/dist/platform/device-paths.d.ts.map +1 -0
- package/dist/platform/device-paths.js +14 -0
- package/dist/platform/device-paths.js.map +1 -0
- package/dist/platform/device.d.ts +77 -0
- package/dist/platform/device.d.ts.map +1 -0
- package/dist/platform/device.js +212 -0
- package/dist/platform/device.js.map +1 -0
- package/dist/platform/host-command-context.d.ts +15 -0
- package/dist/platform/host-command-context.d.ts.map +1 -0
- package/dist/platform/host-command-context.js +26 -0
- package/dist/platform/host-command-context.js.map +1 -0
- package/dist/platform/secure-host-file.d.ts +12 -0
- package/dist/platform/secure-host-file.d.ts.map +1 -0
- package/dist/platform/secure-host-file.js +182 -0
- package/dist/platform/secure-host-file.js.map +1 -0
- package/dist/services/schedule-store.d.ts +6 -2
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +19 -4
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +4 -5
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +45 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/atomic-write.d.ts +13 -0
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/atomic-write.js +89 -9
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/child-env.d.ts +40 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +45 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/pending-input-queue.d.ts +10 -0
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +15 -0
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/worker.js +391 -28
- package/dist/worker.js.map +1 -1
- package/dist/workflows/events/payloads.d.ts +4 -4
- package/dist/workflows/events/schema.d.ts +16 -16
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts +3 -1
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.js +14 -3
- package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
- package/dist/workflows/v3/architect.js +1 -1
- package/dist/workflows/v3/architect.js.map +1 -1
- package/dist/workflows/v3/contract.d.ts +8 -0
- package/dist/workflows/v3/contract.d.ts.map +1 -1
- package/dist/workflows/v3/contract.js.map +1 -1
- package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
- package/dist/workflows/v3/daemon-run.js +2 -0
- package/dist/workflows/v3/daemon-run.js.map +1 -1
- package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -1
- package/dist/workflows/v3/ephemeral-pool.js +10 -2
- package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +24 -24
- package/dist/workflows/v3/runtime.d.ts +5 -0
- package/dist/workflows/v3/runtime.d.ts.map +1 -1
- package/dist/workflows/v3/runtime.js +1 -0
- package/dist/workflows/v3/runtime.js.map +1 -1
- package/package.json +6 -3
- package/dist/dashboard-web/chunks/chunk-I35DXGM4.js +0 -1
- 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 (
|
|
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(
|
|
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
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
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
|
-
|
|
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
|
|
6357
|
-
// it overrides any adapter default). claude → CLAUDE_CONFIG_DIR, codex →
|
|
6358
|
-
// Both are in BOTMUX_INJECTED_ENV_KEYS so the tmux backend forwards
|
|
6359
|
-
// pane; without this the CLI falls back to the global
|
|
6360
|
-
// Seatbelt wrapper denies → it can't read its own
|
|
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(
|
|
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 (
|
|
6701
|
+
if (appliedIsolationCapabilities.length > 0 && persistentSessionName && !willReattachPersistent) {
|
|
6495
6702
|
try {
|
|
6496
|
-
const markerDir = join(
|
|
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
|