import { type KeyObject } from 'node:crypto'; import { type SpawnSyncReturns } from 'node:child_process'; import type { BackendType } from '../adapters/backend/types.js'; import type { ScreenStatus } from '../types.js'; /** * The launch boundary carries only a path to an owner-only, read-once file. * That file contains a fresh Ed25519 private key. The runner consumes and * unlinks it before app-server (and therefore model tools) can start. Only the * public key is persisted by the worker. */ export declare const CODEX_APP_CONTROL_BOOTSTRAP_ENV = "BOTMUX_CODEX_APP_CONTROL_BOOTSTRAP"; export declare const CODEX_APP_CONTROL_LINE_MAX_BYTES = 4096; export declare const CODEX_APP_CONTROL_FINAL_MAX_BYTES = 1048576; export declare const CODEX_APP_CONTROL_FINAL_CHUNK_BYTES = 1536; /** Keep control bootstrap/proof alive for the worker's existing cold-start cap. */ export declare const CODEX_APP_CONTROL_STARTUP_TIMEOUT_MS = 90000; /** Absolute per-connection challenge/accept deadline; transport activity does not extend it. */ export declare const CODEX_APP_CONTROL_HANDSHAKE_TIMEOUT_MS = 5000; declare const CONTROL_STATE_VERSION: 3; declare const CONTROL_WIRE_VERSION: 2; declare const CONTROL_LOCATOR_VERSION: 1; /** * Arm the shared Codex App startup deadline. Keeping the scheduling in one * helper prevents the runner auth gate, bootstrap cleanup, worker proof gate, * and first-prompt hard cap from silently drifting to different lifetimes. */ export declare function armCodexAppControlStartupTimeout(onTimeout: () => void, timeoutMs?: number): ReturnType; export declare function armCodexAppControlHandshakeTimeout(onTimeout: () => void, timeoutMs?: number): ReturnType; export type CodexAppSignedStateReadiness = 'invalid' | 'waiting' | 'ready'; /** Authentication and a signed busy/idle bit are not sufficient readiness. * The runner must explicitly assert that its initialized stdin/app-server path * accepts input. Missing/non-boolean values are protocol violations; `false` * is a valid not-ready state that leaves the absolute proof deadline armed. */ export declare function codexAppSignedStateReadiness(payload: unknown): CodexAppSignedStateReadiness; /** Re-armable proof deadline shared by startup and authenticated disconnects. */ export declare class CodexAppControlProofDeadline { private timer; arm(onTimeout: () => void, timeoutMs?: number): void; clear(): void; get armed(): boolean; } /** * Coalesce one signed control record while its worker-side effects are still * awaiting a durable daemon ACK. Endpoint replacement can replay the same * generation/sequence before the replay window is committed; every replay * must observe the first application result instead of running queue or * liveness effects again. * * The caller releases the entry only after committing the replay window (or * after a rejected application). Keeping release outside the promise closes * the resolution -> replay-commit gap. */ export declare class CodexAppControlRecordApplicationGate { private readonly inFlight; run(generation: string, seq: number, apply: () => boolean | Promise): Promise; release(generation: string, seq: number, application: Promise): void; private key; } export type CodexAppRuntimeScreenStatus = Exclude; /** Authentication alone is not readiness. Until the active generation has * published a signed accepting-input state, an idle screen is fail-closed to * working so reconnect replay cannot expose a false-idle interval. */ export declare function projectCodexAppControlReadinessStatus(base: CodexAppRuntimeScreenStatus, readiness: { controlProven: boolean; signedStateObserved: boolean; inputReady: boolean; }): CodexAppRuntimeScreenStatus; export interface CodexAppControlPathOptions { platform?: NodeJS.Platform; localAppData?: string; homeDirectory?: string; } export interface CodexAppControlFilesystemPolicy { useNoFollow: boolean; verifyUid: boolean; verifyExactMode: boolean; chmodAfterCreate: boolean; verifyPostUnlinkLinkCount: boolean; fsyncDirectory: boolean; } export declare function codexAppControlFilesystemPolicy(platform?: NodeJS.Platform): CodexAppControlFilesystemPolicy; /** * Windows never places control material under SESSION_DATA_DIR: that setting * is bot/user supplied and may point at a shared tree. The fixed root is * instead anchored in the current Windows profile and hardened before any * secret or locator is created beneath it. */ export declare function codexAppWindowsControlRoot(options?: CodexAppControlPathOptions): string; export declare function parseWindowsCurrentSid(output: string): string | undefined; export declare function decodeWindowsAclSnapshot(raw: Buffer): string; /** Locale-independent verification of the SDDL emitted by `icacls /save`. */ export declare function verifyWindowsCodexAppControlDacl(snapshot: string, currentSid: string, kind?: 'directory' | 'file'): boolean; export type WindowsControlCommandRunner = (command: string, args: string[]) => Pick, 'status' | 'stdout' | 'stderr' | 'error'>; /** * Remove inherited ACLs, set the current SID as owner, grant only that SID and * SYSTEM full control, and verify the protected DACL. Unknown explicit ACEs * are retained by icacls but cause exact verification to fail closed. */ export declare function hardenWindowsCodexAppControlDirectory(directory: string, runner?: WindowsControlCommandRunner): void; /** Validate/harden a pre-existing fixed-name state file before trusting it. */ export declare function hardenWindowsCodexAppControlFile(path: string, runner?: WindowsControlCommandRunner): void; /** Create/validate a non-symlink owner-only directory. */ export declare function ensureCodexAppControlDirectory(directory: string, platform?: NodeJS.Platform): void; export declare function codexAppControlStatePath(sessionDataDir: string, sessionId: string): string; export declare function codexAppControlStatePathForPlatform(sessionDataDir: string, sessionId: string, options?: CodexAppControlPathOptions): string; export declare function codexAppControlSocketPath(directory: string, sessionId: string): string; export declare function codexAppPosixControlRoot(uid?: number | undefined): string; export declare function codexAppPosixOwnerLeaseDirectory(controlRoot: string, sessionId: string): string; export declare function generateCodexAppPosixSocketEndpoint(socketDirectory: string): string; export declare function codexAppControlLocatorPath(controlRoot: string, sessionId: string, platform?: NodeJS.Platform): string; export declare function generateCodexAppWindowsPipeEndpoint(): string; /** * A fixed coordination pipe is held for the lifetime of one Windows worker * process. It is not a control endpoint and is never put in a runner locator. * Its only purpose is to serialize locator publishers across the daemon's * kill-then-fork overlap window. The OS releases it if the old worker is * terminated, avoiding a crash-stale filesystem lock. */ export declare function codexAppWindowsOwnerPipeEndpoint(sessionId: string): string; export interface CodexAppControlOwnerLeaseOptions { bind: () => Promise; timeoutMs?: number; retryDelayMs?: number; now?: () => number; wait?: (delayMs: number) => Promise; } /** Retry only a competing bind; every other owner-lease error fails closed. */ export declare function acquireCodexAppControlOwnerLease(options: CodexAppControlOwnerLeaseOptions): Promise; export type CodexAppOwnerProcessStatus = 'alive' | 'dead' | 'unknown'; export interface CodexAppPosixOwnerLease { directory: string; ownerRecordPath: string; isOwned(): boolean; release(): void; } export interface CodexAppPosixOwnerLeaseOptions { controlRoot: string; sessionId: string; platform?: NodeJS.Platform; timeoutMs?: number; retryDelayMs?: number; initializationGraceMs?: number; pid?: number; processStartToken?: string; now?: () => number; wait?: (delayMs: number) => Promise; inspectOwner?: (pid: number, processStartToken: string) => CodexAppOwnerProcessStatus; /** Deterministic handoff-race seam: invoked after mkdir(EEXIST), before observation. */ onContended?: () => void; /** Deterministic crash-resume seam: invoked after this creator pins the new directory inode. */ onOwnerDirectoryCreated?: (directory: string) => void | Promise; /** Deterministic crash-window seam: pauses after owner publication, before the directory CAS. */ onOwnerRecordPublished?: (directory: string, ownerRecordPath: string) => void | Promise; /** Deterministic replacement seam: pauses after a stale owner is pinned, before reaper publication. */ onBeforeReaperRecordPublished?: (directory: string) => void | Promise; /** Deterministic crash-window seam: pauses after reaper publication, before the directory CAS. */ onReaperRecordPublished?: (directory: string, reaperRecordPath: string) => void | Promise; } export declare function codexAppPosixProcessProbeEnv(base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; export declare function readCodexAppProcessStartToken(pid: number, platform?: NodeJS.Platform): string | undefined; /** * Process-lifetime publisher lease for POSIX. * * Both owners and stale cleaners publish complete, random-name actor records. * An actor record is written to `.pending`, synced, and atomically renamed to * `.json`, so SIGKILL cannot create a partial authoritative record. Existing * partial records remain recoverable after a grace interval. Every complete * actor is bound to the exact directory dev+ino it intended to mutate; reapers * additionally bind the exact owner-record inode/process tuple. A cleaner moves * the exact observed random-name inode out of the authority directory before * deleting it; that rename is the CAS boundary which prevents a delayed actor * from poisoning or deleting a successor's record after path replacement. */ export declare function acquireCodexAppPosixOwnerLease(options: CodexAppPosixOwnerLeaseOptions): Promise; export declare function generateCodexAppControlEpoch(): string; export interface CodexAppControlLocator { version: typeof CONTROL_LOCATOR_VERSION; sessionId: string; epoch: string; endpoint: string; } export declare function isValidCodexAppWindowsPipeEndpoint(endpoint: unknown): endpoint is string; export interface CodexAppControlLocatorValidationOptions { platform?: NodeJS.Platform; locatorPath?: string; /** Trusted, worker-owned root; never derive this authority from locatorPath. */ expectedControlRoot?: string; } export declare function validateCodexAppControlLocator(value: unknown, expectedSessionId: string, options?: CodexAppControlLocatorValidationOptions): CodexAppControlLocator | undefined; export interface CodexAppControlIdentity { generation: string; publicKey: string; createdAtMs: number; } /** * pending may contain an old live identity plus a fresh spawn candidate. The * first identity that answers this worker's fresh socket challenge is atomically * collapsed to the sole active identity. No private material is persisted. */ export interface CodexAppControlState { version: typeof CONTROL_STATE_VERSION; status: 'pending' | 'active'; identities: CodexAppControlIdentity[]; updatedAtMs: number; activatedAtMs?: number; } export declare function readCodexAppControlState(path: string, platform?: NodeJS.Platform): CodexAppControlState | undefined; /** Symlink-safe atomic state replacement inside a private directory. */ export declare function writeCodexAppControlState(path: string, state: CodexAppControlState): void; export declare function writeCodexAppControlStateForPlatform(path: string, state: CodexAppControlState, platform: NodeJS.Platform): void; export declare function readCodexAppControlLocator(path: string, expectedSessionId: string, platform?: NodeJS.Platform, expectedControlRoot?: string | undefined): CodexAppControlLocator | undefined; export declare function writeCodexAppControlLocator(path: string, locator: CodexAppControlLocator, platform?: NodeJS.Platform, expectedControlRoot?: string | undefined): void; /** * The ordering contract is security-sensitive on every platform: the random * pipe/socket must already be bound before its locator becomes visible. */ export declare function bindThenPublishCodexAppControlLocator(input: { sessionId: string; epoch: string; endpoint: string; listen: (endpoint: string) => Promise; publish: (locator: CodexAppControlLocator) => void; platform?: NodeJS.Platform; locatorPath?: string; expectedControlRoot?: string; isCurrent?: () => boolean; retire?: () => void; }): Promise; export declare function shouldFailCodexAppControlChannel(input: { channelId: number; currentChannelId: number; stopping: boolean; }): boolean; /** * Runner-side endpoint policy for locator rotations. A never-accepted * locator may be retried (the independently random, protected epoch is still * required for acceptance) until the shared startup deadline. Once accepted, * the pipe name is permanently burned and only a newly published endpoint can * be used. */ export declare class CodexAppControlEndpointTracker { private readonly attemptsByEndpoint; private readonly acceptedEndpoints; take(locator: CodexAppControlLocator): string | undefined; noteAccepted(endpoint: string): void; wasAttempted(endpoint: string): boolean; attemptCount(endpoint: string): number; wasAccepted(endpoint: string): boolean; } /** Read and select one locator endpoint; missing/corrupt files are a poll miss. */ export declare function takeCodexAppControlLocatorEndpoint(input: { locatorPath: string; sessionId: string; tracker: CodexAppControlEndpointTracker; platform?: NodeJS.Platform; expectedControlRoot?: string; }): { endpoint: string; epoch: string; } | undefined; /** Missing/corrupt/legacy generations cannot prove a warm reattach. */ export declare function shouldColdStartCodexAppReattach(input: { cliId?: string; backendType: BackendType; isReattach: boolean; persistedState?: CodexAppControlState; }): boolean; export interface CodexAppControlBootstrap { path: string; identity: CodexAppControlIdentity; } export interface ConsumedCodexAppControlBootstrap { generation: string; privateKey: KeyObject; socketPath?: string; locatorPath?: string; } export type CodexAppControlBootstrapTarget = { kind: 'endpoint'; socketPath: string; } | { kind: 'locator'; locatorPath: string; }; /** Create a fresh asymmetric candidate and one O_EXCL private bootstrap. */ export declare function createCodexAppControlBootstrap(directory: string, sessionId: string, target?: string | CodexAppControlBootstrapTarget, platform?: NodeJS.Platform): CodexAppControlBootstrap; /** Remove crash-orphaned one-shot files for exactly one session generation. */ export declare function cleanupStaleCodexAppControlBootstraps(directory: string, sessionId: string, platform?: NodeJS.Platform): void; /** * One-shot consume on one O_NOFOLLOW fd. The file is unlinked before its bytes * are read and the post-unlink link count must be zero. The private key is * imported here so callers do not retain or forward its encoded form. */ export declare function consumeCodexAppControlBootstrap(path: string, expectedSessionId?: string, platform?: NodeJS.Platform): ConsumedCodexAppControlBootstrap; export declare function mergeCodexAppControlCandidate(existing: CodexAppControlState | undefined, candidate: CodexAppControlIdentity, nowMs?: number): CodexAppControlState; export declare function activateCodexAppControlIdentity(state: CodexAppControlState, generation: string, nowMs?: number): CodexAppControlState; export declare function generateCodexAppControlChallenge(): string; export interface CodexAppControlChallenge { version: typeof CONTROL_WIRE_VERSION; type: 'challenge'; sessionId: string; challenge: string; } export interface CodexAppControlAuth { version: typeof CONTROL_WIRE_VERSION; type: 'auth'; sessionId: string; generation: string; challenge: string; signature: string; } export interface CodexAppControlAccepted { version: typeof CONTROL_WIRE_VERSION; type: 'accepted'; sessionId: string; generation: string; challenge: string; endpointEpoch?: string; } export interface CodexAppControlAck { version: typeof CONTROL_WIRE_VERSION; type: 'ack'; sessionId: string; generation: string; challenge: string; seq: number; } export interface CodexAppSignedControlMarker { version: typeof CONTROL_WIRE_VERSION; type: 'marker'; sessionId: string; generation: string; challenge: string; seq: number; kind: string; payload: Record; signature: string; } export type CodexAppControlWireRecord = CodexAppControlChallenge | CodexAppControlAuth | CodexAppControlAccepted | CodexAppControlAck | CodexAppSignedControlMarker; export declare function encodeCodexAppControlChallenge(sessionId: string, challenge: string): string; export declare function encodeCodexAppControlAccepted(sessionId: string, generation: string, challenge: string, endpointEpoch?: string): string; export declare function encodeCodexAppControlAck(sessionId: string, generation: string, challenge: string, seq: number): string; export declare function encodeCodexAppControlAuth(privateKey: KeyObject, sessionId: string, generation: string, challenge: string): string; export declare function encodeCodexAppSignedControlMarker(privateKey: KeyObject, sessionId: string, generation: string, challenge: string, seq: number, kind: string, payload: Record): string; export declare function parseCodexAppControlWireRecord(line: string): CodexAppControlWireRecord | undefined; export type CodexAppControlRunnerHandshakeAction = { type: 'authenticate'; challenge: string; } | { type: 'accepted'; challenge: string; } | { type: 'ack'; seq: number; } | { type: 'reject'; }; /** * Pure runner-side handshake state machine. Keeping the phase checks here * makes repeated challenges, wrong locator epochs, and out-of-order ACKs * executable in unit tests instead of relying on source-string assertions. */ export declare class CodexAppControlRunnerHandshake { private readonly expectedSessionId; private readonly expectedGeneration; private readonly expectedEndpointEpoch?; private phase; private challengeValue; constructor(expectedSessionId: string, expectedGeneration: string, expectedEndpointEpoch?: string | undefined); handle(record: CodexAppControlWireRecord | undefined, sentThrough: number): CodexAppControlRunnerHandshakeAction; get active(): boolean; } export declare function verifyCodexAppControlAuth(auth: CodexAppControlAuth, publicKey: string): boolean; export declare function authenticateCodexAppControlCandidate(input: { state: CodexAppControlState | undefined; auth: CodexAppControlAuth; sessionId: string; challenge: string; }): CodexAppControlIdentity | undefined; export declare function verifyCodexAppSignedControlMarker(marker: CodexAppSignedControlMarker, publicKey: string): boolean; /** * Marker sequences may begin above one after a worker replacement, but every * record on one authenticated connection must then be contiguous. This keeps * a skipped final fragment from being hidden by a later cumulative ACK. */ export declare class CodexAppControlSequenceFence { private previous; accept(seq: number): boolean; } export type CodexAppFinalAssemblyResult = { status: 'not-final'; } | { status: 'accepted'; } | { status: 'complete'; payload: Record; } | { status: 'reject'; reason: string; }; /** * Per-connection final transaction assembler. Start and chunks deliberately * remain unacknowledged; only a valid, complete end record is eligible for the * cumulative ACK. Any gap or interleaving rejects the connection, so the * runner must re-sign and replay the complete transaction under a new * challenge. */ export declare class CodexAppControlFinalAssembler { private active; accept(kind: string, payload: Record): CodexAppFinalAssemblyResult; clear(): void; private reject; } /** * Per-worker replay window for authenticated runner generations. A runner may * reconnect after losing an ACK and legitimately re-sign the same sequence * under the new connection challenge; the worker ACKs that retry without * applying its lifecycle/final side effects twice. */ export declare class CodexAppControlReplayWindow { private readonly highWaterByGeneration; highWater(generation: string): number; hasSeen(generation: string, seq: number): boolean; commit(generation: string, seq: number): void; retainOnly(generation: string): void; } /** Bounded newline decoder; oversized attacker input is discarded until resync. */ export declare class CodexAppControlLineDecoder { private pending; private droppingOversized; push(chunk: Buffer): { lines: string[]; droppedMalformed: boolean; }; clear(): void; } export {}; //# sourceMappingURL=codex-app-control.d.ts.map