import type { BackendType, PersistentBackendTarget, SessionProbe } from '../adapters/backend/types.js'; import type { Session } from '../types.js'; import type { DaemonSession } from './types.js'; export declare const DEVICE_ISOLATION_PREPARE_PATH = "/api/device-isolation/activation/prepare"; export declare const DEVICE_ISOLATION_COMMIT_PATH = "/api/device-isolation/activation/commit"; export declare const DEVICE_ISOLATION_RELEASE_PATH = "/api/device-isolation/activation/release"; type InventoryBackend = BackendType | 'unknown'; type ProcessIdentity = { pid: number; procStart: string; }; export type DeviceIsolationBlocker = 'adopted_session' | 'unknown_backend' | 'unattested_worker' | 'stale_attestation' | 'process_identity_unavailable' | 'backend_probe_unknown' | 'backend_inconsistent' /** * A durable mojo launcher-env quarantine record with no session row left. * * An explicit `/close` deletes the row, but the mojo child it SIGTERM-ed was * never proven dead (no escalation, no wait, possible detached descendants). * Without this the inventory silently loses the only evidence that a hooked * client may still hold an activated credential on this host. */ | 'mojo_launcher_env_residual' /** * An outstanding containment handle: a turn subtree nobody proved quiescent. * * Same residual problem as the launcher-env record, different evidence. The * handle outlives the worker generation AND the session row, so an explicit * `/close` (which deletes the row) must not be able to make an unproven * credentialed subtree vanish from the inventory. Only * releaseContainmentHandle, which demands a `proven` verdict, removes it. */ | 'mojo_containment_unproven' /** * A mojo session, unconditionally. * * Unlike riff (pure HTTP, nothing executes here), MojoBackend.runTurn() calls * `spawnProcess(bin, args, { env: buildEnv() })` on THIS host for every single * turn, and that child receives the injected credential. So mojo is not an * off-box backend at all; it is a local CLI with a remote control plane. * * Nothing currently available can turn that into a safe_remote proof: * - the attestation IPC carries `credentialIsolated: false` for a fully-remote * mojo (the remote-bypass path applies no isolation) and no `cliPid` (it is * sent right after spawn, when MojoBackend holds no persistent child), so it * evidences neither confinement nor absence of a running child * - a generation-matched attestation only vouches for the CURRENT worker; the * previous generation's child was merely sent an unescalated SIGTERM that * nothing awaited, and may have detached descendants * - the durable ledger is a same-user file the hijacked child can delete * * Until there is per-turn confinement or whole-process-group quiescence proof, * the honest answer is "cannot prove", so mojo is always blocked — never * `quiescent`, and never `safe_remote`. */ | 'mojo_local_turn_unconfined'; export interface DeviceIsolationRuntimeSession { sessionId: string; adopted: boolean; /** Present only for a session re-admitted from the durable quarantine record * because no row survives (see appendResidualMojoLauncherEnvSessions). */ mojoLauncherEnvResidual?: boolean; /** Re-admitted from a durable containment handle with no surviving row * (see appendResidualContainmentSessions). */ containmentResidual?: boolean; /** Backend stamped by daemon-owned state, if this session predates no stamp. */ frozenBackend?: BackendType; /** Exact worker-selected persistent resource. Shared Herdr carries both the * host session and the one Botmux-owned agent inside it. */ persistentBackendTarget?: PersistentBackendTarget; /** PID carried only by an unregistered persisted row. It has no process-start * identity or worker-generation attestation, so a still-live/reused PID must * block activation instead of being mistaken for a quiescent PTY session. */ unregisteredPid?: number; workerPresent: boolean; workerGeneration?: number; worker?: ProcessIdentity; attestation?: { backendType: BackendType; credentialIsolated: boolean; cli?: ProcessIdentity; workerGeneration?: number; }; /** * Remote backends only. True when this session provably executes NOTHING on * this host, so there is no local process identity to prove. * * riff is always true. mojo is NOT: it spawns its binary locally every turn, * and only `cloud: true` (without `localDaemon`) moves the agent's tools * off-box. Left undefined the session is treated as locally-executing, which * is the safe direction — an unproven claim must never authorize credential * activation around a possibly-live local child. */ remoteExecutionProven?: boolean; /** Opaque production handle. It is deliberately excluded from generation. */ source?: DaemonSession; } export interface DeviceIsolationInventoryEntry { sessionId: string; backendType: InventoryBackend; disposition: 'blocked' | 'owned_local' | 'safe_remote' | 'quiescent'; credentialIsolated?: boolean; worker?: ProcessIdentity; cli?: ProcessIdentity; workerGeneration?: number; persistent?: { target: PersistentBackendTarget; probe: SessionProbe; }; blocker?: DeviceIsolationBlocker; } export interface DeviceIsolationInventory { generation: string; entries: DeviceIsolationInventoryEntry[]; blockers: Array<{ sessionId: string; blocker: DeviceIsolationBlocker; }>; } export interface DeviceIsolationDaemonIdentity { larkAppId: string; bootInstanceId: string; } export interface DeviceIsolationDaemonDependencies { now: () => number; listSessions: () => DeviceIsolationRuntimeSession[]; processStart: (pid: number) => string | undefined; processExists: (pid: number) => boolean; signalProcess: (pid: number, signal: NodeJS.Signals) => void; probePersistent: (target: PersistentBackendTarget) => SessionProbe; /** Full sessionId is mandatory so prefix-addressed backends such as ZMX can * re-verify ownership before destructive teardown. */ killPersistent: (target: PersistentBackendTarget, sessionId: string) => void; closeWorker: (session: DeviceIsolationRuntimeSession) => void; readMarker: () => string | null; sleep: (ms: number) => Promise; dataDir: () => string; } export type DeviceIsolationDaemonResult = { status: 200 | 409 | 423 | 503; body: Record; }; /** * Does this session provably execute nothing on this host? * * riff: always (pure HTTP). mojo: only with `cloud` on and `localDaemon` off — * it spawns the binary locally every turn otherwise. Reading the live bot config * is deliberate: the frozen backendType alone cannot answer this, and guessing * `true` would be the fail-open direction. */ export declare function resolveRemoteExecutionProven(ds: DaemonSession): boolean; /** * Add durable, persisted local-resource rows that startup restore intentionally * kept active but could not safely register. Device isolation must see those * ownership records even when the routing registry cannot: otherwise an * inconclusive teardown could disappear from the activation inventory. * * Runtime state wins by complete session id because it carries the current * worker generation and attestation. Queued rows and command scratches have no * running local resource. Legacy rows with a CLI/PID/target marker are still * evidence of a possibly-live local resource even when backendType was never * stamped; include them as `unknown_backend` blockers instead of silently * excluding them from a credential-isolation transaction. */ export declare function mergePersistedDeviceIsolationSessions(runtimeSessions: readonly DeviceIsolationRuntimeSession[], persistedSessions: readonly Session[]): DeviceIsolationRuntimeSession[]; /** * Re-admit sessions that exist ONLY as a durable containment handle. * * An explicit `/close` deletes the row and the worker generation is long gone, * yet the handle says a turn subtree was never proven quiescent — so the session * must keep blocking credential activation instead of disappearing. * * Exported for tests: the whole point is a session with no row anywhere. */ export declare function appendResidualContainmentSessions(sessions: readonly DeviceIsolationRuntimeSession[], containedIds?: readonly string[]): DeviceIsolationRuntimeSession[]; /** * Re-admit sessions that exist ONLY as a durable launcher-env quarantine record. * * Both other sources can lose them: an explicit `/close` deletes the row, and a * daemon restart drops the in-memory ledger — yet the mojo child that was handed * `LD_PRELOAD`/`PATH` may still be running, because its teardown is an * unescalated `SIGTERM` that nothing waits on. Such a session must keep blocking * credential activation instead of vanishing from the inventory. * * Exported for tests: the whole point is a session with no row anywhere. */ export declare function appendResidualMojoLauncherEnvSessions(sessions: readonly DeviceIsolationRuntimeSession[], quarantinedIds?: readonly string[]): DeviceIsolationRuntimeSession[]; export declare function buildDeviceIsolationInventory(): DeviceIsolationInventory; export declare function setDeviceIsolationDaemonIdentity(identity: DeviceIsolationDaemonIdentity | null): void; export declare function prepareDeviceIsolationActivation(body: unknown): DeviceIsolationDaemonResult; export declare function commitDeviceIsolationActivation(body: unknown): Promise; export declare function releaseDeviceIsolationActivation(body: unknown): DeviceIsolationDaemonResult; /** Test seams keep process and backend destruction out of unit tests. */ export declare function setDeviceIsolationDaemonDependenciesForTest(overrides: Partial | null): void; export declare function resetDeviceIsolationDaemonForTest(): void; export declare function logDeviceIsolationActivationError(error: unknown): void; export {}; //# sourceMappingURL=device-isolation-daemon.d.ts.map