UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

650 lines (649 loc) • 41.1 kB
import { redactPeerPayload } from "./peer-handoff.js"; import { deliveryIdSchema } from "./prompt-once.js"; import { createHash, randomUUID } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { conductorPane, readRoleState, resetRoleState, successionSchema, updateConductorAuthority } from "./conductor-role.js"; import { hookPeers, peerRequest } from "./peers.js"; import { isLocalComputer, peerFingerprint } from "./dispatch-hosts.js"; import { atomicInPrivateDir, BridgeError, object } from "./protocol.js"; const actions = z.enum(["dispatch", "hand_off", "returns", "approve", "grants-list", "grants-add", "grants-remove"]); const callSchema = z.object({ id: z.string().uuid(), generation: z.string().uuid(), operationId: z.string().uuid() }).strict(); const computerIdentitySchema = z.object({ id: z.string().uuid(), fingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict(); const operationSchema = z.object({ action: actions, data: z.record(z.string(), z.unknown()), computerIdentity: computerIdentitySchema.optional() }).strict(); const samePane = (left, right) => object(left).server === right.server && object(left).pane === right.pane; const sameTarget = (left, right) => !!left && !!right && Object.keys(left).every(key => left[key] === right[key]); const busy = new Map(); const transitions = new Map(); const readOperation = (action, data) => action === "returns" || action === "grants-list" || action === "hand_off" && data.status === true; const peerData = redactPeerPayload; /** Check the original Hook's actual receipts over its own pinned directory, * before any role reservation, hand-off or successor launch. */ export async function preflightConductorWorkers(from, computerId, peers = hookPeers, request = peerRequest) { const { dispatchStatus } = await import("./dispatch.js"); const directory = await peers().catch(() => []); const receipts = (await dispatchStatus()).filter(receipt => receipt.origin?.server === from.server && receipt.origin.pane === from.pane && !receipt.closedAt && (receipt.approval || !receipt.returned?.read || ["working", "blocked", "stalled"].includes(String(receipt.worker?.state)))); await Promise.all([...new Set(receipts.map(receipt => receipt.computer))].map(async (name) => { const group = receipts.filter(receipt => receipt.computer === name); const peer = directory.find(peer => peer.name === name); if (group.every(receipt => receipt.computerId === computerId || !peer && !receipt.computerId && !receipt.moves?.some(move => move.to.computer) && isLocalComputer(name))) return; if (!peer) throw new BridgeError(409, "A worker computer is not enrolled; the conductor move was refused before changes."); let health; try { health = await request(peer, "/v1/health", undefined, 5_000); } catch { throw new BridgeError(409, "A worker computer is unreachable; the conductor move was refused before changes."); } for (const receipt of group) { const moved = receipt.moves?.slice().reverse().find(move => move.to.computer); const expected = moved?.computerId ?? receipt.computerId; if (expected && object(health.computer).id !== expected || moved?.computerFingerprint && peerFingerprint(peer) !== moved.computerFingerprint) throw new BridgeError(409, "A worker computer's identity changed; the conductor move was refused before changes."); } })); } /** A owns the generation and every receipt/approval/grant. B holds only a * delegated pane and operation outbox. No login or bearer credential crosses * Hooks. Peer writes are authenticated by fetching their intent back over * our own enrolled, pinned SSH account, rather than trusting a claimed alias. */ export class ConductorSuccessionService { options; peers; request; reservedCallers = new Set(); operations = new Map(); constructor(options) { this.options = options; this.peers = options.peers ?? (() => hookPeers(options.root)); this.request = options.request ?? peerRequest; } /** Synchronous admission also pauses conductor calls during preflight. */ reserveCaller(target) { const key = `${target.server}\0${target.pane}`; if (this.reservedCallers.has(key)) throw new BridgeError(409, "This session is already being moved."); this.reservedCallers.add(key); return () => { this.reservedCallers.delete(key); }; } assertCallerAvailable(origin) { const value = object(origin); if (this.reservedCallers.has(`${value.server}\0${value.pane}`)) throw new BridgeError(409, "The conductor is being moved; new conductor operations are paused."); } file(id, suffix = "state") { return path.join(this.options.root, "conductor-moves", z.string().uuid().parse(id), `${suffix}.json`); } async save(state) { await atomicInPrivateDir(this.file(state.id), JSON.stringify(state)); } async status(id) { z.string().uuid().parse(id); const current = (await readRoleState(this.options.root))?.succession; let state; if (current?.id === id) state = current; else { try { state = successionSchema.parse(JSON.parse(await readFile(this.file(id), "utf8"))); } catch { throw new BridgeError(404, "No conductor move with that id is recorded on this Hook."); } } // Resume data is private to A: it includes unsanitized source labels and // paths and must never be copied to B by a status/preparation probe. const { resume: _resume, ...publicState } = state; return publicState; } /** Identity is supplied only by the scoped SSH gateway, never the body. */ async authorize(id, caller, preparingOrigin) { if (!caller) throw new BridgeError(403, "This route requires an authenticated enrolled computer key. Re-enroll legacy peer keys."); let state; try { state = await this.status(id); } catch (error) { if (!(error instanceof BridgeError && error.status === 404 && preparingOrigin)) throw error; } if (state) { if (caller.name !== undefined && caller.name !== state.connection.name || caller.id !== undefined && caller.id !== state.connection.id || caller.fingerprint !== state.connection.fingerprint) throw new BridgeError(403, "This peer is not the move's recorded counterpart."); await this.connection(state); } else { if (caller.id !== undefined && caller.id !== preparingOrigin) throw new BridgeError(403, "This peer is not the preparing origin."); const peers = (await this.peers()).filter(peer => peerFingerprint(peer) === caller.fingerprint && (!caller.name || peer.name === caller.name)); if (peers.length !== 1 || object((await this.request(peers[0], "/v1/health", undefined, 5_000)).computer).id !== preparingOrigin) throw new BridgeError(403, "The preparing peer is not currently verified."); } } async workerIdentities(from) { const { dispatchStatus } = await import("./dispatch.js"); const directory = await this.peers(); const receipts = (await dispatchStatus()).filter(receipt => samePane(receipt.origin, from) && !receipt.closedAt); const identities = new Map(); for (const receipt of receipts) { const peer = directory.find(peer => peer.name === receipt.computer); const moved = receipt.moves?.slice().reverse().find(move => move.to.computer); if (receipt.computerId === this.options.computer.id || !peer && !moved && isLocalComputer(receipt.computer, [this.options.computer.name])) { identities.set(this.options.computer.id, { id: this.options.computer.id }); continue; } if (!peer) throw new BridgeError(409, "A worker computer is not enrolled in the original Hook's directory."); const health = await this.request(peer, "/v1/health", undefined, 5_000); const id = z.string().uuid().parse(object(health.computer).id), fingerprint = peerFingerprint(peer); if ((moved?.computerId ?? receipt.computerId) && (moved?.computerId ?? receipt.computerId) !== id || moved?.computerFingerprint && moved.computerFingerprint !== fingerprint) throw new BridgeError(409, "A worker computer's identity changed."); identities.set(`${id}:${fingerprint}`, { id, fingerprint }); } return [...identities.values()]; } async connection(state) { const peer = (await this.peers()).find(candidate => candidate.name === state.connection.name && candidate.server === state.connection.server && peerFingerprint(candidate) === state.connection.fingerprint); if (!peer) throw new BridgeError(409, "The conductor's enrolled connection was removed or changed."); const health = await this.request(peer, "/v1/health", undefined, 5_000).catch(() => undefined); if (!health || object(health.computer).id !== state.connection.id) throw new BridgeError(409, "The original conductor Hook is unreachable or its identity changed. Conductor operations are unavailable."); return peer; } async change(id, run) { const saved = await updateConductorAuthority(this.options.root, async (role) => { if (role.succession?.id !== id) throw new BridgeError(409, "The conductor ownership generation changed."); return { ...role, succession: run(role.succession) }; }); await this.save(saved.succession); return saved.succession; } async begin(id, expected, from, destination, computerId, resume) { if (busy.get(this.options.root)) throw new BridgeError(409, "The conductor has an operation in flight; try the move again after it completes."); await this.options.checkWorkers(from); if (!destination.fingerprint) throw new BridgeError(409, "The destination has no pinned identity."); const health = await destination.request("/v1/health", undefined, 5_000); if (object(health.computer).id !== computerId || object(health.capabilities).moveConductorToComputer !== true) throw new BridgeError(409, `Update ${destination.name}'s Hook to move the conductor`); const current = (await readRoleState(this.options.root))?.succession; if (current?.id === id) { if (current.phase === "aborted") throw new BridgeError(409, "This conductor move was aborted; use a new move id."); await this.ensurePrepared(id); return current; } const workers = await this.workerIdentities(from); const transaction = { workers, id, generation: randomUUID(), side: "origin", phase: "preparing", originId: this.options.computer.id, destinationId: computerId, previous: expected, from, ...(resume ? { resume } : {}), connection: { name: destination.name, server: destination.server, fingerprint: destination.fingerprint, id: computerId } }; await updateConductorAuthority(this.options.root, async (role) => { if (busy.get(this.options.root)) throw new BridgeError(409, "The conductor has an operation in flight; try the move again after it completes."); if (role.succession && role.succession.phase !== "aborted") throw new BridgeError(409, "A conductor move or delegation is already active."); if (JSON.stringify(role.conductor) !== JSON.stringify(expected)) throw new BridgeError(409, "The conductor changed during the move; nothing was transferred."); return { ...role, succession: transaction }; }); await this.save(transaction); try { const peer = await this.connection(transaction); await this.request(peer, "/v1/conductor/move/prepare", { id, originId: transaction.originId, generation: transaction.generation, workers: transaction.workers ?? [] }, 10_000); return transaction; } catch (error) { await this.abort(id); throw error; } } async ensurePrepared(id) { const state = await this.status(id); if (state.side !== "origin" || state.phase === "aborted") throw new BridgeError(409, "The conductor move is no longer preparing."); const peer = await this.connection(state); await this.request(peer, "/v1/conductor/move/prepare", { id, originId: state.originId, generation: state.generation, workers: state.workers ?? [] }, 10_000); } async prepare(input) { const data = z.object({ id: z.string().uuid(), originId: z.string().uuid(), generation: z.string().uuid(), workers: z.array(computerIdentitySchema).default([]) }).strict().parse(input); const existing = (await readRoleState(this.options.root))?.succession; if (existing?.id === data.id && existing.generation === data.generation) return { ok: true, phase: existing.phase }; const candidates = await Promise.all((await this.peers()).map(async (peer) => { try { const health = await this.request(peer, "/v1/health", undefined, 5_000); if (object(health.computer).id !== data.originId) return undefined; const state = successionSchema.parse(await this.request(peer, `/v1/conductor/move/status?id=${data.id}`, undefined, 5_000)); if (state.side !== "origin" || state.phase === "aborted" || state.generation !== data.generation || state.destinationId !== this.options.computer.id) return undefined; return { peer, state }; } catch { return undefined; } })); const verified = candidates.filter(value => value !== undefined); if (verified.length !== 1) throw new BridgeError(403, "No unique pinned origin Hook authorized this conductor prepare."); const { peer, state } = verified[0]; if (JSON.stringify(data.workers) !== JSON.stringify(state.workers ?? [])) throw new BridgeError(403, "The worker preflight does not match the original Hook intent."); for (const worker of data.workers) await this.resolveComputer(worker); const successor = { ...state, side: "successor", phase: "prepared", connection: { name: peer.name, server: peer.server, fingerprint: peerFingerprint(peer), id: data.originId } }; // Adopt a legacy named role through the same resolver as the overview, // including prefixed names and mux agent-name mappings. const snapshot = await this.options.snapshot(state.connection.server); await conductorPane(state.connection.server, snapshot, this.options.root); await updateConductorAuthority(this.options.root, async (role) => { if (role.conductor || role.succession && role.succession.phase !== "aborted") throw new BridgeError(409, "This Hook already has a conductor or prepared successor."); return { ...role, conductor: null, succession: successor }; }); await this.save(successor); return { ok: true, phase: "prepared" }; } /** The prepared successor pulls the canonical launch over its own pin. * An unrelated enrolled peer cannot pre-empt a known move id with a brief. */ async saveLaunch(id, input) { const state = await this.status(id); if (state.side !== "origin" || state.phase === "aborted") throw new BridgeError(409, "This conductor move is not preparing a launch."); await atomicInPrivateDir(this.file(id, "launch"), JSON.stringify(input)); } async launchIntent(id) { const state = await this.status(id); if (state.side !== "origin" || state.phase === "aborted") throw new BridgeError(409, "This conductor launch is no longer authorized."); return JSON.parse(await readFile(this.file(id, "launch"), "utf8")); } async verifiedLaunch(input) { const state = (await readRoleState(this.options.root))?.succession; if (!state || state.side !== "successor" || state.id !== object(input.brief).id) return input; if (state.phase === "aborted") throw new BridgeError(409, "This prepared conductor move was aborted."); const peer = await this.connection(state); const authority = successionSchema.parse(await this.request(peer, `/v1/conductor/move/status?id=${state.id}`, undefined, 5_000)); if (authority.phase === "aborted" || authority.generation !== state.generation) throw new BridgeError(403, "The conductor launch generation is not authorized."); const canonical = await this.request(peer, `/v1/conductor/move/launch?id=${state.id}`, undefined, 5_000); if (JSON.stringify(canonical) !== JSON.stringify(input)) throw new BridgeError(403, "This is not the origin Hook's prepared conductor launch."); return canonical; } async prepared(id, target) { await this.change(id, state => { if (state.side !== "origin" || state.phase === "aborted") throw new BridgeError(409, "This conductor move is no longer preparing."); if (state.target && !sameTarget(state.target, target)) throw new BridgeError(409, "This move already prepared a different successor."); return { ...state, target, phase: state.phase === "committed" ? "committed" : "prepared" }; }); } async commit(id) { const saved = await updateConductorAuthority(this.options.root, async (role) => { const state = role.succession; if (!state || state.id !== id || state.side !== "origin" || !state.target) throw new BridgeError(409, "The successor was not prepared and confirmed."); if (state.phase === "committed") return role; if (state.phase !== "prepared" || JSON.stringify(role.conductor) !== JSON.stringify(state.previous)) throw new BridgeError(409, "The conductor changed during the move; nothing was transferred."); return { ...role, conductor: null, succession: { ...state, phase: "committed" } }; }); // The ownership write is the commit point. A failed notification or lost // acknowledgement never restores the previous generation. const state = saved.succession; await this.save(state); return state; } async transition(id, run) { const key = this.file(id, "transition"); const work = (transitions.get(key) ?? Promise.resolve()).catch(() => { }).then(run); transitions.set(key, work); try { return await work; } finally { if (transitions.get(key) === work) transitions.delete(key); } } async activate(input) { const id = z.object({ id: z.string().uuid() }).parse(input).id; return this.transition(id, () => this.activateOnce(input)); } async activateOnce(input) { const data = z.object({ id: z.string().uuid(), generation: z.string().uuid() }).strict().parse(input); const successor = await this.status(data.id); if (successor.side !== "successor" || successor.generation !== data.generation) throw new BridgeError(403, "This is not the prepared successor generation."); const peer = await this.connection(successor); const authority = successionSchema.parse(await this.request(peer, `/v1/conductor/move/status?id=${data.id}`, undefined, 5_000)); if (authority.phase !== "committed" || authority.generation !== successor.generation || authority.destinationId !== this.options.computer.id || !authority.target) throw new BridgeError(409, "The original Hook has not committed this successor. You are not the conductor."); const target = authority.target; const pane = await this.options.validate(target); if (!pane || !["idle", "working"].includes(String(pane.agent_status))) throw new BridgeError(409, "The prepared successor is not confirmed running."); await updateConductorAuthority(this.options.root, async (role) => { if (role.succession?.id !== data.id || role.succession.generation !== data.generation || role.succession.phase === "aborted") throw new BridgeError(409, "The successor generation changed."); return { ...role, conductor: { server: target.server, pane: target.pane, workspace: target.workspace, tab: target.tab, source: target.source, session: target.session, ...(typeof pane.terminal_id === "string" ? { terminal: pane.terminal_id } : {}), since: successor.previous.since, by: "owner" }, succession: { ...successor, phase: "committed", target } }; }); await this.options.deliver(target, "The original Hook committed the hand-over. You are now the conductor. Its receipts, approvals and grants stay there; your conductor operations are forwarded there and fail closed if it is unreachable.", `move-${data.id}-conductor-confirmed`); return { ok: true, phase: "committed", target }; } async finish(id) { return this.transition(id, () => this.finishOnce(id)); } async finishOnce(id) { const state = await this.status(id); if (state.side !== "origin" || state.phase !== "committed") throw new BridgeError(409, "The origin has not committed this hand-over."); if (state.activated) return; const peer = await this.connection(state); await this.request(peer, "/v1/conductor/move/commit", { id, generation: state.generation }, 10_000); await this.options.deliver(state.from, "The conductor role moved to another computer. You are no longer the conductor; conductor operations from this chat are fenced. Keep this chat open as its hand-off record.", `move-${id}-conductor-fenced`); await this.change(id, current => ({ ...current, activated: true })); } /** Explicit owner revocation on A ends delegation without restoring the * old chat. This is distinct from rolling back an uncertain commit. */ async stopOrigin(pane) { const current = (await readRoleState(this.options.root))?.succession; if (!current || current.phase === "aborted") return undefined; if (current.side !== "origin") throw new BridgeError(409, "Stop this delegated conductor from its original Hook."); if (current.phase !== "committed") throw new BridgeError(409, "The conductor is being moved; reconcile the move before stopping it."); if (pane !== undefined) throw new BridgeError(409, "The conductor is on another computer. Stop its delegation on the original Hook without a pane id."); const state = await updateConductorAuthority(this.options.root, async (role) => { if (busy.get(this.options.root)) throw new BridgeError(409, "The conductor has an operation in flight; try stopping it again after it completes."); if (role.succession?.id !== current.id || role.succession.generation !== current.generation || role.succession.phase !== "committed") throw new BridgeError(409, "The conductor generation changed."); return { ...role, conductor: null, succession: { ...role.succession, phase: "aborted", revoked: true } }; }); await this.save(state.succession); return { ok: true, stopped: true }; } async abort(id) { // Re-read disk after any uncertain write. A committed generation is never // rolled back, even if the acknowledgement or following status write failed. resetRoleState(); const state = await this.status(id); if (state.phase === "committed") return; const aborted = await updateConductorAuthority(this.options.root, async (role) => { if (role.succession?.id !== id || role.succession.phase === "committed") return role; return { ...role, ...(role.succession.side === "successor" ? { conductor: null } : {}), succession: { ...role.succession, phase: "aborted" } }; }); if (aborted.succession) await this.save(aborted.succession); } async call(action, data, origin, execute) { this.assertCallerAvailable(origin); const role = await readRoleState(this.options.root), state = role?.succession; if (state?.revoked && !samePane(origin, role?.conductor ?? { server: "", pane: "" }) && samePane(origin, state.side === "origin" ? state.from : state.target ?? state.from)) throw new BridgeError(409, "This conductor ownership generation is fenced."); if (!state || state.phase === "aborted") { let held = false; await updateConductorAuthority(this.options.root, async (current) => { this.assertCallerAvailable(origin); if (current.succession && current.succession.phase !== "aborted") throw new BridgeError(409, "The conductor is being moved; new conductor operations are paused."); if (current.conductor && samePane(origin, current.conductor)) { busy.set(this.options.root, (busy.get(this.options.root) ?? 0) + 1); held = true; } return current; }); try { return await execute(); } finally { if (held) busy.set(this.options.root, (busy.get(this.options.root) ?? 1) - 1); } } const local = state.side === "origin" ? state.from : state.target; // Prepared successor operations with a live origin are refused until the // exact conversation is bound and A's generation commit has been proved. if (state.side === "successor" && state.phase !== "committed" && (samePane(origin, state.target ?? { server: "", pane: "" }) || typeof object(origin).server === "string" && typeof object(origin).pane === "string" && await this.options.isSuccessor?.(origin, state.id))) throw new BridgeError(409, "This chat is a prepared successor, not the conductor yet."); if (!local || !samePane(origin, local)) return execute(); if (state.side === "origin") throw new BridgeError(409, state.phase === "committed" ? "This chat is no longer the conductor. Its ownership generation is fenced." : "The conductor is being moved; new conductor operations are paused."); await this.options.validate(state.target); const peer = await this.connection(state); const authority = successionSchema.parse(await this.request(peer, `/v1/conductor/move/status?id=${state.id}`, undefined, 5_000)); if (authority.phase !== "committed" || authority.generation !== state.generation || !sameTarget(authority.target, state.target)) throw new BridgeError(409, "The original Hook no longer authorizes this conductor generation."); const operation = { action, data: peerData(data), ...(["dispatch", "hand_off"].includes(action) && data.computer !== "anywhere" ? { computerIdentity: await this.identifyComputer(data.computer) } : {}) }; const hash = createHash("sha256").update(JSON.stringify(operation)).digest("hex"); const key = this.file(state.id, `call-${hash}`); const pending = this.operations.get(key); if (pending) return pending; const work = (async () => { let saved; try { saved = JSON.parse(await readFile(key, "utf8")); } catch (error) { if (error.code !== "ENOENT") throw error; } // Only an unacknowledged intent is replayed. A successful call starts // a fresh intent next time, especially for draining newly arrived returns. const operationId = !readOperation(action, data) && saved && !saved.result && !saved.failed ? saved.operationId : randomUUID(); await atomicInPrivateDir(this.file(state.id, `out-${operationId}`), JSON.stringify(operation)); await atomicInPrivateDir(key, JSON.stringify({ operationId, at: Date.now() })); try { const result = await this.request(peer, "/v1/conductor/move/operate", { id: state.id, generation: state.generation, operationId }, 65_000); await atomicInPrivateDir(key, JSON.stringify({ operationId, at: Date.now(), result })); if (action === "returns") await this.request(peer, "/v1/conductor/move/ack", { id: state.id, generation: state.generation, operationId }, 10_000).catch(() => undefined); return result; } catch (error) { if (error instanceof BridgeError && error.status >= 400 && error.status < 500) await atomicInPrivateDir(key, JSON.stringify({ operationId, at: Date.now(), failed: true })); throw error; } })(); this.operations.set(key, work); try { return await work; } finally { this.operations.delete(key); } } async identifyComputer(name) { if (name === undefined || typeof name === "string" && isLocalComputer(name, [this.options.computer.name])) return { id: this.options.computer.id }; const peer = (await this.peers()).find(peer => peer.name === name); if (!peer) throw new BridgeError(409, "The selected computer is not in the successor Hook's verified directory."); const health = await this.request(peer, "/v1/health", undefined, 5_000); return { id: z.string().uuid().parse(object(health.computer).id), fingerprint: peerFingerprint(peer) }; } async resolveComputer(identity) { if (identity.id === this.options.computer.id) return this.options.computer.name; const matches = (await Promise.all((await this.peers()).filter(peer => !identity.fingerprint || peerFingerprint(peer) === identity.fingerprint).map(async (peer) => { const health = await this.request(peer, "/v1/health", undefined, 5_000).catch(() => undefined); return object(health?.computer).id === identity.id ? peer.name : undefined; }))).filter((name) => !!name); if (matches.length !== 1) throw new BridgeError(409, "The selected computer identity is not uniquely enrolled on the original conductor Hook."); return matches[0]; } /** Automatic failover has no authority from the fenced old pane. Its * generation is checked against the live successor before executing at A. */ async automatic(action, data, origin, execute) { this.assertCallerAvailable(origin); const state = (await readRoleState(this.options.root))?.succession; if (state?.side !== "origin" || state.phase !== "committed" || !samePane(origin, state.from)) return this.call(action, data, origin, execute); const peer = await this.connection(state); const successor = await this.request(peer, `/v1/conductor/move/status?id=${state.id}`, undefined, 5_000); if (successor.phase !== "committed" || successor.generation !== state.generation || !sameTarget(object(successor.target), state.target)) throw new BridgeError(409, "The delegated conductor is unavailable; automatic dispatch is paused."); await updateConductorAuthority(this.options.root, async (role) => { if (role.succession?.generation !== state.generation || role.succession.phase !== "committed") throw new BridgeError(409, "The conductor generation changed."); busy.set(this.options.root, (busy.get(this.options.root) ?? 0) + 1); return role; }); try { return await execute(); } finally { busy.set(this.options.root, (busy.get(this.options.root) ?? 1) - 1); } } async outgoing(id, operationId) { const state = await this.status(id); if (state.side !== "successor" || state.phase !== "committed") throw new BridgeError(409, "No active successor outbox."); return operationSchema.parse(JSON.parse(await readFile(this.file(id, `out-${z.string().uuid().parse(operationId)}`), "utf8"))); } async operate(input, execute) { const data = callSchema.parse(input), state = await this.status(data.id); if (state.side !== "origin" || state.phase !== "committed" || state.generation !== data.generation) throw new BridgeError(403, "The conductor generation is not authorized."); const peer = await this.connection(state); const operation = operationSchema.parse(await this.request(peer, `/v1/conductor/move/operation?id=${data.id}&operationId=${data.operationId}`, undefined, 5_000)); const key = this.file(data.id, `result-${data.operationId}`); const pending = this.operations.get(key); if (pending) return pending; const work = (async () => { let saved; try { saved = JSON.parse(await readFile(key, "utf8")); } catch (error) { if (error.code !== "ENOENT") throw error; } if (saved?.result) return peerData(object(saved.result)); if (saved?.state === "failed") throw new BridgeError(Number(saved.status), String(saved.error)); if (saved && !readOperation(operation.action, operation.data)) throw new BridgeError(503, "This conductor operation has an uncertain outcome. Inspect its existing receipt; it will not be repeated."); await updateConductorAuthority(this.options.root, async (role) => { if (role.succession?.id !== data.id || role.succession.phase !== "committed" || role.succession.generation !== data.generation) throw new BridgeError(403, "The conductor generation was fenced."); busy.set(this.options.root, (busy.get(this.options.root) ?? 0) + 1); return role; }); try { await atomicInPrivateDir(key, JSON.stringify({ state: "uncertain", operation })); const { server, workspace, tab, pane } = state.from; const payload = { ...operation.data }; if (operation.computerIdentity) payload.computer = await this.resolveComputer(operation.computerIdentity); const result = peerData(await execute(operation.action, payload, { server, workspace, tab, pane }, true)); await atomicInPrivateDir(key, JSON.stringify({ state: "done", result, operation })); return result; } catch (error) { if (error instanceof BridgeError && error.status >= 400 && error.status < 500) await atomicInPrivateDir(key, JSON.stringify({ state: "failed", status: error.status, error: error.message })); throw error; } finally { busy.set(this.options.root, (busy.get(this.options.root) ?? 1) - 1); } })(); this.operations.set(key, work); try { return await work; } finally { this.operations.delete(key); } } async acknowledge(input, acknowledge) { const data = callSchema.parse(input), state = await this.status(data.id); if (state.side !== "origin" || state.phase !== "committed" || state.generation !== data.generation) throw new BridgeError(403, "The conductor generation is not authorized."); const saved = object(JSON.parse(await readFile(this.file(data.id, `result-${data.operationId}`), "utf8"))); if (object(saved.operation).action === "returns" && saved.state === "done") await acknowledge((object(saved.result).returns ?? [])); return { ok: true }; } async forward(origin, text, deliveryId) { const state = (await readRoleState(this.options.root))?.succession; if (state?.side !== "origin" || !samePane(origin, state.from)) return undefined; if (state.phase === "preparing" || state.phase === "prepared") return { delivered: false }; if (state.phase !== "committed") return undefined; const peer = await this.connection(state); await atomicInPrivateDir(this.file(state.id, `notice-${deliveryId}`), JSON.stringify(peerData({ text, deliveryId }))); const sent = await this.request(peer, "/v1/conductor/move/notice", { id: state.id, generation: state.generation, deliveryId }, 10_000); const current = (await readRoleState(this.options.root))?.succession; if (current?.id !== state.id || current.generation !== state.generation || current.phase !== "committed") return { delivered: false }; return { delivered: sent.delivered === true || sent.queued === true }; } async notice(input) { const data = z.object({ id: z.string().uuid(), generation: z.string().uuid(), deliveryId: deliveryIdSchema.unwrap() }).strict().parse(input); const state = await this.status(data.id); if (state.side !== "successor" || state.phase !== "committed" || state.generation !== data.generation || !state.target) throw new BridgeError(403, "This conductor generation is not active."); const peer = await this.connection(state); const authority = successionSchema.parse(await this.request(peer, `/v1/conductor/move/status?id=${state.id}`, undefined, 5_000)); if (authority.phase !== "committed" || authority.generation !== state.generation) throw new BridgeError(403, "The original Hook fenced this conductor generation."); await this.options.validate(state.target); const notice = await this.request(peer, `/v1/conductor/move/notification?id=${state.id}&deliveryId=${data.deliveryId}`, undefined, 5_000); return this.options.deliver(state.target, z.string().max(32768).parse(notice.text), data.deliveryId); } async notification(id, deliveryId) { deliveryIdSchema.unwrap().parse(deliveryId); await this.status(id); return JSON.parse(await readFile(this.file(id, `notice-${deliveryId}`), "utf8")); } async reconcile() { const state = (await readRoleState(this.options.root))?.succession; if (!state || state.phase === "aborted") return; if (state.side === "origin" && state.phase === "committed") await this.finish(state.id); if (state.side === "successor") { const peer = await this.connection(state); const authority = successionSchema.parse(await this.request(peer, `/v1/conductor/move/status?id=${state.id}`, undefined, 5_000)); if (authority.phase === "committed" && state.phase !== "committed") await this.activate({ id: state.id, generation: state.generation }); if (authority.target && !state.target) await this.change(state.id, current => ({ ...current, target: authority.target })); if (authority.phase === "aborted") { if (state.phase === "committed") { await updateConductorAuthority(this.options.root, async (role) => role.succession?.id !== state.id ? role : { ...role, conductor: null, succession: { ...role.succession, phase: "aborted", revoked: true } }); } else await this.abort(state.id); const target = authority.target ?? state.target; if (target) await this.options.deliver(target, authority.revoked ? "The owner stopped the delegated conductor on the original Hook. You are no longer the conductor." : "The hand-over failed. You are not the conductor; the previous conductor keeps the role.", `move-${state.id}-conductor-failed`); } } } }