/** * adjudicateAndAudit — the kernel's complete entry point. * * Sync `adjudicate(envelope, state, policy)` is the load-bearing replay * primitive — pure, deterministic, total. Property tests and the replay * harness depend on those properties. Adding ledger I/O or sink emission * directly to it would break determinism. * * This sibling wraps `adjudicate()` with the four side-effecting concerns * a production path actually needs: * * 1. Ledger consult — if the intentHash already executed, swap the * Decision for a `REPLAY_SUPPRESSED` REFUSE so the executor cannot * double-fire side effects. * 2. MetricsSink — record the decision/refusal so dashboards see traffic. * 3. LearningSink — emit the LearningEvent so the analytics pipeline * catches drift. * 4. AuditSink — write the durable AuditRecord. This is the governance * record of truth; emission is no longer the adopter's optional step. * * Plus the EXECUTE-race fix (T5/#37): after a sync adjudicate() returns * EXECUTE, the wrapper calls `ledger.recordExecution()` and flips the * Decision to REPLAY_SUPPRESSED if the write did not claim the key (i.e., * another caller already EXECUTEd this intentHash). Sequenced so two * parallel callers cannot both side-effect. * * Sink emission throws on failure — adopters who want fail-open audit * compose `multiSinkLossy` from `@adjudicate/audit` themselves. */ import { type AuditPlanSnapshot, type AuditRecord, type AuditSigner, type BudgetGrant, type Supersession } from "../audit.js"; import { type Decision } from "../decision.js"; import { type IntentEnvelope, type RecordedAggregateSnapshot } from "../envelope.js"; import { type Ledger, type LedgerHit } from "../ledger.js"; import { type AuditSink } from "../sink.js"; import type { PolicyBundle } from "./policy.js"; import type { RuntimeContext } from "./runtime-context.js"; /** * 071 — a single bound binding field. `confirmed` is the value the confirmation * was actually resolved with (always recorded forensically); `requested` is the * OPTIONAL value the original REQUEST_CONFIRMATION was issued against. When * `requested` is present the override requires `requested === confirmed`. */ export interface ConfirmationBindingField { /** The value the confirmation was resolved with (the bound, recorded value). */ readonly confirmed: string; /** * Optional: the value the REQUEST_CONFIRMATION was issued against (from the * already-verified pending request). When present, the override requires it to * equal `confirmed`; a mismatch falls through to the original verdict. */ readonly requested?: string; } /** * 071 — the bound (capability, approver, channel) tuple a post-confirmation * EXECUTE is provably tied to. Each field is optional and independently gated: * a caller that can supply only some of the tuple still binds those. */ export interface ConfirmationBinding { /** The capability/grant the confirmation authorizes (opaque to the kernel). */ readonly capability?: ConfirmationBindingField; /** The approver identity who confirmed (distinct from the proposer). */ readonly approver?: ConfirmationBindingField; /** The channel the confirmation arrived on (Slack/email/console/...). */ readonly channel?: ConfirmationBindingField; } /** * 071 — pure equality gate over a `ConfirmationBinding`. Returns `true` when * EVERY present field whose `requested` value is supplied equals its `confirmed` * value (fail-closed: any single mismatch returns `false`). A field with no * `requested` value is forensically recorded but not gated (the caller could not * supply the issued-against value). An absent `binding` is vacuously satisfied — * that is the unchanged back-compat path. * * Pure: no I/O, no clock. Used INSIDE the override predicate so a mismatch on any * bound field defaults to friction (the original REQUEST_CONFIRMATION), never a * bypass (§D-6). */ export declare function confirmationBindingMatches(binding: ConfirmationBinding | undefined): boolean; /** * 071 — project a `ConfirmationBinding` onto the forensic `Supersession.binding` * carrier: the BOUND (confirmed) values only, keys omitted when unsupplied so an * omitted binding leaves the key off the supersession entirely (byte-identical * supersedes / auditHash for non-binding callers, §D-5). Returns `undefined` when * no field carries a confirmed value, so the supersession spread is a no-op. * * Pure. Records the confirmed value (what the EXECUTE is bound TO), not the * issued-against `requested` value (which is a gate input, not the recorded fact). */ export declare function confirmationBindingRecord(binding: ConfirmationBinding | undefined): { capability?: string; approver?: string; channel?: string; } | undefined; export interface AdjudicateAndAuditClock { nowIso(): string; nowMs(): number; } export interface AdjudicateAndAuditDeps { /** * Audit sink. Required — kernel-side audit emission is the load-bearing * change of T1. Adopters compose `multiSink` / `bufferedSink` from * `@adjudicate/audit` to control fail-open vs fail-closed semantics. */ readonly sink: AuditSink; /** * Optional, v5+ (ADR-124). Synchronous post-decision metadata provider. Runs * after `buildAuditRecord` and before `sink.emit`, on BOTH the kill-switch and * main paths. Returns governance/observability metadata (e.g. a hallucination * score) merged onto the record's `metadata` field. MUST NOT throw (wrapped); * MUST NOT affect the Decision (already computed) or any hashed field — * `metadata` is excluded from the auditHash pre-image. */ readonly metadataProvider?: (record: AuditRecord) => Readonly> | undefined; /** * Optional (092) impure-shell audit signer. When supplied, BOTH the * kill-switch and main `buildAuditRecord` call sites attach a real * `signature` over the record's `auditHash` (the kernel `adjudicate()` * stays pure — signing happens in the shell AFTER the decision, §D). The * signature is EXCLUDED from the `auditHash` pre-image, so signing never * invalidates tamper-evidence. * * FAIL-CLOSED (§D inv. 6): a signer that throws propagates out of * `buildAuditRecord` and aborts this call BEFORE `sink.emit` — no unsigned * record is ever emitted when a signer was configured. H16/H15: `record` is * built INSIDE the audit-emit `try` on BOTH the main and kill-switch paths, so * a synchronous signer throw lands in the cleanup tail: the catch's ledger * release fires for a claimed EXECUTE key (no orphaned dedup key suppressing * legitimate retries for the full TTL) and the finally's rate-limit rollback * fires for a non-EXECUTE decision (no poisoned counter). Friction, never * bypass. Omitting the signer keeps records unsigned (a valid, * tamper-evident-only OSS record). */ readonly signer?: AuditSigner; /** * Optional Execution Ledger. When supplied: * - `checkLedger` runs before adjudication; a hit short-circuits the * Decision to REPLAY_SUPPRESSED and skips guard evaluation. * - On EXECUTE, `recordExecution` claims the key. If the SET-NX * returns "exists" (another writer was first), the Decision is * flipped to REPLAY_SUPPRESSED so side effects cannot double-fire. */ readonly ledger?: Ledger; /** Override wall clock for tests. */ readonly clock?: AdjudicateAndAuditClock; /** * Optional resolver for the post-execute resourceVersion. When provided, * the resulting AuditRecord carries `resourceVersion` (e.g., the row * version of the mutated entity) and the ledger record uses it. */ readonly resolveResourceVersion?: (envelope: IntentEnvelope, state: unknown) => string | undefined; /** * Optional plan snapshot accessor. When provided and not undefined, the * AuditRecord v2 `plan` field is populated and `planFingerprint` is * cross-correlated to the LearningEvent. */ readonly plan?: () => Omit | undefined; /** * Optional tenant RuntimeContext. When supplied, metrics + learning * events route through the context's slots; when omitted, they go to * the module-level default singletons (back-compat). The context's * kill switch is consulted ahead of the kernel kill-switch — both * gates apply, so a tenant can revoke authority without flipping the * process-wide default. */ readonly context?: RuntimeContext; /** * Optional (091) policy version snapshot. An immutable, impure-shell-supplied * snapshot of the signed policy/Pack version the kernel decided under. The * pure decision does NOT derive it; the shell injects it (per §D: the kernel * decides, the shell supplies recorded inputs). When supplied, BOTH the * kill-switch and main `buildAuditRecord` call sites thread it onto the * emitted record's `policyVersion`, making the policy identity part of the * tamper-evident, replayable audit record (it IS in the auditHash pre-image). * When omitted, the field is conditionally spread OUT — no `undefined` key, * so adopters that do not inject it keep byte-identical, hash-stable records. */ readonly policyVersion?: string; /** * Optional (091) kernel version snapshot. An immutable, impure-shell-supplied * snapshot of the @adjudicate/core kernel version that produced the decision * (distinct from `context.kernelIdentity.version`, which identifies the kernel * BUILD). Threaded into BOTH `buildAuditRecord` call sites and bound into the * auditHash pre-image like `policyVersion`; omission spreads it out (no * `undefined` key, hash-stable for non-injecting adopters). */ readonly kernelVersion?: string; /** * Optional (052) RECORDED aggregate/limit snapshot. An immutable, * impure-shell-supplied snapshot of the cumulative/velocity counters the * decision was made against (the per-window committed aggregates + the sample * `at`), paired with its content-address (`recordAggregateSnapshot`). The pure * kernel does NOT compute or refetch it; the shell injects it READ-ONLY (per * §D: the kernel decides, the shell supplies recorded inputs) by reading the * durable counting substrate this plan OWNS (`GuardFireStats` + the additive * Postgres upsert). When supplied, BOTH the kill-switch and main * `buildAuditRecord` call sites thread it onto the emitted record's * `aggregateSnapshot`, binding it into the tamper-evident, REPLAYABLE auditHash * pre-image so re-running the pure kernel over the recorded snapshot reproduces * the SAME decision (§D-5, invariant #5). When omitted, the field is * conditionally spread OUT — no `undefined` key, so adopters that do not inject * it keep byte-identical, hash-stable records. NEVER read by `adjudicate()`; * NEVER enters `intentHash` (invariant #4) — the shell never mutates/refetches/ * timestamps it, exactly like the read-only `state` discipline at `:412,:468`. */ readonly aggregateSnapshot?: RecordedAggregateSnapshot; /** * T5 (#41 / top-priority E): rate-limit rollback handle. When the * kernel returns a non-EXECUTE Decision (REFUSE/ESCALATE/DEFER/ * REQUEST_CONFIRMATION/REWRITE-equivalent), the rollback fires so the * rate-limit counter does not advance for unauthorized requests. * Adopters obtain this from `checkRateLimit()`; passing it through * is the recommended pattern when both rate limiting and audit * emission live on the same path. */ readonly rateLimitRollback?: () => Promise; /** * Receipt that the user already affirmatively confirmed this envelope * via a prior REQUEST_CONFIRMATION cycle. When supplied AND the * receipt's `intentHash` matches `envelope.intentHash` AND the kernel * returns `REQUEST_CONFIRMATION`, the kernel substitutes `EXECUTE` * with an appended `confirmation:received` basis recording the * override. State guards, taint guards, and auth guards are still * evaluated in full — only the threshold-style "ask the user first" * step is satisfied. Other Decisions (REFUSE/REWRITE/ESCALATE/DEFER) * are returned unchanged: a state change between request and * confirmation that flipped the answer is correctly surfaced. * * Callers (typically the adapter's `confirm()` flow after taking the * single-use confirmation token) own the integrity of the receipt — * the kernel trusts that the receipt represents an actual user * affirmation. Adopters wiring this directly should ensure the * receipt cannot be forged from untrusted inputs. */ readonly confirmationReceipt?: { readonly intentHash: string; /** ISO-8601 wall-clock of the user's confirmation. */ readonly at: string; /** * Optional (LogicReviewer-004): the `at` timestamp of the original * REQUEST_CONFIRMATION audit row. When provided, stored as * `supersedes.predecessorAt` so audit-chain queries can JOIN on * (predecessorIntentHash, predecessorAt) to locate the predecessor row. * When omitted, falls back to `at` (pre-existing behaviour — use only * when the predecessor row's `at` is unavailable to the caller). * * STRICTLY ADDITIVE: a caller that omits `originalAt` produces a * byte-identical `supersedes` (and therefore identical auditHash) as * before this field existed. */ readonly originalAt?: string; /** * Optional (AuthReviewer-005): opaque single-use token from the * confirmation store. When supplied, the kernel writes it into * `Supersession.token` of the auto-derived `confirmation_resolved` * supersedes link, providing a forensic trail that the confirmation * came from a real token-exchange flow rather than a bare hash * assertion. The kernel does NOT verify the token — that is the * adapter's responsibility (the adapter calls * `confirmationStore.take(token)` before passing this receipt). * * STRICTLY ADDITIVE: a caller that omits `token` produces a * byte-identical `supersedes` (and therefore identical auditHash) as * before this field existed. */ readonly token?: string; /** * Optional (071): the bound (capability, approver, channel) tuple the * post-confirmation EXECUTE is provably tied to. `intentHash` (above) * stays the LOAD-BEARING identity gate; this tuple is an ADDITIONAL, * fail-closed gate that the override consults ONLY when present. * * Because `capability` and `channel` are NOT envelope fields and the * approver is NEVER in `intentHashInput` (`envelope.ts` — invariant #4 is * untouched), the binding values cannot be re-derived from the envelope: * they TRAVEL on the receipt. Each field is a pair: * - `confirmed` — the value the confirmation was actually resolved with * (the approver who confirmed, the channel it arrived on, the * capability presented). ALWAYS the forensically recorded value. * - `requested?` — OPTIONAL: the value the original REQUEST_CONFIRMATION * was issued AGAINST (from the already-verified pending request). When * supplied, the override additionally REQUIRES `requested === confirmed` * for that field; a mismatch on ANY present field falls through to the * original REQUEST_CONFIRMATION verdict (fail-closed, §D-6) — never a * bypass. * * The kernel does NOT verify the capability/token (the adapter's * `confirmationStore.take()` + timing-safe hash compare owns single-use / * tamper defense, `loop.ts`); these fields participate ONLY in the equality * gate and the forensic audit trail. * * STRICTLY ADDITIVE: a caller that omits `binding` (or any sub-field) * produces a byte-identical `supersedes` (and therefore identical * auditHash) as before this field existed — the keys are conditionally * spread off the supersession entirely when unsupplied (§D-5). */ readonly binding?: ConfirmationBinding; }; /** * Budget grant (025 — capabilities-as-budgets). A human-granted, BOUNDED, * STANDING pre-authorization the impure shell asserts so a CLASS of intents * can satisfy the "ask first" threshold up to a declared limit WITHOUT a * per-intent confirmation receipt. When supplied AND the grant's `intentKind` * matches `envelope.kind` AND the kernel returns `REQUEST_CONFIRMATION`, the * kernel substitutes `EXECUTE` with an appended `budget:satisfied` basis (and * auto-derives a `budget_satisfied` supersession), EXACTLY mirroring the * `confirmationReceipt` override above. State guards, taint guards, and auth * guards are still evaluated in full — only the threshold-style "ask the user * first" step is satisfied. Other Decisions (REFUSE/REWRITE/ESCALATE/DEFER/ * EXECUTE) are returned UNCHANGED (monotonicity-preserving, §C; closed * 6-outcome algebra, §D #2 — no new kind, no confidence/metadata). * * The kernel does NOT verify or count the grant — the shell * (`adapter-core/decisions.ts` + `loop.ts`) owns burn-down integrity and only * asserts a grant AFTER a SUCCESSFUL atomic decrement against `limit` (the * `evalIncrCheck` Lua primitive). Over-limit ⇒ no grant asserted ⇒ the kernel * returns the original `REQUEST_CONFIRMATION` (fail-closed to friction, §C). * Adopters wiring this directly MUST ensure the grant cannot be forged from * untrusted inputs and that the decrement preceded the assertion. * * `originalAt` (LogicReviewer, 025): the `at` timestamp of the ORIGINAL * REQUEST_CONFIRMATION audit row this budget substitution supersedes. The * predecessor row is emitted by a SEPARATE, earlier `adjudicateAndAudit` call * (the shell's first pass) at an earlier wall clock; this budget-satisfied * EXECUTE is a SECOND call at a LATER wall clock. So `predecessorAt` MUST be * the predecessor's `at`, NOT this call's `clock.nowIso()` (which equals this * EXECUTE row's OWN `at`). When provided, it is stored as * `supersedes.predecessorAt` so `buildSupersessionChains` (@adjudicate/audit) * can JOIN on (predecessorIntentHash, predecessorAt) and disambiguate the two * records that share the envelope's intentHash — mirroring * `confirmationReceipt.originalAt`. When omitted, falls back to * `clock.nowIso()` (legacy behaviour — use only when the predecessor row's * `at` is unavailable to the caller; the chain walker then cannot disambiguate * and may report the pair as a false cycle/singleton). * * STRICTLY ADDITIVE: this lives on the kernel deps slot, NOT on the recorded * `BudgetGrant` data contract (it is a per-substitution timing detail, not * standing-grant identity), so the basis pre-image is unchanged. */ readonly budgetGrant?: BudgetGrant & { readonly originalAt?: string; }; /** * Optional explicit supersession link (AuditRecord v3). When supplied, * the produced AuditRecord carries this value under `supersedes`. Use this * to attach `defer_resumed`, `rewrite_executed`, or `replay` links — for * `confirmation_resolved`, the kernel auto-derives `supersedes` from * `confirmationReceipt` when this field is not set. */ readonly supersedes?: Supersession; } export interface AdjudicateAndAuditResult { readonly decision: Decision; readonly record: AuditRecord; /** Non-null when an existing ledger entry suppressed re-execution. */ readonly ledgerHit: LedgerHit | null; } /** * Run adjudicate() with ledger + metrics + learning + audit emission. * * Decision flow: * 1. ledger.checkLedger — if hit, build REPLAY_SUPPRESSED REFUSE. * 2. otherwise, sync adjudicate() returns the kernel Decision. * 3. if Decision is EXECUTE, ledger.recordExecution claims the key; * if claim fails ("exists"), flip to REPLAY_SUPPRESSED. * 4. emit MetricsSink + LearningSink events for the final Decision. * 5. build AuditRecord and call sink.emit (throws on failure). * * Sink failures propagate to the caller — adopters compose lossy sinks * upstream if fail-open is desired for non-critical paths. * * Minimum required wiring (APIReviewer-020): `{ sink }` alone is sufficient — * every other dep is optional. With only a sink the kernel adjudicates, emits * the AuditRecord, and routes metrics/learning to the module-level defaults. * `ledger` adds dedup/REPLAY_SUPPRESSED; `context` (RuntimeContext) routes * metrics/learning/kill-switch per tenant; `rateLimitRollback`, * `resolveResourceVersion`, `plan`, `supersedes`, `kernelIdentity`, `clock`, * and the `policyVersion`/`kernelVersion` snapshots (091) are all opt-in. */ export declare function adjudicateAndAudit(envelope: IntentEnvelope, state: S, policy: PolicyBundle, deps: AdjudicateAndAuditDeps): Promise; //# sourceMappingURL=adjudicate-and-audit.d.ts.map