UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

218 lines (217 loc) • 13 kB
/** * design/183 §3 (F-011) — the ROOT-level adoption marker: the durable phase truth of the data-root * adoption state machine (the design/182 rule-bucket marker, generalized to the whole data root). * * Two shapes live in `<dataRoot>/adoption.json`: * - the IN-FLIGHT marker (`marker`): a monotonically-advanced `phase` plus per-leg completion bits * for the one leg class without a self-attesting completion predicate (row rewrites). While this * shape is on disk the root is FROZEN: every file store constructor ANCHORED TO THE DATA ROOT * refuses to build over it ({@link assertAdoptionBootGate} — invariant I6). Coverage is stated * precisely at the gate's own docstring: two faces whose constructor argument is not the root * take an optional `dataRoot` anchor, and a caller omitting it is outside the gate (the seam is * documented and pinned where those faces live). * - the PERMANENT terminal record (`adopted`): never deleted. It is the idempotence anchor (a re-run * with the same (from → toPrincipal) short-circuits to the recorded receipt) and it carries the * IMMUTABLE report — the byte-stable half of every receipt. * * This module is deliberately tiny and dependency-light (node:fs + the atomic-write primitive): every * file store imports the gate from here, so it must never grow an import back into a store. */ /** Who the data root belonged to before adoption. Shape-identical to the rule domain's `RuleOwner` * (design/182): local-single-user identity is a STRUCTURAL ABSENCE, never a sentinel string. */ export type AdoptionSource = { kind: "local-owner"; } | { kind: "principal"; principal: string; }; /** Typed refusal codes (design/183 §7.3): every adoption refusal is discriminable — from `not_found`, * from a broken deployment, and from each other. */ export type AdoptionErrorCode = /** I6 boot gate: a store construction (or engine boot) over a root whose adoption is mid-flight. */ "adoption_in_flight" /** A DIFFERENT adoption is mid-flight in this root — resume the original or repair the marker. */ | "adoption_in_flight_mismatch" /** The root already completed an adoption toward another principal; a second adoption is a * multi-tenant transfer, out of this protocol's scope (design/183 §1 non-goals / D7). */ | "adoption_retarget" /** A live instance (engine or another adoption) owns the data root right now. */ | "adoption_locked" /** A row-rewrite target exists with DIFFERENT bytes: merging two existing rows is not a rewrite. */ | "adoption_row_collision" /** The marker exists but cannot be read as a marker. Fail-closed: after phase 4 the marker IS the * owner-resolution truth, so guessing would fork ownership. Repair by hand, then resume. */ | "adoption_marker_corrupt" /** A config-witness receipt does not bind (wrong adoption id / value / unknown entry). */ | "adoption_witness_mismatch" /** A resume supplies LESS than the recorded migration plan (a dropped rules/carriage leg or a * changed credentials claim) — completing the arc on the thinner plan would terminalize an * incompletely migrated root, so the mismatch is refused instead. */ | "adoption_plan_mismatch" /** The nested permission-rule leg refused (its own collision / lock / marker grammar) — surfaced * under the root arc's typed contract, original reason preserved in the message. */ | "adoption_rule_leg_refused" /** Malformed request (empty principal, from == to, …). */ | "adoption_bad_request"; export declare class AdoptionError extends Error { readonly code: AdoptionErrorCode; constructor(code: AdoptionErrorCode, message: string); } /** One deployment-config the adoption obligates (design/183 §6): configs the state machine cannot * reach carry `migrated: false` and stay on the outstanding account until a consumer WITNESSES them. */ export interface AffectedDeploymentConfig { /** e.g. "web-client-bff" | "cli" | "env" | "core-decl" | … */ deployment: string; key: string; /** The value the consumer must now carry (the new principal, or "withdrawn" for a retired seat). */ requiredValue: string; /** true ⇔ the adoption itself changed it; false ⇒ an explicit operator action remains. */ migrated: boolean; } /** Per-store line of the immutable report (design/183 §6 `legs`). */ export interface AdoptionLegReport { store: string; action: "bucket-rebind" | "row-rewrite" | "carried" | "none" | "reset"; rows?: number; quarantined?: number; } /** The IMMUTABLE half of every receipt (design/183 §6): persisted byte-stable in the terminal record, * identical on the first success and on every idempotent re-run. Current state (outstanding configs, * live quarantine count) is NOT in here — it is derived fresh per answer ({@link AdoptionReceipt}). */ export interface AdoptionReport { /** Minted at phase ② — the binding anchor config-witness receipts must carry. */ adoptionId: string; from: AdoptionSource; toPrincipal: string; atMs: number; /** The claim REFERENCE for the new identity's credentials — never the credentials themselves (the * marker is a plain-text sidecar, not a credential store). The claim must be durable and * repeatedly redeemable (server obligation, design/183 §4.3). */ credentials: { issuedBy: "server"; ref: string; }; affectedDeploymentConfigs: AffectedDeploymentConfig[]; legs: AdoptionLegReport[]; /** design/183 §6 (r4) — the machine-readable CLOSED SET of assets not migrated BY DESIGN, each * naming its ruling. Black-box discriminable from "forgotten": an asset outside this list that * did not arrive is a defect; the list growing beyond the design's written set is DRIFT (§10.5 * ① — new members enter through a design ruling, never through an implementation). */ notMigratedByDesign: Array<{ asset: string; ruling: string; }>; } /** The one receipt envelope (design/183 §6): first success and every idempotent re-run answer with * this same shape. `immutableReport` is the persisted byte-stable snapshot; `current` is derived. */ export interface AdoptionReceipt { status: "adopted"; immutableReport: AdoptionReport; current: { /** `deployment:key` of every config-account entry not yet WITNESSED (an operator ack does not * clear the account — only a consumer's bound runtime read-back does; design/183 §3.3). */ outstandingConfigs: string[]; /** Live quarantine-area row count (local truth = what is actually on disk; design/183 §7.2). */ quarantined: number; }; } /** The durable root marker file. */ export type RootAdoptionFile = { schemaVersion: 1; marker: { adoptionId: string; from: AdoptionSource; toPrincipal: string; phase: 2 | 3 | 4 | 5; /** The migration plan recorded at phase ② and grown MONOTONICALLY across resumes — what a * resume must (at least) re-supply. Without it, an ordinary restart with thinner options * would skip a stalled leg and terminalize an incompletely migrated root; the rules leg is * bound to its canonical DIRECTORY (a boolean would accept a substituted bucket). REQUIRED: * a marker without a readable plan is corrupt (fail-closed), never re-bound to the caller's * current options (adversarial-review findings, rounds 1-2). */ plan: { rulesDir?: string; carriage: string[]; credentialsRef: string; }; /** Completion bits for legs without a self-attesting predicate (row rewrites), plus the * carriage RESULTS folded in at the phase-⑤ publish: the bits (and their counts, which the * terminal report needs) survive a crash between a stage and the next phase publish — a * ⑤→⑥ crash must not let the resumed run report a carried store as untouched. */ legs?: Record<string, { done: true; rows?: number; quarantined?: number; }>; }; } | { schemaVersion: 1; adopted: { from: AdoptionSource; toPrincipal: string; atMs: number; immutableReport: AdoptionReport; }; }; export declare const ROOT_ADOPTION_FILE = "adoption.json"; /** * design/183 §6 (r4) — the CLOSED SET of by-design-not-migrated assets, verbatim from the design's * written list. The WRITER emits exactly this list and the drift pin (§10.5 ① — the design-vs-code * two-cell guard) lives in the TEST GRID, where a ruled growth of the set is a deliberate edit. * * #133: the terminal READ validator deliberately does NOT compare a stored report against this * constant. A persisted report is a snapshot of the set AS RULED AT ADOPTION TIME; the set is * allowed to grow by ruling (§10.5 ① names that road), so an exact-equality read check would mark * every already-adopted root corrupt on the release after any growth (and again on rollback) — 13 * construction sites all refusing to serve. The read validator checks SHAPE and integrity * (non-empty, well-formed entries, no duplicate assets — what an empty/mangled account rebuild * actually needs), never version-crossed content equality. */ export declare const NOT_MIGRATED_BY_DESIGN: ReadonlyArray<{ asset: string; ruling: string; }>; /** The five baseline obligation IDENTITIES (design/183 §6 minimum face). Since #133 the terminal READ * validator deliberately does NOT compare a stored report against this table (the stored report is a * snapshot of the set as ruled at adoption time; see the validator's own note) — the enforcement that * the WRITER emits exactly this set lives in the test grid (test/file-adoption-root.test.ts pins the * fresh report's identities against this constant), where a drift is a deliberate, reviewable edit. */ export declare const BASELINE_CONFIG_IDENTITIES: ReadonlyArray<{ deployment: string; key: string; }>; /** * Read the root adoption marker; `undefined` when none. A corrupt marker THROWS (typed): after * phase 4 the marker is the resolution truth, so a guessed read could serve a half-migrated root or * invent an adoption that never happened — the same fail-closed stance as the rule-domain marker. */ export declare function readRootAdoptionFile(root: string): RootAdoptionFile | undefined; /** Atomically publish the root marker (write-temp → fsync → rename, same discipline as every store). */ export declare function writeRootAdoptionFile(root: string, content: RootAdoptionFile): void; /** * Invariant I6 (design/183 §3.1) — the adoption BOOT GATE: an in-flight root marker means the root is * mid-adoption (possibly after a crash), and serving a half-migrated root is refused LOUDLY. The * refusal names the in-flight arc and the way forward (resume `adoptLocalDataRoot` to completion). A * terminal record passes: a completed adoption is a normal, readable root. A corrupt marker throws * (fail-closed, see {@link readRootAdoptionFile}). * * WHO calls it, precisely: every store constructor whose argument IS the data root (that is most of * the family — they derive their own subdirectory from it). The two faces whose argument is a * caller-chosen directory instead — `FileDurableRulePartitionProvider` (a rule bucket) and * `createFileTaskListStore` (the document's directory) — cannot anchor on their argument, so each * takes the data root as an explicit option and is gated only when given one. `root` here is always * the ANCHOR, never "wherever this store keeps its bytes": passing a subdirectory reads a marker that * is not there and passes unconditionally. * * This gate is what turns "the engine must be stopped during adoption" from an operational assumption * into a machine-checked invariant across crashes: the adoption's own locks die with its process, but * the marker (and this gate) survive. Boundary, stated honestly: the gate fires at CONSTRUCTION time, * and it is the ONLY thing this function does — it never reaches an object that already exists. So a * store instance constructed BEFORE the marker landed keeps writing for as long as it is held, in * ANOTHER OS process (cross-process sharing of one data dir is the file family's documented * UNSUPPORTED shape for the task-list store — F-14; the mailbox store is the exception since its per-box lock upgrade, see `stores/file/mailbox-store.ts`) and, symmetrically, in THIS one (an independent * store face built before the adoption started; the arc's `LOCK` acquisition excludes a live * `FileStorageBackend`, which is the only shape that takes that lock). Adoption adds no new promise on * either side of that seam — see the freeze note on `adoptLocalDataRoot` for the full division. */ export declare function assertAdoptionBootGate(root: string, storeName: string): void;