botmux 3.3.0 → 3.4.1
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/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/cli/create-group-resolver.d.ts +19 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -1
- package/dist/cli/create-group-resolver.js +30 -0
- package/dist/cli/create-group-resolver.js.map +1 -1
- 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 +36 -1
- package/dist/cli.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +21 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- 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/types.d.ts +9 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +35 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +5 -0
- package/dist/daemon.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/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/group-creator.d.ts +8 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +42 -0
- package/dist/services/group-creator.js.map +1 -1
- package/dist/types.d.ts +9 -0
- 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/input-gate.d.ts +41 -0
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +41 -0
- package/dist/utils/input-gate.js.map +1 -1
- package/dist/worker.js +328 -22
- 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/package.json +6 -3
package/dist/worker.js
CHANGED
|
@@ -13,18 +13,18 @@
|
|
|
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';
|
|
25
25
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
26
26
|
import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
|
|
27
|
-
import { shouldReleaseFirstPromptTimeout, shouldWriteNow } from './utils/input-gate.js';
|
|
27
|
+
import { decideHardTimeoutAction, decideSettleMarkReady, shouldReleaseFirstPromptTimeout, shouldWriteNow } from './utils/input-gate.js';
|
|
28
28
|
import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirst } from './core/inject-queue-policy.js';
|
|
29
29
|
import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
|
|
30
30
|
import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.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';
|
|
@@ -689,6 +690,10 @@ let lastCliExitSignal = null;
|
|
|
689
690
|
* of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
|
|
690
691
|
let isPipeMode = false;
|
|
691
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;
|
|
692
697
|
/** pty-under-zellij backend (BACKEND_TYPE=zellij). Behaves like the non-tmux
|
|
693
698
|
* pty path for the worker (renderer screenshots, relay web terminal) but owns
|
|
694
699
|
* a persistent zellij session that survives daemon restart. */
|
|
@@ -832,6 +837,15 @@ let isSettlingFirstFlush = false;
|
|
|
832
837
|
* ready yet, or flushPending will be blocked by isSettlingFirstFlush and a
|
|
833
838
|
* later markPromptReady call would return early with the first prompt stranded. */
|
|
834
839
|
let promptReadyDetectedDuringSettle = false;
|
|
840
|
+
/** While the ready-gate is holding, the IdleDetector may still fire on a real
|
|
841
|
+
* readyPattern (e.g. Hermes's ❯) — proving the input box exists — but
|
|
842
|
+
* markPromptReady() returns early because the gate is armed. Record that the
|
|
843
|
+
* pattern was seen so the gate's timeout-fallback settle can mark the prompt
|
|
844
|
+
* ready immediately instead of delivering into a !isPromptReady state that
|
|
845
|
+
* flushPending() rejects for non-type-ahead adapters. Without this, a Hermes
|
|
846
|
+
* spawn that renders ❯ but never fires BOTMUX_READY_COMMAND waits the full
|
|
847
|
+
* hard timeout (and previously never delivered at all). */
|
|
848
|
+
let readyPatternSeenDuringHold = false;
|
|
835
849
|
/** Wait until the PTY has been quiet for READY_FLUSH_SETTLE_MS (Ink render
|
|
836
850
|
* drained), capped at READY_FLUSH_SETTLE_CAP_MS, then flush the held prompt.
|
|
837
851
|
* A real SessionStart/BOTMUX_READY_COMMAND signal is itself authoritative
|
|
@@ -843,8 +857,13 @@ function settleThenFlush(startedAtMs, promptReadyAfterSettle) {
|
|
|
843
857
|
const quietForMs = now - lastPtyOutputAtMs;
|
|
844
858
|
if (quietForMs >= READY_FLUSH_SETTLE_MS || now - startedAtMs >= READY_FLUSH_SETTLE_CAP_MS) {
|
|
845
859
|
isSettlingFirstFlush = false;
|
|
846
|
-
const shouldMarkPromptReady =
|
|
860
|
+
const shouldMarkPromptReady = decideSettleMarkReady({
|
|
861
|
+
promptReadyAfterSettle,
|
|
862
|
+
promptReadyDetectedDuringSettle,
|
|
863
|
+
readyPatternSeenDuringHold,
|
|
864
|
+
});
|
|
847
865
|
promptReadyDetectedDuringSettle = false;
|
|
866
|
+
readyPatternSeenDuringHold = false;
|
|
848
867
|
log(`Ready-gate settle done (quiet ${quietForMs}ms); ${shouldMarkPromptReady ? 'marking prompt ready' : 'delivering held first prompt'}`);
|
|
849
868
|
if (shouldMarkPromptReady) {
|
|
850
869
|
markPromptReady();
|
|
@@ -4519,6 +4538,11 @@ function markPromptReady() {
|
|
|
4519
4538
|
// releaseReadyGate() drives flushPending() once the real signal lands, and a
|
|
4520
4539
|
// later genuine idle then runs this fully. No-op for non-armed gates.
|
|
4521
4540
|
if (readyGate.shouldHold()) {
|
|
4541
|
+
// A real readyPattern fired while the gate was holding — the input box
|
|
4542
|
+
// exists. Remember it so the gate's timeout-fallback settle can mark the
|
|
4543
|
+
// prompt ready (see settleThenFlush) instead of letting flushPending()
|
|
4544
|
+
// reject the held message for non-type-ahead adapters.
|
|
4545
|
+
readyPatternSeenDuringHold = true;
|
|
4522
4546
|
log('Idle detected but holding for SessionStart ready signal (startup selector guard)');
|
|
4523
4547
|
return;
|
|
4524
4548
|
}
|
|
@@ -5581,6 +5605,40 @@ function scheduleBusyPatternIdleProbe(source) {
|
|
|
5581
5605
|
}
|
|
5582
5606
|
async function spawnCli(cfg, opts = {}) {
|
|
5583
5607
|
clearSessionRenameInFlight();
|
|
5608
|
+
currentCliCredentialIsolated = false;
|
|
5609
|
+
// Enrollment writes the fixed marker before any device credential appears.
|
|
5610
|
+
// From that instant onward every NEW local CLI must carry a credential
|
|
5611
|
+
// boundary, regardless of adapter capability or optional sandbox toggles.
|
|
5612
|
+
// lstat (not existsSync) deliberately treats a hostile/broken symlink as a
|
|
5613
|
+
// present authority signal and therefore fails closed.
|
|
5614
|
+
const hostHomeDir = homedir();
|
|
5615
|
+
const defaultBotmuxHome = join(hostHomeDir, '.botmux');
|
|
5616
|
+
const configuredBotmuxHome = process.env.SESSION_DATA_DIR
|
|
5617
|
+
? dirname(process.env.SESSION_DATA_DIR)
|
|
5618
|
+
: defaultBotmuxHome;
|
|
5619
|
+
const hostEntryExistsNoFollow = (path) => {
|
|
5620
|
+
try {
|
|
5621
|
+
lstatSync(path);
|
|
5622
|
+
return true;
|
|
5623
|
+
}
|
|
5624
|
+
catch {
|
|
5625
|
+
return false;
|
|
5626
|
+
}
|
|
5627
|
+
};
|
|
5628
|
+
const deviceIsolationMarkerExists = hostEntryExistsNoFollow(deviceCredentialIsolationMarkerPath(hostHomeDir));
|
|
5629
|
+
const deviceCredentialExists = [...new Set([defaultBotmuxHome, configuredBotmuxHome])]
|
|
5630
|
+
.some(root => hostEntryExistsNoFollow(join(root, DEVICE_AUTHORITY_DIRECTORY, DEVICE_CREDENTIAL_FILE))
|
|
5631
|
+
// Upgrade fail-safe: a pre-dedicated-directory credential still activates
|
|
5632
|
+
// mandatory confinement until the host explicitly removes/migrates it.
|
|
5633
|
+
|| hostEntryExistsNoFollow(join(root, DEVICE_CREDENTIAL_FILE)));
|
|
5634
|
+
const mandatoryCredentialIsolation = credentialIsolationRequired({
|
|
5635
|
+
markerExists: deviceIsolationMarkerExists,
|
|
5636
|
+
deviceCredentialExists,
|
|
5637
|
+
});
|
|
5638
|
+
if (mandatoryCredentialIsolation && cfg.adoptMode) {
|
|
5639
|
+
throw new Error(`[device-credential-isolation] refusing adopt session ${cfg.sessionId}: `
|
|
5640
|
+
+ 'an already-running external CLI cannot be retrofitted with the mandatory credential boundary');
|
|
5641
|
+
}
|
|
5584
5642
|
// (startupCommands one-shot is re-armed below, AFTER the reattach-vs-fresh
|
|
5585
5643
|
// prediction — only a genuinely fresh CLI process replays them; see
|
|
5586
5644
|
// willReattachPersistent.)
|
|
@@ -5940,6 +5998,56 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5940
5998
|
// established (unsandboxable backend, no SESSION_DATA_DIR) is a hard error at the
|
|
5941
5999
|
// spawn site below, never a silent unconfined run.
|
|
5942
6000
|
const willWriteSandbox = process.platform === 'darwin' && wantsFileSandbox;
|
|
6001
|
+
if (willWriteSandbox && !process.env.SESSION_DATA_DIR) {
|
|
6002
|
+
throw new Error(`[file-sandbox] refusing to start session ${cfg.sessionId}: missing SESSION_DATA_DIR`);
|
|
6003
|
+
}
|
|
6004
|
+
const fullIsolationCoversCredentials = !riffRemoteBackend && ((process.platform === 'darwin' && (willReadIsolate || willWriteSandbox))
|
|
6005
|
+
|| (process.platform === 'linux' && wantsFileSandbox));
|
|
6006
|
+
let credentialMechanismAvailable = true;
|
|
6007
|
+
let credentialMechanismExecutable;
|
|
6008
|
+
if (mandatoryCredentialIsolation && !riffRemoteBackend && !fullIsolationCoversCredentials) {
|
|
6009
|
+
if (process.platform === 'darwin') {
|
|
6010
|
+
const probe = probeHostCredentialIsolationMechanism();
|
|
6011
|
+
credentialMechanismAvailable = probe.supported;
|
|
6012
|
+
if (probe.supported)
|
|
6013
|
+
credentialMechanismExecutable = probe.executable;
|
|
6014
|
+
}
|
|
6015
|
+
else {
|
|
6016
|
+
credentialMechanismAvailable = process.platform === 'linux'
|
|
6017
|
+
? credentialOnlySandboxAvailable()
|
|
6018
|
+
: false;
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
const credentialIsolationGate = evaluateCredentialOnlyIsolationGate({
|
|
6022
|
+
markerExists: deviceIsolationMarkerExists,
|
|
6023
|
+
deviceCredentialExists,
|
|
6024
|
+
remoteBackend: riffRemoteBackend,
|
|
6025
|
+
platform: process.platform,
|
|
6026
|
+
mechanismAvailable: credentialMechanismAvailable,
|
|
6027
|
+
fullIsolationCoversCredentials,
|
|
6028
|
+
});
|
|
6029
|
+
if (credentialIsolationGate.mode === 'blocked') {
|
|
6030
|
+
throw new Error(`[device-credential-isolation] refusing to start session ${cfg.sessionId}: `
|
|
6031
|
+
+ credentialIsolationGate.failClosedReason);
|
|
6032
|
+
}
|
|
6033
|
+
const credentialBoundaryActive = credentialIsolationGate.required
|
|
6034
|
+
&& credentialIsolationGate.mode !== 'remote-bypass';
|
|
6035
|
+
const credentialOnlySeatbelt = credentialIsolationGate.mode === 'seatbelt';
|
|
6036
|
+
const credentialOnlyBwrap = credentialIsolationGate.mode === 'bwrap';
|
|
6037
|
+
const appliedIsolationCapabilities = [];
|
|
6038
|
+
if (credentialBoundaryActive
|
|
6039
|
+
|| willReadIsolate
|
|
6040
|
+
|| (process.platform === 'linux' && wantsFileSandbox)) {
|
|
6041
|
+
appliedIsolationCapabilities.push('credential');
|
|
6042
|
+
}
|
|
6043
|
+
if (willReadIsolate)
|
|
6044
|
+
appliedIsolationCapabilities.push('read');
|
|
6045
|
+
if (willWriteSandbox || (process.platform === 'linux' && wantsFileSandbox)) {
|
|
6046
|
+
appliedIsolationCapabilities.push('write');
|
|
6047
|
+
}
|
|
6048
|
+
currentCliCredentialIsolated = appliedIsolationCapabilities.includes('credential');
|
|
6049
|
+
const isolationRuntimeDataDir = process.env.SESSION_DATA_DIR
|
|
6050
|
+
?? join(defaultBotmuxHome, 'data');
|
|
5943
6051
|
// Every bot — isolated OR not — gets its own BOT_HOME dir as a ready-made private-
|
|
5944
6052
|
// storage slot. An isolated sibling denies this path regardless of whether the owner
|
|
5945
6053
|
// is isolated (deny uses the full bots.json), so a non-isolated bot can drop private
|
|
@@ -6021,7 +6129,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6021
6129
|
// so the probe below sees no pane and we cold-spawn fresh isolated. A current-
|
|
6022
6130
|
// policy pane survives daemon restarts and suspend→resume safely because the
|
|
6023
6131
|
// confinement remains attached to the live process.
|
|
6024
|
-
if (
|
|
6132
|
+
if (appliedIsolationCapabilities.length > 0 && persistentSessionName && effectiveBackendType !== 'pty') {
|
|
6025
6133
|
const paneLive = effectiveBackendType === 'tmux'
|
|
6026
6134
|
? TmuxBackend.hasSession(persistentSessionName)
|
|
6027
6135
|
: effectiveBackendType === 'zellij'
|
|
@@ -6029,8 +6137,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6029
6137
|
: HerdrBackend.hasSession(persistentSessionName);
|
|
6030
6138
|
if (paneLive) {
|
|
6031
6139
|
let marker = null;
|
|
6032
|
-
marker = readRegularHostFileNoFollow(join(
|
|
6033
|
-
if (isolatedPaneReattachSafe(marker)) {
|
|
6140
|
+
marker = readRegularHostFileNoFollow(join(isolationRuntimeDataDir, 'read-isolation', `${cfg.sessionId}.boot`));
|
|
6141
|
+
if (isolatedPaneReattachSafe(marker, appliedIsolationCapabilities)) {
|
|
6034
6142
|
// Pane was spawned under the current isolation policy → still confined
|
|
6035
6143
|
// on the running process across daemon restarts; warm reattach preserves
|
|
6036
6144
|
// resume/context + tmux idle-suspend.
|
|
@@ -6270,16 +6378,20 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6270
6378
|
const sessionDataDir = process.env.SESSION_DATA_DIR;
|
|
6271
6379
|
// Regex rules cannot be realpath'd after construction, so build their home
|
|
6272
6380
|
// prefix from the canonical path up front (Seatbelt matches canonical paths).
|
|
6273
|
-
const
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6381
|
+
const canonicalSandboxPath = (path) => {
|
|
6382
|
+
try {
|
|
6383
|
+
return realpathSync(path);
|
|
6384
|
+
}
|
|
6385
|
+
catch {
|
|
6386
|
+
return path;
|
|
6387
|
+
}
|
|
6388
|
+
};
|
|
6389
|
+
const sandboxHome = canonicalSandboxPath(homedir());
|
|
6279
6390
|
writeSandboxRules = buildWriteSandboxRules({
|
|
6280
6391
|
homeDir: sandboxHome,
|
|
6281
|
-
botmuxHome: dirname(sessionDataDir),
|
|
6282
|
-
|
|
6392
|
+
botmuxHome: canonicalSandboxPath(dirname(sessionDataDir)),
|
|
6393
|
+
defaultBotmuxHome: canonicalSandboxPath(join(homedir(), '.botmux')),
|
|
6394
|
+
sessionDataDir: canonicalSandboxPath(sessionDataDir),
|
|
6283
6395
|
workingDir: cfg.workingDir,
|
|
6284
6396
|
currentAppId: cfg.larkAppId,
|
|
6285
6397
|
// TMPDIR on macOS resolves under /private/var/folders (already allowed), but
|
|
@@ -6536,6 +6648,31 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6536
6648
|
sessionDataDirs: [readIsolationCtx.sessionDataDir],
|
|
6537
6649
|
});
|
|
6538
6650
|
}
|
|
6651
|
+
if (credentialBoundaryActive) {
|
|
6652
|
+
const credentialRules = buildCredentialIsolationRules({
|
|
6653
|
+
homeDir: canonical(hostHomeDir),
|
|
6654
|
+
botmuxHome: canonical(configuredBotmuxHome),
|
|
6655
|
+
defaultBotmuxHome: canonical(defaultBotmuxHome),
|
|
6656
|
+
});
|
|
6657
|
+
denyPaths = [...new Set([...denyPaths, ...credentialRules.denyPaths.map(canonical)])];
|
|
6658
|
+
denyRegexes = [...new Set([...denyRegexes, ...credentialRules.denyRegexes])];
|
|
6659
|
+
protectedWrites = {
|
|
6660
|
+
denyWritePaths: [...new Set([
|
|
6661
|
+
...(protectedWrites?.denyWritePaths ?? []),
|
|
6662
|
+
...credentialRules.denyWritePaths.map(canonical),
|
|
6663
|
+
])],
|
|
6664
|
+
denyWriteRegexes: [...new Set([
|
|
6665
|
+
...(protectedWrites?.denyWriteRegexes ?? []),
|
|
6666
|
+
...credentialRules.denyWriteRegexes,
|
|
6667
|
+
])],
|
|
6668
|
+
denyWriteLiterals: [...new Set([
|
|
6669
|
+
...(protectedWrites?.denyWriteLiterals ?? []),
|
|
6670
|
+
...credentialRules.denyWriteLiterals.map(canonical),
|
|
6671
|
+
defaultBotmuxHome,
|
|
6672
|
+
configuredBotmuxHome,
|
|
6673
|
+
])],
|
|
6674
|
+
};
|
|
6675
|
+
}
|
|
6539
6676
|
if (mcpRuntimeManifest) {
|
|
6540
6677
|
finalDenyPaths.push(...sessionMcpRuntimeHostOnlyPaths(mcpRuntimeManifest, config.session.dataDir).map(canonical));
|
|
6541
6678
|
}
|
|
@@ -6561,29 +6698,30 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6561
6698
|
allowWritePaths: writeSandboxRules.allowWritePaths.map(canonical),
|
|
6562
6699
|
allowWriteRegexes: writeSandboxRules.allowWriteRegexes,
|
|
6563
6700
|
denyWritePaths: writeSandboxRules.denyWritePaths.map(canonical),
|
|
6701
|
+
denyWriteRegexes: writeSandboxRules.denyWriteRegexes,
|
|
6564
6702
|
}
|
|
6565
6703
|
: undefined;
|
|
6566
6704
|
if (!locateOnPath('sandbox-exec')) {
|
|
6567
6705
|
throw new Error(`[file-sandbox] refusing to start session ${cfg.sessionId}: sandbox-exec not found`);
|
|
6568
6706
|
}
|
|
6569
|
-
const profileDir = join(
|
|
6707
|
+
const profileDir = join(isolationRuntimeDataDir, 'read-isolation');
|
|
6570
6708
|
mkdirSync(profileDir, { recursive: true });
|
|
6571
6709
|
const profilePath = join(profileDir, `${cfg.sessionId}.sb`);
|
|
6572
6710
|
replaceManagedOriginCapabilityFile(profilePath, buildSeatbeltProfile(denyPaths, allowPaths, finalDenyPaths, traverseDirs, denyRegexes, writeRules, protectedWrites));
|
|
6573
6711
|
seatbeltProfilePath = profilePath;
|
|
6574
6712
|
spawnArgs = ['-f', profilePath, spawnBin, ...spawnArgs];
|
|
6575
|
-
spawnBin = 'sandbox-exec';
|
|
6713
|
+
spawnBin = credentialMechanismExecutable ?? '/usr/bin/sandbox-exec';
|
|
6576
6714
|
log(`[file-sandbox] wrapping ${cliAdapter.id} in Seatbelt (read-isolation=${!!readIsolationCtx}, write-sandbox=${!!writeRules}): sandbox-exec -f ${profilePath}`);
|
|
6577
6715
|
}
|
|
6578
6716
|
// Fresh sandboxed spawn on a persistent backend: stamp the pane with this daemon's
|
|
6579
6717
|
// policy version + boot id so a later suspend→resume reattach can be trusted (see the stale-pane
|
|
6580
6718
|
// guard above). Applies to read-isolation AND write-sandbox panes (both carry the
|
|
6581
6719
|
// Seatbelt confinement on the live process). pty needs no marker (never reattached).
|
|
6582
|
-
if (
|
|
6720
|
+
if (appliedIsolationCapabilities.length > 0 && persistentSessionName && !willReattachPersistent) {
|
|
6583
6721
|
try {
|
|
6584
|
-
const markerDir = join(
|
|
6722
|
+
const markerDir = join(isolationRuntimeDataDir, 'read-isolation');
|
|
6585
6723
|
mkdirSync(markerDir, { recursive: true });
|
|
6586
|
-
replaceManagedOriginCapabilityFile(join(markerDir, `${cfg.sessionId}.boot`), isolationPaneMarkerContent(cfg.daemonBootId ?? ''));
|
|
6724
|
+
replaceManagedOriginCapabilityFile(join(markerDir, `${cfg.sessionId}.boot`), isolationPaneMarkerContent(cfg.daemonBootId ?? '', appliedIsolationCapabilities));
|
|
6587
6725
|
}
|
|
6588
6726
|
catch { /* non-fatal: worst case a same-lifetime reattach cold-spawns instead */ }
|
|
6589
6727
|
}
|
|
@@ -6836,6 +6974,143 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6836
6974
|
}
|
|
6837
6975
|
}
|
|
6838
6976
|
}
|
|
6977
|
+
// Mandatory credential-only confinement is the OUTERMOST launch wrapper so
|
|
6978
|
+
// wrapperCli and every descendant it starts inherit the boundary. Full
|
|
6979
|
+
// Seatbelt/bwrap sessions were already wrapped above and never enter these
|
|
6980
|
+
// branches (gate mode `covered`), avoiding nested/double sandboxes.
|
|
6981
|
+
if (!willReattachPersistent && credentialOnlySeatbelt) {
|
|
6982
|
+
const canonical = (path) => {
|
|
6983
|
+
try {
|
|
6984
|
+
return realpathSync(path);
|
|
6985
|
+
}
|
|
6986
|
+
catch {
|
|
6987
|
+
return path;
|
|
6988
|
+
}
|
|
6989
|
+
};
|
|
6990
|
+
const rules = buildCredentialIsolationRules({
|
|
6991
|
+
homeDir: canonical(hostHomeDir),
|
|
6992
|
+
botmuxHome: canonical(configuredBotmuxHome),
|
|
6993
|
+
defaultBotmuxHome: canonical(defaultBotmuxHome),
|
|
6994
|
+
});
|
|
6995
|
+
const profileDir = join(isolationRuntimeDataDir, 'read-isolation');
|
|
6996
|
+
mkdirSync(profileDir, { recursive: true });
|
|
6997
|
+
const profilePath = join(profileDir, `${cfg.sessionId}.sb`);
|
|
6998
|
+
replaceManagedOriginCapabilityFile(profilePath, buildSeatbeltProfile(rules.denyPaths.map(canonical), [], [], [], rules.denyRegexes, undefined, {
|
|
6999
|
+
denyWritePaths: [...new Set([
|
|
7000
|
+
...rules.denyWritePaths.map(canonical),
|
|
7001
|
+
canonical(profileDir),
|
|
7002
|
+
])],
|
|
7003
|
+
denyWriteRegexes: rules.denyWriteRegexes,
|
|
7004
|
+
denyWriteLiterals: [...new Set([
|
|
7005
|
+
...rules.denyWriteLiterals.map(canonical),
|
|
7006
|
+
canonical(profileDir),
|
|
7007
|
+
defaultBotmuxHome,
|
|
7008
|
+
configuredBotmuxHome,
|
|
7009
|
+
])],
|
|
7010
|
+
}));
|
|
7011
|
+
seatbeltProfilePath = profilePath;
|
|
7012
|
+
spawnArgs = ['-f', profilePath, spawnBin, ...spawnArgs];
|
|
7013
|
+
// Absolute path only: a pre-activation unconfined CLI could plant a fake
|
|
7014
|
+
// sandbox-exec earlier on PATH. Prefer the probe result; fall back to the
|
|
7015
|
+
// well-known system location used by full Seatbelt wrapping above.
|
|
7016
|
+
spawnBin = credentialMechanismExecutable ?? '/usr/bin/sandbox-exec';
|
|
7017
|
+
log(`[device-credential-isolation] wrapping ${cliAdapter.id} in credential-only Seatbelt: ${spawnBin} -f ${profilePath}`);
|
|
7018
|
+
}
|
|
7019
|
+
if (!willReattachPersistent && credentialOnlyBwrap) {
|
|
7020
|
+
const panePolicyDir = join(isolationRuntimeDataDir, 'read-isolation');
|
|
7021
|
+
mkdirSync(panePolicyDir, { recursive: true });
|
|
7022
|
+
const hideDirectories = new Set();
|
|
7023
|
+
const hideFiles = new Set();
|
|
7024
|
+
const processedRoots = new Set();
|
|
7025
|
+
const isDashboardAuthorityBasename = (name) => name === '.dashboard-secret'
|
|
7026
|
+
|| name.startsWith('.dashboard-secret.')
|
|
7027
|
+
|| name === '.dashboard-token'
|
|
7028
|
+
|| name.startsWith('.dashboard-token.');
|
|
7029
|
+
for (const rawRoot of [defaultBotmuxHome, configuredBotmuxHome]) {
|
|
7030
|
+
let root = rawRoot;
|
|
7031
|
+
try {
|
|
7032
|
+
root = realpathSync(rawRoot);
|
|
7033
|
+
}
|
|
7034
|
+
catch { /* gate authority root is checked below */ }
|
|
7035
|
+
if (processedRoots.has(root))
|
|
7036
|
+
continue;
|
|
7037
|
+
let rootStat;
|
|
7038
|
+
try {
|
|
7039
|
+
rootStat = lstatSync(root);
|
|
7040
|
+
}
|
|
7041
|
+
catch {
|
|
7042
|
+
// An unrelated/default root can be absent when the activation signal
|
|
7043
|
+
// came from a custom BOTMUX_HOME. At least one signalled root must
|
|
7044
|
+
// survive; the builder below rejects an empty root set.
|
|
7045
|
+
continue;
|
|
7046
|
+
}
|
|
7047
|
+
if (!rootStat.isDirectory()) {
|
|
7048
|
+
throw new Error(`[device-credential-isolation] authority root is not a directory: ${rawRoot}`);
|
|
7049
|
+
}
|
|
7050
|
+
processedRoots.add(root);
|
|
7051
|
+
const authorityDirectory = join(root, DEVICE_AUTHORITY_DIRECTORY);
|
|
7052
|
+
try {
|
|
7053
|
+
const authorityStat = lstatSync(authorityDirectory);
|
|
7054
|
+
if (!authorityStat.isDirectory()) {
|
|
7055
|
+
throw new Error(`[device-credential-isolation] device authority path is not a directory: ${authorityDirectory}`);
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
catch (error) {
|
|
7059
|
+
if (error.code !== 'ENOENT')
|
|
7060
|
+
throw error;
|
|
7061
|
+
// A fixed empty mount target lets the child mask the entire authority
|
|
7062
|
+
// namespace while leaving BOTMUX_HOME itself live and writable.
|
|
7063
|
+
mkdirSync(authorityDirectory, { mode: 0o700 });
|
|
7064
|
+
}
|
|
7065
|
+
hideDirectories.add(realpathSync(authorityDirectory));
|
|
7066
|
+
for (const name of readdirSync(root)) {
|
|
7067
|
+
if (name === DEVICE_AUTHORITY_DIRECTORY)
|
|
7068
|
+
continue;
|
|
7069
|
+
if (!isCredentialIsolationReservedBasename(name)
|
|
7070
|
+
&& !isDashboardAuthorityBasename(name))
|
|
7071
|
+
continue;
|
|
7072
|
+
const entry = join(root, name);
|
|
7073
|
+
try {
|
|
7074
|
+
const stat = lstatSync(entry);
|
|
7075
|
+
if (!stat.isFile()) {
|
|
7076
|
+
// Never follow a host-authority symlink or special file. Exact mask
|
|
7077
|
+
// destinations must be stable regular files or the launch fails
|
|
7078
|
+
// closed before an untrusted CLI starts.
|
|
7079
|
+
throw new Error(`[device-credential-isolation] authority entry is not a regular file: ${entry}`);
|
|
7080
|
+
}
|
|
7081
|
+
hideFiles.add(entry);
|
|
7082
|
+
}
|
|
7083
|
+
catch (error) {
|
|
7084
|
+
if (error.code !== 'ENOENT')
|
|
7085
|
+
throw error;
|
|
7086
|
+
// A racing removal needs no mask; a later exact credential rotation
|
|
7087
|
+
// is host-only and the long-lived device secret namespace itself is
|
|
7088
|
+
// protected wholesale above.
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
let credentialCliBin = spawnBin;
|
|
7093
|
+
try {
|
|
7094
|
+
credentialCliBin = realpathSync(spawnBin);
|
|
7095
|
+
}
|
|
7096
|
+
catch { /* spawn will fail closed if unresolved */ }
|
|
7097
|
+
const credentialSandbox = prepareCredentialOnlySandbox({
|
|
7098
|
+
hideDirectories: [...hideDirectories],
|
|
7099
|
+
hideFiles: [...hideFiles],
|
|
7100
|
+
readonlyPaths: [realpathSync(panePolicyDir)],
|
|
7101
|
+
workingDir: spawnCwd,
|
|
7102
|
+
cliBin: credentialCliBin,
|
|
7103
|
+
cliArgs: spawnArgs,
|
|
7104
|
+
});
|
|
7105
|
+
if (!credentialSandbox) {
|
|
7106
|
+
throw new Error(`[device-credential-isolation] refusing to start session ${cfg.sessionId}: `
|
|
7107
|
+
+ 'credential-only bubblewrap could not be established');
|
|
7108
|
+
}
|
|
7109
|
+
spawnBin = credentialSandbox.bin;
|
|
7110
|
+
spawnArgs = credentialSandbox.args;
|
|
7111
|
+
log(`[device-credential-isolation] wrapping ${cliAdapter.id} in credential-only bwrap `
|
|
7112
|
+
+ `(${hideDirectories.size} authority dir(s), ${hideFiles.size} exact file(s))`);
|
|
7113
|
+
}
|
|
6839
7114
|
backend.spawn(spawnBin, spawnArgs, {
|
|
6840
7115
|
cwd: spawnCwd,
|
|
6841
7116
|
cols: PTY_COLS,
|
|
@@ -6848,6 +7123,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6848
7123
|
// can verify they were spawned inside a botmux session by walking the
|
|
6849
7124
|
// process tree and looking for a matching pid file in this directory.
|
|
6850
7125
|
const cliPid = backend.getChildPid?.();
|
|
7126
|
+
publishLocalProcessAttestation(cliPid ?? undefined);
|
|
6851
7127
|
if (cliPid && process.env.SESSION_DATA_DIR) {
|
|
6852
7128
|
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
6853
7129
|
try {
|
|
@@ -6890,6 +7166,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6890
7166
|
// Per-tick maybeFollowSessionRotationViaPid (bridge 1s poller) reads the
|
|
6891
7167
|
// module-level bridgeCliPid and re-points to the real CLI's jsonl.
|
|
6892
7168
|
bridgeCliPid = realPid;
|
|
7169
|
+
publishLocalProcessAttestation(realPid);
|
|
6893
7170
|
},
|
|
6894
7171
|
schedule: (fn, ms) => { setTimeout(fn, ms); },
|
|
6895
7172
|
});
|
|
@@ -6924,6 +7201,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6924
7201
|
return;
|
|
6925
7202
|
const pid = backend.getChildPid?.();
|
|
6926
7203
|
if (pid) {
|
|
7204
|
+
publishLocalProcessAttestation(pid);
|
|
6927
7205
|
if (process.env.SESSION_DATA_DIR && !cliPidMarker) {
|
|
6928
7206
|
try {
|
|
6929
7207
|
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
@@ -7090,6 +7368,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7090
7368
|
}
|
|
7091
7369
|
isSettlingFirstFlush = false;
|
|
7092
7370
|
promptReadyDetectedDuringSettle = false;
|
|
7371
|
+
readyPatternSeenDuringHold = false;
|
|
7093
7372
|
// Reset quiescence baseline so the settle measures silence from THIS spawn.
|
|
7094
7373
|
lastPtyOutputAtMs = Date.now();
|
|
7095
7374
|
const readyHookAvailable = effectiveReadyHookInstall
|
|
@@ -7315,8 +7594,23 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7315
7594
|
// invoking markPromptReady() would claim the CLI is idle while it's still
|
|
7316
7595
|
// mid-boot, so flushPending() alone is safer — it respects typeAheadAllowed
|
|
7317
7596
|
// and drains pendingMessages now.
|
|
7318
|
-
|
|
7597
|
+
//
|
|
7598
|
+
// Non-type-ahead adapters (Hermes etc.) flushPending() rejects the held
|
|
7599
|
+
// message while isPromptReady is false — it bails on
|
|
7600
|
+
// `!isPromptReady && !typeAheadAllowed`. The hard cap means we've waited
|
|
7601
|
+
// long enough. By now the ready gate's 45s fallback has already released
|
|
7602
|
+
// the gate (READY_SIGNAL_TIMEOUT_MS < this 90s hard cap) and the post-
|
|
7603
|
+
// release settle has drained, so markPromptReady() proceeds: it sets
|
|
7604
|
+
// isPromptReady and drains the held first prompt. Without this, a spawn
|
|
7605
|
+
// that never fires the ready signal (and whose readyPattern the idle
|
|
7606
|
+
// detector never matched) would hold the first queued message forever —
|
|
7607
|
+
// the previous code only logged "forcing flush" without actually flushing
|
|
7608
|
+
// for non-type-ahead adapters.
|
|
7609
|
+
if (decideHardTimeoutAction(cliAdapter?.supportsTypeAhead === true) === 'flush') {
|
|
7319
7610
|
flushPending();
|
|
7611
|
+
return;
|
|
7612
|
+
}
|
|
7613
|
+
markPromptReady();
|
|
7320
7614
|
};
|
|
7321
7615
|
setTimeout(() => releaseFirstPromptTimeout(FIRST_PROMPT_TIMEOUT_MS, false), FIRST_PROMPT_TIMEOUT_MS);
|
|
7322
7616
|
// Riff (and other remote HTTP backends) have no local boot process — the
|
|
@@ -7329,6 +7623,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7329
7623
|
}
|
|
7330
7624
|
}
|
|
7331
7625
|
function killCli(opts = {}) {
|
|
7626
|
+
currentCliCredentialIsolated = false;
|
|
7332
7627
|
stopSessionMcpGatewayHost();
|
|
7333
7628
|
stopCodexRpcEngine();
|
|
7334
7629
|
destroyCrashDiagnosticTerminal('killCli');
|
|
@@ -7347,6 +7642,7 @@ function killCli(opts = {}) {
|
|
|
7347
7642
|
}
|
|
7348
7643
|
isSettlingFirstFlush = false;
|
|
7349
7644
|
promptReadyDetectedDuringSettle = false;
|
|
7645
|
+
readyPatternSeenDuringHold = false;
|
|
7350
7646
|
stopScreenAnalyzer();
|
|
7351
7647
|
stopScreenUpdates();
|
|
7352
7648
|
backend?.kill();
|
|
@@ -8639,6 +8935,16 @@ function send(msg) {
|
|
|
8639
8935
|
}
|
|
8640
8936
|
process.send?.(payload);
|
|
8641
8937
|
}
|
|
8938
|
+
function publishLocalProcessAttestation(cliPid) {
|
|
8939
|
+
const cliProcStart = cliPid ? readProcessStartIdentity(cliPid) : undefined;
|
|
8940
|
+
send({
|
|
8941
|
+
type: 'local_process_attestation',
|
|
8942
|
+
backendType: effectiveBackendType,
|
|
8943
|
+
credentialIsolated: currentCliCredentialIsolated,
|
|
8944
|
+
...(cliPid ? { cliPid } : {}),
|
|
8945
|
+
...(cliProcStart ? { cliProcStart } : {}),
|
|
8946
|
+
});
|
|
8947
|
+
}
|
|
8642
8948
|
/** Deliver a terminal IPC message before exiting the worker. `process.send()`
|
|
8643
8949
|
* only queues asynchronously; calling process.exit() on the next line can drop
|
|
8644
8950
|
* the exact startup diagnostic the user needs and recreate the silent-bot bug.
|