UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

661 lines (621 loc) • 30.1 kB
/** * KXM Runtime intake contract (unified plan M1 + M6, the durable part of M2). * * One persistent, project-scoped coordinator identity carries an authority * ceiling; internal messages arrive against that identity with an idempotency * key; and a project pause blocks *fresh dispatch* without losing the intent. * These are separate facts and are recorded separately: received, persisted, * and dispatch-ready each have their own state. * * Nothing here opens a model, a peer session, or an HTTP surface. The Runtime is * the only writer, and adapters that later expose this must stay thin over it. * Until they exist, no control is advertised — which is the honest M0 answer: * unintegrated capability is unavailable, never a success-shaped stub. */ import { createHash } from "node:crypto"; import { newId } from "./protocol.ts"; import { kxmCanonicalJson, validateCoordinator, validateIntakeMessage, type JsonValue, } from "./project-config.ts"; import type { KxmRuntimeContext } from "./runtime-service.ts"; import { runtimeError, type KxmIntakeMessageRow, type KxmProjectControlRow } from "./runtime-store.ts"; /** Largest intake payload we are willing to hash and persist, in bytes. */ export const MAX_INTAKE_CONTENT_BYTES = 16_384; export type KxmIntakeClassification = "public" | "project" | "sensitive" | "secret"; export type KxmIntakeDispatchState = "ready" | "held_paused" | "admitted" | "refused"; export type KxmIntakeSourceKind = "adapter" | "hub" | "operator" | "peer" | "schedule"; export interface KxmCoordinatorAuthority { repositoryAccess: "none" | "read" | "write"; effects: readonly string[]; tools?: { preset?: string; allow?: readonly string[]; deny?: readonly string[]; }; } export interface KxmCoordinatorRecord { schema: "kxm.coordinator.v1"; coordinatorId: string; projectId: string; role: string; channel: string; authority: KxmCoordinatorAuthority; boundAt: string; boundBy: { kind: "human" | "runtime" | "hub" | "agent" | "adapter"; id: string }; configRevision: string; ceilingHash: string; rebindOf?: string; rebindReason?: string; } export interface KxmIntakeMessage { schema: "kxm.intake-message.v1"; messageId: string; projectId: string; coordinatorId: string; receivedAt: string; source: { kind: KxmIntakeSourceKind; id: string }; idempotencyKey: string; contentHash: string; content?: string; contentOmittedReason?: "secret-classified"; classification: KxmIntakeClassification; dispatch: { state: KxmIntakeDispatchState; reason?: string; updatedAt?: string; runId?: string; }; } /** The persisted shape behind the project-control columns. */ interface KxmProjectControlRecord { schema: "kxm.project-control.v1"; projectId: string; paused: boolean; reason?: string; updatedAt: string; actor: string; } const ACCESS_ORDER: Record<KxmCoordinatorAuthority["repositoryAccess"], number> = { none: 0, read: 1, write: 2, }; const IDENTIFIER_RE = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/; const COORDINATOR_ID_RE = /^[a-z][a-z0-9]{1,15}_[A-Za-z0-9][A-Za-z0-9_-]{5,127}$/; const ACTOR_ID_RE = /^.{1,200}$/; /** Canonical SHA-256 over the authority ceiling — the coordinator's fingerprint. */ export function kxmCeilingHash(authority: KxmCoordinatorAuthority): string { return `sha256:${createHash("sha256").update(stableStringify(normalizeAuthority(authority)), "utf8").digest("hex")}`; } /** * Does a stored coordinator already express the ceiling being requested? * * Stored fingerprint only. Recomputing the fingerprint over the stored authority — the * previous behaviour — existed to tolerate rows written before set canonicalisation, and * that compatibility is gone with the single-operator decision: no migration lanes, no * second schema or hash shape carried forward. A stale row is re-bound, not forgiven. */ function ceilingsMatch(stored: KxmCoordinatorRecord, ceilingHash: string): boolean { return stored.ceilingHash === ceilingHash; } function normalizeAuthority(authority: KxmCoordinatorAuthority): KxmCoordinatorAuthority { const tools = authority.tools; return { repositoryAccess: authority.repositoryAccess, effects: canonicalSet(authority.effects ?? []), ...(tools !== undefined ? { tools: { ...(tools.preset !== undefined ? { preset: tools.preset } : {}), ...(tools.allow !== undefined ? { allow: canonicalSet(tools.allow) } : {}), ...(tools.deny !== undefined ? { deny: canonicalSet(tools.deny) } : {}), }, } : {}), }; } function canonicalSet(values: readonly string[]): string[] { return [...new Set(values)].sort(); } /** * Bind the coordinator for a (project, role, channel) slot. Binding is * create-once and idempotent: presenting the same ceiling again returns the * existing identity rather than minting a second one. * * A different ceiling is a **rebind** and needs explicit policy; it may narrow * the ceiling but never widen it, because a message or a model swap must not be * able to grow authority. */ export function bindKxmCoordinator( context: KxmRuntimeContext, input: { role: string; channel?: string; authority: KxmCoordinatorAuthority; actor: { kind: KxmCoordinatorRecord["boundBy"]["kind"]; id: string }; now?: string; rebind?: { approvedBy: { kind: KxmCoordinatorRecord["boundBy"]["kind"]; id: string }; reason: string }; }, ): { coordinator: KxmCoordinatorRecord; created: boolean } { const role = requireIdentifier(input.role, "role"); const channel = requireIdentifier(input.channel ?? "primary", "channel"); const authority = validateAuthority(input.authority); const actor = validateActor(input.actor, "actor"); const now = requireTimestamp(input.now ?? new Date().toISOString(), "now"); const ceilingHash = kxmCeilingHash(authority); const existing = context.eventStore.coordinatorInSlot(context.projectId, role, channel); if (existing) { const current = JSON.parse(existing.record) as KxmCoordinatorRecord; if (ceilingsMatch(current, ceilingHash)) { return { coordinator: current, created: false }; } if (!input.rebind) { throw runtimeError( "coordinator_rebind_requires_policy", existing.coordinatorId, `coordinator for role ${role}/${channel} is already bound with a different ceiling; rebinding needs explicit policy`, ); } assertCeilingNotWidened(current.authority, authority, existing.coordinatorId); const reason = requireText(input.rebind.reason, "rebind.reason", 512); const record: KxmCoordinatorRecord = { schema: "kxm.coordinator.v1", coordinatorId: newId("crd"), projectId: context.projectId, role, channel, authority, boundAt: now, boundBy: validateActor(input.rebind.approvedBy, "rebind.approvedBy"), configRevision: current.configRevision, ceilingHash, rebindOf: existing.coordinatorId, rebindReason: reason, }; validateCoordinator(record, record.coordinatorId); const replaced = context.eventStore.replaceCoordinatorInSlot(existing.coordinatorId, { coordinatorId: record.coordinatorId, projectId: record.projectId, role: record.role, channel: record.channel, ceilingHash: record.ceilingHash, configRevision: record.configRevision, boundAt: record.boundAt, record: kxmCanonicalJson(record as unknown as JsonValue), }); if (!replaced) { // Another process reached the same ceiling first. Return its identity when it // is the ceiling we asked for; anything else is a real conflict. const winner = context.eventStore.coordinatorInSlot(context.projectId, role, channel); const record2 = winner ? (JSON.parse(winner.record) as KxmCoordinatorRecord) : undefined; if (record2 && ceilingsMatch(record2, ceilingHash)) { return { coordinator: record2, created: false }; } throw runtimeError("coordinator_write_lost", existing.coordinatorId, "the coordinator slot changed underneath this rebind"); } return { coordinator: record, created: true }; } if (input.rebind) { throw runtimeError("coordinator_rebind_without_subject", role, "there is no bound coordinator to rebind"); } const record: KxmCoordinatorRecord = { schema: "kxm.coordinator.v1", coordinatorId: newId("crd"), projectId: context.projectId, role, channel, authority, boundAt: now, boundBy: actor, configRevision: context.configRevision, ceilingHash, }; return persistCoordinator(context, record); } /** Resolve a coordinator by id; unknown identity fails closed. */ export function resolveKxmCoordinator(context: KxmRuntimeContext, coordinatorId: string): KxmCoordinatorRecord { const row = context.eventStore.coordinatorById(requireText(coordinatorId, "coordinatorId", 144)); if (!row) throw runtimeError("coordinator_unknown", coordinatorId, "no coordinator is bound in this project"); return JSON.parse(row.record) as KxmCoordinatorRecord; } /** * Accept one internal message at the intake boundary. * * Duplicate ingress (same idempotency key, same content) returns the original * record and cannot produce a second admission record. This layer deduplicates * *intake and admission*; creating a task is the M2 consumer's job, so nothing here can * promise anything about tasks. The same key with **different** content * is rejected: a reused key must not smuggle a different payload. Payloads * classified `secret` are never persisted — only their hash, so a caller that * classifies honestly gets a store that will not hold that payload. The scope is * exactly that: classification is caller-asserted, so this is a storage decision * under a label, not secret detection, and intake is still not a place to keep * credentials. While the project is paused the message is durable with * a held dispatch intent instead of being dropped. */ export function acceptKxmIntakeMessage( context: KxmRuntimeContext, input: { coordinatorId: string; idempotencyKey: string; content: string; classification?: KxmIntakeClassification; source: { kind: KxmIntakeSourceKind; id: string }; now?: string; }, ): { message: KxmIntakeMessage; duplicate: boolean } { const coordinator = resolveKxmCoordinator(context, input.coordinatorId); const key = requireText(input.idempotencyKey, "idempotencyKey", 200); if (typeof input.content !== "string" || input.content.length === 0) { throw runtimeError("intake_content_missing", coordinator.coordinatorId, "intake content must be a non-empty string"); } const classification = input.classification ?? "project"; if (!["public", "project", "sensitive", "secret"].includes(classification)) { throw runtimeError("intake_classification_invalid", coordinator.coordinatorId, `unknown intake classification ${classification}`); } const sourceKind = input.source?.kind; if (!["adapter", "hub", "operator", "peer", "schedule"].includes(String(sourceKind))) { throw runtimeError("intake_source_invalid", coordinator.coordinatorId, `unknown intake source kind ${String(sourceKind)}`); } const sourceId = requireText(input.source.id, "source.id", 200); const now = requireTimestamp(input.now ?? new Date().toISOString(), "now"); const bytes = Buffer.byteLength(input.content, "utf8"); const contentHash = `sha256:${createHash("sha256").update(input.content, "utf8").digest("hex")}`; // One transaction covers the slot probe, the pause read and the write, so a // concurrent resume cannot strand this row in an unpaused project and a racing // writer with different content cannot be mistaken for a duplicate. return context.eventStore.transaction(() => { const existingRow = context.eventStore.intakeBySlot(context.projectId, coordinator.coordinatorId, key); if (existingRow) { return { message: requireSamePayload(existingRow, contentHash, classification, key), duplicate: true }; } if (bytes > MAX_INTAKE_CONTENT_BYTES) { throw runtimeError( "intake_content_too_large", coordinator.coordinatorId, `intake content is ${bytes} bytes; the limit is ${MAX_INTAKE_CONTENT_BYTES}`, ); } // Only secrets are withheld, and that guarantee is about *this layer's* storage // decision: classification is supplied by the caller, so an untrusted adapter // can still label a credential "project" and have it persisted here. Widening // this to detection is a separate, reviewed slice. const persistContent = classification !== "secret"; const paused = isKxmProjectPaused(context); const message: KxmIntakeMessage = { schema: "kxm.intake-message.v1", messageId: newId("msg"), projectId: context.projectId, coordinatorId: coordinator.coordinatorId, receivedAt: now, source: { kind: sourceKind, id: sourceId }, idempotencyKey: key, contentHash, ...(persistContent ? { content: input.content } : { contentOmittedReason: "secret-classified" as const }), classification, dispatch: paused ? { state: "held_paused", reason: "project_paused", updatedAt: now } : { state: "ready" }, }; validateIntakeMessage(message, message.messageId); const inserted = context.eventStore.insertIntakeMessageIfAbsent({ messageId: message.messageId, projectId: message.projectId, coordinatorId: message.coordinatorId, idempotencyKey: message.idempotencyKey, contentHash: message.contentHash, receivedAt: message.receivedAt, dispatchState: message.dispatch.state, record: kxmCanonicalJson(message as unknown as JsonValue), }); if (!inserted) { // Lost the race for the slot. The winner is only a duplicate if it carried // the same content; anything else is the same conflict we refuse serially. const winner = context.eventStore.intakeBySlot(context.projectId, coordinator.coordinatorId, key); if (!winner) throw runtimeError("intake_write_lost", coordinator.coordinatorId, "intake write lost its slot and left no record"); return { message: requireSamePayload(winner, contentHash, classification, key), duplicate: true }; } return { message, duplicate: false }; }); } function requireSamePayload( row: { record: string }, contentHash: string, classification: KxmIntakeClassification, key: string, ): KxmIntakeMessage { const message = JSON.parse(row.record) as KxmIntakeMessage; if (message.contentHash !== contentHash) { throw runtimeError( "intake_payload_conflict", message.messageId, `idempotency key ${key} was already used with different content`, ); } // Re-labelling the same payload is not a no-op: a retry marked `secret` must not // quietly hand back a stored plaintext record, and a retry marked `project` must // not launder a row that was withheld. if (message.classification !== classification) { throw runtimeError( "intake_classification_conflict", message.messageId, `idempotency key ${key} was already recorded as ${message.classification}, not ${classification}`, ); } return message; } /** Pending intake that may be dispatched, oldest first. Empty while paused. */ export function listKxmDispatchableIntake(context: KxmRuntimeContext, limit = 50): KxmIntakeMessage[] { if (isKxmProjectPaused(context)) return []; return context.eventStore .intakeInStates(context.projectId, ["ready"], limit) .map((row) => JSON.parse(row.record) as KxmIntakeMessage); } /** * Bind a ready message to its run. Admission is idempotent for the same run and * refuses a second one, so duplicate ingress cannot create another task. While * paused, nothing may be admitted at all. */ export function admitKxmIntakeRun( context: KxmRuntimeContext, messageId: string, input: { runId: string; now?: string }, ): KxmIntakeMessage { const id = requireText(messageId, "messageId", 144); const runId = requireText(input.runId, "runId", 144); if (!COORDINATOR_ID_RE.test(runId)) throw runtimeError("intake_run_id_invalid", id, "runId is not a well-formed opaque id"); const now = requireTimestamp(input.now ?? new Date().toISOString(), "now"); // The pause read, the state read and the guarded transition commit together, so // a pause cannot slip in between "not paused" and "admitted". return context.eventStore.transaction(() => { if (isKxmProjectPaused(context)) { throw runtimeError("intake_paused", id, "the project is paused; no fresh dispatch may be admitted"); } const row = context.eventStore.intakeMessage(id); if (!row) throw runtimeError("intake_message_unknown", id, "no such intake message in this project"); const message = JSON.parse(row.record) as KxmIntakeMessage; if (message.dispatch.state === "admitted") { if (message.dispatch.runId === runId) return message; throw runtimeError("intake_second_admission", id, `message already admitted as ${message.dispatch.runId}`); } if (message.dispatch.state !== "ready") { throw runtimeError("intake_not_dispatchable", id, `intake state ${message.dispatch.state} cannot be admitted`); } const next: KxmIntakeMessage = { ...message, dispatch: { state: "admitted", runId, updatedAt: now } }; validateIntakeMessage(next, next.messageId); const updated = context.eventStore.updateIntakeDispatch(id, "ready", { state: "admitted", record: kxmCanonicalJson(next as unknown as JsonValue) }); if (!updated) { const racer = context.eventStore.intakeMessage(id); const current = racer ? (JSON.parse(racer.record) as KxmIntakeMessage) : undefined; if (current?.dispatch.state === "admitted" && current.dispatch.runId === runId) return current; throw runtimeError("intake_dispatch_race", id, "the intake record changed underneath this admission"); } return next; }); } /** * Pause or resume intake dispatch for the project. Pausing does not cancel work * that is already admitted; it stops fresh dispatch and holds the intent. A * message that arrives *during* the pause is stored as `held_paused`, and a * resume releases exactly those, in received order. Messages that were already * `ready` stay ready — pause blocks the dispatch decision, not the record — so * nothing is lost or duplicated across the pause window either way. */ export function setKxmProjectPause( context: KxmRuntimeContext, input: { paused: boolean; actor: { kind: KxmCoordinatorRecord["boundBy"]["kind"]; id: string }; reason?: string; now?: string }, ): { control: KxmProjectControlRow; released: KxmIntakeMessage[] } { const actor = validateActor(input.actor, "actor"); const now = requireTimestamp(input.now ?? new Date().toISOString(), "now"); const reason = input.reason === undefined ? undefined : requireText(input.reason, "reason", 512); const record: KxmProjectControlRecord = { schema: "kxm.project-control.v1", projectId: context.projectId, paused: input.paused, ...(reason !== undefined ? { reason } : {}), updatedAt: now, actor: `${actor.kind}:${actor.id}`, }; const control: KxmProjectControlRow = { projectId: context.projectId, paused: input.paused, ...(reason !== undefined ? { reason } : {}), updatedAt: now, actor: record.actor, record: kxmCanonicalJson(record as unknown as JsonValue), }; // Control write and the held-intent release are one transaction: a crash leaves // either a paused project with held rows, or a resumed project with none. return context.eventStore.transaction(() => { context.eventStore.putProjectControl(control); const released = input.paused ? [] : releaseHeldIntake(context, now); return { control, released }; }); } /** Whether fresh dispatch is currently blocked for this project. */ export function isKxmProjectPaused(context: KxmRuntimeContext): boolean { return context.eventStore.projectControl(context.projectId)?.paused === true; } /** How many held rows one drain page reads. Bounds a page, not the whole drain. */ const INTAKE_DRAIN_PAGE_ROWS = 500; function releaseHeldIntake(context: KxmRuntimeContext, now: string): KxmIntakeMessage[] { const released: KxmIntakeMessage[] = []; // Drain every held row. A paging loop, not a single capped page: stranding the // 501st message behind a "resume releases held intent" claim is a lie of omission. // // What is still true after that fix: the loop holds one write transaction and // retains every released message, so total work and memory grow with the held // backlog even though each read is bounded. Measured on an in-memory store: 150k // held rows with 64-byte payloads took 4.5 s synchronously and ~95 MiB of heap; // 500k maximum-size payloads would retain ~7.6 GiB before any database overhead, // and an allocation failure will not reliably arrive as `intake_drain_stalled`. // Resume is finite because the write lock keeps ingress out of the loop, so this // is a throughput and memory limit, not a correctness hole. Bounding it is a // pre-condition of M2 sustained traffic, not of this contract — see "Still open". for (;;) { const held = context.eventStore.intakeInStates(context.projectId, ["held_paused"], INTAKE_DRAIN_PAGE_ROWS); if (held.length === 0) return released; let progressed = false; for (const row of held) { const message = JSON.parse(row.record) as KxmIntakeMessage; const next: KxmIntakeMessage = { ...message, dispatch: { state: "ready", updatedAt: now } }; validateIntakeMessage(next, next.messageId); if (context.eventStore.updateIntakeDispatch(row.messageId, "held_paused", { state: "ready", record: kxmCanonicalJson(next as unknown as JsonValue) })) { released.push(next); progressed = true; } } if (!progressed) { // Cannot move anything: fail loudly inside the caller's transaction, which // rolls the resume back, rather than half-resuming and leaving rows held. throw runtimeError("intake_drain_stalled", context.projectId, `held intake did not drain; ${String(held.length)} row(s) still held`); } } } function persistCoordinator(context: KxmRuntimeContext, record: KxmCoordinatorRecord): { coordinator: KxmCoordinatorRecord; created: boolean } { validateCoordinator(record, record.coordinatorId); const inserted = context.eventStore.insertCoordinatorIfAbsent({ coordinatorId: record.coordinatorId, projectId: record.projectId, role: record.role, channel: record.channel, ceilingHash: record.ceilingHash, configRevision: record.configRevision, boundAt: record.boundAt, record: kxmCanonicalJson(record as unknown as JsonValue), }); if (inserted) return { coordinator: record, created: true }; // Lost the race for an empty slot: binding is create-once, so the winner is the // answer whenever it reached the same ceiling. Only a different ceiling is a // conflict worth reporting. The loser must not report that it created anything. const winner = context.eventStore.coordinatorInSlot(record.projectId, record.role, record.channel); const won = winner ? (JSON.parse(winner.record) as KxmCoordinatorRecord) : undefined; if (won && ceilingsMatch(won, record.ceilingHash)) return { coordinator: won, created: false }; throw runtimeError("coordinator_write_lost", record.coordinatorId, "the coordinator slot was claimed by a different ceiling"); } function assertCeilingNotWidened( previous: KxmCoordinatorAuthority, next: KxmCoordinatorAuthority, coordinatorId: string, ): void { if (ACCESS_ORDER[next.repositoryAccess] > ACCESS_ORDER[previous.repositoryAccess]) { throw runtimeError("coordinator_rebind_widens_access", coordinatorId, "a rebind may not widen repository access"); } const had = new Set(previous.effects); const added = next.effects.filter((effect) => !had.has(effect)); if (added.length > 0) { throw runtimeError("coordinator_rebind_widens_effects", coordinatorId, `a rebind may not add effects: ${added.join(", ")}`); } // Tool ceilings are only narrowing-safe if every restriction is checked. An // omitted list or a changed preset can select a broader default, so those moves // are refused outright until their semantics are defined here. if ((previous.tools === undefined) !== (next.tools === undefined)) { throw runtimeError("coordinator_rebind_widens_tools", coordinatorId, "a rebind may not add or remove the tool policy"); } if (previous.tools && next.tools) { const previousTools = previous.tools; const nextTools = next.tools; if (previousTools.preset !== nextTools.preset) { throw runtimeError("coordinator_rebind_changes_preset", coordinatorId, "a rebind may not change the tool preset; its default is not defined here"); } const allowedBefore = new Set(previousTools.allow ?? []); const newlyAllowed = (nextTools.allow ?? []).filter((tool) => !allowedBefore.has(tool)); if (newlyAllowed.length > 0) { throw runtimeError("coordinator_rebind_widens_tools", coordinatorId, `a rebind may not allow new tools: ${newlyAllowed.join(", ")}`); } // Tool evaluation applies no allowlist restriction when the list is empty or // absent (`commands.ts` gates only on a non-empty allow list), so dropping a // populated list is a widening even though every entry it named is gone. if (allowedBefore.size > 0 && (nextTools.allow ?? []).length === 0) { throw runtimeError("coordinator_rebind_clears_allowlist", coordinatorId, "a rebind may not drop or empty a populated allow list; an absent allow list imposes no restriction"); } const deniedBefore = new Set(previousTools.deny ?? []); const undenied = [...deniedBefore].filter((tool) => !(nextTools.deny ?? []).includes(tool)); if (undenied.length > 0) { throw runtimeError("coordinator_rebind_removes_denials", coordinatorId, `a rebind may not lift denials: ${undenied.join(", ")}`); } } } function validateAuthority(authority: KxmCoordinatorAuthority): KxmCoordinatorAuthority { if (!authority || typeof authority !== "object") { throw runtimeError("coordinator_authority_invalid", "authority", "authority must be an object"); } if (!(authority.repositoryAccess in ACCESS_ORDER)) { throw runtimeError("coordinator_authority_invalid", "repositoryAccess", "repositoryAccess must be none, read or write"); } if (!Array.isArray(authority.effects)) { throw runtimeError("coordinator_authority_invalid", "effects", "effects must be an array of identifiers"); } const effects = authority.effects.map((effect) => requireIdentifier(effect, "effects[]")); if (new Set(effects).size !== effects.length) { throw runtimeError("coordinator_authority_invalid", "effects", "effects must not repeat"); } if (effects.length > 64) { throw runtimeError("coordinator_authority_invalid", "effects", "effects may not exceed 64 entries"); } const tools = authority.tools; if (tools !== undefined) { const lists = [tools.allow ?? [], tools.deny ?? []]; for (const list of lists) { for (const tool of list) requireIdentifier(tool, "tools"); if (new Set(list).size !== list.length) { throw runtimeError("coordinator_authority_invalid", "tools", "tool lists must not repeat"); } } if (tools.preset !== undefined) requireIdentifier(tools.preset, "tools.preset"); } // Sets are stored canonically: order and repeats carry no authority, and leaving // them as supplied would let an equivalent ceiling masquerade as a rebind. return normalizeAuthority(authority); } function validateActor<T extends { kind: KxmCoordinatorRecord["boundBy"]["kind"]; id: string }>(actor: T, field: string): T { const kinds: string[] = ["human", "runtime", "hub", "agent", "adapter"]; if (!actor || typeof actor !== "object" || !kinds.includes(actor.kind)) { throw runtimeError("coordinator_actor_invalid", field, "actor kind must be one of " + kinds.join(", ")); } const id = requireText(actor.id, `${field}.id`, 200); if (!ACTOR_ID_RE.test(id)) throw runtimeError("coordinator_actor_invalid", `${field}.id`, "actor id is out of bounds"); return { ...actor, id }; } function requireIdentifier(value: string, field: string): string { const text = requireText(value, field, 64); if (!IDENTIFIER_RE.test(text)) { throw runtimeError("coordinator_identifier_invalid", field, `${text} is not a kebab/snake identifier`); } return text; } function requireText(value: unknown, field: string, max: number): string { if (typeof value !== "string" || value.trim().length === 0) { throw runtimeError("intake_field_invalid", field, `${field} must be a non-empty string`); } const text = value.trim(); if (text.length > max) throw runtimeError("intake_field_invalid", field, `${field} exceeds ${max} characters`); return text; } function requireTimestamp(value: string, field: string): string { if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z$/.test(value)) { throw runtimeError("intake_field_invalid", field, `${field} must be an ISO8601 UTC timestamp`); } if (!Number.isFinite(Date.parse(value))) { throw runtimeError("intake_field_invalid", field, `${field} is not a real timestamp`); } return value; } function stableStringify(value: unknown): string { return JSON.stringify(sortDeep(value)); } function sortDeep(value: unknown): unknown { if (Array.isArray(value)) return value.map(sortDeep); if (value && typeof value === "object") { const out: Record<string, unknown> = {}; for (const key of Object.keys(value as Record<string, unknown>).sort()) { const child = (value as Record<string, unknown>)[key]; if (child !== undefined) out[key] = sortDeep(child); } return out; } return value; }