/** * Request/response control channel for chat-daemon commands that must be * executed *by* the running owner instead of terminating it. * * `control.json` is the lifecycle channel: every request the owner recognizes * there ends its serving loop (stop/reload). Adopting an existing Slack root * must not stop a healthy daemon, so operator commands travel on this separate * per-request channel and are answered in place. * * Invariants: * - Every request is addressed to an exact owner (`ownerId`, `pid`, * `incarnation`, daemon `generation`). A daemon that is not that exact owner * answers `owner_changed` and performs no work, so a replaced or restarted * owner can never satisfy a request captured against its predecessor. The * same tuple is re-proven inside the commit fence and echoed in the answer, * so the submitter can verify who actually acted. * - `.response.json` is a single-winner arbitration object created with * `O_CREAT|O_EXCL`. The serving daemon must win it *before* it commits, and a * submitter that has given up must win it before reporting failure. Whoever * loses learns so definitively, which removes the commit-vs-timeout race * without any sleep: a definitive `cancelled` answer proves no dispatch can * follow within this submission's race, and a lost cancellation is reported * as `unknown` rather than as a failure the caller could act on. The * arbitration is scoped to that race under the trust boundary below; it is * not durable settlement across a crashed and resurrected daemon. * - Documents live in an owner-only (`0700`) directory below the agent * directory, are bounded, and are rejected unless they are ordinary regular * files. This matches the product's existing same-UID local trust boundary; * it is not a defence against a hostile process running as the same user. * - Only identifiers travel through the channel. Tokens, message bodies, and * control secrets are never written here. * * The channel proves *correlation*, never authorship: every field a response * echoes is copied verbatim out of the plaintext request published beside it, * so a definitive answer is only actionable once the caller corroborates the * mutation against an authority outside this directory. */ import { type ChatDaemonKind } from "./chat-daemon-control"; export declare const CHAT_DAEMON_COMMAND_VERSION = 1; export declare const DEFAULT_CHAT_DAEMON_COMMAND_TIMEOUT_MS = 15000; export type ChatDaemonCommandName = "bind-thread"; export type ChatDaemonCommandStatus = "ok" | "rejected" | "owner_changed" | "expired" | "outcome_unknown"; /** Exact owner authority a request is addressed to. */ export interface ChatDaemonCommandOwner { ownerId: string; pid: number; incarnation: string; generation: number; } export interface ChatDaemonCommandRequest extends ChatDaemonCommandOwner { version: typeof CHAT_DAEMON_COMMAND_VERSION; requestId: string; kind: ChatDaemonKind; command: ChatDaemonCommandName; sessionId: string; rootTs: string; createdAt: number; expiresAt: number; } /** * The answer to exactly one request. * * The owner tuple, session, and root are the *addressed* request's, echoed * verbatim. A response therefore carries the complete request envelope, and a * submitter can prove that the document in front of it is correlated with its * own request rather than with a replayed or concurrent one. * * That is correlation, not authentication. Every echoed field is public: it is * copied out of the plaintext request published in the same command directory, * so a stale or planted document can satisfy the envelope without the addressed * daemon ever running. A caller that needs a definitive outcome must corroborate * it against an authority outside this directory (the durable mutation itself). */ export interface ChatDaemonCommandResponse extends ChatDaemonCommandOwner { version: typeof CHAT_DAEMON_COMMAND_VERSION; requestId: string; kind: ChatDaemonKind; command: ChatDaemonCommandName; sessionId: string; rootTs: string; status: ChatDaemonCommandStatus; /** Machine-readable rejection category; never a message body or credential. */ code?: string; endpointGeneration?: number; teamId?: string; channelId?: string; completedAt: number; } /** * Result of one handler dispatch, carrying explicit commit certainty. * * A failure is never just a code. `rejected` asserts that no mapping changed, so * the caller may be told the binding definitively failed. `unknown` asserts the * opposite: commit authority was exercised and the mapping may already be * applied, so no definitive rejection may be reported for it. Handlers state * this directly instead of leaving the channel to infer it from error text. */ export type ChatDaemonCommandOutcome = { ok: true; sessionId: string; endpointGeneration: number; teamId: string; channelId: string; rootTs: string; } | { ok: false; certainty: "rejected"; code: string; } | { ok: false; certainty: "unknown"; code: string; }; export interface ChatDaemonCommandBindInput { sessionId: string; rootTs: string; /** * Terminal authority for this exact request. It must be awaited inside the * store fence, immediately before the commit, and the commit must be * abandoned when it answers `false`. It re-proves the exact daemon owner * tuple, that the request is still published and unexpired, and takes the * single-winner response claim, after which no cancellation can succeed. */ commitAuthority?: () => Promise; } /** Daemon-side executor. Implemented by the runtime that owns the live transports. */ export interface ChatDaemonCommandHandler { bindExistingRoot(input: ChatDaemonCommandBindInput): Promise; } export declare function isChatDaemonCommandRequest(value: unknown): value is ChatDaemonCommandRequest; export declare function isChatDaemonCommandResponse(value: unknown): value is ChatDaemonCommandResponse; /** * The owner-only directory the channel exchanges its documents in. * * `directory` is a plain pathname: this codebase already trusts same-UID local * processes, and every document below it is bounded and validated, so the * channel does not attempt hostile-filesystem hardening it cannot honestly * provide from TypeScript. */ export interface ChatDaemonCommandScope { readonly directory: string; } export interface OpenChatDaemonCommandScopeInput { agentDir: string; kind: ChatDaemonKind; /** Creates the daemon and command directories owner-only when absent. */ create?: boolean; } /** Capture the command directory, or report the channel unusable. */ export declare function openChatDaemonCommandScope(input: OpenChatDaemonCommandScopeInput): Promise; /** Build a request addressed to one exact owner. */ export declare function buildChatDaemonCommandRequest(input: { kind: ChatDaemonKind; command: ChatDaemonCommandName; owner: ChatDaemonCommandOwner; sessionId: string; rootTs: string; now?: number; ttlMs?: number; requestId?: string; }): ChatDaemonCommandRequest; export interface SubmitChatDaemonCommandInput { agentDir: string; kind: ChatDaemonKind; owner: ChatDaemonCommandOwner; command: ChatDaemonCommandName; sessionId: string; rootTs: string; timeoutMs?: number; pollIntervalMs?: number; /** Bounded wait after a lost cancellation, before the outcome is reported unknown. */ settleGraceMs?: number; ttlMs?: number; now?: () => number; sleep?: (ms: number) => Promise; requestId?: string; } /** * Terminal result of one submission. * * `cancelled` is the only definitive failure: the submitter won the response * claim, so the addressed owner cannot dispatch this request from here on within * this submission's race. `unknown` means the daemon won that claim first and its * answer was not observed; the caller must not report a definitive failure, * because a commit may already be applied. * * `untrusted` means the response object under this identifier is a complete, * well-formed document that does not carry this request's envelope. It is * channel corruption — stale or planted — so it may not be read as an answer of * any status, and because the identifier is now occupied the real outcome is * unknowable from here; the caller must treat it exactly like `unknown`. */ export type ChatDaemonCommandSubmission = { outcome: "answered"; response: ChatDaemonCommandResponse; } | { outcome: "cancelled"; } | { outcome: "unknown"; } | { outcome: "untrusted"; code: "response_envelope_mismatch"; } | { outcome: "unavailable"; code: "command_channel_unavailable" | "request_id_unavailable"; }; /** * Publish a command for the addressed owner and wait for its answer. * * The wait ends in exactly one of four states, and the commit-vs-timeout race is * arbitrated by the exclusive creation of the response object rather than by any * timing assumption. */ export declare function submitChatDaemonCommand(input: SubmitChatDaemonCommandInput): Promise; export interface ServeChatDaemonCommandsInput { agentDir: string; kind: ChatDaemonKind; /** The serving daemon's own proven authority. */ ownerId: string; pid: number; incarnation: string; generation: number; handler: ChatDaemonCommandHandler; /** * Re-proves that this process still holds the persisted owner record. It is * checked before any work is dispatched *and again* inside the commit fence, * so a daemon that lost ownership after dispatch mutates nothing. */ verifyOwnership?: () => Promise; now?: () => number; } /** * Answer every pending request addressed to this exact owner. Requests aimed at * any other owner identity are answered `owner_changed` without doing work, and * expired requests are answered `expired`; neither performs a mutation. */ export declare function serveChatDaemonCommandsOnce(input: ServeChatDaemonCommandsInput): Promise; /** * Answer every pending request against an already-captured command scope. * * Exposed so a caller that already holds the directory authority drives exactly * the production path. */ export declare function serveChatDaemonCommandsAgainstScope(scope: ChatDaemonCommandScope, input: ServeChatDaemonCommandsInput): Promise;