/** * src/engine/attestation.ts — C4: hash-verified run attestation sidecars. * * Inspired by the pi-background-tasks benchmark (`bg_run_pi_attested` writes * a local attestation sidecar after a successful run; reports/pi-subagents/ * benchmark/deep/pi-background-tasks.md §4.2): every settled delegation run * gets a `pi-subagents.attestation.v1` sidecar at * `/attestations/.json` recording WHAT ran (agent/model/ * exitCode/stopReason/status), WHEN (startedAt/endedAt) and at WHAT cost * (usage) — plus hash-only evidence of the artifacts: `outputHash` (sha-256 * of the full child output) and `sessionHash` (sha-256 of the session file * when present). * * HASH-ONLY INVARIANT (I1): the sidecar NEVER contains a body — no output, * task, prompt, stderr, or escalation text; only sha-256 digests plus scalar * metadata. Like the benchmark, this is LOCAL EVIDENCE, not cryptographic * proof (no signing): it detects post-settle tampering of the output/session * and divergence between the sidecar and the ledger-recorded hashes. * * `verifyAttestation` recomputes digests from caller-provided artifacts and * returns detailed per-check results (name/ok/expected/actual); checks that * cannot be performed (missing artifact) are reported `skipped` and never * flip `valid`. `verifyAttestationSidecar` reads a sidecar ref and returns * `undefined` when absent/unreadable (clean fallback — callers expose * `verified` only when a sidecar exists and NEVER block on verification). * * Zero @earendil-works/* imports; uses only node:fs and node:path. */ import type { ChildResult } from "../core/types.js"; import type { DelegationRunView } from "./runs.js"; /** Sidecar schema marker (versioned; bump on breaking shape changes). */ export declare const ATTESTATION_SCHEMA = "pi-subagents.attestation.v1"; /** Token usage snapshot copied into the sidecar (B4 run totals). */ export type AttestationUsage = ChildResult["usage"]; /** * The attestation sidecar payload. Hash-only: every field is a scalar or a * sha-256 digest — raw bodies are structurally absent (see invariant I1). */ export interface Attestation { schema: typeof ATTESTATION_SCHEMA; runId: string; agent: string; model?: string; exitCode: number; stopReason?: string; /** sha-256 of the FULL child output (undefined when the output was empty). */ outputHash?: string; /** sha-256 of the session file contents when the file exists at settle. */ sessionHash?: string; usage: AttestationUsage; /** ISO timestamps of the run view start/end. */ startedAt: string; endedAt: string; /** Terminal run status at settle (complete/failed/aborted/steered/escalated). */ status: string; } /** One named verification outcome (detailed checks for consumers). */ export interface AttestationCheck { name: string; ok: boolean; expected?: string; actual?: string; /** True when the check could not be performed (missing artifact) — never flips `valid`. */ skipped?: boolean; } /** Artifacts to verify against; each is optional (absent => skipped check). */ export interface VerifyAttestationOptions { /** Raw child output — recomputed to sha-256 and compared to `outputHash`. */ output?: string; /** * Known digest of the child output (e.g. the run-view/ledger `outputHash`) * used when the raw body is unavailable — compared directly. */ outputHash?: string; /** Session file path — contents re-hashed and compared to `sessionHash`. */ sessionFile?: string; } /** Result of `verifyAttestation`: overall validity + per-check detail. */ export interface AttestationVerification { valid: boolean; checks: AttestationCheck[]; } /** Sidecar-level verification result (ref + mismatch names for responses). */ export interface SidecarVerification { ref: string; verified: boolean; mismatches: string[]; checks: AttestationCheck[]; } /** * Build the hash-only attestation for a settled run. `run` supplies the * lifecycle view (id/status/timestamps/sessionPath); `result` supplies the * settled child facts (agent/model/exitCode/stopReason/usage/output). The * session hash is taken from the session file WHEN PRESENT (absent file => * `sessionHash` omitted, not an error). NEVER stores a body. */ export declare function buildAttestation(run: DelegationRunView, result: ChildResult): Attestation; /** * Write the sidecar to `/.json` ATOMICALLY (tmp file + rename in * the same directory) and return the final path. Creates the directory when * missing. Throws on fs errors — callers treat the write as best-effort. */ export declare function writeAttestationSidecar(dir: string, attestation: Attestation): string; /** * Verify an attestation against caller-provided artifacts by RECOMPUTING the * digests and comparing. Checks: * - `schema` — always present; * - `outputHash` — present when attested: raw `output` re-hashed, or the * known `outputHash` digest compared directly; skipped * (never invalid) when neither is supplied; * - `sessionHash` — present when attested: `sessionFile` contents re-hashed; * skipped when no file supplied or it is unreadable. * `valid` is true iff no performed check failed. */ export declare function verifyAttestation(attestation: Attestation, options?: VerifyAttestationOptions): AttestationVerification; /** * Read and parse a sidecar file; undefined when absent/unreadable/invalid * (clean fallback — the caller simply reports no verification). */ export declare function readAttestationSidecar(ref: string): Attestation | undefined; /** * Read + verify a sidecar by ref. Returns undefined when the sidecar is * absent/unreadable (callers expose `verified` only when this returns a * result); otherwise the verification with `mismatches` listing the FAILED * check names. Never throws, never blocks the caller. */ export declare function verifyAttestationSidecar(ref: string, options?: VerifyAttestationOptions): SidecarVerification | undefined;