import { type ControlReply, type EpCaller, type EpInstanceLiveness } from "@cotal-ai/core"; import { type NatsConnection } from "@nats-io/transport-node"; import { CommandFailure, type ControlAuth } from "@cotal-ai/workspace"; import { type SpaceAuth } from "@cotal-ai/core"; /** The control auth shape and target resolver live in `@cotal-ai/workspace` (shared with every * command surface that addresses the manager: this CLI, `cotal run`, the web dashboard). Re-exported * so this module's importers keep resolving them from here. */ export { resolveControlTarget, type ControlAuth } from "@cotal-ai/workspace"; /** Client-side request window for the manager's readiness-waiting launch ops (`start`, and the * manifest `launch` — both funnel into the same startAgent readiness wait). #159 B1: the manager * replies only on a REAL outcome — presence join, process exit, or its ~30s readiness backstop — * so these requests must OUTLIVE that window, not the 5s op default. The tier rule forbids * importing the manager's READINESS_TIMEOUT_MS here; the launch-parity smoke enforces the * relation by test. */ export declare const START_TIMEOUT_MS = 40000; /** Operator reach for one targeted control call: `owner` rides the caller's own-domain verb rows * (the spawn capability's standing mint), `any` the admin instrument's cross-agent rows (§13.2 * any-mode). Replaces the deleted manager ctl tiers as the CLI's mode selector (1d). */ export type ControlReach = "owner" | "any"; export interface ManagerRequestIntent { requestId: string; requestedGoalId?: string; } export interface ManagerRequestReceipt { requestId: string; managerInstanceId: string; managerEpoch: number; goalId?: string; fingerprint?: string; allocation?: { name: string; owner: string; actor: string; lifecycleUid: string; }; } /** {@link ControlReply} plus the one fact a caller cannot recover from the rendered message: whether * the call went UNANSWERED, as core marks it (`EP_UNANSWERED`: no responder, or the reply * deadline elapsed with nothing attributed to the request). `up`'s resume readiness poll keys on it; * it used to key on the message prefix, which turned an operator-facing string into a control-flow * predicate in another file. `answered` marks a refusal a manager responder sent itself; one the * broker or this client raised leaves it unset. */ export type ManagerReply = ControlReply & { unanswered?: boolean; answered?: true; preSubmitRefusal?: true; managerInstanceId?: string; managerEpoch?: number; observationCause?: { code?: string; outcome?: string; }; intent?: ManagerRequestIntent; request?: ManagerRequestReceipt; nativeOutcome?: "executed" | "not-executed" | "unknown"; }; /** Classify only a proved pre-effect refusal as refused; a missing effect outcome is not proof. * Every message names the agent when the target carries one and ends with the manager's own * reason, kept to one line. */ export declare function managerFailure(reply: ManagerReply, operation: string, target: unknown): CommandFailure; /** What the calling command declares about pinning. Passed ONLY by a command that offers `--on` * (`ps`, `models`, `stop`, `attach`, `spawn --detach`), with `instanceId` set to what the operator * typed, if anything. Its presence is what lets the renderer offer `--on` as a remedy: a command * without the flag (`up`, `down`) rides the same rails, splits the same way, and must not be told * to type a flag it does not have. Absence of a pin therefore never means "not passed". */ export interface ManagerPin { instanceId?: string; /** A picker-selected incarnation, checked after name resolution and before publication. */ expectedTarget?: { id: string; lifecycleUid: string; }; /** Composite operations persist their original request before publication and the attributed * allocation before following a goal. A journal refusal never triggers another submission. */ journal?: { beforeSubmit(intent: ManagerRequestIntent): void | Promise; accepted(receipt: ManagerRequestReceipt): void | Promise; }; } /** `--on `: address ONE manager instance instead of the class queue. Shared by * ps/stop/attach/input/describe/models so they cannot drift. For stop and attach it is the seat-locality * escape hatch: the manager that can act on a seat is the one HOSTING it, which is not necessarily * the one that wins the class queue. */ export declare const onFlag: { readonly name: "on"; readonly type: "string"; readonly value: ""; readonly description: "ask only this manager instance, by the id `ps` prints, in a space with several managers"; }; /** Read `--on` at the site that declares it. Absent stays absent (class rails). An EMPTY value * (`--on=`, `--on ""`, or `--on "$INSTANCE"` with the variable unset) is refused here, up front: * it is falsy, so every `if (on)` branch would treat it as absent and drop the pin (a `stop` would * fall through to seat locality, an open-mesh `ps` to the scatter), while the mint and core's * route builder treat it as PRESENT and refuse it as an invalid token. Two answers for one input; * a dropped pin is a silent fallback, so neither branch gets to see it. A non-empty value is also * shape-checked here now, against core's own lifecycle-token grammar: the mint's bare grammar * error named neither the identifier `--on` wants nor where to read it, which cost operators * spawn attempts on a format mismatch (#423). */ export declare function onInstance(on: string | undefined, verb: string): string | undefined; /** {@link onInstance} for a command that reports no structured output: the refusal is printed and * ends the process with exit 1. */ export declare function onInstanceOrExit(on: string | undefined, verb: string): string | undefined; /** Render an ep-rail failure for the operator. Three outcomes, told apart by core's markers and never * by the catalog code: a responder's own `ok:false` describe reply is rethrown under ITS code * (`unavailable` included), and a store read after an answered describe raises the same code, so * the code says nothing about whether anyone answered. * - UNANSWERED ({@link unansweredRequest}: no responder, or the reply deadline elapsed). The * reachability verdict "no manager reachable" is stated here and only here, and only unpinned: * an unanswered PINNED call names the instance instead, since three managers may be answering * while the one the operator typed is not there, and "no manager reachable" sends them to the * broker for a typo. Measured on a live three-manager mesh during review. The verdict is also * scoped to the RAIL ({@link unansweredRail}): on the versioned rail it names that rail and says what the * silence does not establish, because SPEC 13.15 keeps the two rails disjoint at the broker. * - a REGISTRY READ on this side failed ({@link registryReadFailed}: the scatter's freeze or its * reconcile). The managers were not the failure and may all be up; a verdict on them here sent * the operator to the managers for a broker read. * - everything else answered, or failed on this side with its own cause, and is printed as is. * Prepending a verdict made the headline contradict the body: a describe REFUSED BY THE BROKER * read as an unreachable manager, which is precisely the misreading the refusal was reworded to * stop. * A failure that is not an {@link EpEnvelopeError} carries no answer provenance at all, so no verdict * is stated for it either: its message stands alone. */ export declare function epRailFailure(e: unknown, pin?: ManagerPin): ManagerReply; /** Send one control command to the manager over the v0.4 service-endpoint rails and disconnect — * since 1d the manager's ONLY control door (the `ctl` tiers are deleted). The target is already * reachability- + auth-preflighted by {@link resolveControlTarget}. `reach` picks the operator * mode for the two targeted ops (stop/attach): `owner` = the caller's own domain (the spawn * capability's standing mint / a user bearer's own owner), `any` = the admin instrument's * cross-agent reach. Three auth shapes reach the rails: a static instrument's caller triple, a * user bearer's triple, or an OPEN mesh (no credential system — a bare connection under a * synthesized DEV_OWNER triple, since the manager registered under DEV_OWNER and the broker * enforces nothing). A raw `--creds` file from an older generation carries no ep rows and is * refused loud (no silent ctl fallback exists anymore). */ export declare function askManager(space: string, server: string, op: string, args?: Record, auth?: ControlAuth, reach?: ControlReach, timeoutMs?: number, pin?: ManagerPin): Promise; /** What this CLI established about a silent instance's LIVENESS, as opposed to its answer. Kept * separate from `reachable` because they are different questions and folding them is what let a * registration outlive its host unnoticed: * - `gone` — the BROKER answered no-responders on that instance's own rail. Affirmative: the * record claims a live instance and there is nothing behind it, so the row can say so and name * the deregistration. * - `unknown` — a probe went out and nothing came back. A live-but-slow host and a wedged one are * the same observation from here, so nothing is claimed. * - `not-probed` — no probe was PUBLISHED for it: it is outside this credential's pinned set (it * registered after the freeze this instrument was minted against), or this command had no * probe path at all (user mode holds no instance rails). * - `probe-refused` — the broker refused the probe publish. The grant is missing, which is a * local misconfiguration and NOT a statement about the instance; it is named on stderr rather * than left to expire into `unknown`. */ export type ScatterInstanceLiveness = "gone" | "unknown" | "not-probed" | "probe-refused"; /** One instance's slot in a class scatter (P2 item 3): a REACHABLE instance carries its attributed * reply (`data` on ok, `error` on a per-instance failure); an UNREACHABLE one (a frozen slot that * produced no on-time reply — a severed/stalled manager) is reported, NEVER omitted (SPEC §13.5 pin 3), * and carries what the liveness probe established about it ({@link ScatterInstanceLiveness}). */ export interface ScatterInstanceReply { instanceId: string; reachable: boolean; data?: unknown; error?: string; /** The endpoint's error code beside `error`, when its refusal carried one. */ code?: string; /** Set on unreachable slots only: a slot that ANSWERED needs no liveness verdict. */ liveness?: ScatterInstanceLiveness; } export type ScatterReply = { ok: true; instances: ScatterInstanceReply[]; } | { ok: false; error: string; code?: string; unanswered?: boolean; }; /** Open one short-lived control connection under whichever of the three auth shapes this command * holds (static creds / user bearer + sentinel / bare open mesh) and hand it to `fn`. Extracted so * the scatter's TWO connections cannot drift in their connect options: they differ in credential * and in nothing else. The selected dialer matters because the raw node transport refuses a * `wss://` mesh behind an HTTPS edge even when the other endpoint rails can reach it. */ export declare function withControlConnection(server: string, auth: ControlAuth, fn: (nc: NatsConnection) => Promise): Promise; /** * THE §13.5 LIVENESS HOOK this CLI hands to a scatter, and the record of what it established. * * It is built HERE and not in core because it is entirely a statement about this caller's * CREDENTIAL. A probe publishes on an instance's own rail, and only the layer that minted the * credential knows which instance rails it carries; core cannot know, and a core that guessed would * publish requests the broker must refuse — invisibly, since a refused publish looks exactly like a * slow instance from the caller's side. * * TWO RULES, and both are about not asking questions this credential cannot ask: * - an id OUTSIDE the pinned set gets `unknown` WITHOUT PUBLISHING. The scatter re-freezes the * class on its own connection, so it can name an instance that registered after the freeze this * credential was minted against. That instance is NEW, not dead, and the right answer for it is * the deadline — reached without sending a request that would be refused. * - an id INSIDE the set that the broker refuses anyway settles IMMEDIATELY on the violation * rather than expiring into `unknown` at the budget, and is reported by name. * * Only `gone` ever licenses anything downstream, so every mistake this can make is a slower and * more truthful answer, never a faster wrong one. */ export declare function pinnedLivenessProbe(nc: NatsConnection, opts: { space: string; endpoint: string; caller: EpCaller; /** The exact ids this connection's credential holds `inst` rails for. */ pinned: ReadonlySet; probeDeadlineMs?: number; /** Where a refusal is announced. Defaults to stderr; the suite captures it. */ report?: (line: string) => void; }): { probeLiveness: (instanceId: string) => Promise; /** What was established about one silent slot, for the row that will describe it. */ livenessOf: (instanceId: string) => ScatterInstanceLiveness; }; /** * Whether the scatter's liveness probe rides a BARE connection, a freshly minted pinned instrument, * or nothing at all. The sixth seed reach on the attach path, made explicit. * * Named and exported so it is GRADABLE, because it is the one site on this path that still answers * "is this an open mesh?" from the absence of a credential. That inference is issue #1205 itself, * and it is only tolerable here because of what this answer is allowed to DO: it selects whether a * frozen instance's liveness is probed, so a wrong answer downgrades a row to `not-probed` and * never refuses an attach. A caller that ever turns this into a refusal must change this function, * which is the point of it having a name. * * bare — no credential and no bearer: an open mesh, whose connection can already publish. * mint — a sealed mesh that holds its seed AND arrived on a credential: re-mint pinned. * as-is — everything else (a user bearer, a raw off-registry cred, a sealed mesh with no seed): * keep the credential it arrived with and probe nothing, which is the pre-existing * behaviour and prints `not-probed` rather than degrading silently. */ export declare function scatterProbeMaterial(auth: ControlAuth, spaceAuth: SpaceAuth | undefined): { kind: "bare"; } | { kind: "mint"; auth: SpaceAuth; } | { kind: "as-is"; }; /** SCATTER one untargeted read (`ps`) across EVERY registered manager instance in the space and merge * the attributed results — the `cotal ps` default in a multi-manager space (P2 item 3). Auth shapes * match {@link askManager}: a minted instrument or user bearer rides its own caller triple; a raw * pre-1c `--creds` file is refused loud; an OPEN mesh synthesizes a DEV_OWNER triple and connects * bare (the broker enforces nothing, so the records freeze reads freely). `spaceAuth` is the * resolved mesh's trust material from {@link resolveControlTarget}: present ⇒ the scatter re-mints * its instrument pinned to the frozen instance ids and can probe their liveness; absent ⇒ it runs * exactly as it did before and every silent slot is reported `not-probed`. */ export declare function scatterManager(space: string, server: string, op: string, auth?: ControlAuth, spaceAuth?: SpaceAuth, timeoutMs?: number): Promise; /** The refusal `ps` gives, raised before an unpinned operator read when the registry lists no live * manager, rather than after the read's describe has waited out its whole deadline. Only the * operator instrument holds the registry rows, so a caller passes a user's manager view or a pinned * call straight to the read. Any other registry outcome, an unreadable registry included, leaves * the read to decide. `purpose` completes "no manager is running for this space to …". */ export declare function refuseWithoutLiveManager(space: string, server: string, auth: ControlAuth, target: unknown, purpose: string): Promise; /** A manager read that did not answer, as the classified failure a structured command reports: the * same sentence, the endpoint's own code under `error.cause`, and the stale-store hint as its remedy * when one applies. A request nobody answered carries `deadline-exceeded`, an unavailable target. */ export declare function readFailure(reply: { error?: string; code?: string; details?: unknown; }, target?: unknown): CommandFailure; export declare function failIfNotOk(reply: ControlReply): void; //# sourceMappingURL=control.d.ts.map