import { randomUUID } from "node:crypto"; import * as path from "node:path"; import { stripVTControlCharacters } from "node:util"; import type { HerdrMachineReference, HerdrRemoteGitStatus } from "../../shared/types.ts"; import { connectHerdrMachine, HerdrRpcError, HerdrTransportError, remoteShellCommand, shellQuoteRemote, type HerdrForwardedConnection } from "./herdr-connection.ts"; import { HerdrPlacedRunOwner, herdrStatusSubscriptions, ownsHerdrPane, parseHerdrAgent, reconcileHerdrPlacedRun, type HerdrReconnectCandidate, type HerdrRunIdentity } from "./herdr-placed-run.ts"; export type HerdrExternalAdapterId = "claude-code" | "claude-code-writer" | "cursor-agent" | "cursor-agent-writer" | "codex-exec" | "codex-exec-writer"; export type HerdrExternalKind = "claude" | "cursor" | "codex"; export interface HerdrExternalCapabilities { stop: true; steer: false; resume: false; supervisor: "unsupported" } export interface HerdrExternalLaunch { adapter: HerdrExternalAdapterId; kind: HerdrExternalKind; nativeSessionId: string; args: string[]; capabilities: HerdrExternalCapabilities } export interface HerdrExternalResult { protocol: 1; adapter: HerdrExternalAdapterId; runId: string; requestId: string; nativeSessionId: "unverified"; nativeTurnId: "unverified"; outcome: "partial"; output: string; initialGit?: HerdrRemoteGitStatus; finalGit?: HerdrRemoteGitStatus; settlement?: { verification: "best-effort/unverified"; evidence: "sanitized-terminal-snapshots" }; } export interface HerdrExternalEvidenceInput { runId: string; requestId: string; task: string; cwd: string; nativeSessionId: string; requestedModel?: string; initialGit?: unknown; finalGit?: unknown } export interface HerdrExternalCommandEvidence { binary: string; args: readonly string[]; status: number; stdout: string; stderr: string } export interface HerdrExternalPreflightEvidence { version: HerdrExternalCommandEvidence; help: HerdrExternalCommandEvidence } export interface HerdrExternalAdapter { readonly id: HerdrExternalAdapterId; readonly kind: HerdrExternalKind; preflight(evidence: HerdrExternalPreflightEvidence): void; launch(input: Omit[0], "adapter">): HerdrExternalLaunch; normalize(input: HerdrExternalEvidenceInput, evidence: Buffer | string): HerdrExternalResult; } const CAPABILITIES: HerdrExternalCapabilities = { stop: true, steer: false, resume: false, supervisor: "unsupported" }; const MAX_EVIDENCE_BYTES = 4 * 1024 * 1024, MAX_OUTPUT_BYTES = 1024 * 1024, MAX_PREFLIGHT_BYTES = 64 * 1024, MAX_GIT_BYTES = 2048; const CODEX_OUTPUT_BYTES = 64 * 1024, EXTERNAL_STARTUP_TIMEOUT_MS = 45_000, EXTERNAL_STARTUP_BACKOFF_AFTER_MS = 15_000, EXTERNAL_STARTUP_SLOW_POLL_MS = 2_500, CODEX_STARTUP_GRACE_MS = 15_000, CODEX_QUIET_MS = 8_000, CODEX_POLL_MS = 1_000, CODEX_DEFAULT_TIMEOUT_MS = 30 * 60_000; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu, SHA = /^[0-9a-f]{4,64}$/iu; function boundedString(value: unknown, label: string, maximum = MAX_OUTPUT_BYTES): string { if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is missing.`); if (Buffer.byteLength(value) > maximum) throw new Error(`${label} exceeded its byte bound.`); return value; } function exactUuid(value: unknown, label: string): string { const result = boundedString(value, label, 64); if (!UUID.test(result)) throw new Error(`${label} is not a UUID.`); return result; } function normalizeGit(value: unknown, label: string): HerdrRemoteGitStatus | undefined { if (value === undefined) return undefined; if (!value || typeof value !== "object" || Array.isArray(value) || Buffer.byteLength(JSON.stringify(value)) > MAX_GIT_BYTES) throw new Error(`${label} is malformed or oversized.`); const data = value as Record, unknown = Object.keys(data).filter((key) => key !== "head" && key !== "branch" && key !== "dirty"); if (unknown.length) throw new Error(`${label} has unsupported keys.`); if (data.head !== undefined && (typeof data.head !== "string" || !SHA.test(data.head))) throw new Error(`${label}.head is invalid.`); if (data.branch !== undefined && (typeof data.branch !== "string" || !data.branch.trim() || Buffer.byteLength(data.branch) > 256 || /[\x00-\x1f\x7f]/u.test(data.branch))) throw new Error(`${label}.branch is invalid.`); if (data.dirty !== undefined && typeof data.dirty !== "boolean") throw new Error(`${label}.dirty is invalid.`); return { ...(data.head !== undefined ? { head: data.head } : {}), ...(data.branch !== undefined ? { branch: data.branch } : {}), ...(data.dirty !== undefined ? { dirty: data.dirty } : {}) }; } function resultGit(input: HerdrExternalEvidenceInput) { const initialGit = normalizeGit(input.initialGit, "initial Git evidence"), finalGit = normalizeGit(input.finalGit, "final Git evidence"); return { ...(initialGit ? { initialGit } : {}), ...(finalGit ? { finalGit } : {}) }; } function atLeast(actual: [number, number, number], floor: [number, number, number]): boolean { return actual[0] > floor[0] || actual[0] === floor[0] && (actual[1] > floor[1] || actual[1] === floor[1] && actual[2] >= floor[2]); } function requireHelp(help: string, values: string[], label: string): void { if (!/^(?:Usage:|Claude Code|Codex|Start the Cursor Agent)/u.test(help.trimStart())) throw new Error(`${label} help response has an unsupported header.`); for (const value of values) { const matches = help.match(new RegExp(`(^|\\s)${value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?=\\s|[=,.;:]|$)`, "gmu")); if (!matches?.length) throw new Error(`${label} help does not document required interactive option ${JSON.stringify(value)}.`); } } function validateCommand(record: HerdrExternalCommandEvidence, binary: string, args: readonly string[], label: string): void { if (!path.posix.isAbsolute(record.binary) || path.posix.basename(record.binary) !== binary || record.args.length !== args.length || record.args.some((value, index) => value !== args[index])) throw new Error(`${label} preflight command identity is invalid.`); if (record.status !== 0) throw new Error(`${label} preflight exited ${record.status}: ${record.stderr.slice(0, 512)}`); for (const [name, value] of [["stdout", record.stdout], ["stderr", record.stderr]] as const) if (typeof value !== "string" || Buffer.byteLength(value) > MAX_PREFLIGHT_BYTES) throw new Error(`${label} preflight ${name} exceeded its bound.`); } function adapterKind(adapter: HerdrExternalAdapterId): HerdrExternalKind { return adapter.startsWith("claude") ? "claude" : adapter.startsWith("cursor") ? "cursor" : "codex"; } function adapterBinary(adapter: HerdrExternalAdapterId): string { return adapterKind(adapter) === "claude" ? "claude" : adapterKind(adapter) === "cursor" ? "cursor-agent" : "codex"; } function expectedStartedArgv(launch: HerdrExternalLaunch): string[] { return [launch.kind, ...launch.args]; } function sameStrings(actual: unknown, expected: readonly string[]): actual is string[] { return Array.isArray(actual) && actual.length === expected.length && actual.every((value, index) => value === expected[index]); } function validateStartedArgv(launch: HerdrExternalLaunch, actual: unknown): string[] { const expected = expectedStartedArgv(launch); if (!sameStrings(actual, expected)) throw new Error(`Herdr started ${launch.kind} with unexpected native argv.`); return [...actual]; } function canonicalPreflightBinary(adapter: HerdrExternalAdapterId, evidence: HerdrExternalPreflightEvidence): string { const binary = evidence.version.binary; if (!path.posix.isAbsolute(binary) || evidence.help.binary !== binary || path.posix.basename(binary) !== adapterBinary(adapter)) throw new Error("External preflight canonical binary identity is inconsistent."); return binary; } function matchesExternalProcess(entry: Record, cwd: string, canonicalArgv0: string): boolean { const name = entry.name; return typeof entry.pid === "number" && typeof name === "string" && Boolean(name.trim()) && Buffer.byteLength(name) <= 128 && !/[\x00-\x1f\x7f]/u.test(name) && entry.argv0 === canonicalArgv0 && entry.cwd === cwd; } export function reconcileHerdrExternalRun(identity: HerdrRunIdentity, candidate: HerdrReconnectCandidate) { const check = reconcileHerdrPlacedRun(identity, candidate, () => ({ ok: true, cursor: 0 })); if (!check.ok) return check; if (check.paneId !== identity.paneId) return { ok: false as const, reason: "External reconnect refused pane identity drift." }; return check; } export function validateHerdrExternalPreflight(adapter: HerdrExternalAdapterId, evidence: HerdrExternalPreflightEvidence): void { const kind = adapterKind(adapter), binary = adapterBinary(adapter); validateCommand(evidence.version, binary, ["--version"], binary); validateCommand(evidence.help, binary, ["--help"], binary); canonicalPreflightBinary(adapter, evidence); const version = evidence.version.stdout.trim(), match = kind === "claude" ? version.match(/^(\d+)\.(\d+)\.(\d+) \(Claude Code\)$/u) : kind === "codex" ? version.match(/^codex-cli (\d+)\.(\d+)\.(\d+)$/u) : version.match(/^(\d{4})\.(\d{2})\.(\d{2})-[0-9a-f]+$/u); if (!match) throw new Error(`Unsupported ${binary} version response: ${JSON.stringify(version)}.`); const parsed: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])]; const floor: [number, number, number] = kind === "claude" ? [2, 1, 269] : kind === "codex" ? [0, 154, 0] : [2026, 9, 10]; if (!atLeast(parsed, floor)) throw new Error(`Remote ${binary} is below the tested pane-native capability floor.`); requireHelp(evidence.help.stdout, kind === "claude" ? ["--session-id", "--restricted", "--permission-mode", "--tools", "--strict-mcp-config", "--mcp-config", "--disable-slash-commands", "--no-chrome"] : kind === "cursor" ? ["--mode", "--sandbox", "--workspace", "--trust"] : ["--sandbox", "--ask-for-approval", "--no-alt-screen"], binary); } function commandEvidence(binary: string, args: readonly string[], result: ReturnType): HerdrExternalCommandEvidence { return { binary, args, status: result.status ?? -1, stdout: String(result.stdout), stderr: String(result.stderr) }; } export function runHerdrExternalPreflight(owner: HerdrPlacedRunOwner, adapter: HerdrExternalAdapterId): HerdrExternalPreflightEvidence { const name = adapterBinary(adapter), located = owner.runRemote(remoteShellCommand(`command -v ${name}`), { timeout: 10_000, maxBuffer: 4096 }), binary = String(located.stdout).trim(); if (located.status !== 0 || !path.posix.isAbsolute(binary) || path.posix.basename(binary) !== name || binary.includes("\n")) throw new Error(`Remote canonical ${name} binary could not be resolved.`); const invoke = (args: readonly string[]) => commandEvidence(binary, args, owner.runRemote(remoteShellCommand(adapterKind(adapter) === "cursor" ? `exec /usr/bin/env AGENT_CLI_CREDENTIAL_STORE=file ${shellQuoteRemote(binary)} "$@"` : `exec ${shellQuoteRemote(binary)} "$@"`, [...args]), { timeout: 10_000, maxBuffer: MAX_PREFLIGHT_BYTES })); const evidence = { version: invoke(["--version"]), help: invoke(["--help"]) }; validateHerdrExternalPreflight(adapter, evidence); return evidence; } export function createHerdrExternalAdapterLaunch(input: { adapter: HerdrExternalAdapterId; remoteRuntimeDir: string; cwd?: string; nativeSessionId?: string; model?: string; environment?: Readonly>; resources?: Readonly> }): HerdrExternalLaunch { if (!path.posix.isAbsolute(input.remoteRuntimeDir) || path.posix.basename(input.remoteRuntimeDir).length < 8) throw new Error("External adapter requires the accepted run-private remote runtime root."); if (input.environment && Object.keys(input.environment).length) throw new Error("Pane-native external adapters reject caller environment bindings; credentials remain machine-owned."); if (input.resources && Object.keys(input.resources).length) throw new Error("Pane-native external adapters do not accept expanded prompts, local paths, callbacks, tools, skills, or MCP bindings."); if (input.model !== undefined) throw new Error("Pane-native M2b adapters use the remote managed model registry and reject model override."); const nativeSessionId = input.nativeSessionId ?? randomUUID(); exactUuid(nativeSessionId, "External adapter native session identity"); const kind = adapterKind(input.adapter); if (kind === "claude") { const writer = input.adapter === "claude-code-writer"; return { adapter: input.adapter, kind, nativeSessionId, capabilities: CAPABILITIES, args: ["--session-id", nativeSessionId, "--restricted", "--permission-mode", writer ? "acceptEdits" : "plan", "--tools", writer ? "Read,Write,Edit,Glob,Grep" : "", "--strict-mcp-config", "--mcp-config", "{\"mcpServers\":{}}", "--disable-slash-commands", "--no-chrome"] }; } if (kind === "cursor") { if (!input.cwd || !path.posix.isAbsolute(input.cwd)) throw new Error("Pane-native Cursor requires the exact remote-absolute owned workspace."); return { adapter: input.adapter, kind, nativeSessionId, capabilities: CAPABILITIES, args: [...(input.adapter === "cursor-agent" ? ["--mode", "ask"] : []), "--sandbox", "enabled", "--workspace", input.cwd] }; } return { adapter: input.adapter, kind, nativeSessionId, capabilities: CAPABILITIES, args: ["--sandbox", input.adapter === "codex-exec-writer" ? "workspace-write" : "read-only", "--ask-for-approval", "never", "--no-alt-screen"] }; } export interface HerdrCodexSnapshot { at: number; workspaceId: string; paneId: string; terminalId: string; pid: number; text: string; source?: "visible" | "recent_unwrapped" } export interface HerdrCodexMonitorClock { now(): number; wait(ms: number): Promise } export class HerdrExternalNeedsAttentionError extends Error { readonly code = "HERDR_EXTERNAL_NEEDS_ATTENTION"; readonly identity: { machineId: string; workspaceId: string; paneId: string; terminalId: string }; constructor(message: string, identity: { machineId: string; workspaceId: string; paneId: string; terminalId: string }) { super(message); this.identity = identity; } } function sanitizeTerminal(input: string): string { const clean = stripVTControlCharacters(input).replace(/\r/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "").replace(/[ \t]+$/gmu, "").trim(); const bytes = Buffer.from(clean); return bytes.length <= CODEX_OUTPUT_BYTES ? clean : bytes.subarray(bytes.length - CODEX_OUTPUT_BYTES).toString("utf8").replace(/^\uFFFD/u, ""); } function codexAttention(text: string): boolean { return /(?:trust this|do you trust the contents of this directory|approval required|approve\?|review (?:hook|action)|action required|permission required|error:|fatal:)/iu.test(text); } function codexWorking(text: string): boolean { return /(?:working|thinking|running|esc to interrupt|ctrl\+c to interrupt|•\s*[^\n]+…)/iu.test(text); } function codexReady(text: string): boolean { return /(?:›|>)\s*(?:Ask|Describe|Type)\b|(?:Ask|Describe) anything/iu.test(text); } function codexSubmissionEnd(text: string, task: string): number { const lines = text.split("\n").map((line) => line.trimEnd()), taskLines = task.split(/\r?\n/u).map((line) => line.trimEnd()); for (let index = 0; index < lines.length; index++) { const first = lines[index]!.match(/^[›>]\s?(.*)$/u); if (!first || first[1] !== taskLines[0]) continue; if (taskLines.slice(1).every((line, offset) => lines[index + offset + 1] === line)) return index + taskLines.length; } return -1; } function codexAssistantOutput(text: string, task: string): string { const lines = text.split("\n").map((line) => line.trimEnd()), submittedEnd = codexSubmissionEnd(text, task); if (submittedEnd < 0) return ""; const after = lines.slice(submittedEnd).filter((line) => line.trim() && !codexReady(line) && !/^tokens? used\b/iu.test(line)); return after.join("\n").trim(); } export async function monitorHerdrCodex(input: { machineId: string; task: string; preSubmitText: string; identity: Omit; snapshot(): Promise; stop(): Promise; timeoutMs?: number; clock?: HerdrCodexMonitorClock }): Promise<{ output: string; snapshots: number }> { const clock = input.clock ?? { now: () => Date.now(), wait: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) }, started = clock.now(), deadline = started + (input.timeoutMs ?? CODEX_DEFAULT_TIMEOUT_MS), expected = input.identity, before = sanitizeTerminal(input.preSubmitText); if (codexSubmissionEnd(before, input.task) >= 0) throw new HerdrExternalNeedsAttentionError("Codex pre-submit screen already contained the task; correlation is ambiguous.", { machineId: input.machineId, workspaceId: expected.workspaceId, paneId: expected.paneId, terminalId: expected.terminalId }); let previous = before, previousSource: HerdrCodexSnapshot["source"], lastChange = started, meaningful = false, stable = 0, reads = 0; const attention = (message: string) => new HerdrExternalNeedsAttentionError(message, { machineId: input.machineId, workspaceId: expected.workspaceId, paneId: expected.paneId, terminalId: expected.terminalId }); while (clock.now() <= deadline) { const sample = await input.snapshot(); reads++; if (sample.workspaceId !== expected.workspaceId || sample.paneId !== expected.paneId || sample.terminalId !== expected.terminalId || sample.pid !== expected.pid) throw attention("Placed Codex identity drifted; truthful pane retained for inspection."); const text = sanitizeTerminal(sample.text), sourceChanged = previousSource !== undefined && sample.source !== previousSource; if (codexAttention(text)) throw attention("Placed Codex requires attention; truthful pane retained for inspection."); if (text !== previous || sourceChanged) { if (text !== previous) meaningful = true; previous = text; previousSource = sample.source; lastChange = clock.now(); stable = 0; } else { previousSource = sample.source; stable++; } const output = codexAssistantOutput(text, input.task), quiet = clock.now() - lastChange >= CODEX_QUIET_MS; if (sample.source === "recent_unwrapped" && clock.now() - started >= CODEX_STARTUP_GRACE_MS && meaningful && quiet && stable >= 1 && !codexWorking(text) && codexReady(text) && output) { await input.stop(); return { output: sanitizeTerminal(output), snapshots: reads }; } await clock.wait(CODEX_POLL_MS); } throw attention("Placed Codex settlement remained ambiguous until timeout; truthful pane retained for inspection."); } function normalizePlacedTerminal(input: HerdrExternalEvidenceInput, output: string, adapter: HerdrExternalAdapterId): HerdrExternalResult { if (input.requestedModel !== undefined) throw new Error("Pane-native external adapters use the remote managed model registry and reject model override."); return { protocol: 1, adapter, runId: input.runId, requestId: input.requestId, nativeSessionId: "unverified", nativeTurnId: "unverified", outcome: "partial", output: `[best-effort/unverified]\n${boundedString(sanitizeTerminal(output), "sanitized terminal output", CODEX_OUTPUT_BYTES)}`, settlement: { verification: "best-effort/unverified", evidence: "sanitized-terminal-snapshots" }, ...resultGit(input) }; } export function createHerdrExternalAdapter(id: HerdrExternalAdapterId): HerdrExternalAdapter { const kind = adapterKind(id); return { id, kind, preflight: (evidence) => validateHerdrExternalPreflight(id, evidence), launch: (input) => createHerdrExternalAdapterLaunch({ adapter: id, ...input }), normalize: (input, evidence) => normalizePlacedTerminal(input, Buffer.isBuffer(evidence) ? evidence.toString("utf8") : evidence, id) }; } function unwrap>(value: unknown, type: string, key: string): T { if (!value || typeof value !== "object" || (value as Record).type !== type || !(key in (value as Record))) throw new Error(`Herdr returned invalid ${type} evidence.`); return (value as Record)[key]!; } export class HerdrExternalSession { readonly owner: HerdrPlacedRunOwner; readonly launch: HerdrExternalLaunch; readonly preflight: HerdrExternalPreflightEvidence; readonly #adapter: HerdrExternalAdapter; readonly #connect: typeof connectHerdrMachine; #settled = false; #prompted = false; #nativePid?: number; #cursorWorkspace?: { realpath: string; device: number; inode: number; uid: number }; #reconnectPromise?: Promise; #disposePromise?: Promise; #reconnectCandidatesRemaining = 3; #reconnectDeadline?: number; #reconnectFailure?: Error; constructor(owner: HerdrPlacedRunOwner, launch: HerdrExternalLaunch, preflight: HerdrExternalPreflightEvidence, connect: typeof connectHerdrMachine = connectHerdrMachine) { this.owner = owner; this.launch = launch; this.preflight = preflight; this.#adapter = createHerdrExternalAdapter(launch.adapter); this.#connect = connect; } settle(input: HerdrExternalEvidenceInput, evidence: Buffer | string): HerdrExternalResult { if (this.#settled || this.#disposePromise) throw new Error("Pane-native external run evidence already settled or disposed."); if (input.nativeSessionId !== this.launch.nativeSessionId) throw new Error("Pane-native external evidence session identity changed."); const result = this.#adapter.normalize(input, evidence); this.#settled = true; return result; } async promptAndSettle(input: HerdrExternalEvidenceInput, options: { timeoutMs?: number; clock?: HerdrCodexMonitorClock; snapshot?: () => Promise } = {}): Promise { if (this.#prompted || this.#settled || this.#disposePromise) throw new Error("Pane-native external task was already submitted, settled, or disposed."); this.#prompted = true; if (!this.owner.agentName || !this.owner.owned || !this.owner.terminalId) throw new Error("Pane-native external owner identity is incomplete."); let preSubmitText = ""; if (this.launch.kind === "cursor") { try { this.#cursorWorkspace = this.#proveCursorWorkspace(); } catch (error) { throw this.#needsAttention(`Placed Cursor requires attention before input: ${error instanceof Error ? error.message : String(error)}; truthful pane retained for inspection.`); } } if (this.launch.kind === "cursor" || this.launch.kind === "codex") { const ready = await this.#waitForStartup(options.clock); preSubmitText = ready.text; this.#nativePid = ready.pid; } else this.#nativePid = (await this.#exactProcess()).pid; const waitTimeout = Math.min(Math.max(1, options.timeoutMs ?? 90_000), CODEX_DEFAULT_TIMEOUT_MS), params: Record = { target: this.owner.agentName, text: input.task }; if (this.launch.kind !== "codex") params.wait = { until: ["idle", "done", "blocked"], timeout_ms: waitTimeout }; let response: unknown, recovered = false; try { response = await this.owner.connection.client.call("agent.prompt", params, waitTimeout + 5_000); } catch (error) { if (error instanceof HerdrRpcError || !(error instanceof HerdrTransportError)) throw error; if (this.owner.snapshot.connection !== "unknown") throw this.#needsAttention(`Placed external prompt settlement is unresolved: ${error.message}; truthful pane retained for inspection.`); await this.reconnect(); response = await this.owner.connection.client.call("agent.get", { target: this.owner.agentName }); recovered = true; } const prompted = parseHerdrAgent(response, recovered ? "agent_info" : "agent_prompted"); if (!ownsHerdrPane(prompted, this.owner.terminalId, this.owner.owned.paneId)) throw new Error("Herdr prompt settlement identity changed."); if (this.launch.kind !== "codex" && prompted.agent_status !== "idle" && prompted.agent_status !== "done") throw this.#needsAttention(`Herdr external prompt did not settle safely (status ${String(prompted.agent_status)}); truthful pane retained for inspection.`); await this.#exactProcessAfterReconnect(this.#nativePid); if (this.launch.kind === "cursor") this.#assertSameCursorWorkspace(); if (this.launch.kind !== "codex") { const terminal = await this.#readSettledPane(); this.#settled = true; return normalizePlacedTerminal(input, terminal, this.launch.adapter); } const expected = { workspaceId: this.owner.owned.workspaceId, paneId: this.owner.owned.paneId, terminalId: this.owner.terminalId, pid: this.#nativePid }, snapshot = options.snapshot ?? (() => this.#codexSnapshot(this.#nativePid!)); const monitored = await monitorHerdrCodex({ machineId: this.owner.machine.id, task: input.task, preSubmitText, identity: expected, snapshot, stop: () => this.owner.cleanup(true), timeoutMs: waitTimeout, clock: options.clock }); this.#settled = true; return normalizePlacedTerminal(input, monitored.output, this.launch.adapter); } #needsAttention(message: string): HerdrExternalNeedsAttentionError { return new HerdrExternalNeedsAttentionError(message, { machineId: this.owner.machine.id, workspaceId: this.owner.owned!.workspaceId, paneId: this.owner.owned!.paneId, terminalId: this.owner.terminalId! }); } #proveCursorWorkspace(): { realpath: string; device: number; inode: number; uid: number } { const script = `const fs=require("node:fs"),p=process.argv[1],s=fs.lstatSync(p),r=fs.realpathSync(p),u=process.getuid();if(!s.isDirectory()||s.isSymbolicLink()||r!==p||s.uid!==u||(s.mode&18)!==0)process.exit(65);process.stdout.write(JSON.stringify({realpath:r,device:s.dev,inode:s.ino,uid:s.uid}))`, raw = this.#remoteEvidence(script, [this.owner.machine.cwd]); let value: unknown; try { value = JSON.parse(raw); } catch { throw new Error("Cursor workspace identity evidence is malformed."); } const data = value as Record; if (!data || data.realpath !== this.owner.machine.cwd || !Number.isSafeInteger(data.device) || !Number.isSafeInteger(data.inode) || !Number.isSafeInteger(data.uid)) throw new Error("Cursor workspace is not the exact canonical owned safe directory."); return data as unknown as { realpath: string; device: number; inode: number; uid: number }; } #assertSameCursorWorkspace(): void { const current = this.#proveCursorWorkspace(), expected = this.#cursorWorkspace; if (!expected || current.realpath !== expected.realpath || current.device !== expected.device || current.inode !== expected.inode || current.uid !== expected.uid) throw new Error("Cursor workspace identity changed before settlement."); } async #readCurrentPane(): Promise { const value = await this.owner.connection.client.call("pane.read", { pane_id: this.owner.owned!.paneId, source: "visible", strip_ansi: true }), read = unwrap>(value, "pane_read", "read"); if (read.pane_id !== this.owner.owned!.paneId) throw new Error("External pane read identity changed."); return String(read.text ?? ""); } async #readSettledPane(): Promise { const value = await this.owner.connection.client.call("pane.read", { pane_id: this.owner.owned!.paneId, source: "recent_unwrapped", lines: 400, strip_ansi: true }), read = unwrap>(value, "pane_read", "read"); if (read.pane_id !== this.owner.owned!.paneId || read.source !== "recent_unwrapped") throw new Error("External terminal evidence identity or source changed."); return String(read.text ?? ""); } #cursorReady(text: string): boolean { if (/trust (?:this|workspace|folder)|untrusted/iu.test(text) || codexAttention(text)) throw this.#needsAttention("Placed Cursor requires trust or other attention before input; truthful pane retained for inspection."); return /(?:Ask|Plan|Agent)\b/iu.test(text); } async #processProjection(): Promise<{ processes: Record[]; canonical: Record[] }> { validateStartedArgv(this.launch, this.owner.startedArgv); const canonicalArgv0 = path.posix.basename(canonicalPreflightBinary(this.launch.adapter, this.preflight)), value = await this.owner.connection.client.call("pane.process_info", { pane_id: this.owner.owned!.paneId }), info = unwrap>(value, "pane_process_info", "process_info"); if (info.pane_id !== this.owner.owned!.paneId) throw new Error("External process evidence pane identity changed."); if (!Array.isArray(info.foreground_processes)) throw new Error("External process evidence foreground process projection is malformed."); const processes = info.foreground_processes as Record[]; return { processes, canonical: processes.filter((entry) => matchesExternalProcess(entry, this.owner.machine.cwd, canonicalArgv0)) }; } async #exactProcess(requiredPid?: number): Promise<{ pid: number }> { const projection = await this.#processProjection(), canonical = projection.canonical[0]; if (projection.canonical.length !== 1 || requiredPid !== undefined && canonical?.pid !== requiredPid) throw new Error(`Could not prove exactly one canonical ${path.posix.basename(this.preflight.version.binary)} native PID with the exact cwd${requiredPid === undefined ? "" : " and retained PID"}.`); return { pid: canonical!.pid as number }; } async #exactProcessAfterReconnect(requiredPid: number): Promise<{ pid: number }> { try { return await this.#exactProcess(requiredPid); } catch (error) { if (!(error instanceof HerdrTransportError) || this.owner.snapshot.connection !== "unknown") throw error; await this.reconnect(); return this.#exactProcess(requiredPid); } } async #waitForStartup(clock: HerdrCodexMonitorClock = { now: () => Date.now(), wait: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) }): Promise<{ pid: number; text: string }> { const started = clock.now(), deadline = started + EXTERNAL_STARTUP_TIMEOUT_MS; let last = "not ready"; for (;;) { const text = await this.#readCurrentPane(); let uiReady: boolean; if (this.launch.kind === "cursor") uiReady = this.#cursorReady(text); else { if (codexAttention(text)) throw this.#needsAttention("Placed Codex requires attention before input; truthful pane retained for inspection."); uiReady = codexReady(text); } const projection = await this.#processProjection(); const current = parseHerdrAgent(await this.owner.connection.client.call>("agent.get", { target: this.owner.agentName }), "agent_info"); if (!ownsHerdrPane(current, this.owner.terminalId!, this.owner.owned!.paneId) || current.name !== this.owner.agentName) throw new Error("External startup agent owner, pane, terminal, or name identity changed."); const status = current.agent_status; if (status === "blocked" || status === "action_required") throw this.#needsAttention(`Placed ${this.launch.kind === "cursor" ? "Cursor" : "Codex"} requires attention before input; truthful pane retained for inspection.`); if (current.interactive_ready === true && current.agent !== this.launch.kind) throw new Error("External startup agent kind identity changed."); const managedReady = current.agent === this.launch.kind && current.interactive_ready === true && current.launch_pending !== true && (status === "idle" || status === "done"); if (uiReady && projection.canonical.length === 1 && managedReady) return { pid: projection.canonical[0]!.pid as number, text }; last = `${uiReady ? "ready UI" : "startup UI"}; ${projection.canonical.length} canonical processes; managed ${managedReady ? "ready" : String(status ?? "unknown")}`; const now = clock.now(); if (now >= deadline) break; const poll = now - started < EXTERNAL_STARTUP_BACKOFF_AFTER_MS ? CODEX_POLL_MS : EXTERNAL_STARTUP_SLOW_POLL_MS; await clock.wait(Math.min(poll, deadline - now)); } throw this.#needsAttention(`Placed ${this.launch.kind === "cursor" ? "Cursor" : "Codex"} startup did not stabilize (${last}); truthful pane retained for inspection.`); } async #codexSnapshotOnce(pid: number): Promise { const current = parseHerdrAgent(await this.owner.connection.client.call>("agent.get", { target: this.owner.agentName }), "agent_info"); if (!ownsHerdrPane(current, this.owner.terminalId!, this.owner.owned!.paneId)) throw new Error("Codex owner identity changed."); const status = current.agent_status, source = status === "idle" || status === "done" ? "recent_unwrapped" : "visible", process = await this.#exactProcess(pid), read = unwrap>(await this.owner.connection.client.call("pane.read", { pane_id: this.owner.owned!.paneId, source, lines: 400, strip_ansi: true }), "pane_read", "read"); if (read.pane_id !== this.owner.owned!.paneId || read.source !== source) throw new Error("Codex pane read identity or source changed."); return { at: Date.now(), workspaceId: this.owner.owned!.workspaceId, paneId: String(read.pane_id), terminalId: this.owner.terminalId!, pid: process.pid, text: String(read.text ?? ""), source }; } async #codexSnapshot(pid: number): Promise { try { return await this.#codexSnapshotOnce(pid); } catch (error) { if (!(error instanceof HerdrTransportError) || this.owner.snapshot.connection !== "unknown") throw error; await this.reconnect(); return this.#codexSnapshotOnce(pid); } } #remoteEvidence(script: string, args: string[]): string { const result = this.owner.runRemote(remoteShellCommand(`exec node -e ${shellQuoteRemote(script)} "$@"`, args), { timeout: 10_000, maxBuffer: MAX_EVIDENCE_BYTES }); if (result.status !== 0 || result.stderr) throw new Error(`Could not read exact owned external evidence: ${String(result.stderr).slice(0, 512)}`); return String(result.stdout); } handleDisconnect(observed: HerdrForwardedConnection = this.owner.connection): void { const owner = this.owner as HerdrPlacedRunOwner & { markConnectionUnknown?: (connection: HerdrForwardedConnection) => boolean }; if (owner.markConnectionUnknown ? !owner.markConnectionUnknown(observed) : this.owner.connection !== observed) return; void this.reconnect().catch(() => {}); } reconnect(): Promise { if (this.#reconnectFailure) return Promise.reject(this.#reconnectFailure); return this.#reconnectPromise ??= this.#performReconnect().catch((error) => { const failure = error instanceof Error ? error : new Error(String(error)); if (/reconnect remains unknown/u.test(failure.message)) this.#reconnectFailure = failure; throw failure; }).finally(() => { this.#reconnectPromise = undefined; }); } async #performReconnect(): Promise { if (this.#disposePromise) throw new Error("External reconnect is blocked because disposal or retention has begun."); if (!this.owner.identity) throw new Error("External reconnect has no persisted owner identity."); this.#reconnectDeadline ??= Date.now() + 15_000; let last: unknown; while (this.#reconnectCandidatesRemaining > 0 && Date.now() < this.#reconnectDeadline) { this.#reconnectCandidatesRemaining--; let connection: HerdrForwardedConnection | undefined, unsubscribe = () => {}, adopted = false, candidateLost: Error | undefined; try { if (this.#disposePromise) throw new Error("External reconnect is blocked because disposal or retention has begun."); connection = await this.#connect(this.owner.machine); const value = await connection.client.call>("session.snapshot"), envelope = unwrap>(value, "session_snapshot", "snapshot"), raw = Array.isArray(envelope.agents) ? envelope.agents as Record[] : [], agents = raw.map((agent) => ({ terminal_id: typeof agent.terminal_id === "string" ? agent.terminal_id : undefined, pane_id: typeof agent.pane_id === "string" ? agent.pane_id : undefined, agent_status: typeof agent.agent_status === "string" ? agent.agent_status : undefined })); const check = reconcileHerdrExternalRun(this.owner.identity, { endpoint: connection.endpoint, agents, bridge: {} }); if (!check.ok) throw new Error(check.reason); if (this.#nativePid !== undefined) { validateStartedArgv(this.launch, this.owner.startedArgv); const canonicalArgv0 = path.posix.basename(canonicalPreflightBinary(this.launch.adapter, this.preflight)), process = unwrap>(await connection.client.call("pane.process_info", { pane_id: this.owner.identity.paneId }), "pane_process_info", "process_info"), rows = Array.isArray(process.foreground_processes) ? process.foreground_processes as Record[] : [], canonical = rows.filter((row) => matchesExternalProcess(row, this.owner.identity!.cwd, canonicalArgv0)); if (process.pane_id !== this.owner.identity.paneId || canonical.length !== 1 || canonical[0]!.pid !== this.#nativePid) throw new Error("External canonical native PID, cwd, or argv0 changed during reconnect."); } const observed = connection; unsubscribe = await connection.client.subscribe(herdrStatusSubscriptions(check.paneId), (event) => this.owner.observeEvent?.(event), (error) => { if (!adopted) { candidateLost ??= error; return; } this.handleDisconnect(observed); }); if (candidateLost) throw candidateLost; if (this.#disposePromise) throw new Error("External reconnect is blocked because disposal or retention has begun."); adopted = true; await this.owner.replaceConnection({ connection, unsubscribe, paneId: check.paneId, state: check.state }); return; } catch (error) { last = error; unsubscribe(); await connection?.close(); } } if (this.#disposePromise) throw new Error("External reconnect is blocked because disposal or retention has begun."); throw new Error(`Pane-native external reconnect remains unknown: ${last instanceof Error ? last.message : String(last)}`); } retain(): Promise { return this.#disposePromise ??= this.owner.cleanup(false); } abort(): Promise { return this.#dispose(); } dispose(): Promise { return this.#dispose(); } #dispose(): Promise { return this.#disposePromise ??= this.owner.cleanup(true); } } export async function prepareInternalHerdrExternalAdapter(input: { adapter: HerdrExternalAdapterId; machine: HerdrMachineReference; runId: string }, launch: Omit[0], "adapter" | "remoteRuntimeDir" | "cwd">, createOwner: typeof HerdrPlacedRunOwner.create = HerdrPlacedRunOwner.create): Promise { let evidence: HerdrExternalPreflightEvidence | undefined; const owner = await createOwner(input.machine, input.runId, (owned) => { evidence = runHerdrExternalPreflight(owned, input.adapter); }); try { const descriptor = createHerdrExternalAdapterLaunch({ adapter: input.adapter, remoteRuntimeDir: owner.runtimeDir, ...launch, cwd: input.machine.cwd }); let session: HerdrExternalSession | undefined; const owned = await owner.provision({}); await owner.subscribe(() => {}, () => { void session?.reconnect().catch(() => {}); }); const agentName = `${descriptor.kind}-${input.runId.slice(-20)}`, started = await owner.start(descriptor.kind, agentName, descriptor.args); validateStartedArgv(descriptor, started.argv); owner.journal({ runId: input.runId, machineId: input.machine.id, target: input.machine.target, session: owner.connection.endpoint.session, workspaceId: owned.workspaceId, tabId: owned.tabId, paneId: owned.paneId, terminalId: started.terminalId, agentName, nativeSessionId: descriptor.nativeSessionId, cwd: input.machine.cwd, runtimeDir: owner.runtimeDir }); return session = new HerdrExternalSession(owner, descriptor, evidence!); } catch (error) { try { await owner.cleanup(true); } catch (cleanupError) { throw new AggregateError([error, cleanupError], "External adapter preparation and cleanup both failed.", { cause: error }); } throw error; } }