@veritasacta/verify
Advanced tools
| { | ||
| "type": "scopeblind.agent_vault.receipt.v1", | ||
| "id": "restraint-850d4d9a240d1390", | ||
| "tool": "gate.restrain", | ||
| "decision": "DENY", | ||
| "input_sha256": "9575d64a26f53163614e26c4709831cfd0688f537f4cb1e035546bb13562983c", | ||
| "result_sha256": "0a2210cc9d2c2cdf08489dff0f2465ca6c6b36c4ec31d79de77dd71fbf9cd518", | ||
| "at": "2026-06-21T12:00:00.000Z", | ||
| "signature": "6e8cbe47c16ab29064c197379b55cde79fbb1a4f90dfeb979f6d11f4880ec98e086dc8e9ce5abd58caf0f42b0b0e6adb173d2620b60e9f2ed90688d418d2e803", | ||
| "verification_key": "409cb9511071b8687f7884cd9cde813d7a0907bef16bd68a80a8b0bac045cd34", | ||
| "restraint": { | ||
| "salt": "sample-restraint-salt-000000000000", | ||
| "proposed": { | ||
| "symbol": "TSLA", | ||
| "side": "buy", | ||
| "qty": 5000, | ||
| "notional": 2000000 | ||
| }, | ||
| "outcome": { | ||
| "determining": [ | ||
| "manifest:forbidden-instrument", | ||
| "concentration-single-name", | ||
| "gross-leverage", | ||
| "sector-concentration", | ||
| "cash-floor" | ||
| ], | ||
| "risk_band": "authority", | ||
| "mandate_digest": "1b92a72197028340", | ||
| "layers": { | ||
| "manifest": "deny", | ||
| "mandate": "deny" | ||
| } | ||
| } | ||
| } | ||
| } |
| /** | ||
| * @veritasacta/verify — Re-executable Policy Decision Receipt engine (AIP-0010). | ||
| * | ||
| * Verifies an authorization decision by RE-EXECUTING the policy engine over the | ||
| * policy and inputs carried in the receipt and comparing the result to the | ||
| * recorded decision. Correctness of the decision therefore rests on re-execution | ||
| * of open, deterministic code, not on trusting the party that produced the | ||
| * receipt. See specs/aip/AIP-0010-reexecutable-policy-decision-receipts.md. | ||
| * | ||
| * The bundled evaluator implements a faithful SUBSET of Cedar's evaluation | ||
| * semantics (default-deny, forbid-overrides-permit, scope plus when/unless | ||
| * conditions over context and entity attributes), sufficient to exercise and | ||
| * demonstrate the re-execution mechanism. Production verifiers pass the official | ||
| * Cedar engine identified by `receipt.engine` via the `engine` option. | ||
| * | ||
| * @module verify-cli/src/engines/policy-decision-reexec | ||
| * @license Apache-2.0 | ||
| */ | ||
| import { canonicalize, sha256Hex } from "../util/canonical.js"; | ||
| const RECEIPT_TYPE = "scopeblind.policy_decision.v1"; | ||
| /** Commitment helper: "sha256:" + hex(JCS(obj)). */ | ||
| export function commit(obj) { | ||
| return `sha256:${sha256Hex(canonicalize(obj))}`; | ||
| } | ||
| // ───────────────────────── faithful Cedar subset evaluator ───────────────────────── | ||
| /** | ||
| * Evaluate a Cedar-subset policy set over a request and entity store. | ||
| * | ||
| * policySet: { policies: [ { id, effect: "permit"|"forbid", | ||
| * principal?: scope, action?: scope, resource?: scope, | ||
| * conditions?: [ { kind: "when"|"unless", expr } ] } ] } | ||
| * scope: { op: "All" } | { op: "==", entity } | { op: "in", entity } | ||
| * expr: { op: "==|!=|<|<=|>|>=", left: ref, right: ref } | ||
| * | { op: "and"|"or", clauses: [expr...] } | { op: "not", clause: expr } | ||
| * ref: { attr: "context.x" | "resource.x" | "principal.x" } | { value: literal } | ||
| * | ||
| * Returns { outcome: "permit"|"deny", determining_policies: string[], errors: [] }. | ||
| * Semantics: default deny; a request is permitted iff at least one permit policy | ||
| * is satisfied and no forbid policy is satisfied (forbid overrides). | ||
| */ | ||
| export function evaluateCedarSubset(policySet, request, entities) { | ||
| const errors = []; | ||
| const entityMap = new Map((entities || []).map((e) => [e.uid, e])); | ||
| const policies = (policySet && policySet.policies) || []; | ||
| const satisfiedForbid = []; | ||
| const satisfiedPermit = []; | ||
| for (const p of policies) { | ||
| let sat; | ||
| try { | ||
| sat = policySatisfied(p, request, entityMap); | ||
| } catch (e) { | ||
| errors.push({ policy: p.id, error: String(e.message || e) }); | ||
| sat = false; // a policy that errors is not satisfied (Cedar skips erroring policies) | ||
| } | ||
| if (!sat) continue; | ||
| if (p.effect === "forbid") satisfiedForbid.push(p.id); | ||
| else if (p.effect === "permit") satisfiedPermit.push(p.id); | ||
| } | ||
| if (satisfiedForbid.length > 0) { | ||
| return { outcome: "deny", determining_policies: satisfiedForbid.sort(), errors }; | ||
| } | ||
| if (satisfiedPermit.length > 0) { | ||
| return { outcome: "permit", determining_policies: satisfiedPermit.sort(), errors }; | ||
| } | ||
| return { outcome: "deny", determining_policies: [], errors }; | ||
| } | ||
| function policySatisfied(policy, request, entityMap) { | ||
| if (!scopeSatisfied(policy.principal, request.principal, entityMap)) return false; | ||
| if (!scopeSatisfied(policy.action, request.action, entityMap)) return false; | ||
| if (!scopeSatisfied(policy.resource, request.resource, entityMap)) return false; | ||
| for (const cond of policy.conditions || []) { | ||
| const value = Boolean(evalExpr(cond.expr, request, entityMap)); | ||
| if (cond.kind === "when" && !value) return false; | ||
| if (cond.kind === "unless" && value) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function scopeSatisfied(scope, uid, entityMap) { | ||
| if (!scope || scope.op === "All") return true; | ||
| if (scope.op === "==") return uid === scope.entity; | ||
| if (scope.op === "in") return uid === scope.entity || isDescendantOf(uid, scope.entity, entityMap); | ||
| throw new Error(`unsupported_scope_op:${scope.op}`); | ||
| } | ||
| function isDescendantOf(uid, ancestor, entityMap, seen = new Set()) { | ||
| if (seen.has(uid)) return false; | ||
| seen.add(uid); | ||
| const ent = entityMap.get(uid); | ||
| const parents = (ent && ent.parents) || []; | ||
| if (parents.includes(ancestor)) return true; | ||
| return parents.some((p) => isDescendantOf(p, ancestor, entityMap, seen)); | ||
| } | ||
| function evalExpr(expr, request, entityMap) { | ||
| if (!expr || typeof expr !== "object") throw new Error("bad_expr"); | ||
| switch (expr.op) { | ||
| case "and": return (expr.clauses || []).every((c) => Boolean(evalExpr(c, request, entityMap))); | ||
| case "or": return (expr.clauses || []).some((c) => Boolean(evalExpr(c, request, entityMap))); | ||
| case "not": return !evalExpr(expr.clause, request, entityMap); | ||
| case "==": case "!=": case "<": case "<=": case ">": case ">=": { | ||
| const l = resolveRef(expr.left, request, entityMap); | ||
| const r = resolveRef(expr.right, request, entityMap); | ||
| return compare(expr.op, l, r); | ||
| } | ||
| default: throw new Error(`unsupported_op:${expr.op}`); | ||
| } | ||
| } | ||
| function compare(op, l, r) { | ||
| switch (op) { | ||
| case "==": return l === r; | ||
| case "!=": return l !== r; | ||
| case "<": return l < r; | ||
| case "<=": return l <= r; | ||
| case ">": return l > r; | ||
| case ">=": return l >= r; | ||
| default: return false; | ||
| } | ||
| } | ||
| function resolveRef(ref, request, entityMap) { | ||
| if (ref == null || typeof ref !== "object") throw new Error("bad_ref"); | ||
| if ("value" in ref) return ref.value; | ||
| if ("attr" in ref) { | ||
| const [root, ...path] = String(ref.attr).split("."); | ||
| let base; | ||
| if (root === "context") base = request.context || {}; | ||
| else if (root === "principal") base = attrsOf(request.principal, entityMap); | ||
| else if (root === "resource") base = attrsOf(request.resource, entityMap); | ||
| else if (root === "action") base = attrsOf(request.action, entityMap); | ||
| else throw new Error(`unknown_ref_root:${root}`); | ||
| let cur = base; | ||
| for (const key of path) { | ||
| if (cur == null) return undefined; | ||
| cur = cur[key]; | ||
| } | ||
| return cur; | ||
| } | ||
| throw new Error("bad_ref"); | ||
| } | ||
| function attrsOf(uid, entityMap) { | ||
| const e = entityMap.get(uid); | ||
| return (e && e.attrs) || {}; | ||
| } | ||
| // ───────────────────────── the verifier ───────────────────────── | ||
| /** | ||
| * Verify a Re-executable Policy Decision Receipt. | ||
| * | ||
| * @param {object} receipt | ||
| * @param {object} [opts] | ||
| * @param {function} [opts.engine] policy engine (default: bundled Cedar subset) | ||
| * @param {function} [opts.verifySignature] (receipt) => boolean, authenticity check | ||
| * @param {string} [opts.expectedPolicyDigest] catalog/transparency-pinned digest | ||
| * @param {{name:string,version:string,digest?:string}} [opts.engineInfo] local engine identity | ||
| * @param {boolean} [opts.allowCommitted=true] treat committed-mode as authenticity-only valid | ||
| * @returns {{ valid:boolean, reason:string, checks:object, claims:object, recomputed?:object }} | ||
| */ | ||
| export function verifyPolicyDecisionReceipt(receipt, opts = {}) { | ||
| const engine = opts.engine || evaluateCedarSubset; | ||
| const checks = {}; | ||
| const claims = {}; | ||
| const done = (valid, reason, extra = {}) => ({ valid, reason, checks, claims, ...extra }); | ||
| if (!receipt || receipt.type !== RECEIPT_TYPE) return done(false, "wrong_type"); | ||
| if (!receipt.policy || !receipt.decision || !receipt.engine) return done(false, "missing_fields"); | ||
| // 1. Authenticity (optional, independent of correctness). | ||
| if (opts.verifySignature && receipt.signature) { | ||
| const ok = Boolean(opts.verifySignature(receipt)); | ||
| checks.authenticity = ok ? "verified" : "failed"; | ||
| if (!ok) return done(false, "signature_invalid"); | ||
| claims.authenticity = "signature"; | ||
| } else { | ||
| checks.authenticity = "skipped"; | ||
| } | ||
| // 2. Policy integrity (digest of inline policy, plus optional catalog pin). | ||
| if (receipt.policy.inline !== undefined) { | ||
| const digest = commit(receipt.policy.inline); | ||
| if (receipt.policy.digest && digest !== receipt.policy.digest) { | ||
| checks.policy_integrity = "mismatch"; | ||
| return done(false, "policy_digest_mismatch"); | ||
| } | ||
| checks.policy_integrity = "verified"; | ||
| } else { | ||
| checks.policy_integrity = "unresolved"; // policy carried by ref; caller must resolve+hash | ||
| } | ||
| if (opts.expectedPolicyDigest && receipt.policy.digest !== opts.expectedPolicyDigest) { | ||
| checks.policy_identity = "substituted"; | ||
| return done(false, "policy_substitution"); | ||
| } | ||
| // 3. Engine pin. | ||
| if (opts.engineInfo) { | ||
| const e = receipt.engine; | ||
| const pinned = opts.engineInfo.name === e.name && opts.engineInfo.version === e.version | ||
| && (!e.digest || !opts.engineInfo.digest || e.digest === opts.engineInfo.digest); | ||
| if (!pinned) { | ||
| checks.engine = "unavailable"; | ||
| claims.correctness = "engine_unavailable"; | ||
| return done(false, "engine_unavailable"); | ||
| } | ||
| checks.engine = "pinned"; | ||
| } | ||
| // 4. Committed mode: inputs withheld -> correctness CANNOT be checked here. | ||
| if (receipt.disclosure === "committed") { | ||
| checks.input_integrity = "committed"; | ||
| checks.correctness = "not_checked_inputs_hidden"; | ||
| claims.correctness = "not_checked_inputs_hidden"; | ||
| const valid = opts.allowCommitted !== false; // authenticity-only validity | ||
| return done(valid, "inputs_committed_authenticity_only", { authenticityOnly: true }); | ||
| } | ||
| // 5. Input integrity: the disclosed inputs must match the commitment. | ||
| const recomputedCommit = commit({ request: receipt.request, entities: receipt.entities }); | ||
| if (receipt.input_commitment && recomputedCommit !== receipt.input_commitment) { | ||
| checks.input_integrity = "mismatch"; | ||
| return done(false, "input_commitment_mismatch"); | ||
| } | ||
| checks.input_integrity = "verified"; | ||
| // 6. RE-EXECUTION (the core): recompute the decision and compare. | ||
| let recomputed; | ||
| try { | ||
| recomputed = engine(receipt.policy.inline, receipt.request, receipt.entities); | ||
| } catch (e) { | ||
| return done(false, `engine_error:${e.message || e}`); | ||
| } | ||
| const outcomeMatch = normOutcome(recomputed.outcome) === normOutcome(receipt.decision.outcome); | ||
| const detMatch = sameSet(recomputed.determining_policies, receipt.decision.determining_policies); | ||
| if (!outcomeMatch || !detMatch) { | ||
| checks.correctness = "decision_mismatch"; | ||
| return done(false, "decision_mismatch", { recomputed }); | ||
| } | ||
| checks.correctness = "verified_by_reexecution"; | ||
| claims.correctness = "re-execution"; | ||
| return done(true, "valid", { recomputed }); | ||
| } | ||
| function normOutcome(o) { | ||
| return o === "forbid" ? "deny" : o; // treat forbid/deny as the same negative outcome | ||
| } | ||
| function sameSet(a, b) { | ||
| const sa = [...new Set(a || [])].sort(); | ||
| const sb = [...new Set(b || [])].sort(); | ||
| return sa.length === sb.length && sa.every((x, i) => x === sb[i]); | ||
| } |
| /** | ||
| * EAT output mode (packaging sprint A4). | ||
| * | ||
| * Re-serializes a verified Veritas Acta / Legate receipt as an RFC 9711 EAT | ||
| * (Entity Attestation Token) claims-set, so a Legate receipt is ALSO a valid | ||
| * EAT/TRACE record that EAT-aware tooling can ingest. Two serializations: | ||
| * - JSON: CWT/EAT claim NAMES (RFC 8392 JSON mapping), human-readable. | ||
| * - CBOR: CWT claim KEYS (integers), the canonical EAT/CWT wire form. | ||
| * | ||
| * Honest boundary: this maps and re-serializes an already-Ed25519-signed | ||
| * receipt; it does NOT mint a fresh COSE_Sign1 (that requires the issuer key, | ||
| * which the offline verifier does not hold). Authenticity round-trips to the | ||
| * embedded Acta receipt and its signature, which the verifier already checked. | ||
| * A hardware attestation quote, if the receipt carries one, is passed through | ||
| * verbatim as the EAT `submods`/`tee_evidence` claim (carry, do not appraise). | ||
| */ | ||
| // ── CWT / EAT claim keys (IANA CWT registry; RFC 8392 + RFC 9711) ──────────── | ||
| const CWT_ISS = 1; | ||
| const CWT_SUB = 2; | ||
| const CWT_IAT = 6; | ||
| const CWT_CTI = 7; | ||
| const EAT_NONCE = 10; | ||
| const EAT_PROFILE = 265; | ||
| const EAT_MEASUREMENTS = 273; | ||
| // Private-use claim keys (negative ints are reserved for private use, RFC 8392). | ||
| const PRIV_ACTA_RECEIPT = -70000; // the full original Acta receipt (canonical JSON) | ||
| const PRIV_VERIFICATION = -70001; // the offline verification summary | ||
| const PRIV_TEE_EVIDENCE = -70002; // a carried hardware attestation quote, verbatim | ||
| const EAT_PROFILE_URI = 'https://veritasacta.com/eat-profile/legate-receipt/v1'; | ||
| // Governed/gate receipts nest their action fields under `payload`; look there too. | ||
| function f(receipt, name) { | ||
| const v = receipt[name]; | ||
| if (v !== undefined && v !== null) return v; | ||
| return receipt.payload ? receipt.payload[name] : undefined; | ||
| } | ||
| function iatFrom(receipt) { | ||
| const s = f(receipt, 'signed_at') || f(receipt, 'at') || f(receipt, 'timestamp') || f(receipt, 'issued_at') || f(receipt, 'created_at'); | ||
| if (!s) return null; | ||
| const t = Date.parse(s); | ||
| return Number.isNaN(t) ? null : Math.floor(t / 1000); | ||
| } | ||
| function subjectFrom(receipt) { | ||
| return f(receipt, 'tool') || f(receipt, 'action') || f(receipt, 'action_id') || f(receipt, 'summary') || f(receipt, 'id') || null; | ||
| } | ||
| function ctiFrom(receipt) { | ||
| return f(receipt, 'id') || f(receipt, 'receipt_id') || f(receipt, 'cti') || null; | ||
| } | ||
| function issuerFrom(receipt) { | ||
| return receipt.verification_key || f(receipt, 'signer') || f(receipt, 'issuer') || null; | ||
| } | ||
| function teeEvidenceFrom(receipt) { | ||
| return f(receipt, 'tee_evidence') || f(receipt, 'attestation') || f(receipt, 'attestation_quote') || null; | ||
| } | ||
| function measurementsFrom(receipt) { | ||
| const m = {}; | ||
| const inp = f(receipt, 'input_sha256'); | ||
| const res = f(receipt, 'result_sha256'); | ||
| const pol = f(receipt, 'policy_sha256'); | ||
| const cfr = f(receipt, 'committed_fields_root'); | ||
| if (inp) m.input_sha256 = inp; | ||
| if (res) m.result_sha256 = res; | ||
| if (pol) m.policy_sha256 = pol; | ||
| if (cfr) m.committed_fields_root = cfr; | ||
| return Object.keys(m).length ? m : null; | ||
| } | ||
| function verificationSummary(result) { | ||
| return { | ||
| valid: result?.valid === true, | ||
| format: result?.format ?? result?.modeLabel ?? null, | ||
| signer: result?.signer ?? result?.publicKey ?? null, | ||
| error: result?.error ?? null, | ||
| }; | ||
| } | ||
| /** Build the EAT claims-set with JSON claim NAMES. */ | ||
| export function toEatClaims(receipt, result) { | ||
| const claims = { eat_profile: EAT_PROFILE_URI }; | ||
| const iss = issuerFrom(receipt); | ||
| if (iss) claims.iss = iss; | ||
| const sub = subjectFrom(receipt); | ||
| if (sub) claims.sub = sub; | ||
| const iat = iatFrom(receipt); | ||
| if (iat != null) claims.iat = iat; | ||
| const cti = ctiFrom(receipt); | ||
| if (cti) claims.cti = cti; | ||
| if (receipt.nonce || receipt.eat_nonce) claims.eat_nonce = receipt.nonce || receipt.eat_nonce; | ||
| const meas = measurementsFrom(receipt); | ||
| if (meas) claims.measurements = meas; | ||
| const tee = teeEvidenceFrom(receipt); | ||
| if (tee) claims.tee_evidence = tee; | ||
| claims.verification = verificationSummary(result); | ||
| claims.acta_receipt = receipt; // full receipt, preserving its Ed25519 signature | ||
| return claims; | ||
| } | ||
| export function emitEatJson(receipt, result) { | ||
| return JSON.stringify(toEatClaims(receipt, result), null, 2); | ||
| } | ||
| // ── Minimal deterministic CBOR (RFC 8949 core, definite-length, sorted maps) ─ | ||
| function cborHead(major, n) { | ||
| const mt = major << 5; | ||
| if (n < 24) return Uint8Array.of(mt | n); | ||
| if (n < 0x100) return Uint8Array.of(mt | 24, n); | ||
| if (n < 0x10000) return Uint8Array.of(mt | 25, n >> 8, n & 0xff); | ||
| if (n < 0x100000000) return Uint8Array.of(mt | 26, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff); | ||
| // 64-bit length (major 27); split into two 32-bit halves. | ||
| const hi = Math.floor(n / 0x100000000); | ||
| const lo = n >>> 0; | ||
| return Uint8Array.of(mt | 27, (hi >>> 24) & 0xff, (hi >>> 16) & 0xff, (hi >>> 8) & 0xff, hi & 0xff, (lo >>> 24) & 0xff, (lo >>> 16) & 0xff, (lo >>> 8) & 0xff, lo & 0xff); | ||
| } | ||
| function concatBytes(arrs) { | ||
| let len = 0; | ||
| for (const a of arrs) len += a.length; | ||
| const out = new Uint8Array(len); | ||
| let o = 0; | ||
| for (const a of arrs) { out.set(a, o); o += a.length; } | ||
| return out; | ||
| } | ||
| /** Encode a value to deterministic CBOR. Supports the JSON value set + integers. */ | ||
| export function cborEncode(value) { | ||
| if (value === null || value === undefined) return Uint8Array.of(0xf6); // null | ||
| if (value === true) return Uint8Array.of(0xf5); | ||
| if (value === false) return Uint8Array.of(0xf4); | ||
| if (typeof value === 'number') { | ||
| if (!Number.isInteger(value)) { | ||
| // Avoid float ambiguity: carry non-integers as their JSON text. | ||
| return cborEncode(String(value)); | ||
| } | ||
| return value >= 0 ? cborHead(0, value) : cborHead(1, -value - 1); | ||
| } | ||
| if (typeof value === 'string') { | ||
| const bytes = new TextEncoder().encode(value); | ||
| return concatBytes([cborHead(3, bytes.length), bytes]); | ||
| } | ||
| if (value instanceof Uint8Array) { | ||
| return concatBytes([cborHead(2, value.length), value]); | ||
| } | ||
| if (Array.isArray(value)) { | ||
| return concatBytes([cborHead(4, value.length), ...value.map(cborEncode)]); | ||
| } | ||
| if (typeof value === 'object') { | ||
| // Map: encode each entry, sort by encoded-key bytes (RFC 8949 §4.2.1). | ||
| const entries = []; | ||
| for (const [k, v] of Object.entries(value)) { | ||
| if (v === undefined) continue; | ||
| // Numeric-looking keys carried as integers (used for CWT claim keys). | ||
| const keyEnc = /^-?\d+$/.test(k) ? cborEncode(parseInt(k, 10)) : cborEncode(k); | ||
| entries.push([keyEnc, cborEncode(v)]); | ||
| } | ||
| entries.sort((a, b) => { | ||
| const x = a[0], y = b[0]; | ||
| const n = Math.min(x.length, y.length); | ||
| for (let i = 0; i < n; i++) if (x[i] !== y[i]) return x[i] - y[i]; | ||
| return x.length - y.length; | ||
| }); | ||
| return concatBytes([cborHead(5, entries.length), ...entries.flatMap((e) => e)]); | ||
| } | ||
| throw new Error(`cborEncode: unsupported type ${typeof value}`); | ||
| } | ||
| /** Build the CWT/EAT claims-set keyed by integer claim keys, for CBOR. */ | ||
| export function toEatClaimsCbor(receipt, result) { | ||
| const m = {}; | ||
| m[EAT_PROFILE] = EAT_PROFILE_URI; | ||
| const iss = issuerFrom(receipt); | ||
| if (iss) m[CWT_ISS] = iss; | ||
| const sub = subjectFrom(receipt); | ||
| if (sub) m[CWT_SUB] = sub; | ||
| const iat = iatFrom(receipt); | ||
| if (iat != null) m[CWT_IAT] = iat; | ||
| const cti = ctiFrom(receipt); | ||
| if (cti) m[CWT_CTI] = new TextEncoder().encode(String(cti)); // CWT cti is a byte string | ||
| if (receipt.nonce || receipt.eat_nonce) m[EAT_NONCE] = String(receipt.nonce || receipt.eat_nonce); | ||
| const meas = measurementsFrom(receipt); | ||
| if (meas) m[EAT_MEASUREMENTS] = JSON.stringify(meas); | ||
| const tee = teeEvidenceFrom(receipt); | ||
| if (tee) m[PRIV_TEE_EVIDENCE] = JSON.stringify(tee); | ||
| m[PRIV_VERIFICATION] = JSON.stringify(verificationSummary(result)); | ||
| // Carry the full receipt as canonical JSON text to preserve its exact bytes | ||
| // (and its Ed25519 signature) without float-encoding ambiguity. | ||
| m[PRIV_ACTA_RECEIPT] = JSON.stringify(receipt); | ||
| return m; | ||
| } | ||
| /** Hex-encoded CBOR CWT/EAT claims-set. */ | ||
| export function emitEatCbor(receipt, result) { | ||
| const bytes = cborEncode(toEatClaimsCbor(receipt, result)); | ||
| return Buffer.from(bytes).toString('hex'); | ||
| } |
+22
-65
| # Changelog | ||
| ## 0.9.0 (2026-06-22) | ||
| ## Unreleased | ||
| ### Legate adherence / restraint proof packs | ||
| ### Restraint receipts: prove what was prevented, offline and position-blind | ||
| Recognizes and verifies `scopeblind.legate.proof-pack.v1`: the allocator-facing, | ||
| position-blind record a Legate desk produces of what the gate prevented over a | ||
| session (held / blocked, attributed to the committed-mandate rules) plus order-path | ||
| shadow evidence (what it would have blocked on a FIX feed), bound to the mandate | ||
| digest, the signed book provenance, and a receipt Merkle root. The signature is | ||
| Ed25519 over the canonical (deep-sorted, no-whitespace) bytes of the pack minus its | ||
| `signature`, `sha256`, and `hybrid_signature` fields, against the embedded runtime | ||
| `verification_key` (pin it with `--key`). Cross-implementation tested against a real | ||
| desktop-runtime-signed fixture (`samples/legate-proof-pack.json`); tampering with any | ||
| field, and a wrong pinned key, fail. An optional `hybrid_signature` (Ed25519 + | ||
| ML-DSA-65) is recognized and reported; classical Ed25519 is verified here, with PQ | ||
| verification documented as an optional add-on in the restraint-receipts draft. | ||
| A restraint receipt (`tool: gate.restrain`) is already verified as a Legate | ||
| governed receipt. This release re-verifies and surfaces its openable detail, so | ||
| the receipt proves not merely that a deny was signed but exactly WHAT was | ||
| prevented and WHY: | ||
| ## 0.8.0 (2026-06-13) | ||
| - The disclosed denial outcome (the determining rules, risk band, mandate | ||
| digest) is re-hashed and confirmed to bind to the signed `result_sha256`, and | ||
| the proposed blocked order is re-hashed under its salt and confirmed to bind to | ||
| `input_sha256`. Re-hashing uses the same JCS canonicalization the runtime used, | ||
| so altering the disclosed detail flips the binding to a failure while the | ||
| signature stays valid over the original hashes (you cannot lie about what was | ||
| blocked). | ||
| - Position-blind by construction: the blocked order can be withheld, and the | ||
| outcome still verifies because the order is salt-committed into the signed input | ||
| hash and openable to a regulator on demand. | ||
| - Terminal output gains a `Prevented:` block (risk band, determining rules, the | ||
| blocked order or a position-blind note, and the two binding checks). New | ||
| `verifyRestraintBindings` export. A sample lives at | ||
| `samples/sample-restraint-receipt.json`; see `RESTRAINT-DEMO.md`. | ||
| - Added a `verify` bin alias (alongside `verify-artifact`) so | ||
| `npx @veritasacta/verify <receipt>` reads naturally. | ||
| ### RFC 6962 transparency log for macro track records | ||
| Recognizes `scopeblind.macro.transparency-head/1` and | ||
| `scopeblind.macro.transparency-witness/1`, and verifies the `transparency` | ||
| evidence carried in a macro track-record bundle: every record's Merkle | ||
| inclusion proof against the signed head (RFC 6962 hashing: leaf = | ||
| sha256(0x00||digest), node = sha256(0x01||l||r)), plus an independent witness | ||
| co-signature. The bundle result reports `Transparency: witness-anchored / | ||
| self-signed / not anchored`; a tampered head or a missing inclusion fails the | ||
| chain check. No new signing crypto — inclusion is recomputed from the path and | ||
| checked against the head root. This is what makes a dropped or rewritten record | ||
| detectable to anyone who retained a head, not merely a single un-trimmed export. | ||
| ### ScopeBlind macro-engine snapshots and track-record bundles | ||
| New engine `src/engines/macro-snapshot.js` recognizes the macro-engine | ||
| schemas (`scopeblind.macro.market-state/1`, `regime-snapshot/1`, | ||
| `tape-snapshot/1`, `vulnerability/1`, `alert/1`, `journal-entry/1`) and the | ||
| signed track-record bundle (`scopeblind.macro.track-record-bundle/1`). Macro | ||
| snapshots already verified cryptographically as generic Gate tuples; this adds | ||
| schema recognition, a per-schema semantic-contract check, and a schema-aware | ||
| summary in the output (no more "unrecognized schema" for macro records). No | ||
| new cryptography: crypto is delegated to `verifyGateTuple` and canonicalization | ||
| to `canonicalGateJSON`. | ||
| The track-record bundle verifier mirrors the Gate evidence-bundle: it checks | ||
| every record's signature, single-signer custody (every record including the | ||
| manifest shares one model key), exact manifest completeness (entries enumerate | ||
| the snapshots then journal entries in order), the snapshot/journal counts, and | ||
| the `history_head_digest` over the ordered record digests. Dropping or | ||
| tampering any record breaks the bundle. `--mode macro` forces the bundle path. | ||
| Anchored exports additionally carry a monotonic manifest sequence, previous | ||
| manifest/history-head links, retained prior manifests, and a signed checkpoint | ||
| chain. Verification rejects deletion of any previously manifested record. | ||
| `--key` is now reported explicitly as the operator-identity trust boundary; | ||
| without it the result proves embedded-key integrity only. `--history-head` and | ||
| `--anchor-head` pin independently retained anti-rollback checkpoints. | ||
| ## 0.7.0 (2026-06-12) | ||
| ### Release rule for execution evidence | ||
| Bundles whose entries carry fills under an unreleased decision fail the | ||
| chain check: fills require an ALLOW parent, or an APPROVAL_REQUIRED | ||
| parent with a present approval whose decision is approved. A DENY or | ||
| REVIEW parent with fills always fails. Without this rule a bundle could | ||
| present individually valid signatures as evidence of an unauthorized | ||
| execution. | ||
| ### ScopeBlind Gate receipt tuples and evidence bundles | ||
@@ -73,0 +30,0 @@ |
+18
-40
@@ -47,6 +47,3 @@ #!/usr/bin/env node | ||
| import { verifyGateTuple, verifyGateBundle } from './src/engines/gate-receipt.js'; | ||
| import { verifyMacroTrackRecord } from './src/engines/macro-snapshot.js'; | ||
| import { verifyLegateGovernedReceipt } from './src/engines/legate-governed-receipt.js'; | ||
| import { verifyLegateProofPack } from './src/engines/legate-proof-pack.js'; | ||
| import { verifyTrustedContextPack } from './src/engines/trusted-context-pack.js'; | ||
| import { verifyVoprfToken } from './src/engines/voprf-token.js'; | ||
@@ -81,3 +78,2 @@ import { verifyKnowledgeUnit } from './src/engines/knowledge-unit.js'; | ||
| import { resolveFromJwks } from './src/util/jwks.js'; | ||
| import { loadKnownIssuers, labelFor } from './src/util/known-issuers.js'; | ||
| import { appendAuditEntry } from './src/util/audit-log.js'; | ||
@@ -93,5 +89,3 @@ import { fipsStatus } from './src/util/fips.js'; | ||
| formatGateBundleResult, | ||
| formatMacroTrackRecordResult, | ||
| formatGovernedReceiptResult, | ||
| formatTrustedContextPackResult, | ||
| formatKuResult, | ||
@@ -121,6 +115,3 @@ formatSelfCheckResult, | ||
| 'gate-evidence-bundle': 'ScopeBlind Gate evidence bundle (receipt tuples + chain links)', | ||
| 'macro-track-record': 'ScopeBlind macro-engine track-record bundle (signed snapshots + completeness manifest)', | ||
| 'legate-governed-receipt': 'Legate governed receipt (Ed25519 over canonical action payload)', | ||
| 'legate-proof-pack': 'Legate adherence / restraint proof pack (Ed25519 over canonical bytes, position-blind)', | ||
| 'trusted-context-pack': 'ScopeBlind Trusted Context Pack (signed parsed-context attestation)', | ||
| }; | ||
@@ -166,2 +157,3 @@ | ||
| emitVerificationReceipt: false, | ||
| emitEat: null, | ||
| fips: false, | ||
@@ -174,4 +166,2 @@ subcommand: null, | ||
| frameworkOverride: null, | ||
| historyHead: null, | ||
| anchorHead: null, | ||
| }; | ||
@@ -189,7 +179,4 @@ | ||
| case '-k': opts.publicKey = next(); break; | ||
| case '--known-issuers': opts.knownIssuers = next(); break; | ||
| case '--jwks': opts.jwksUrl = next(); break; | ||
| case '--trust-anchor': opts.trustAnchor = next(); break; | ||
| case '--history-head': opts.historyHead = next(); break; | ||
| case '--anchor-head': opts.anchorHead = next(); break; | ||
| case '--stdin': opts.stdin = true; break; | ||
@@ -221,2 +208,4 @@ case '--mode': opts.mode = next(); break; | ||
| case '--emit-verification-receipt': opts.emitVerificationReceipt = true; break; | ||
| case '--emit-eat': opts.emitEat = 'json'; break; | ||
| case '--emit-eat-cbor': opts.emitEat = 'cbor'; break; | ||
| case '--fips': opts.fips = true; break; | ||
@@ -315,4 +304,2 @@ case '--allow-partial-voprf': opts.allowPartialVoprf = true; break; | ||
| --trust-anchor <file> Local trust-anchor JSON with public keys | ||
| --history-head <hex> Pin the expected macro track-record history head | ||
| --anchor-head <hex> Pin the expected signed macro anchor digest | ||
| --mode <m> Force mode: receipt|voprf|ku|auto (default: auto) | ||
@@ -521,5 +508,3 @@ --bundle Verify as audit bundle | ||
| else if (forced === 'gate-bundle') detected.mode = 'gate-evidence-bundle'; | ||
| else if (forced === 'macro' || forced === 'macro-track-record') detected.mode = 'macro-track-record'; | ||
| else if (forced === 'governed') detected.mode = 'legate-governed-receipt'; | ||
| else if (forced === 'context' || forced === 'context-pack') detected.mode = 'trusted-context-pack'; | ||
| } | ||
@@ -559,6 +544,2 @@ if (opts.bundle) detected.mode = 'ed25519-bundle'; | ||
| } | ||
| case 'macro-track-record': { | ||
| const r = verifyMacroTrackRecord(input, subOpts); | ||
| return { ...r, modeLabel: MODE_LABELS['macro-track-record'] }; | ||
| } | ||
| case 'gate-receipt-tuple': { | ||
@@ -572,10 +553,2 @@ const r = verifyGateTuple(input, subOpts); | ||
| } | ||
| case 'legate-proof-pack': { | ||
| const r = verifyLegateProofPack(input, subOpts); | ||
| return { ...r, modeLabel: MODE_LABELS['legate-proof-pack'] }; | ||
| } | ||
| case 'trusted-context-pack': { | ||
| const r = verifyTrustedContextPack(input, subOpts); | ||
| return { ...r, modeLabel: MODE_LABELS['trusted-context-pack'] }; | ||
| } | ||
| case 'knowledge-unit': { | ||
@@ -1215,2 +1188,17 @@ const r = await verifyKnowledgeUnit(input, subOpts); | ||
| // EAT output mode (A4): re-serialize the verified receipt as an RFC 9711 EAT | ||
| // claims-set so it is also a valid EAT/TRACE record (JSON names, or CBOR with | ||
| // integer claim keys). Authenticity round-trips to the embedded Acta receipt. | ||
| if (opts.emitEat) { | ||
| const { emitEatJson, emitEatCbor } = await import('./src/output/eat.js'); | ||
| const out = opts.emitEat === 'cbor' ? emitEatCbor(input, result) : emitEatJson(input, result); | ||
| if (opts.output) { | ||
| writeFileSync(opts.output, out); | ||
| console.error(dim(`wrote ${opts.output}`)); | ||
| } else { | ||
| console.log(out); | ||
| } | ||
| process.exit(result.valid ? 0 : exitCodeFor(result.error)); | ||
| } | ||
| // Audit log | ||
@@ -1246,10 +1234,2 @@ if (opts.auditLog) { | ||
| // Resolve a human label for the signer key, if one is known. Display aid only; | ||
| // it never changes the verification result. | ||
| if (result && typeof result === 'object' && typeof result.publicKey === 'string') { | ||
| const issuers = loadKnownIssuers(opts.knownIssuers, join(__dirname, 'known-issuers.json')); | ||
| const label = labelFor(result.publicKey, issuers); | ||
| if (label) result.signerLabel = label; | ||
| } | ||
| // Output | ||
@@ -1276,6 +1256,4 @@ if (opts.json) { | ||
| else if (result.format === 'gate-evidence-bundle') console.log(formatGateBundleResult(result, opts)); | ||
| else if (result.format === 'macro-track-record') console.log(formatMacroTrackRecordResult(result, opts)); | ||
| else if (result.format === 'gate-tuple') console.log(formatGateTupleResult(result, opts)); | ||
| else if (result.format === 'legate-governed-receipt') console.log(formatGovernedReceiptResult(result, opts)); | ||
| else if (result.format === 'trusted-context-pack') console.log(formatTrustedContextPackResult(result, opts)); | ||
| else if (result.total !== undefined) console.log(formatBundleResult(result, opts)); | ||
@@ -1282,0 +1260,0 @@ else console.log(formatReceiptResult(result, opts)); |
+9
-5
| { | ||
| "name": "@veritasacta/verify", | ||
| "version": "0.9.0", | ||
| "version": "0.9.1", | ||
| "mcpName": "io.github.tomjwxf/veritasacta-verify", | ||
@@ -9,2 +9,3 @@ "description": "Offline verifier for Veritas Acta signed receipts. Powers protect-mcp, ScopeBlind cold-chain hardware, and Microsoft AGT Lesson 18.", | ||
| "bin": { | ||
| "verify": "cli.js", | ||
| "verify-artifact": "cli.js" | ||
@@ -22,5 +23,8 @@ }, | ||
| "ROADMAP.md", | ||
| "samples/", | ||
| "test/conformance.js", | ||
| "known-issuers.json" | ||
| "samples/sample-bundle.json", | ||
| "samples/sample-gate-bundle.json", | ||
| "samples/sample-gate-tuple.json", | ||
| "samples/sample-receipt.json", | ||
| "samples/sample-restraint-receipt.json", | ||
| "test/conformance.js" | ||
| ], | ||
@@ -53,3 +57,3 @@ "dependencies": { | ||
| "scripts": { | ||
| "test": "node --test test/unit/*.test.js test/integration/*.test.js", | ||
| "test": "node --test 'test/unit/**/*.test.js' 'test/integration/**/*.test.js'", | ||
| "test:conformance": "node test/conformance.js", | ||
@@ -56,0 +60,0 @@ "self-test": "node cli.js --self-test", |
+0
-19
@@ -41,3 +41,2 @@ # @veritasacta/verify | ||
| | Gate receipt / bundle | ScopeBlind Gate receipt tuples (`scopeblind.gate.*`) and signed-manifest `scopeblind.gate.evidence-bundle/2` exports with semantic and exact chain checks | T1 | | ||
| | Macro track record | Signed macro snapshots, append-only manifest sequence, and signed history checkpoints | T1 | | ||
@@ -102,20 +101,2 @@ ## Subcommands | ||
| ### ScopeBlind macro track records | ||
| Macro exports verify offline. An embedded key proves internal signature | ||
| integrity, not who controls that key. Pin the operator key and an independently | ||
| retained anti-rollback head for identity and historical assurance: | ||
| ```bash | ||
| npx @veritasacta/verify@0.8.0 legate-macro-track-record.json \ | ||
| --key <operator-ed25519-public-key> \ | ||
| --history-head <expected-history-head> \ | ||
| --anchor-head <expected-anchor-digest> | ||
| ``` | ||
| The verifier checks every record, the exact current manifest inventory, prior | ||
| manifest links, retention of previously manifested records, and the signed | ||
| checkpoint chain. Publication at a mutable URL is not itself a transparency | ||
| log; retain or independently timestamp checkpoint heads. | ||
| ### Pre-built sandbox profiles | ||
@@ -122,0 +103,0 @@ |
+8
-9
| { | ||
| "sigil_version": 1, | ||
| "fingerprint": "b309bd8b", | ||
| "name": "Woven Meadow", | ||
| "sigil_hash": "b309bd8b8644efcc07e737cff69cb7a87eaa9c8c9d728baf751c89b882c2c1cd", | ||
| "fingerprint": "677a8a81", | ||
| "name": "Open Wind", | ||
| "sigil_hash": "677a8a812c7a9d896bee2d7f8dfde02d347ac742fa4cd8830c9e292e92b9e676", | ||
| "project_public_key": "fe665e861867cec7e171c0c13bbc873c3362079faef21f54df7804b7fb9ae8af", | ||
@@ -10,4 +10,4 @@ "policy": { | ||
| "package": "@veritasacta/verify", | ||
| "package_version": "0.7.0", | ||
| "source_hash": "ab926d490a512418c8028cd46eb766a100602e28b9c5f26381585030763a2fab", | ||
| "package_version": "0.5.4", | ||
| "source_hash": "4eca3a4aa71e50cf983285520ff8ec93e2ece12d2912f0b7b60b5c7034b879f9", | ||
| "monitored_files": [ | ||
@@ -19,3 +19,2 @@ "cli.js", | ||
| "src/engines/ed25519-receipt.js", | ||
| "src/engines/gate-receipt.js", | ||
| "src/engines/voprf-token.js", | ||
@@ -62,6 +61,6 @@ "src/engines/knowledge-unit.js", | ||
| ], | ||
| "created_at": 1781269520160 | ||
| "created_at": 1776673390228 | ||
| }, | ||
| "policy_hash": "7d517dbbb5824d09186ec8b2344febeb5e62e5dc54bcb191c59da13f2ee03b9a", | ||
| "derived_at": "2026-06-12T13:05:20.160Z" | ||
| "policy_hash": "3d9bbcbea4af4032c7e77fc1f0a8606553e9a53bf8df0f09ed0bcd0859faa92a", | ||
| "derived_at": "2026-04-20T08:23:10.228Z" | ||
| } |
+0
-43
@@ -14,3 +14,2 @@ /** | ||
| * - 'gate-evidence-bundle' — ScopeBlind Gate evidence bundle (scopeblind.gate.evidence-bundle/2) | ||
| * - 'macro-track-record' — ScopeBlind macro-engine track-record bundle (scopeblind.macro.track-record-bundle/1) | ||
| * - 'unknown' | ||
@@ -80,29 +79,2 @@ * | ||
| // ScopeBlind macro-engine track-record bundle: explicit schema marker + | ||
| // snapshots[] + a signed manifest tuple. | ||
| if ( | ||
| input.schema === 'scopeblind.macro.track-record-bundle/1' | ||
| && Array.isArray(input.snapshots) | ||
| && input.manifest && typeof input.manifest === 'object' && !Array.isArray(input.manifest) | ||
| ) { | ||
| signals.push(`schema=${input.schema}`, 'snapshots[]', 'manifest'); | ||
| return { mode: 'macro-track-record', signals, hasSelectiveDisclosure: false, isBundle: true }; | ||
| } | ||
| // ScopeBlind Trusted Context Pack: a Gate-tuple-shaped envelope whose payload | ||
| // carries the TCB schema marker. Detected BEFORE the generic gate tuple (it | ||
| // matches that shape too) and routed to engines/trusted-context-pack.js so a | ||
| // third party re-verifies the parsed-context attestation, its confidence, and | ||
| // its gate decision offline. | ||
| if ( | ||
| input.payload && typeof input.payload === 'object' && !Array.isArray(input.payload) | ||
| && input.payload.schema === 'scopeblind.trusted_context_pack.v1' | ||
| && typeof input.digest === 'string' | ||
| && typeof input.signature === 'string' | ||
| && typeof input.verification_key === 'string' | ||
| ) { | ||
| signals.push('schema=scopeblind.trusted_context_pack.v1'); | ||
| return { mode: 'trusted-context-pack', signals, hasSelectiveDisclosure: false, isBundle: false }; | ||
| } | ||
| // ScopeBlind Gate receipt tuple: { payload, digest, signature, verification_key }. | ||
@@ -122,17 +94,2 @@ // The flat hex signature string distinguishes it from the Passport | ||
| // Legate adherence / restraint proof pack: type scopeblind.legate.proof-pack.v1, | ||
| // signed by the runtime key (verification_key) over the canonical bytes of the pack | ||
| // minus signature/sha256. A position-blind record of what the gate prevented (held / | ||
| // blocked, by rule) plus order-path shadow evidence. Detected before the v1-flat | ||
| // catch-all so it routes to engines/legate-proof-pack.js. Routed by its type. | ||
| if ( | ||
| input.type === 'scopeblind.legate.proof-pack.v1' | ||
| && typeof input.signature === 'string' | ||
| && (typeof input.verification_key === 'string' | ||
| || (input.runtime && typeof input.runtime.verification_key === 'string')) | ||
| ) { | ||
| signals.push('type=scopeblind.legate.proof-pack.v1', 'signature+verification_key'); | ||
| return { mode: 'legate-proof-pack', signals, hasSelectiveDisclosure: false, isBundle: false }; | ||
| } | ||
| // Legate governed receipt: a FLAT, pipe-delimited canonical payload | ||
@@ -139,0 +96,0 @@ // scopeblind.receipt.v1|<id>|<tool>|<decision>|<input_sha256>|<result_sha256>|<at> |
@@ -7,3 +7,2 @@ /** ScopeBlind Gate receipt and evidence-bundle verifier. */ | ||
| import { hexToBytes, bytesToHex } from '../util/hex.js'; | ||
| import { isMacroSchema, macroSchemaErrors, macroSummary, MACRO_PROVES, MACRO_LIMITATIONS } from './macro-snapshot.js'; | ||
@@ -20,4 +19,2 @@ export const GATE_BUNDLE_SCHEMA = 'scopeblind.gate.evidence-bundle/2'; | ||
| 'scopeblind.gate.evidence-manifest/1': 'signed bundle manifest', | ||
| 'scopeblind.gate.reconciliation/1': 'custodian-statement reconciliation', | ||
| 'scopeblind.gate.stress/1': 'regime stress-test result', | ||
| 'scopeblind.mandate.delegation/1': 'issuer-signed mandate delegation', | ||
@@ -141,6 +138,4 @@ }; | ||
| const schema = isString(payload.schema) ? payload.schema : null; | ||
| const macro = schema !== null && isMacroSchema(schema); | ||
| const schemaRecognized = schema !== null && (Object.hasOwn(GATE_SCHEMAS, schema) || macro); | ||
| Object.assign(base, { schema, schemaRecognized, macroSchema: macro, type: schema || undefined, payloadFields: collectGatePayloadFields(payload), chainFields: macro ? null : collectGateChainFields(payload) }); | ||
| if (macro) base.macroSummary = macroSummary(payload); | ||
| const schemaRecognized = schema !== null && Object.hasOwn(GATE_SCHEMAS, schema); | ||
| Object.assign(base, { schema, schemaRecognized, type: schema || undefined, payloadFields: collectGatePayloadFields(payload), chainFields: collectGateChainFields(payload) }); | ||
| if (!isString(tuple.signature)) return { valid: false, error: 'missing_signature', ...base }; | ||
@@ -158,16 +153,5 @@ if (!isString(tuple.digest) || !HEX_64.test(tuple.digest)) return { valid: false, error: 'malformed_hex', ...base, detail: 'tuple digest must be 64 lowercase hex characters' }; | ||
| } | ||
| const semanticErrors = macro ? macroSchemaErrors(payload) : schemaErrors(payload); | ||
| const semanticErrors = schemaErrors(payload); | ||
| if (semanticErrors.length) return { valid: false, error: 'schema_invalid', ...base, digest: tuple.digest, publicKey: tuple.verification_key, detail: semanticErrors.join('; '), semanticErrors }; | ||
| return { | ||
| valid: true, | ||
| ...base, | ||
| digest: tuple.digest, | ||
| publicKey: tuple.verification_key, | ||
| keySource: pinned ? 'embedded-tuple (pinned via --key)' : 'embedded-tuple', | ||
| signerPinned: pinned, | ||
| identityStatus: pinned ? 'pinned_expected_key' : 'embedded_key_only', | ||
| algorithm: 'ed25519', | ||
| proves: macro ? MACRO_PROVES : GATE_PROVES, | ||
| limitations: macro ? MACRO_LIMITATIONS : GATE_LIMITATIONS, | ||
| }; | ||
| return { valid: true, ...base, digest: tuple.digest, publicKey: tuple.verification_key, keySource: pinned ? 'embedded-tuple (pinned via --key)' : 'embedded-tuple', algorithm: 'ed25519', proves: GATE_PROVES, limitations: GATE_LIMITATIONS }; | ||
| } | ||
@@ -292,23 +276,2 @@ | ||
| if (e.approval) add(e.approval, 'approval', i, approvalCheck(e.approval, parent, parent?.digest), false); | ||
| // Execution evidence requires a RELEASED decision. An ALLOW parent is | ||
| // released by definition; an APPROVAL_REQUIRED parent is released only by | ||
| // a present approval whose decision is "approved" (its own crypto and | ||
| // chain validity are checked above). Fills under a DENY, REVIEW, held, or | ||
| // declined parent are evidence of an unauthorized execution and fail the | ||
| // bundle even when every signature is individually valid. | ||
| const fillsPresent = Array.isArray(e.fills) && e.fills.length > 0; | ||
| if (fillsPresent) { | ||
| const decision = parent?.payload?.decision; | ||
| const released = decision === 'ALLOW' | ||
| || (decision === 'APPROVAL_REQUIRED' && e.approval?.payload?.decision === 'approved'); | ||
| results.chainChecks++; | ||
| if (!released) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| const why = decision === 'APPROVAL_REQUIRED' | ||
| ? (e.approval ? `approval decision is ${JSON.stringify(e.approval?.payload?.decision)}` : 'no approval is present') | ||
| : `parent decision is ${JSON.stringify(decision)}`; | ||
| results.errors.push(`[chain] Entry ${i + 1}: fills present but the decision was never released (${why})`); | ||
| } | ||
| } | ||
| const fillsByDigest = new Map(); | ||
@@ -315,0 +278,0 @@ for (const fill of Array.isArray(e.fills) ? e.fills : []) { const err = fillCheck(fill, parent, legsById, legTuples); add(fill, 'fill', i, err, false); fillsByDigest.set(fill?.digest, fill); } |
@@ -28,2 +28,3 @@ /** | ||
| import { hexToBytes } from '../util/hex.js'; | ||
| import { canonicalize, sha256Hex } from '../util/canonical.js'; | ||
@@ -58,3 +59,32 @@ /** The canonical payload prefix this engine recognizes (v1). */ | ||
| const isString = (v) => typeof v === 'string' && v.length > 0; | ||
| const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v); | ||
| /** | ||
| * Re-verify the openable detail of a restraint receipt. A restraint receipt | ||
| * binds the disclosed denial outcome into result_sha256 and the (optionally | ||
| * withheld) proposed order into input_sha256, both via the same canonical | ||
| * hashing the runtime used. Re-hashing here proves the disclosed detail is | ||
| * exactly what was signed — so the receipt proves WHAT was prevented and WHY, | ||
| * not merely that some deny was signed. | ||
| * | ||
| * @param {object} receipt a governed receipt with a `restraint` detail block | ||
| * @returns {{ outcome_bound: boolean, proposed_bound: boolean|null, determining: string[], risk_band: string|null, mandate_digest: string|null, proposed: object|null }} | ||
| */ | ||
| export function verifyRestraintBindings(receipt) { | ||
| const r = isObject(receipt?.restraint) ? receipt.restraint : {}; | ||
| const outcome_bound = isObject(r.outcome) && sha256Hex(canonicalize(r.outcome)) === receipt.result_sha256; | ||
| let proposed_bound = null; // null = position-blind (proposed withheld) | ||
| if (r.proposed != null) { | ||
| proposed_bound = isString(r.salt) && sha256Hex(`${r.salt}|${canonicalize(r.proposed)}`) === receipt.input_sha256; | ||
| } | ||
| return { | ||
| outcome_bound, | ||
| proposed_bound, | ||
| determining: Array.isArray(r.outcome?.determining) ? r.outcome.determining : [], | ||
| risk_band: isString(r.outcome?.risk_band) ? r.outcome.risk_band : null, | ||
| mandate_digest: isString(r.outcome?.mandate_digest) ? r.outcome.mandate_digest : null, | ||
| proposed: r.proposed ?? null, | ||
| }; | ||
| } | ||
| /** Reconstruct the exact bytes the daemon + phone signed. */ | ||
@@ -134,3 +164,3 @@ export function legateReceiptPayload(r) { | ||
| return { | ||
| const result = { | ||
| valid: true, | ||
@@ -141,5 +171,32 @@ ...base, | ||
| signedPayload: payload, | ||
| proves: GOVERNED_PROVES, | ||
| limitations: GOVERNED_LIMITATIONS, | ||
| proves: [...GOVERNED_PROVES], | ||
| limitations: [...GOVERNED_LIMITATIONS], | ||
| }; | ||
| // Restraint receipts carry openable detail: the disclosed outcome (the rules | ||
| // that blocked the action) and, unless position-blind, the proposed order. | ||
| // Re-hash both and confirm they bind to the signed hashes, so the receipt | ||
| // proves exactly WHAT was prevented and WHY, not merely that a deny was signed. | ||
| if (kind === 'restraint' && isObject(receipt.restraint)) { | ||
| const bindings = verifyRestraintBindings(receipt); | ||
| result.restraint = bindings; | ||
| if (bindings.outcome_bound) { | ||
| result.proves.push( | ||
| 'Restraint: the gate blocked this action before it could execute, and the disclosed outcome (the determining rules and risk band) re-hashes to the signed result hash, so it is exactly what was committed.', | ||
| ); | ||
| } else { | ||
| result.limitations.push( | ||
| 'The disclosed restraint outcome did NOT re-hash to the signed result hash: the detail shown was altered after signing (the signature itself is still valid over the original hashes).', | ||
| ); | ||
| } | ||
| if (bindings.proposed_bound === null) { | ||
| result.proves.push( | ||
| 'Position-blind: the blocked order is withheld but salt-committed into the signed input hash, so the restraint is provable without disclosing the position.', | ||
| ); | ||
| } else if (bindings.proposed_bound === false) { | ||
| result.limitations.push('The disclosed blocked order did NOT re-hash to the signed input hash (the shown order was altered after signing).'); | ||
| } | ||
| } | ||
| return result; | ||
| } |
+29
-210
@@ -213,59 +213,2 @@ /** | ||
| /** | ||
| * Render the salient-field lines for a recognized macro snapshot summary. | ||
| * | ||
| * @param {Object} s macroSummary object from engines/macro-snapshot.js | ||
| * @returns {string[]} | ||
| */ | ||
| function macroSummaryLines(s) { | ||
| const lines = []; | ||
| if (s.description) lines.push(` Snapshot: ${dim(s.description)}`); | ||
| if (s.as_of) lines.push(` As of: ${dim(s.as_of)}`); | ||
| switch (s.schema) { | ||
| case 'scopeblind.macro.market-state/1': | ||
| lines.push(` Class: ${s.classification}${s.confidence !== undefined ? dim(` (confidence ${s.confidence})`) : ''}`); | ||
| if (s.pillars) lines.push(` Pillars: ${dim(Object.entries(s.pillars).map(([k, v]) => `${k} ${v >= 0 ? '+' : ''}${v}`).join(', '))}`); | ||
| break; | ||
| case 'scopeblind.macro.regime-snapshot/1': | ||
| lines.push(` Regime: ${s.regime}${s.candidate_regime && s.candidate_regime !== s.regime ? dim(` (candidate ${s.candidate_regime})`) : ''}`); | ||
| if (s.liquidity_overlay) lines.push(` Liquidity: ${s.liquidity_overlay}`); | ||
| if (s.confidence !== undefined) lines.push(` Confidence: ${dim(String(s.confidence))}`); | ||
| break; | ||
| case 'scopeblind.macro.tape-snapshot/1': | ||
| lines.push(` Tape type: ${s.tape_type}${s.material !== undefined ? dim(` (${s.material ? 'material' : 'immaterial'})`) : ''}`); | ||
| if (s.coherence !== undefined) lines.push(` Coherence: ${dim(String(s.coherence))}`); | ||
| if (s.attribution_tier) lines.push(` Attributed: ${dim(s.attribution_tier)}`); | ||
| break; | ||
| case 'scopeblind.macro.vulnerability/1': | ||
| if (s.regime || s.market_state) lines.push(` Posture: ${dim([s.regime, s.market_state].filter(Boolean).join(' / '))}`); | ||
| if (Array.isArray(s.top_vulnerabilities) && s.top_vulnerabilities.length) { | ||
| lines.push(` Top risk: ${dim(s.top_vulnerabilities.map((v) => `${v.factor} (pain ${v.pain})`).join(', '))}`); | ||
| } | ||
| break; | ||
| case 'scopeblind.macro.alert/1': | ||
| lines.push(` Severity: ${s.severity}${s.kind ? dim(` (${s.kind})`) : ''}`); | ||
| if (s.title) lines.push(` Title: ${s.title}`); | ||
| break; | ||
| case 'scopeblind.macro.journal-entry/1': | ||
| if (s.author) lines.push(` Author: ${s.author}`); | ||
| lines.push(` References: ${dim(`${s.reference_count} signed snapshot(s)`)}`); | ||
| break; | ||
| case 'scopeblind.macro.track-record-manifest/1': | ||
| lines.push(` Inventory: ${dim(`${s.snapshot_count} snapshots, ${s.journal_count} journal entries`)}`); | ||
| break; | ||
| case 'scopeblind.macro.transparency-head/1': | ||
| if (s.log_id) lines.push(` Log: ${dim(s.log_id)}`); | ||
| lines.push(` Tree size: ${dim(`${s.tree_size} leaves`)}`); | ||
| if (s.root_hash) lines.push(` Root: ${dim(`${String(s.root_hash).slice(0, 16)}...`)}`); | ||
| break; | ||
| case 'scopeblind.macro.transparency-witness/1': | ||
| if (s.head_digest) lines.push(` Head: ${dim(`${String(s.head_digest).slice(0, 16)}...`)}`); | ||
| lines.push(` Tree size: ${dim(`${s.tree_size} leaves`)}`); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| return lines; | ||
| } | ||
| /** | ||
| * Format a ScopeBlind Gate receipt-tuple result. | ||
@@ -292,9 +235,7 @@ * | ||
| lines.push(`\n${icon} Signature: ${status}`); | ||
| lines.push(` Format: ${result.macroSchema ? 'ScopeBlind macro-engine snapshot' : 'ScopeBlind Gate receipt tuple'}`); | ||
| lines.push(` Format: ScopeBlind Gate receipt tuple`); | ||
| if (result.schema) { | ||
| const recog = result.macroSchema && result.schemaRecognized | ||
| ? dim('(recognized macro schema)') | ||
| : result.schemaRecognized | ||
| ? dim('(recognized)') | ||
| : yellow('(unrecognized schema; verified as a generic tuple)'); | ||
| const recog = result.schemaRecognized | ||
| ? dim('(recognized)') | ||
| : yellow('(unrecognized schema; verified as a generic tuple)'); | ||
| lines.push(` Schema: ${result.schema} ${recog}`); | ||
@@ -304,9 +245,6 @@ } else { | ||
| } | ||
| if (result.macroSummary) { | ||
| for (const line of macroSummaryLines(result.macroSummary)) lines.push(line); | ||
| } | ||
| if (result.algorithm) lines.push(` Algorithm: ${result.algorithm} (over the SHA-256 payload digest)`); | ||
| if (result.digest) lines.push(` Digest: ${dim(result.digest)}`); | ||
| if (result.recomputedDigest) lines.push(` Recomputed: ${red(result.recomputedDigest)}`); | ||
| if (result.publicKey) lines.push(` Signer: ${result.signerLabel ? bold(result.signerLabel) + ' ' : ''}${dim(result.publicKey)}`); | ||
| if (result.publicKey) lines.push(` Signer: ${dim(result.publicKey)}`); | ||
| if (result.keySource) lines.push(` Key: ${result.keySource}`); | ||
@@ -392,84 +330,33 @@ | ||
| if (result.algorithm) lines.push(` Algorithm: ${result.algorithm} ${dim('(over the canonical action payload, not a digest)')}`); | ||
| if (result.publicKey) lines.push(` Signer: ${result.signerLabel ? bold(result.signerLabel) + ' ' : ''}${dim(result.publicKey)}`); | ||
| if (result.publicKey) lines.push(` Signer: ${dim(result.publicKey)}`); | ||
| if (result.keySource) lines.push(` Key: ${result.keySource}`); | ||
| if (result.error && !result.valid) { | ||
| lines.push(` Error: ${red(result.error)}`); | ||
| if (result.error === 'invalid_signature') lines.push(` Detail: ${yellow('the signed bytes were altered or the key does not match — this receipt was tampered with')}`); | ||
| else if (result.detail) lines.push(` Detail: ${yellow(result.detail)}`); | ||
| if (result.expectedKey) lines.push(` Expected: ${yellow(result.expectedKey)} ${dim('(--key)')}`); | ||
| } | ||
| if (result.valid && Array.isArray(result.proves)) { | ||
| lines.push(` ${bold('This proves:')}`); | ||
| for (const p of result.proves) lines.push(` ${green('•')} ${dim(p)}`); | ||
| lines.push(` ${bold('This does not prove:')}`); | ||
| for (const l of (result.limitations || [])) lines.push(` ${yellow('!')} ${dim(l)}`); | ||
| } | ||
| lines.push(''); | ||
| lines.push(WAYFINDING); | ||
| lines.push(''); | ||
| return lines.join('\n'); | ||
| } | ||
| /** | ||
| * Format a Trusted Context Pack result: the signed attestation that a source file | ||
| * parsed to this context at this confidence and freshness. The output leads with | ||
| * the signer and the gate decision (usable / needs approval / blocked), names the | ||
| * source type, and is explicit that this attests the parse, not the provenance. | ||
| * | ||
| * @param {Object} result from src/engines/trusted-context-pack.js | ||
| * @param {Object} opts cli options | ||
| * @returns {string} | ||
| */ | ||
| export function formatTrustedContextPackResult(result, opts = {}) { | ||
| const lines = []; | ||
| if (result.valid && result.publicKey && result.publicKey.length === 64 && !isCI && !opts.noSigil) { | ||
| // Restraint receipts: surface WHAT was prevented and re-verify that the | ||
| // disclosed detail binds to the signed hashes. This is the negative-space | ||
| // proof: most governance can only show what an agent did; this shows, offline | ||
| // and position-blind, what it was stopped from doing and why. | ||
| if (result.valid && result.restraint) { | ||
| const rb = result.restraint; | ||
| lines.push(''); | ||
| lines.push(renderTerminalSigil(result.publicKey)); | ||
| lines.push(` ${bold('Prevented:')} ${dim('an out-of-mandate action the gate blocked before it could execute')}`); | ||
| if (rb.risk_band) lines.push(` Risk band: ${rb.risk_band}`); | ||
| if (rb.determining.length) lines.push(` Blocked by: ${rb.determining.join(', ')}`); | ||
| if (rb.proposed) { | ||
| const p = rb.proposed; | ||
| const notional = typeof p.notional === 'number' ? ` ${dim(`($${p.notional.toLocaleString('en-US')})`)}` : ''; | ||
| lines.push(` Blocked order: ${[p.side, p.qty, p.symbol].filter((x) => x != null && x !== '').join(' ')}${notional}`); | ||
| } else { | ||
| lines.push(` Blocked order: ${dim('withheld (position-blind)')}`); | ||
| } | ||
| lines.push(` ${rb.outcome_bound ? green('✓') : red('✗')} the outcome above re-hashes to the signed result hash`); | ||
| if (rb.proposed_bound === null) { | ||
| lines.push(` ${green('✓')} the blocked order is salt-committed to the signed input hash ${dim('(openable to a regulator)')}`); | ||
| } else { | ||
| lines.push(` ${rb.proposed_bound ? green('✓') : red('✗')} the disclosed order re-hashes (under its salt) to the signed input hash`); | ||
| } | ||
| } | ||
| const icon = result.valid ? green('✓') : red('✗'); | ||
| const status = result.valid ? green('VALID') : red('INVALID'); | ||
| lines.push(`\n${icon} Signature: ${status}`); | ||
| lines.push(` Format: ScopeBlind Trusted Context Pack`); | ||
| if (result.schema) { | ||
| lines.push(` Schema: ${result.schema} ${result.schemaRecognized ? dim('(recognized)') : yellow('(unrecognized)')}`); | ||
| } | ||
| const pf = result.payloadFields || {}; | ||
| if (pf.source_type) lines.push(` Source: ${pf.source_type}${pf.source_format ? dim(` (${pf.source_format})`) : ''}`); | ||
| if (pf.file_name) lines.push(` File: ${pf.file_name}`); | ||
| if (pf.file_hash) lines.push(` File hash: ${dim(`sha256:${String(pf.file_hash).slice(0, 16)}…`)}`); | ||
| // The gate decision is the headline: usable feeds a decision, the others do not. | ||
| const gate = result.gateStatus || pf.gate_status; | ||
| if (gate) { | ||
| const tone = gate === 'usable' ? green(gate) : gate === 'needs_approval' ? yellow(gate) : red(gate); | ||
| const note = gate === 'usable' ? 'may feed a gate decision' | ||
| : gate === 'needs_approval' ? 'held: requires explicit human approval before use' | ||
| : 'blocked: cannot feed a decision'; | ||
| lines.push(` Gate: ${tone} ${dim(`(${note})`)}`); | ||
| } | ||
| if (result.confidence !== undefined) lines.push(` Confidence: ${result.confidence}`); | ||
| if (pf.freshness) { | ||
| const dated = pf.freshness.as_of ? `as of ${String(pf.freshness.as_of).slice(0, 10)}` : yellow('no as-of date'); | ||
| const fresh = pf.freshness.stale ? red('(stale)') : dim('(fresh)'); | ||
| lines.push(` Freshness: ${dated} ${fresh}`); | ||
| } | ||
| if (Array.isArray(pf.warnings) && pf.warnings.length) { | ||
| lines.push(` ${bold('Warnings:')}`); | ||
| for (const w of pf.warnings) lines.push(` ${yellow('!')} ${dim(w)}`); | ||
| } | ||
| if (result.algorithm) lines.push(` Algorithm: ${result.algorithm} ${dim('(over the SHA-256 payload digest)')}`); | ||
| if (result.digest) lines.push(` Digest: ${dim(result.digest)}`); | ||
| if (result.recomputedDigest) lines.push(` Recomputed: ${red(result.recomputedDigest)}`); | ||
| if (result.publicKey) lines.push(` Signer: ${result.signerLabel ? bold(result.signerLabel) + ' ' : ''}${dim(result.publicKey)}`); | ||
| if (result.keySource) lines.push(` Key: ${result.keySource}`); | ||
| if (result.error && !result.valid) { | ||
| lines.push(` Error: ${red(result.error)}`); | ||
| if (result.error === 'invalid_signature') lines.push(` Detail: ${yellow('the signed bytes were altered or the key does not match; this pack was tampered with')}`); | ||
| if (result.error === 'invalid_signature') lines.push(` Detail: ${yellow('the signed bytes were altered or the key does not match — this receipt was tampered with')}`); | ||
| else if (result.detail) lines.push(` Detail: ${yellow(result.detail)}`); | ||
@@ -552,70 +439,2 @@ if (result.expectedKey) lines.push(` Expected: ${yellow(result.expectedKey)} ${dim('(--key)')}`); | ||
| /** | ||
| * Format a ScopeBlind macro-engine track-record bundle result. Like the Gate | ||
| * bundle formatter, crypto failures and chain (single-signer / manifest / | ||
| * history) failures are reported separately. | ||
| * | ||
| * @param {Object} result from src/engines/macro-snapshot.js | ||
| * @param {Object} opts cli options | ||
| * @returns {string} | ||
| */ | ||
| export function formatMacroTrackRecordResult(result, opts = {}) { | ||
| const lines = []; | ||
| const icon = result.valid ? green('✓') : red('✗'); | ||
| const status = result.valid ? green('VALID') : red('INVALID'); | ||
| lines.push(`\n${icon} Macro track-record bundle: ${status}`); | ||
| lines.push(` Schema: ${result.schema || '(missing)'}`); | ||
| if (result.exportedAt) lines.push(` Exported: ${dim(result.exportedAt)}`); | ||
| if (result.period) lines.push(` Period: ${dim(`${result.period.from} → ${result.period.to}`)}`); | ||
| if (result.custody) lines.push(` Custody: ${dim(result.custody)}`); | ||
| lines.push(` Identity: ${result.signerPinned ? green('pinned expected key') : yellow('embedded key only — integrity, not operator identity')}`); | ||
| if (result.sequence) lines.push(` Sequence: ${result.sequence}`); | ||
| lines.push(` Snapshots: ${result.snapshotCount}`); | ||
| lines.push(` Journal: ${result.journalCount}`); | ||
| lines.push(` Records: ${result.total} (${green(String(result.passed))} passed, ${result.failed > 0 ? red(String(result.failed)) : '0'} failed)`); | ||
| const cryptoOk = result.total - result.cryptoFailed; | ||
| lines.push(` Signatures: ${result.cryptoFailed > 0 ? red(`${cryptoOk}/${result.total} valid`) : green(`${cryptoOk}/${result.total} valid`)}`); | ||
| const chainOk = result.chainChecks - result.chainFailed; | ||
| lines.push(` Chain links: ${result.chainFailed > 0 ? red(`${chainOk}/${result.chainChecks} consistent`) : green(`${chainOk}/${result.chainChecks} consistent`)}`); | ||
| if (result.manifestValid !== null) lines.push(` Signed manifest: ${result.manifestValid ? green('valid and exact') : red('missing or inconsistent')}`); | ||
| lines.push(` Single-key consistency: ${result.singleSigner ? green('yes') : red('no')}`); | ||
| lines.push(` Append-only history: ${result.historyChainValid === true ? green('linked and retained') : result.historyChainValid === false ? red('invalid') : yellow('not established by this export')}`); | ||
| lines.push(` Signed checkpoint: ${result.historyAnchored ? green('present') : yellow('absent')}`); | ||
| if (result.transparencyAnchor) { | ||
| const t = result.transparencyAnchor; | ||
| const leaves = t.tree_size !== null ? `${t.tree_size} leaves` : 'Merkle log'; | ||
| if (t.anchor === 'witnessed') lines.push(` Transparency: ${green(`witness-anchored (Merkle log, ${leaves})`)}`); | ||
| else if (t.anchor === 'self_signed') lines.push(` Transparency: ${yellow(`self-signed (Merkle log, ${leaves})`)}`); | ||
| else lines.push(` Transparency: ${red('not anchored')}`); | ||
| } | ||
| if (result.historyHeadPinned) lines.push(` History head: ${green('matches independently pinned head')}`); | ||
| if (result.anchorHeadPinned) lines.push(` Anchor head: ${green('matches independently pinned head')}`); | ||
| if (Array.isArray(result.signers) && result.signers.length > 0) { | ||
| lines.push(` ${bold('Signing keys seen:')}`); | ||
| for (const s of result.signers) { | ||
| const tag = s.isModelKey ? green('model key') : yellow('other key'); | ||
| lines.push(` ${dim(s.key)} ${tag} ${dim(`(${s.roles.join(', ')})`)}`); | ||
| } | ||
| } | ||
| if (Array.isArray(result.errors) && result.errors.length > 0) { | ||
| lines.push(`\n ${red('Failures:')} ${dim('([crypto] record forged or modified; [chain] custody, manifest, or history-head inconsistent)')}`); | ||
| for (const e of result.errors) lines.push(` ${red('•')} ${e}`); | ||
| } | ||
| if (result.valid) { | ||
| lines.push(''); | ||
| lines.push(` ${bold('This proves:')}`); | ||
| for (const p of (result.proves || [])) lines.push(` ${green('•')} ${dim(p)}`); | ||
| lines.push(` ${bold('This does not prove:')}`); | ||
| for (const l of (result.limitations || [])) lines.push(` ${yellow('!')} ${dim(l)}`); | ||
| } | ||
| lines.push(''); | ||
| lines.push(WAYFINDING); | ||
| lines.push(''); | ||
| return lines.join('\n'); | ||
| } | ||
| export function formatKuResult(result, opts = {}) { | ||
@@ -622,0 +441,0 @@ const lines = []; |
| { | ||
| "_comment": "Map an Ed25519 verification_key (lowercase hex) to a human label so the CLI prints 'Signer: <label>' instead of raw hex. Add your desk/issuer keys here, or pass your own file with --known-issuers <path>. This is a display aid layered on --key pinning, never a trust shortcut: an unlabeled key still verifies, and a label is only as good as the map you chose to load.", | ||
| "_example": "0000000000000000000000000000000000000000000000000000000000000000 -> Example Desk (remove this)" | ||
| } |
| { | ||
| "type": "scopeblind.legate.proof-pack.v1", | ||
| "generated_at": "2026-06-22T23:31:19.884Z", | ||
| "period": null, | ||
| "position_blind": true, | ||
| "runtime": { | ||
| "name": "Toms-MacBook-Pro.local", | ||
| "verification_key": "ecfc8fe8e791d84204e4889c2ccae46da41d90c8bb88f028bee1f96790c3ae2a" | ||
| }, | ||
| "mandate": { | ||
| "name": "Global Macro IMA", | ||
| "sha256": "5ac8a3803737c74cbb5221ed0983ac8c1cc28954b42ed75e372ef275fd918df2", | ||
| "mode": "enforce", | ||
| "rule_count": 9, | ||
| "coverage": { | ||
| "local": [ | ||
| "gross-exposure", | ||
| "net-exposure", | ||
| "soft-gross-band", | ||
| "class-gross-rates", | ||
| "class-gross-fx", | ||
| "class-gross-equity", | ||
| "class-gross-credit", | ||
| "class-gross-commodity", | ||
| "dv01-book" | ||
| ], | ||
| "order_context": [], | ||
| "delegated": [] | ||
| } | ||
| }, | ||
| "policy": { | ||
| "name": "Legate default agent mandate", | ||
| "sha256": "650d036c3b05675f3719168a41bf0266482728b5f0156c646b2649cd804c040d", | ||
| "version": "1", | ||
| "rule_count": 5 | ||
| }, | ||
| "book": null, | ||
| "governed_actions": { | ||
| "evaluated": 0, | ||
| "allowed": 0, | ||
| "held": 0, | ||
| "blocked": 0, | ||
| "would_block": 0 | ||
| }, | ||
| "restraint": { | ||
| "blocked": 0, | ||
| "held": 0, | ||
| "by_rule": [] | ||
| }, | ||
| "shadow": { | ||
| "observed": 2, | ||
| "would_block": 0, | ||
| "would_hold": 2, | ||
| "by_rule": [ | ||
| { | ||
| "rule": "gross-exposure", | ||
| "count": 2 | ||
| }, | ||
| { | ||
| "rule": "net-exposure", | ||
| "count": 2 | ||
| }, | ||
| { | ||
| "rule": "class-gross-rates", | ||
| "count": 2 | ||
| }, | ||
| { | ||
| "rule": "class-gross-fx", | ||
| "count": 2 | ||
| }, | ||
| { | ||
| "rule": "class-gross-equity", | ||
| "count": 2 | ||
| }, | ||
| { | ||
| "rule": "class-gross-credit", | ||
| "count": 2 | ||
| }, | ||
| { | ||
| "rule": "class-gross-commodity", | ||
| "count": 2 | ||
| } | ||
| ], | ||
| "first_at": "2026-06-22T23:31:19.814Z", | ||
| "last_at": "2026-06-22T23:31:19.849Z" | ||
| }, | ||
| "session_merkle_root": "66308d59fa859823fe9ff774f4f4e91cb025f391327858c0e4192ab84504346d", | ||
| "receipt_count": 1, | ||
| "signer_kid": "sb:legate:ecfc8fe8", | ||
| "verification_key": "ecfc8fe8e791d84204e4889c2ccae46da41d90c8bb88f028bee1f96790c3ae2a", | ||
| "sha256": "b6c421a1d86cbfe34f9748b3976f2a8a3d3cd64cb6467f07c85e58f43ab7c360", | ||
| "signature": "31f1b2090f74157a55445411c1555141f656a88f089a459488e88e155f37d5a633fa62786d4974699e19536221328882651e6d8f471cc1226450f5d203e8eb0b" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.alert/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "alert_id": "915720cc951de3dfb99437ebac0362f02947160eca28841e6803c37979019cbb", | ||
| "kind": "market_state_change", | ||
| "severity": "critical", | ||
| "title": "Market state: mixed -> stress", | ||
| "detail": "Daily market-state classification changed (confidence 0.8).", | ||
| "refs": [ | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "d9602f7915b69bcbf7f86c0cf1b0387965e1b448918be92339ed96d138449d80" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-02-05", | ||
| "digest": "d47770c9fdcbe921596497fc1f63d18f86d8bbef302abc060d4d0e1cb7dc9e53" | ||
| } | ||
| ], | ||
| "budget": { | ||
| "position": 1, | ||
| "max_per_day": 2 | ||
| } | ||
| }, | ||
| "digest": "bb942d2f4fe547ffba9e7eec6518d32d5d61019c6c85da6aad4dfa051b3df460", | ||
| "signature": "a3ddc18d8087ae63a4b4d967d3d12a28db2476ef143f0ed21d2d1b4ecae7187eb8310a2e87ee5534c2250a533deeadb450c6e1003bcf1c43b731f96929738800", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.journal-entry/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "author": "demo-model", | ||
| "note": "Regime read stagflation while the daily tape printed stress. Cut gross and lengthened nothing; the book is long the wrong factors here.", | ||
| "references": [ | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.vulnerability/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "d277793511a0dcda199889d8beedbe849146cf0fc4cb216ce1dc77cadf20b237" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "as_of": "2026-05-01", | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591" | ||
| } | ||
| ], | ||
| "tags": [ | ||
| "risk-off", | ||
| "stagflation", | ||
| "vulnerability" | ||
| ] | ||
| }, | ||
| "digest": "95415964066eaf10e5de3e49dd18763c8c7d79099baa0bbe670b402f52243923", | ||
| "signature": "adce443ba5de6d2e2edea9b5cc19744ae0a7842ad552860c03e895e5393434f4f0448bb5eb5d81f2aae7cbf222494931bfcab30ad465931f073ca2350e042b0c", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "universe_digest": "e71b066c5c05624984187e810d8dc3422964825d7cefa5338f738d57a1e1dd76", | ||
| "inputs_digest": "51b8add91569d3da3b761f5c0a1fefba25fabbe486214fe36a6204347cc904e3", | ||
| "pillars": { | ||
| "trend": -2, | ||
| "breadth": 0, | ||
| "liquidity": -2, | ||
| "credit": -2, | ||
| "volatility": -2 | ||
| }, | ||
| "evidence": { | ||
| "trend": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "SPY", | ||
| "feature": "ma_structure", | ||
| "value": -2, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "QQQ", | ||
| "ACWI" | ||
| ] | ||
| }, | ||
| "breadth": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "RSP/SPY", | ||
| "feature": "ret20d_z", | ||
| "value": 0, | ||
| "vote": 0 | ||
| }, | ||
| { | ||
| "symbol": "IWM/SPY", | ||
| "feature": "ret20d_z", | ||
| "value": 0, | ||
| "vote": 0 | ||
| } | ||
| ], | ||
| "missing": [] | ||
| }, | ||
| "liquidity": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "UUP", | ||
| "feature": "ret20d_z_inv", | ||
| "value": -6.554929, | ||
| "vote": -2 | ||
| }, | ||
| { | ||
| "symbol": "BTCUSD", | ||
| "feature": "ret20d_z", | ||
| "value": -6.478535, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "EEM/EFA", | ||
| "SMH/SPY" | ||
| ] | ||
| }, | ||
| "credit": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "HYG/IEF", | ||
| "feature": "ret20d_z", | ||
| "value": -6.352958, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "LQD/IEF", | ||
| "XLF/SPY" | ||
| ] | ||
| }, | ||
| "volatility": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "SPY", | ||
| "feature": "rvol20_pctile_1y", | ||
| "value": 0.932806, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "VIX" | ||
| ] | ||
| } | ||
| }, | ||
| "classification": "stress", | ||
| "confidence": 0.8, | ||
| "would_change": [ | ||
| "robust: no single-pillar one-notch change alters this classification" | ||
| ], | ||
| "notes": [ | ||
| "HYG: HYG/IEF ratio proxies HY OAS direction intraday; true OAS (daily, lagged) comes from FRED BAMLH0A0HYM2 in the regime layer.", | ||
| "RSP: Equal-weight ratio proxies constituent breadth; true %>200dma needs constituent data (licensing-gated).", | ||
| "UUP: DXY proxy; futures DX licensing-gated at this tier.", | ||
| "degraded: missing inputs ACWI, EEM/EFA, LQD/IEF, QQQ, SMH/SPY, VIX, XLF/SPY" | ||
| ] | ||
| }, | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94", | ||
| "signature": "0f93202e78e81ea84ba36e2c2e787ef493aba32c61140a948553f314a3403f042220ac64cf04e8d22e9044b2e6e9712587b1e023cb0dd796353543609aec380a", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.price-snapshot/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2026-06-12T20:00:00Z", | ||
| "source": "Massive Market Data daily settle (delayed feed; not a live tick)", | ||
| "delay_minutes": 15, | ||
| "levels": { | ||
| "rates": { | ||
| "level": 85.77, | ||
| "instrument": "TLT", | ||
| "unit": "price", | ||
| "note": "long-duration UST proxy; loads via DV01, not notional" | ||
| }, | ||
| "equity": { | ||
| "level": 741.75, | ||
| "instrument": "SPY", | ||
| "unit": "price", | ||
| "multiplier": 500, | ||
| "note": "ES notional = level x 500 (x10 index ratio, $50 point value)" | ||
| }, | ||
| "fx_eur": { | ||
| "level": 1.15655, | ||
| "instrument": "EURUSD", | ||
| "unit": "fx_rate" | ||
| }, | ||
| "fx_jpy": { | ||
| "level": 160.215, | ||
| "instrument": "USDJPY", | ||
| "unit": "fx_rate" | ||
| }, | ||
| "credit_ig": { | ||
| "level": 109.01, | ||
| "instrument": "LQD", | ||
| "unit": "price", | ||
| "note": "IG proxy; loads via CS01, not notional" | ||
| }, | ||
| "credit_hy": { | ||
| "level": 79.94, | ||
| "instrument": "HYG", | ||
| "unit": "price", | ||
| "note": "HY proxy; loads via CS01, not notional" | ||
| }, | ||
| "commodity_oil": { | ||
| "level": 125.43, | ||
| "instrument": "USO", | ||
| "unit": "price", | ||
| "note": "crude proxy ETF (roll-affected); provenance only" | ||
| }, | ||
| "commodity_gold": { | ||
| "level": 386.54, | ||
| "instrument": "GLD", | ||
| "unit": "price", | ||
| "note": "gold proxy ETF; provenance only" | ||
| } | ||
| }, | ||
| "coverage": [ | ||
| "commodity_gold", | ||
| "commodity_oil", | ||
| "credit_hy", | ||
| "credit_ig", | ||
| "equity", | ||
| "fx_eur", | ||
| "fx_jpy", | ||
| "rates" | ||
| ], | ||
| "missing": [], | ||
| "notes": [ | ||
| "levels are official daily closes (settle) for the 2026-06-12 session, from a delayed feed", | ||
| "instruments are liquid ETF/FX proxies, not the futures themselves; the snapshot never overstates what it observed", | ||
| "only the equity (ES) factor carries a notional multiplier; the rest are signed provenance and load via DV01/CS01" | ||
| ] | ||
| }, | ||
| "digest": "32a46e47fd2639b9991dfd8133c600c8ed7e9a86b56f3ec5372870c35624c165", | ||
| "signature": "676678d1d4ce516869d80f185bf3fd207d33c02109e4deb4e6fc5a8a2a557ed3ac6c12b3f8f12d55c03b7670a20e457dd86ccfbd5fe994015d0ed2e8f782a50b", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2026-05-01", | ||
| "pillars": { | ||
| "growth": -2, | ||
| "inflation": 2, | ||
| "liquidity": -2, | ||
| "policy_freedom": -2, | ||
| "credit_conditions": -2 | ||
| }, | ||
| "inputs": [ | ||
| { | ||
| "series_id": "ICSA", | ||
| "transform": "diff13w", | ||
| "value": 35123.671753, | ||
| "z": -6.764746, | ||
| "vote": -2, | ||
| "asof_digest": "73fa36d5ba21ff19e82757760ffc56899c4d33ca617cbdf79523e2eb1550f573", | ||
| "latest_vintage": "2026-04-29" | ||
| }, | ||
| { | ||
| "series_id": "PAYEMS", | ||
| "transform": "mom3_ann", | ||
| "value": -0.036344, | ||
| "z": -3.501479, | ||
| "vote": -2, | ||
| "asof_digest": "64ca109e3b7d52348a813e5c7fa3695ce0801b30a9ac8cc2aa7cc248e6292abf", | ||
| "latest_vintage": "2026-04-08" | ||
| }, | ||
| { | ||
| "series_id": "INDPRO", | ||
| "transform": "mom6_ann", | ||
| "value": -0.036233, | ||
| "z": -4.775509, | ||
| "vote": -2, | ||
| "asof_digest": "880c1c9898d45bca6a5d9803d3a687e7702ada5b4dee082d68e685fb8540be82", | ||
| "latest_vintage": "2026-04-17" | ||
| }, | ||
| { | ||
| "series_id": "RSXFS", | ||
| "transform": "mom3_ann", | ||
| "value": -0.036344, | ||
| "z": -3.485408, | ||
| "vote": -2, | ||
| "asof_digest": "87bb798c4e9efdfb41f31a11cc3f49f96fe9db48d753cd5aaa0643f3b4a59e1d", | ||
| "latest_vintage": "2026-04-17" | ||
| }, | ||
| { | ||
| "series_id": "USALOLITONOSTSAM", | ||
| "transform": "diff6m", | ||
| "value": -1.152544, | ||
| "z": -5.744365, | ||
| "vote": -2, | ||
| "asof_digest": "4e7ae6048b479286b801201eebf6db1d28ada976b1841d8b41cb5d95c99d9929", | ||
| "latest_vintage": "2026-04-10" | ||
| }, | ||
| { | ||
| "series_id": "CPILFESL", | ||
| "transform": "mom3_ann", | ||
| "value": 0.069551, | ||
| "z": 3.342129, | ||
| "vote": 2, | ||
| "asof_digest": "b0a91055ce6a6a879010796266ef9236bb911a888a54ce6af7b24e6010183ca3", | ||
| "latest_vintage": "2026-04-14" | ||
| }, | ||
| { | ||
| "series_id": "PCEPILFE", | ||
| "transform": "mom3_ann", | ||
| "value": 0.067001, | ||
| "z": 3.342148, | ||
| "vote": 2, | ||
| "asof_digest": "b53a27bb365e01fa1e2ac25f446930ff6f852c4ed2a6fd2eefff38cc96c9da57", | ||
| "latest_vintage": "2026-04-29" | ||
| }, | ||
| { | ||
| "series_id": "T10YIE", | ||
| "transform": "diff3m", | ||
| "value": 0.153311, | ||
| "z": 5.226471, | ||
| "vote": 2, | ||
| "asof_digest": "b839addb59d3c4a84a0a4b27e6ad47636b83a3729b1cc7144bea8dd58db2ff73", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "DCOILWTICO", | ||
| "transform": "mom3_ann", | ||
| "value": 0.653636, | ||
| "z": 5.382054, | ||
| "vote": 2, | ||
| "asof_digest": "97178dd3c33fa82c7479b66c6132a7d8d96e47ccc39ffc4ba93d64cc38ab65b8", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "AHETPI", | ||
| "transform": "mom3_ann", | ||
| "value": 0.067001, | ||
| "z": 3.3094, | ||
| "vote": 2, | ||
| "asof_digest": "783bab21a0e6c7c761d4c62ceab7c582cefa6d050b2e03898a4a62f80971ef90", | ||
| "latest_vintage": "2026-04-08" | ||
| }, | ||
| { | ||
| "series_id": "WALCL", | ||
| "transform": "diff13w", | ||
| "value": -355139.244176, | ||
| "z": -6.539508, | ||
| "vote": -2, | ||
| "asof_digest": "af2dd3799a152fdb8171b203aec25bd0b0e276e87df7be9bdc8a989f48a51346", | ||
| "latest_vintage": "2026-04-25" | ||
| }, | ||
| { | ||
| "series_id": "WTREGEN", | ||
| "transform": "diff13w", | ||
| "value": 98346.28091, | ||
| "z": -6.764746, | ||
| "vote": -2, | ||
| "asof_digest": "0e493e335271dcbeeae69f95d4683b35988eba5248e525fed6151b840f127989", | ||
| "latest_vintage": "2026-04-25" | ||
| }, | ||
| { | ||
| "series_id": "RRPONTSYD", | ||
| "transform": "diff13w", | ||
| "value": 158.625566, | ||
| "z": -5.60151, | ||
| "vote": -2, | ||
| "asof_digest": "62bd49d946bd279358ff87d32d370fa6d0a68c37682b1ba7ad99e3bfd1687daa", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "M2SL", | ||
| "transform": "mom6_ann", | ||
| "value": -0.036233, | ||
| "z": -4.775509, | ||
| "vote": -2, | ||
| "asof_digest": "8b96e538db1b6659919cb95408ff3d5ee407783a03e7f26e0920aa50123e9a19", | ||
| "latest_vintage": "2026-04-27" | ||
| }, | ||
| { | ||
| "series_id": "DTWEXBGS", | ||
| "transform": "diff3m", | ||
| "value": 7.998853, | ||
| "z": -5.226471, | ||
| "vote": -2, | ||
| "asof_digest": "05d20fa0c9d7d5f2dff3d84ec7ff62579e7eaea87ff66bb50a2a8e869165de5d", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "DFEDTARU", | ||
| "transform": "level", | ||
| "value": 0.999608, | ||
| "z": null, | ||
| "vote": null, | ||
| "asof_digest": "3922373305be0976cddffb6778d61343e9289d3f6d3fe8039a2ca95a765c735f", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "DFII10", | ||
| "transform": "diff3m", | ||
| "value": 0.119983, | ||
| "z": -5.226471, | ||
| "vote": -2, | ||
| "asof_digest": "474a4dd5f297eb398235b01334b7fc283979b30c96c1b1ac57351eedc6064cdf", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "T5YIFR", | ||
| "transform": "level", | ||
| "value": 3.201024, | ||
| "z": null, | ||
| "vote": -2, | ||
| "asof_digest": "b8e076fbcd90990be22e3f6fc53461a74d5f0a4553cb0152b70c4fea50f4530f", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "BAMLH0A0HYM2", | ||
| "transform": "diff13w", | ||
| "value": 1.110379, | ||
| "z": -5.60151, | ||
| "vote": -2, | ||
| "asof_digest": "8ec3d58658b36d414849b002fef15350817a81ddcb81e9e203a9ada8bd8d1c3f", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "BAMLC0A0CM", | ||
| "transform": "diff13w", | ||
| "value": 0.073323, | ||
| "z": -5.226471, | ||
| "vote": -2, | ||
| "asof_digest": "370a4e107534d46a1a542d1e16ce9f9f4a17aeeb377f9e5c4481f1e0f3a2facb", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "NFCI", | ||
| "transform": "level", | ||
| "value": 0.550016, | ||
| "z": null, | ||
| "vote": -2, | ||
| "asof_digest": "f0f61455659b69270627f8c5cf767fc2f5dc72985c36d682edad857629df4d9d", | ||
| "latest_vintage": "2026-04-29" | ||
| }, | ||
| { | ||
| "series_id": "NET_LIQUIDITY", | ||
| "transform": "diff13w", | ||
| "value": -650.297517, | ||
| "z": -6.328823, | ||
| "vote": -2, | ||
| "asof_digest": "1e43b543c63afcf7659de4c5ddaf99ba96dc4d1c7f55052aa392b0aadd1207a9", | ||
| "latest_vintage": "2026-04-25" | ||
| }, | ||
| { | ||
| "series_id": "REAL_POLICY_RATE", | ||
| "transform": "level", | ||
| "value": -3.905295, | ||
| "z": null, | ||
| "vote": -2, | ||
| "asof_digest": "9427fbf69d076b6f3a9536742e8d4f2ac15c4e4085075a8b60273d34970b0f4d", | ||
| "latest_vintage": "2026-05-01" | ||
| } | ||
| ], | ||
| "vintage_digest": "77304f7e1203df3211824c78c7c9e800a0dd52b744f5173324524174ad96d3bc", | ||
| "candidate_regime": "stagflation", | ||
| "regime": "stagflation", | ||
| "liquidity_overlay": "contraction", | ||
| "hysteresis": { | ||
| "current": "stagflation", | ||
| "candidate": null, | ||
| "candidate_weeks": 0, | ||
| "weeks_since_switch": 999 | ||
| }, | ||
| "transition": null, | ||
| "playbook": { | ||
| "prefer": [ | ||
| "energy", | ||
| "gold", | ||
| "dollar", | ||
| "defensives", | ||
| "cash" | ||
| ], | ||
| "avoid": [ | ||
| "unhedged cyclicals", | ||
| "levered credit" | ||
| ], | ||
| "confidence": "medium" | ||
| }, | ||
| "confidence": 0.956522, | ||
| "would_change": [ | ||
| "robust: no single-pillar one-notch change alters this candidate regime" | ||
| ], | ||
| "notes": [ | ||
| "degraded: no usable vote from DFEDTARU", | ||
| "Net liquidity (WALCL - TGA - RRP) validity is regime-dependent: strongest when reserve scarcity binds.", | ||
| "PMI family is licensing-gated: growth diffusion uses claims, payrolls, production, retail, and OECD CLI." | ||
| ] | ||
| }, | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591", | ||
| "signature": "806c531043618f2c4da47180ace2020def35fbf12bbf603061e4fb3264b97940c2bcfd50e395d16c8f2e45bdbe2ff69a84c00ddbe330fcaa29bc95296da66207", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.tape-snapshot/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "session": "daily_close", | ||
| "tape_type": "mixed", | ||
| "coherence": 0, | ||
| "material": true, | ||
| "signals": [ | ||
| { | ||
| "id": "breadth_rel", | ||
| "z": 0 | ||
| }, | ||
| { | ||
| "id": "credit", | ||
| "z": -2.631816 | ||
| }, | ||
| { | ||
| "id": "dollar", | ||
| "z": 3.61392 | ||
| }, | ||
| { | ||
| "id": "duration", | ||
| "z": 3.20995 | ||
| }, | ||
| { | ||
| "id": "equity", | ||
| "z": -3.860863 | ||
| }, | ||
| { | ||
| "id": "high_beta", | ||
| "z": -4.467346 | ||
| }, | ||
| { | ||
| "id": "smallcap_rel", | ||
| "z": 0 | ||
| } | ||
| ], | ||
| "unavailable": [ | ||
| "banks_rel", | ||
| "copper", | ||
| "gold", | ||
| "growth_beta", | ||
| "oil" | ||
| ], | ||
| "untestable_types": [ | ||
| "inflation_shock" | ||
| ], | ||
| "attribution": { | ||
| "tier": "unknown", | ||
| "detail": "material moves without a coherent cross-asset pattern or scheduled catalyst", | ||
| "events": [] | ||
| }, | ||
| "inputs_digest": "c202156a133cdee47311f843fbc18082311ca653dd4293496ea1c47346fa68c7", | ||
| "would_change": [ | ||
| "a coherent cross-asset pattern forming would move this off mixed" | ||
| ], | ||
| "notes": [ | ||
| "degraded: unavailable signals banks_rel, copper, gold, growth_beta, oil", | ||
| "untestable tape types with current data: inflation_shock", | ||
| "session basis: daily close-to-close; intraday windows plug into the same rules when a live feed is configured" | ||
| ] | ||
| }, | ||
| "digest": "43e98d770cb3b4e47c915164d7fe0aa7ec257968978a5cd1aa914e9e2403decd", | ||
| "signature": "f7cf06b796186a7a6b76053f30b5dd3b96affb6b3853df78550719576d0059bc4a36d34c74afd3d122770e34cbdda03e3f0adcd794c314396a411d3ee5f2590f", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "schema": "scopeblind.macro.track-record-bundle/1", | ||
| "version": "0.1.0", | ||
| "exported_at": "2026-05-01T00:00:00Z", | ||
| "model_verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3", | ||
| "custody": "dev-deterministic", | ||
| "period": { | ||
| "from": "2025-01-02", | ||
| "to": "2026-05-01" | ||
| }, | ||
| "snapshots": [ | ||
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "universe_digest": "e71b066c5c05624984187e810d8dc3422964825d7cefa5338f738d57a1e1dd76", | ||
| "inputs_digest": "51b8add91569d3da3b761f5c0a1fefba25fabbe486214fe36a6204347cc904e3", | ||
| "pillars": { | ||
| "trend": -2, | ||
| "breadth": 0, | ||
| "liquidity": -2, | ||
| "credit": -2, | ||
| "volatility": -2 | ||
| }, | ||
| "evidence": { | ||
| "trend": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "SPY", | ||
| "feature": "ma_structure", | ||
| "value": -2, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "QQQ", | ||
| "ACWI" | ||
| ] | ||
| }, | ||
| "breadth": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "RSP/SPY", | ||
| "feature": "ret20d_z", | ||
| "value": 0, | ||
| "vote": 0 | ||
| }, | ||
| { | ||
| "symbol": "IWM/SPY", | ||
| "feature": "ret20d_z", | ||
| "value": 0, | ||
| "vote": 0 | ||
| } | ||
| ], | ||
| "missing": [] | ||
| }, | ||
| "liquidity": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "UUP", | ||
| "feature": "ret20d_z_inv", | ||
| "value": -6.554929, | ||
| "vote": -2 | ||
| }, | ||
| { | ||
| "symbol": "BTCUSD", | ||
| "feature": "ret20d_z", | ||
| "value": -6.478535, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "EEM/EFA", | ||
| "SMH/SPY" | ||
| ] | ||
| }, | ||
| "credit": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "HYG/IEF", | ||
| "feature": "ret20d_z", | ||
| "value": -6.352958, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "LQD/IEF", | ||
| "XLF/SPY" | ||
| ] | ||
| }, | ||
| "volatility": { | ||
| "inputs": [ | ||
| { | ||
| "symbol": "SPY", | ||
| "feature": "rvol20_pctile_1y", | ||
| "value": 0.932806, | ||
| "vote": -2 | ||
| } | ||
| ], | ||
| "missing": [ | ||
| "VIX" | ||
| ] | ||
| } | ||
| }, | ||
| "classification": "stress", | ||
| "confidence": 0.8, | ||
| "would_change": [ | ||
| "robust: no single-pillar one-notch change alters this classification" | ||
| ], | ||
| "notes": [ | ||
| "HYG: HYG/IEF ratio proxies HY OAS direction intraday; true OAS (daily, lagged) comes from FRED BAMLH0A0HYM2 in the regime layer.", | ||
| "RSP: Equal-weight ratio proxies constituent breadth; true %>200dma needs constituent data (licensing-gated).", | ||
| "UUP: DXY proxy; futures DX licensing-gated at this tier.", | ||
| "degraded: missing inputs ACWI, EEM/EFA, LQD/IEF, QQQ, SMH/SPY, VIX, XLF/SPY" | ||
| ] | ||
| }, | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94", | ||
| "signature": "0f93202e78e81ea84ba36e2c2e787ef493aba32c61140a948553f314a3403f042220ac64cf04e8d22e9044b2e6e9712587b1e023cb0dd796353543609aec380a", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| }, | ||
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.tape-snapshot/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "session": "daily_close", | ||
| "tape_type": "mixed", | ||
| "coherence": 0, | ||
| "material": true, | ||
| "signals": [ | ||
| { | ||
| "id": "breadth_rel", | ||
| "z": 0 | ||
| }, | ||
| { | ||
| "id": "credit", | ||
| "z": -2.631816 | ||
| }, | ||
| { | ||
| "id": "dollar", | ||
| "z": 3.61392 | ||
| }, | ||
| { | ||
| "id": "duration", | ||
| "z": 3.20995 | ||
| }, | ||
| { | ||
| "id": "equity", | ||
| "z": -3.860863 | ||
| }, | ||
| { | ||
| "id": "high_beta", | ||
| "z": -4.467346 | ||
| }, | ||
| { | ||
| "id": "smallcap_rel", | ||
| "z": 0 | ||
| } | ||
| ], | ||
| "unavailable": [ | ||
| "banks_rel", | ||
| "copper", | ||
| "gold", | ||
| "growth_beta", | ||
| "oil" | ||
| ], | ||
| "untestable_types": [ | ||
| "inflation_shock" | ||
| ], | ||
| "attribution": { | ||
| "tier": "unknown", | ||
| "detail": "material moves without a coherent cross-asset pattern or scheduled catalyst", | ||
| "events": [] | ||
| }, | ||
| "inputs_digest": "c202156a133cdee47311f843fbc18082311ca653dd4293496ea1c47346fa68c7", | ||
| "would_change": [ | ||
| "a coherent cross-asset pattern forming would move this off mixed" | ||
| ], | ||
| "notes": [ | ||
| "degraded: unavailable signals banks_rel, copper, gold, growth_beta, oil", | ||
| "untestable tape types with current data: inflation_shock", | ||
| "session basis: daily close-to-close; intraday windows plug into the same rules when a live feed is configured" | ||
| ] | ||
| }, | ||
| "digest": "43e98d770cb3b4e47c915164d7fe0aa7ec257968978a5cd1aa914e9e2403decd", | ||
| "signature": "f7cf06b796186a7a6b76053f30b5dd3b96affb6b3853df78550719576d0059bc4a36d34c74afd3d122770e34cbdda03e3f0adcd794c314396a411d3ee5f2590f", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| }, | ||
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2026-05-01", | ||
| "pillars": { | ||
| "growth": -2, | ||
| "inflation": 2, | ||
| "liquidity": -2, | ||
| "policy_freedom": -2, | ||
| "credit_conditions": -2 | ||
| }, | ||
| "inputs": [ | ||
| { | ||
| "series_id": "ICSA", | ||
| "transform": "diff13w", | ||
| "value": 35123.671753, | ||
| "z": -6.764746, | ||
| "vote": -2, | ||
| "asof_digest": "73fa36d5ba21ff19e82757760ffc56899c4d33ca617cbdf79523e2eb1550f573", | ||
| "latest_vintage": "2026-04-29" | ||
| }, | ||
| { | ||
| "series_id": "PAYEMS", | ||
| "transform": "mom3_ann", | ||
| "value": -0.036344, | ||
| "z": -3.501479, | ||
| "vote": -2, | ||
| "asof_digest": "64ca109e3b7d52348a813e5c7fa3695ce0801b30a9ac8cc2aa7cc248e6292abf", | ||
| "latest_vintage": "2026-04-08" | ||
| }, | ||
| { | ||
| "series_id": "INDPRO", | ||
| "transform": "mom6_ann", | ||
| "value": -0.036233, | ||
| "z": -4.775509, | ||
| "vote": -2, | ||
| "asof_digest": "880c1c9898d45bca6a5d9803d3a687e7702ada5b4dee082d68e685fb8540be82", | ||
| "latest_vintage": "2026-04-17" | ||
| }, | ||
| { | ||
| "series_id": "RSXFS", | ||
| "transform": "mom3_ann", | ||
| "value": -0.036344, | ||
| "z": -3.485408, | ||
| "vote": -2, | ||
| "asof_digest": "87bb798c4e9efdfb41f31a11cc3f49f96fe9db48d753cd5aaa0643f3b4a59e1d", | ||
| "latest_vintage": "2026-04-17" | ||
| }, | ||
| { | ||
| "series_id": "USALOLITONOSTSAM", | ||
| "transform": "diff6m", | ||
| "value": -1.152544, | ||
| "z": -5.744365, | ||
| "vote": -2, | ||
| "asof_digest": "4e7ae6048b479286b801201eebf6db1d28ada976b1841d8b41cb5d95c99d9929", | ||
| "latest_vintage": "2026-04-10" | ||
| }, | ||
| { | ||
| "series_id": "CPILFESL", | ||
| "transform": "mom3_ann", | ||
| "value": 0.069551, | ||
| "z": 3.342129, | ||
| "vote": 2, | ||
| "asof_digest": "b0a91055ce6a6a879010796266ef9236bb911a888a54ce6af7b24e6010183ca3", | ||
| "latest_vintage": "2026-04-14" | ||
| }, | ||
| { | ||
| "series_id": "PCEPILFE", | ||
| "transform": "mom3_ann", | ||
| "value": 0.067001, | ||
| "z": 3.342148, | ||
| "vote": 2, | ||
| "asof_digest": "b53a27bb365e01fa1e2ac25f446930ff6f852c4ed2a6fd2eefff38cc96c9da57", | ||
| "latest_vintage": "2026-04-29" | ||
| }, | ||
| { | ||
| "series_id": "T10YIE", | ||
| "transform": "diff3m", | ||
| "value": 0.153311, | ||
| "z": 5.226471, | ||
| "vote": 2, | ||
| "asof_digest": "b839addb59d3c4a84a0a4b27e6ad47636b83a3729b1cc7144bea8dd58db2ff73", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "DCOILWTICO", | ||
| "transform": "mom3_ann", | ||
| "value": 0.653636, | ||
| "z": 5.382054, | ||
| "vote": 2, | ||
| "asof_digest": "97178dd3c33fa82c7479b66c6132a7d8d96e47ccc39ffc4ba93d64cc38ab65b8", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "AHETPI", | ||
| "transform": "mom3_ann", | ||
| "value": 0.067001, | ||
| "z": 3.3094, | ||
| "vote": 2, | ||
| "asof_digest": "783bab21a0e6c7c761d4c62ceab7c582cefa6d050b2e03898a4a62f80971ef90", | ||
| "latest_vintage": "2026-04-08" | ||
| }, | ||
| { | ||
| "series_id": "WALCL", | ||
| "transform": "diff13w", | ||
| "value": -355139.244176, | ||
| "z": -6.539508, | ||
| "vote": -2, | ||
| "asof_digest": "af2dd3799a152fdb8171b203aec25bd0b0e276e87df7be9bdc8a989f48a51346", | ||
| "latest_vintage": "2026-04-25" | ||
| }, | ||
| { | ||
| "series_id": "WTREGEN", | ||
| "transform": "diff13w", | ||
| "value": 98346.28091, | ||
| "z": -6.764746, | ||
| "vote": -2, | ||
| "asof_digest": "0e493e335271dcbeeae69f95d4683b35988eba5248e525fed6151b840f127989", | ||
| "latest_vintage": "2026-04-25" | ||
| }, | ||
| { | ||
| "series_id": "RRPONTSYD", | ||
| "transform": "diff13w", | ||
| "value": 158.625566, | ||
| "z": -5.60151, | ||
| "vote": -2, | ||
| "asof_digest": "62bd49d946bd279358ff87d32d370fa6d0a68c37682b1ba7ad99e3bfd1687daa", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "M2SL", | ||
| "transform": "mom6_ann", | ||
| "value": -0.036233, | ||
| "z": -4.775509, | ||
| "vote": -2, | ||
| "asof_digest": "8b96e538db1b6659919cb95408ff3d5ee407783a03e7f26e0920aa50123e9a19", | ||
| "latest_vintage": "2026-04-27" | ||
| }, | ||
| { | ||
| "series_id": "DTWEXBGS", | ||
| "transform": "diff3m", | ||
| "value": 7.998853, | ||
| "z": -5.226471, | ||
| "vote": -2, | ||
| "asof_digest": "05d20fa0c9d7d5f2dff3d84ec7ff62579e7eaea87ff66bb50a2a8e869165de5d", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "DFEDTARU", | ||
| "transform": "level", | ||
| "value": 0.999608, | ||
| "z": null, | ||
| "vote": null, | ||
| "asof_digest": "3922373305be0976cddffb6778d61343e9289d3f6d3fe8039a2ca95a765c735f", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "DFII10", | ||
| "transform": "diff3m", | ||
| "value": 0.119983, | ||
| "z": -5.226471, | ||
| "vote": -2, | ||
| "asof_digest": "474a4dd5f297eb398235b01334b7fc283979b30c96c1b1ac57351eedc6064cdf", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "T5YIFR", | ||
| "transform": "level", | ||
| "value": 3.201024, | ||
| "z": null, | ||
| "vote": -2, | ||
| "asof_digest": "b8e076fbcd90990be22e3f6fc53461a74d5f0a4553cb0152b70c4fea50f4530f", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "BAMLH0A0HYM2", | ||
| "transform": "diff13w", | ||
| "value": 1.110379, | ||
| "z": -5.60151, | ||
| "vote": -2, | ||
| "asof_digest": "8ec3d58658b36d414849b002fef15350817a81ddcb81e9e203a9ada8bd8d1c3f", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "BAMLC0A0CM", | ||
| "transform": "diff13w", | ||
| "value": 0.073323, | ||
| "z": -5.226471, | ||
| "vote": -2, | ||
| "asof_digest": "370a4e107534d46a1a542d1e16ce9f9f4a17aeeb377f9e5c4481f1e0f3a2facb", | ||
| "latest_vintage": "2026-05-01" | ||
| }, | ||
| { | ||
| "series_id": "NFCI", | ||
| "transform": "level", | ||
| "value": 0.550016, | ||
| "z": null, | ||
| "vote": -2, | ||
| "asof_digest": "f0f61455659b69270627f8c5cf767fc2f5dc72985c36d682edad857629df4d9d", | ||
| "latest_vintage": "2026-04-29" | ||
| }, | ||
| { | ||
| "series_id": "NET_LIQUIDITY", | ||
| "transform": "diff13w", | ||
| "value": -650.297517, | ||
| "z": -6.328823, | ||
| "vote": -2, | ||
| "asof_digest": "1e43b543c63afcf7659de4c5ddaf99ba96dc4d1c7f55052aa392b0aadd1207a9", | ||
| "latest_vintage": "2026-04-25" | ||
| }, | ||
| { | ||
| "series_id": "REAL_POLICY_RATE", | ||
| "transform": "level", | ||
| "value": -3.905295, | ||
| "z": null, | ||
| "vote": -2, | ||
| "asof_digest": "9427fbf69d076b6f3a9536742e8d4f2ac15c4e4085075a8b60273d34970b0f4d", | ||
| "latest_vintage": "2026-05-01" | ||
| } | ||
| ], | ||
| "vintage_digest": "77304f7e1203df3211824c78c7c9e800a0dd52b744f5173324524174ad96d3bc", | ||
| "candidate_regime": "stagflation", | ||
| "regime": "stagflation", | ||
| "liquidity_overlay": "contraction", | ||
| "hysteresis": { | ||
| "current": "stagflation", | ||
| "candidate": null, | ||
| "candidate_weeks": 0, | ||
| "weeks_since_switch": 999 | ||
| }, | ||
| "transition": null, | ||
| "playbook": { | ||
| "prefer": [ | ||
| "energy", | ||
| "gold", | ||
| "dollar", | ||
| "defensives", | ||
| "cash" | ||
| ], | ||
| "avoid": [ | ||
| "unhedged cyclicals", | ||
| "levered credit" | ||
| ], | ||
| "confidence": "medium" | ||
| }, | ||
| "confidence": 0.956522, | ||
| "would_change": [ | ||
| "robust: no single-pillar one-notch change alters this candidate regime" | ||
| ], | ||
| "notes": [ | ||
| "degraded: no usable vote from DFEDTARU", | ||
| "Net liquidity (WALCL - TGA - RRP) validity is regime-dependent: strongest when reserve scarcity binds.", | ||
| "PMI family is licensing-gated: growth diffusion uses claims, payrolls, production, retail, and OECD CLI." | ||
| ] | ||
| }, | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591", | ||
| "signature": "806c531043618f2c4da47180ace2020def35fbf12bbf603061e4fb3264b97940c2bcfd50e395d16c8f2e45bdbe2ff69a84c00ddbe330fcaa29bc95296da66207", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| }, | ||
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.vulnerability/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "posture": { | ||
| "regime": "stagflation", | ||
| "market_state": "stress", | ||
| "refs": [ | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.tape-snapshot/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "43e98d770cb3b4e47c915164d7fe0aa7ec257968978a5cd1aa914e9e2403decd" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "as_of": "2026-05-01", | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591" | ||
| } | ||
| ] | ||
| }, | ||
| "factor_exposures": [ | ||
| { | ||
| "factor": "equity_beta", | ||
| "net": 0.55, | ||
| "gross": 0.55, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "SPY", | ||
| "weight": 0.45 | ||
| }, | ||
| { | ||
| "symbol": "EEM", | ||
| "weight": 0.1 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "duration", | ||
| "net": 0.3, | ||
| "gross": 0.3, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "IEF", | ||
| "weight": 0.3 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "credit", | ||
| "net": 0.15, | ||
| "gross": 0.15, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "HYG", | ||
| "weight": 0.15 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "usd_sensitivity", | ||
| "net": 0.1, | ||
| "gross": 0.1, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "EEM", | ||
| "weight": 0.1 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "global", | ||
| "net": 0.1, | ||
| "gross": 0.1, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "EEM", | ||
| "weight": 0.1 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "betas": [ | ||
| { | ||
| "factor": "equity_beta", | ||
| "proxy": "SPY", | ||
| "beta": 0.562937, | ||
| "r2": 0.926912, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "duration", | ||
| "proxy": "IEF", | ||
| "beta": -1.916486, | ||
| "r2": 0.368425, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "credit", | ||
| "proxy": "HYG", | ||
| "beta": 0.344651, | ||
| "r2": 0.727716, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "usd", | ||
| "proxy": "UUP", | ||
| "beta": -1.562837, | ||
| "r2": 0.635209, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "liquidity_beta", | ||
| "proxy": "BTCUSD", | ||
| "beta": 0.318435, | ||
| "r2": 0.917171, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "commodity", | ||
| "proxy": "DBC", | ||
| "beta": null, | ||
| "r2": null, | ||
| "n": 0, | ||
| "note": "proxy DBC unavailable or short" | ||
| }, | ||
| { | ||
| "factor": "real_assets", | ||
| "proxy": "GLD", | ||
| "beta": null, | ||
| "r2": null, | ||
| "n": 0, | ||
| "note": "proxy GLD unavailable or short" | ||
| } | ||
| ], | ||
| "vulnerabilities": [ | ||
| { | ||
| "factor": "equity_beta", | ||
| "exposure": 0.55, | ||
| "scenario_sensitivity": -2, | ||
| "pain": 1.1, | ||
| "note": "net long equity_beta; scenario expects equity_beta down (-2)" | ||
| }, | ||
| { | ||
| "factor": "duration", | ||
| "exposure": 0.3, | ||
| "scenario_sensitivity": -1, | ||
| "pain": 0.3, | ||
| "note": "net long duration; scenario expects duration down (-1)" | ||
| }, | ||
| { | ||
| "factor": "credit", | ||
| "exposure": 0.15, | ||
| "scenario_sensitivity": -2, | ||
| "pain": 0.3, | ||
| "note": "net long credit; scenario expects credit down (-2)" | ||
| } | ||
| ], | ||
| "inputs_digest": "10f7e77fbbec4240ac6e203bf443a6ee03fb19184dceec2950774c066796f8c6", | ||
| "would_change": [ | ||
| "reducing net equity_beta exposure toward zero removes the top vulnerability", | ||
| "a regime or market-state transition reselects the scenario stress vector" | ||
| ], | ||
| "notes": [ | ||
| "decision aid with stated scenario priors; not a covariance risk model and not a VaR claim" | ||
| ] | ||
| }, | ||
| "digest": "d277793511a0dcda199889d8beedbe849146cf0fc4cb216ce1dc77cadf20b237", | ||
| "signature": "416c38e1ed67583d2cff5b8c4020accd9d550eb49dde5956af750f4dae5b0d0ca994f564fe3db7b4a40aaab062b6236f0f8962ee5a4f4d9d00fde5400ef64506", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } | ||
| ], | ||
| "journal": [ | ||
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.journal-entry/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "author": "demo-model", | ||
| "note": "Regime read stagflation while the daily tape printed stress. Cut gross and lengthened nothing; the book is long the wrong factors here.", | ||
| "references": [ | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.vulnerability/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "d277793511a0dcda199889d8beedbe849146cf0fc4cb216ce1dc77cadf20b237" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "as_of": "2026-05-01", | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591" | ||
| } | ||
| ], | ||
| "tags": [ | ||
| "risk-off", | ||
| "stagflation", | ||
| "vulnerability" | ||
| ] | ||
| }, | ||
| "digest": "95415964066eaf10e5de3e49dd18763c8c7d79099baa0bbe670b402f52243923", | ||
| "signature": "adce443ba5de6d2e2edea9b5cc19744ae0a7842ad552860c03e895e5393434f4f0448bb5eb5d81f2aae7cbf222494931bfcab30ad465931f073ca2350e042b0c", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } | ||
| ], | ||
| "manifest": { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.track-record-manifest/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "exported_at": "2026-05-01T00:00:00Z", | ||
| "model_verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3", | ||
| "period": { | ||
| "from": "2025-01-02", | ||
| "to": "2026-05-01" | ||
| }, | ||
| "entries": [ | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.tape-snapshot/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "43e98d770cb3b4e47c915164d7fe0aa7ec257968978a5cd1aa914e9e2403decd" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "as_of": "2026-05-01", | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.vulnerability/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "d277793511a0dcda199889d8beedbe849146cf0fc4cb216ce1dc77cadf20b237" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.journal-entry/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "95415964066eaf10e5de3e49dd18763c8c7d79099baa0bbe670b402f52243923" | ||
| } | ||
| ], | ||
| "snapshot_count": 4, | ||
| "journal_count": 1, | ||
| "history_head_digest": "15fdd1acd962b562e05b73ca37b9ce3cce064b072fee5b27528edc9cd61eb62f", | ||
| "sequence": 1, | ||
| "previous_manifest_digest": null, | ||
| "previous_history_head_digest": null | ||
| }, | ||
| "digest": "e5697c073e213bc94b2e73a53a2bd140f80821b905daf4e547a9c5674f29643f", | ||
| "signature": "c7356a7845c73f0c055144c9e784c8b77826ed85cd6e6271acf2a2c5e4b60be8131bb78a0a914e58de57bc9c0294753700b5ae1f5d72ddf4af494a7a2b8e2e0f", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| }, | ||
| "prior_manifests": [], | ||
| "anchor_chain": [ | ||
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.track-record-anchor/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "anchored_at": "2026-05-01T00:00:00Z", | ||
| "sequence": 1, | ||
| "manifest_digest": "e5697c073e213bc94b2e73a53a2bd140f80821b905daf4e547a9c5674f29643f", | ||
| "history_head_digest": "15fdd1acd962b562e05b73ca37b9ce3cce064b072fee5b27528edc9cd61eb62f", | ||
| "previous_anchor_digest": null | ||
| }, | ||
| "digest": "062188f17b7d0ef9d1acb11e4e85b4ab8c61abb5c192c4ce0d7a784bbf7fb07c", | ||
| "signature": "b41012fe8a60cf65da5d28368251994e6d51ab81b81427078297aab11aa74f4dcdc163959cf14d742bb7a67a5c1ccccb332cde49a701d570535af2691a4a6d09", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } | ||
| ], | ||
| "transparency": { | ||
| "head": { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.transparency-head/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "log_id": "scopeblind.macro.demo-log", | ||
| "tree_size": 5, | ||
| "root_hash": "40efcc8314749f9b68e2d9555e16d6d223dba67069434108808e18249eb46c3c", | ||
| "timestamp": "2026-05-01T00:00:00Z", | ||
| "previous_root_hash": null | ||
| }, | ||
| "digest": "6fd187a85cb09a7e01f383e6dee761801b044c04e3c518f00473db4edd626e78", | ||
| "signature": "629823481769449e85c793ccd06cfe819b60192ae0a0e72f62b6108b3082b66e8fab5f86c7ae738f599b8ec0a079747ecf308b9540168d8fdc59afbc75dfd409", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| }, | ||
| "witness": { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.transparency-witness/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "head_digest": "6fd187a85cb09a7e01f383e6dee761801b044c04e3c518f00473db4edd626e78", | ||
| "root_hash": "40efcc8314749f9b68e2d9555e16d6d223dba67069434108808e18249eb46c3c", | ||
| "tree_size": 5, | ||
| "witnessed_at": "2026-05-01T00:00:00Z", | ||
| "note": "demo transparency witness (independent dev key)" | ||
| }, | ||
| "digest": "dd0978d2a1aaefa180e346f8b28d64f31c3bdf7f90b3d2c1f632e45c05cac911", | ||
| "signature": "87348b1ac8cc85a280cac613489696ece8a6c668d42cb87d5799930b96978aa39ea4598419425b664365a97c8c0c9618b1ab534506264755d6c108178e4c1002", | ||
| "verification_key": "f65f4bc4a3a2d5dc899cb8d99682cd8492789c2d73e5f9e939caa3adc875c322" | ||
| }, | ||
| "inclusions": [ | ||
| { | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94", | ||
| "proof": { | ||
| "leaf_index": 0, | ||
| "tree_size": 5, | ||
| "audit_path": [ | ||
| "5ed6de6f809567a968c710ba86d0737c2f811bcabcddc437a8cfa53f83ffe09f", | ||
| "888f0215bbdc34f19b01cfbe9010e5883d2404aec1b1b17fb28c9798f46895df", | ||
| "d043a0e988ecdfe94fb63804f73d51365e514243e9a248e2ab16b317ce0b3f8d" | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "digest": "43e98d770cb3b4e47c915164d7fe0aa7ec257968978a5cd1aa914e9e2403decd", | ||
| "proof": { | ||
| "leaf_index": 1, | ||
| "tree_size": 5, | ||
| "audit_path": [ | ||
| "74d3d8019a6944bbfb0324a1bf9a54e2bf61fb7c46cb43f700df1f158894b5e6", | ||
| "888f0215bbdc34f19b01cfbe9010e5883d2404aec1b1b17fb28c9798f46895df", | ||
| "d043a0e988ecdfe94fb63804f73d51365e514243e9a248e2ab16b317ce0b3f8d" | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591", | ||
| "proof": { | ||
| "leaf_index": 2, | ||
| "tree_size": 5, | ||
| "audit_path": [ | ||
| "a5947d673a413432359ca27bb4d2903f119fa05a8770bb3bae40c90f0e019038", | ||
| "7c8ed8da587a98ec9a30df018b41bd793f33a938459fdfa96c549abcd0f05f85", | ||
| "d043a0e988ecdfe94fb63804f73d51365e514243e9a248e2ab16b317ce0b3f8d" | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "digest": "d277793511a0dcda199889d8beedbe849146cf0fc4cb216ce1dc77cadf20b237", | ||
| "proof": { | ||
| "leaf_index": 3, | ||
| "tree_size": 5, | ||
| "audit_path": [ | ||
| "31f0efb1db7235d4c11de485b83bbb9a9989a38069b24f443835a73b118fca22", | ||
| "7c8ed8da587a98ec9a30df018b41bd793f33a938459fdfa96c549abcd0f05f85", | ||
| "d043a0e988ecdfe94fb63804f73d51365e514243e9a248e2ab16b317ce0b3f8d" | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "digest": "95415964066eaf10e5de3e49dd18763c8c7d79099baa0bbe670b402f52243923", | ||
| "proof": { | ||
| "leaf_index": 4, | ||
| "tree_size": 5, | ||
| "audit_path": [ | ||
| "43feda61a2b32f67b92add7d712447aa9ddcc96af2bda2553b5cca926cfdc897" | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.transparency-head/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "log_id": "scopeblind.macro.demo-log", | ||
| "tree_size": 5, | ||
| "root_hash": "40efcc8314749f9b68e2d9555e16d6d223dba67069434108808e18249eb46c3c", | ||
| "timestamp": "2026-05-01T00:00:00Z", | ||
| "previous_root_hash": null | ||
| }, | ||
| "digest": "6fd187a85cb09a7e01f383e6dee761801b044c04e3c518f00473db4edd626e78", | ||
| "signature": "629823481769449e85c793ccd06cfe819b60192ae0a0e72f62b6108b3082b66e8fab5f86c7ae738f599b8ec0a079747ecf308b9540168d8fdc59afbc75dfd409", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.transparency-witness/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "head_digest": "6fd187a85cb09a7e01f383e6dee761801b044c04e3c518f00473db4edd626e78", | ||
| "root_hash": "40efcc8314749f9b68e2d9555e16d6d223dba67069434108808e18249eb46c3c", | ||
| "tree_size": 5, | ||
| "witnessed_at": "2026-05-01T00:00:00Z", | ||
| "note": "demo transparency witness (independent dev key)" | ||
| }, | ||
| "digest": "dd0978d2a1aaefa180e346f8b28d64f31c3bdf7f90b3d2c1f632e45c05cac911", | ||
| "signature": "87348b1ac8cc85a280cac613489696ece8a6c668d42cb87d5799930b96978aa39ea4598419425b664365a97c8c0c9618b1ab534506264755d6c108178e4c1002", | ||
| "verification_key": "f65f4bc4a3a2d5dc899cb8d99682cd8492789c2d73e5f9e939caa3adc875c322" | ||
| } |
| { | ||
| "payload": { | ||
| "schema": "scopeblind.macro.vulnerability/1", | ||
| "engine_version": "macro-engine/0.1.0", | ||
| "as_of": "2025-08-23", | ||
| "posture": { | ||
| "regime": "stagflation", | ||
| "market_state": "stress", | ||
| "refs": [ | ||
| { | ||
| "schema": "scopeblind.macro.market-state/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "f469ddc93ce803456bf6eed265c1ffa94690bb3ed972e9dff95c7dab52972e94" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.tape-snapshot/1", | ||
| "as_of": "2025-08-23", | ||
| "digest": "43e98d770cb3b4e47c915164d7fe0aa7ec257968978a5cd1aa914e9e2403decd" | ||
| }, | ||
| { | ||
| "schema": "scopeblind.macro.regime-snapshot/1", | ||
| "as_of": "2026-05-01", | ||
| "digest": "27a4b9dd293ae77487e919adbf428e4911da62907c39dd017c50376ad9e77591" | ||
| } | ||
| ] | ||
| }, | ||
| "factor_exposures": [ | ||
| { | ||
| "factor": "equity_beta", | ||
| "net": 0.55, | ||
| "gross": 0.55, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "SPY", | ||
| "weight": 0.45 | ||
| }, | ||
| { | ||
| "symbol": "EEM", | ||
| "weight": 0.1 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "duration", | ||
| "net": 0.3, | ||
| "gross": 0.3, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "IEF", | ||
| "weight": 0.3 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "credit", | ||
| "net": 0.15, | ||
| "gross": 0.15, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "HYG", | ||
| "weight": 0.15 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "usd_sensitivity", | ||
| "net": 0.1, | ||
| "gross": 0.1, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "EEM", | ||
| "weight": 0.1 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "factor": "global", | ||
| "net": 0.1, | ||
| "gross": 0.1, | ||
| "contributors": [ | ||
| { | ||
| "symbol": "EEM", | ||
| "weight": 0.1 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "betas": [ | ||
| { | ||
| "factor": "equity_beta", | ||
| "proxy": "SPY", | ||
| "beta": 0.562937, | ||
| "r2": 0.926912, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "duration", | ||
| "proxy": "IEF", | ||
| "beta": -1.916486, | ||
| "r2": 0.368425, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "credit", | ||
| "proxy": "HYG", | ||
| "beta": 0.344651, | ||
| "r2": 0.727716, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "usd", | ||
| "proxy": "UUP", | ||
| "beta": -1.562837, | ||
| "r2": 0.635209, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "liquidity_beta", | ||
| "proxy": "BTCUSD", | ||
| "beta": 0.318435, | ||
| "r2": 0.917171, | ||
| "n": 120 | ||
| }, | ||
| { | ||
| "factor": "commodity", | ||
| "proxy": "DBC", | ||
| "beta": null, | ||
| "r2": null, | ||
| "n": 0, | ||
| "note": "proxy DBC unavailable or short" | ||
| }, | ||
| { | ||
| "factor": "real_assets", | ||
| "proxy": "GLD", | ||
| "beta": null, | ||
| "r2": null, | ||
| "n": 0, | ||
| "note": "proxy GLD unavailable or short" | ||
| } | ||
| ], | ||
| "vulnerabilities": [ | ||
| { | ||
| "factor": "equity_beta", | ||
| "exposure": 0.55, | ||
| "scenario_sensitivity": -2, | ||
| "pain": 1.1, | ||
| "note": "net long equity_beta; scenario expects equity_beta down (-2)" | ||
| }, | ||
| { | ||
| "factor": "duration", | ||
| "exposure": 0.3, | ||
| "scenario_sensitivity": -1, | ||
| "pain": 0.3, | ||
| "note": "net long duration; scenario expects duration down (-1)" | ||
| }, | ||
| { | ||
| "factor": "credit", | ||
| "exposure": 0.15, | ||
| "scenario_sensitivity": -2, | ||
| "pain": 0.3, | ||
| "note": "net long credit; scenario expects credit down (-2)" | ||
| } | ||
| ], | ||
| "inputs_digest": "10f7e77fbbec4240ac6e203bf443a6ee03fb19184dceec2950774c066796f8c6", | ||
| "would_change": [ | ||
| "reducing net equity_beta exposure toward zero removes the top vulnerability", | ||
| "a regime or market-state transition reselects the scenario stress vector" | ||
| ], | ||
| "notes": [ | ||
| "decision aid with stated scenario priors; not a covariance risk model and not a VaR claim" | ||
| ] | ||
| }, | ||
| "digest": "d277793511a0dcda199889d8beedbe849146cf0fc4cb216ce1dc77cadf20b237", | ||
| "signature": "416c38e1ed67583d2cff5b8c4020accd9d550eb49dde5956af750f4dae5b0d0ca994f564fe3db7b4a40aaab062b6236f0f8962ee5a4f4d9d00fde5400ef64506", | ||
| "verification_key": "e324c4149db27151f65df25d528d3037d3b3bc4165b0d28c0769369cba958ae3" | ||
| } |
| /** | ||
| * Legate adherence / restraint proof pack verifier. | ||
| * | ||
| * A proof pack (type "scopeblind.legate.proof-pack.v1") is the allocator-facing, | ||
| * POSITION-BLIND record a Legate desk produces: what the gate prevented over a | ||
| * session (held / blocked, attributed to rules), what the order-path shadow would | ||
| * have blocked, and the digests it is bound to (committed mandate, signed book | ||
| * provenance, receipt Merkle root). It carries its own runtime verification key, so | ||
| * a third party re-verifies it offline with nothing but this CLI. | ||
| * | ||
| * Signature recipe (legate-proof-pack.cjs signProofPack): Ed25519 over the UTF-8 | ||
| * bytes of the canonical (deep-sorted, no-whitespace) JSON of the pack MINUS its | ||
| * `signature`, `sha256`, and `hybrid_signature` fields; `sha256` is that canonical's | ||
| * SHA-256 digest. The runtime public key is `verification_key` (and runtime. | ||
| * verification_key), raw 32-byte hex. Signing is over the bytes directly, like the | ||
| * Legate governed receipt, NOT over a pre-hash like the Gate tuple. | ||
| * | ||
| * @module verify-cli/src/engines/legate-proof-pack | ||
| */ | ||
| import { ed25519 } from '@noble/curves/ed25519'; | ||
| import { sha256 } from '@noble/hashes/sha256'; | ||
| import { utf8ToBytes } from '@noble/hashes/utils'; | ||
| import { canonicalize } from '../util/canonical.js'; | ||
| import { hexToBytes, bytesToHex } from '../util/hex.js'; | ||
| export const PROOF_PACK_TYPE = 'scopeblind.legate.proof-pack.v1'; | ||
| const isString = (v) => typeof v === 'string' && v.length > 0; | ||
| const HEX = (s) => typeof s === 'string' && /^[0-9a-f]+$/i.test(s) && s.length % 2 === 0; | ||
| const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0); | ||
| const PROVES = [ | ||
| 'The pack was signed by the holder of the runtime key (verification_key) and not altered since: every field is bound by the Ed25519 signature over the canonical bytes.', | ||
| 'The restraint counts (held / blocked, by rule) and the order-path shadow counts are the runtime\'s own, attributed to the committed mandate digest.', | ||
| 'The artifact is position-blind: it contains digests and counts only, never positions, so adherence is verifiable without disclosing the book.', | ||
| ]; | ||
| const LIMITATIONS = [ | ||
| 'That the underlying receipts behind the Merkle root and the signed book behind book.sha256 are themselves correct, unless those are separately verified.', | ||
| 'That the runtime key belongs to the desk you expect; pin it with --key to bind the pack to a known runtime.', | ||
| ]; | ||
| /** | ||
| * Reconstruct the exact canonical string that was signed: the pack minus the | ||
| * signature, the bound digest, and any hybrid signature. | ||
| */ | ||
| export function proofPackSignedCanonical(pack) { | ||
| const { signature, sha256: _digest, hybrid_signature, ...rest } = pack; | ||
| return canonicalize(rest); | ||
| } | ||
| /** | ||
| * @param {object} pack the proof pack envelope | ||
| * @param {object} opts { publicKey?: pinned runtime key (hex) } | ||
| */ | ||
| export function verifyLegateProofPack(pack, opts = {}) { | ||
| const base = { | ||
| format: 'legate-proof-pack', | ||
| schema: isString(pack?.type) ? pack.type : PROOF_PACK_TYPE, | ||
| algorithm: 'ed25519', | ||
| }; | ||
| if (pack === null || typeof pack !== 'object' || Array.isArray(pack)) { | ||
| return { valid: false, error: 'unknown_format', ...base, detail: 'proof pack is not an object' }; | ||
| } | ||
| if (pack.type !== PROOF_PACK_TYPE) { | ||
| return { valid: false, error: 'unknown_format', ...base, detail: `type is not ${PROOF_PACK_TYPE}` }; | ||
| } | ||
| const vk = isString(pack.verification_key) | ||
| ? pack.verification_key | ||
| : (pack.runtime && isString(pack.runtime.verification_key) ? pack.runtime.verification_key : undefined); | ||
| if (!isString(pack.signature)) return { valid: false, error: 'missing_signature', ...base }; | ||
| if (!isString(vk)) return { valid: false, error: 'no_public_key', ...base }; | ||
| if (!HEX(pack.signature) || !HEX(vk)) { | ||
| return { valid: false, error: 'malformed_hex', ...base, detail: 'signature and verification_key must be hex' }; | ||
| } | ||
| const pinned = isString(opts.publicKey); | ||
| if (pinned && opts.publicKey.toLowerCase() !== vk.toLowerCase()) { | ||
| return { valid: false, error: 'key_mismatch', ...base, publicKey: vk, expectedKey: opts.publicKey }; | ||
| } | ||
| // The bound digest must match the canonical the signature covers. | ||
| const canonical = proofPackSignedCanonical(pack); | ||
| if (isString(pack.sha256)) { | ||
| const recomputed = bytesToHex(sha256(utf8ToBytes(canonical))); | ||
| if (recomputed !== pack.sha256) { | ||
| return { valid: false, error: 'digest_mismatch', ...base, detail: 'sha256 does not match the canonical bytes (the pack was modified after signing)', publicKey: vk }; | ||
| } | ||
| } | ||
| let ok = false; | ||
| try { | ||
| ok = ed25519.verify(hexToBytes(pack.signature), utf8ToBytes(canonical), hexToBytes(vk)); | ||
| } catch (e) { | ||
| return { valid: false, error: 'malformed_hex', ...base, detail: e.message, publicKey: vk }; | ||
| } | ||
| if (!ok) { | ||
| return { valid: false, error: 'invalid_signature', ...base, publicKey: vk }; | ||
| } | ||
| const mandate = pack.mandate || {}; | ||
| const restraint = pack.restraint || {}; | ||
| const shadow = pack.shadow || null; | ||
| const book = pack.book || {}; | ||
| return { | ||
| valid: true, | ||
| ...base, | ||
| publicKey: vk, | ||
| keySource: pinned ? 'embedded-pack (pinned via --key)' : 'embedded-pack', | ||
| signerKid: isString(pack.signer_kid) ? pack.signer_kid : undefined, | ||
| generatedAt: isString(pack.generated_at) ? pack.generated_at : undefined, | ||
| positionBlind: pack.position_blind !== false, | ||
| mandate: { name: mandate.name, sha256: mandate.sha256, mode: mandate.mode, ruleCount: num(mandate.rule_count) }, | ||
| restraint: { blocked: num(restraint.blocked), held: num(restraint.held), byRule: Array.isArray(restraint.by_rule) ? restraint.by_rule : [] }, | ||
| shadow: shadow ? { observed: num(shadow.observed), wouldBlock: num(shadow.would_block), wouldHold: num(shadow.would_hold) } : null, | ||
| bookDigest: isString(book.sha256) ? book.sha256 : null, | ||
| sessionMerkleRoot: isString(pack.session_merkle_root) ? pack.session_merkle_root : null, | ||
| receiptCount: num(pack.receipt_count), | ||
| // A hybrid post-quantum signature, when present, is recognized and reported. | ||
| // Classical Ed25519 is verified here; ML-DSA-65 verification is an optional add-on | ||
| // (the runtime is dependency-free and signs classically; PQ is documented in the | ||
| // restraint-receipts draft as an optional field for surfaces that can produce it). | ||
| hybridSignaturePresent: Boolean(pack.hybrid_signature), | ||
| proves: PROVES, | ||
| limitations: LIMITATIONS, | ||
| }; | ||
| } |
| /** | ||
| * ScopeBlind macro-engine snapshot and track-record verifier. | ||
| * | ||
| * The macro engine signs its snapshots (market-state, regime, tape, | ||
| * vulnerability, alert, journal) and its exported track-record bundle with | ||
| * the EXACT same tuple format the Gate uses: { payload, digest, signature, | ||
| * verification_key }, where digest = sha256(canonicalGateJSON(payload)) and | ||
| * signature = Ed25519 over the bytes of the hex digest. | ||
| * | ||
| * These tuples already verify cryptographically through | ||
| * engines/gate-receipt.js verifyGateTuple. This module ADDS: | ||
| * - recognition of the scopeblind.macro.* schemas, | ||
| * - a semantic-contract check per schema (macroSchemaErrors), | ||
| * - a schema-aware salient-field summary for display (macroSummary), | ||
| * - a track-record BUNDLE verifier (verifyMacroTrackRecord) that mirrors | ||
| * verifyGateBundle: every record's signature, single-signer custody, an | ||
| * exact manifest inventory, count checks, and the history_head_digest. | ||
| * | ||
| * No new cryptography is introduced. Crypto is delegated to verifyGateTuple | ||
| * and canonicalization to canonicalGateJSON, both from gate-receipt.js. | ||
| * | ||
| * @module verify-cli/src/engines/macro-snapshot | ||
| * @license Apache-2.0 | ||
| */ | ||
| import { sha256 } from '@noble/hashes/sha256'; | ||
| import { utf8ToBytes, bytesToHex } from '@noble/hashes/utils'; | ||
| import { canonicalGateJSON, verifyGateTuple } from './gate-receipt.js'; | ||
| export const MACRO_BUNDLE_SCHEMA = 'scopeblind.macro.track-record-bundle/1'; | ||
| export const MACRO_MANIFEST_SCHEMA = 'scopeblind.macro.track-record-manifest/1'; | ||
| export const MACRO_ANCHOR_SCHEMA = 'scopeblind.macro.track-record-anchor/1'; | ||
| export const MACRO_TRANSPARENCY_HEAD_SCHEMA = 'scopeblind.macro.transparency-head/1'; | ||
| export const MACRO_TRANSPARENCY_WITNESS_SCHEMA = 'scopeblind.macro.transparency-witness/1'; | ||
| export const MACRO_SCHEMA_PREFIX = 'scopeblind.macro.'; | ||
| /** Human descriptions for every recognized macro schema. */ | ||
| export const MACRO_SCHEMAS = { | ||
| 'scopeblind.macro.market-state/1': 'daily cross-asset market-state classification', | ||
| 'scopeblind.macro.regime-snapshot/1': 'macro regime snapshot from economic series', | ||
| 'scopeblind.macro.tape-snapshot/1': 'intraday/session tape attribution snapshot', | ||
| 'scopeblind.macro.vulnerability/1': 'book vulnerability decision aid', | ||
| 'scopeblind.macro.alert/1': 'state-change alert', | ||
| 'scopeblind.macro.journal-entry/1': 'model journal entry referencing snapshots', | ||
| 'scopeblind.macro.price-snapshot/1': 'signed point-in-time price levels per risk factor', | ||
| [MACRO_MANIFEST_SCHEMA]: 'signed track-record export manifest', | ||
| [MACRO_ANCHOR_SCHEMA]: 'signed append-only track-record checkpoint', | ||
| [MACRO_TRANSPARENCY_HEAD_SCHEMA]: 'signed RFC 6962 transparency-log head over snapshot digests', | ||
| [MACRO_TRANSPARENCY_WITNESS_SCHEMA]: 'independent witness co-signature over a transparency-log head', | ||
| }; | ||
| const HEX_64 = /^[0-9a-f]{64}$/; | ||
| const MARKET_STATE_CLASSES = new Set(['risk_on', 'constructive', 'mixed', 'deteriorating', 'stress']); | ||
| const MARKET_STATE_PILLARS = ['trend', 'breadth', 'liquidity', 'credit', 'volatility']; | ||
| const REGIMES = new Set(['goldilocks', 'reflation', 'stagflation', 'deflation_bust', 'liquidity_led_recovery', 'tightening_squeeze', 'transitional']); | ||
| const TAPE_TYPES = new Set(['credit_stress', 'tightening_shock', 'growth_scare', 'inflation_shock', 'liquidity_risk_on', 'reflation_risk_on', 'mixed']); | ||
| const ALERT_SEVERITIES = new Set(['info', 'action', 'critical']); | ||
| const ALERT_KINDS = new Set(['regime_transition', 'market_state_change', 'tape_type_change']); | ||
| const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v); | ||
| const isString = (v) => typeof v === 'string' && v.length > 0; | ||
| const isNumber = (v) => typeof v === 'number' && Number.isFinite(v); | ||
| const isInteger = (v) => Number.isInteger(v); | ||
| const isArray = (v) => Array.isArray(v); | ||
| /** Whether a schema string is a recognized macro snapshot/journal/manifest schema. */ | ||
| export function isMacroSchema(schema) { | ||
| return typeof schema === 'string' && schema.startsWith(MACRO_SCHEMA_PREFIX); | ||
| } | ||
| /** A market-state/regime/tape/etc snapshot proves authenticity + integrity + schema validity. */ | ||
| export const MACRO_PROVES = [ | ||
| 'Signature integrity: the snapshot verifies under its carried Ed25519 key.', | ||
| 'Integrity: neither payload nor digest has been modified since signing.', | ||
| 'Schema validity: the recognized macro snapshot satisfies its required semantic contract.', | ||
| ]; | ||
| export const MACRO_LIMITATIONS = [ | ||
| 'The real-world identity controlling the carried key unless the expected key is independently pinned with --key.', | ||
| 'Correctness of the underlying market or economic series the snapshot was computed from.', | ||
| 'That the classification, regime, or tape attribution is a good or predictive read of the market.', | ||
| 'Independent corroboration of the inputs unless separately attested; the model signs what it computed.', | ||
| ]; | ||
| export const MACRO_BUNDLE_PROVES = [ | ||
| 'Signature integrity: every included snapshot, journal entry, manifest, and anchor verifies under one Ed25519 key.', | ||
| 'Integrity: no record payload or digest has been modified since signing.', | ||
| 'Single-key consistency: all records, including the manifest, share one model verification key.', | ||
| 'Manifest completeness: the signed manifest exactly enumerates every exported snapshot and journal entry, in order.', | ||
| 'History binding: the manifest history_head_digest commits to the exact ordered set of record digests.', | ||
| ]; | ||
| export const MACRO_BUNDLE_LIMITATIONS = [ | ||
| 'Correctness of the underlying market or economic data the snapshots were computed from.', | ||
| 'That the recorded calls were good predictions; the bundle is a tamper-evident track record, not a performance claim.', | ||
| 'The real-world identity controlling an embedded key unless the expected key is independently pinned with --key.', | ||
| 'Global history completeness unless prior manifests are chained and the latest history/anchor head is independently retained or timestamped.', | ||
| ]; | ||
| function requireFields(payload, fields, errors) { | ||
| for (const field of fields) { | ||
| if (payload[field] === undefined || payload[field] === null || payload[field] === '') errors.push(`missing ${field}`); | ||
| } | ||
| } | ||
| /** Validate an array of { schema, as_of, digest } reference objects with 64-hex digests. */ | ||
| function refErrors(refs, label, errors, { nonEmpty = true } = {}) { | ||
| if (!isArray(refs)) { errors.push(`${label} must be an array`); return; } | ||
| if (nonEmpty && refs.length === 0) { errors.push(`${label} must be non-empty`); return; } | ||
| for (const ref of refs) { | ||
| if (!isObject(ref) || !isString(ref.schema) || !isString(ref.as_of) || !HEX_64.test(ref.digest || '')) { | ||
| errors.push(`${label} entry must be { schema, as_of, digest(64-hex) }`); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Semantic-contract validation for a recognized macro payload. | ||
| * Returns [] for valid payloads and for unrecognized (non-macro or unknown | ||
| * macro) schemas; returns one or more human-readable error strings otherwise. | ||
| * | ||
| * @param {Object} payload signed macro payload | ||
| * @returns {string[]} | ||
| */ | ||
| export function macroSchemaErrors(payload) { | ||
| const errors = []; | ||
| if (!isObject(payload)) return errors; | ||
| const schema = payload.schema; | ||
| if (!Object.hasOwn(MACRO_SCHEMAS, schema)) return errors; | ||
| if (schema === 'scopeblind.macro.market-state/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'universe_digest', 'inputs_digest', 'pillars', 'evidence', 'classification', 'confidence', 'would_change', 'notes'], errors); | ||
| if (!MARKET_STATE_CLASSES.has(payload.classification)) errors.push('invalid classification'); | ||
| if (!isObject(payload.pillars)) errors.push('pillars must be an object'); | ||
| else { | ||
| for (const p of MARKET_STATE_PILLARS) { | ||
| if (!isInteger(payload.pillars[p]) || payload.pillars[p] < -2 || payload.pillars[p] > 2) errors.push(`invalid pillar ${p} (integer in [-2,2])`); | ||
| } | ||
| } | ||
| if (!isNumber(payload.confidence) || payload.confidence < 0 || payload.confidence > 1) errors.push('confidence must be a number in [0,1]'); | ||
| } else if (schema === 'scopeblind.macro.regime-snapshot/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'pillars', 'inputs', 'vintage_digest', 'candidate_regime', 'regime', 'liquidity_overlay', 'hysteresis', 'playbook', 'confidence', 'would_change', 'notes'], errors); | ||
| if (!REGIMES.has(payload.regime)) errors.push('invalid regime'); | ||
| if (!REGIMES.has(payload.candidate_regime)) errors.push('invalid candidate_regime'); | ||
| if (!HEX_64.test(payload.vintage_digest || '')) errors.push('invalid vintage_digest'); | ||
| if (!isObject(payload.pillars)) errors.push('pillars must be an object'); | ||
| if (!isArray(payload.inputs)) errors.push('inputs must be an array'); | ||
| if (!isNumber(payload.confidence) || payload.confidence < 0 || payload.confidence > 1) errors.push('confidence must be a number in [0,1]'); | ||
| } else if (schema === 'scopeblind.macro.tape-snapshot/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'session', 'tape_type', 'coherence', 'material', 'signals', 'unavailable', 'untestable_types', 'attribution', 'inputs_digest', 'would_change', 'notes'], errors); | ||
| if (!TAPE_TYPES.has(payload.tape_type)) errors.push('invalid tape_type'); | ||
| if (!isNumber(payload.coherence) || payload.coherence < 0 || payload.coherence > 1) errors.push('coherence must be a number in [0,1]'); | ||
| if (typeof payload.material !== 'boolean') errors.push('material must be a boolean'); | ||
| if (!isArray(payload.signals)) errors.push('signals must be an array'); | ||
| if (!isObject(payload.attribution) || !isString(payload.attribution.tier)) errors.push('attribution must carry a tier'); | ||
| } else if (schema === 'scopeblind.macro.vulnerability/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'posture', 'factor_exposures', 'betas', 'vulnerabilities', 'inputs_digest', 'would_change', 'notes'], errors); | ||
| if (!isObject(payload.posture)) errors.push('posture must be an object'); | ||
| if (!isArray(payload.factor_exposures)) errors.push('factor_exposures must be an array'); | ||
| if (!isArray(payload.betas)) errors.push('betas must be an array'); | ||
| if (!isArray(payload.vulnerabilities)) errors.push('vulnerabilities must be an array'); | ||
| } else if (schema === 'scopeblind.macro.alert/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'alert_id', 'kind', 'severity', 'title', 'detail', 'refs', 'budget'], errors); | ||
| if (!HEX_64.test(payload.alert_id || '')) errors.push('invalid alert_id'); | ||
| if (!ALERT_SEVERITIES.has(payload.severity)) errors.push('invalid severity'); | ||
| if (!ALERT_KINDS.has(payload.kind)) errors.push('invalid kind'); | ||
| refErrors(payload.refs, 'refs', errors); | ||
| } else if (schema === 'scopeblind.macro.journal-entry/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'author', 'note', 'references', 'tags'], errors); | ||
| if (!isArray(payload.tags)) errors.push('tags must be an array'); | ||
| refErrors(payload.references, 'references', errors); | ||
| } else if (schema === 'scopeblind.macro.price-snapshot/1') { | ||
| requireFields(payload, ['schema', 'engine_version', 'as_of', 'source', 'delay_minutes', 'levels', 'coverage', 'missing', 'notes'], errors); | ||
| if (!isInteger(payload.delay_minutes) || payload.delay_minutes < 0) errors.push('delay_minutes must be a non-negative integer'); | ||
| if (!isString(payload.source)) errors.push('source must be a non-empty string'); | ||
| if (!isArray(payload.coverage)) errors.push('coverage must be an array'); | ||
| if (!isArray(payload.missing)) errors.push('missing must be an array'); | ||
| if (!isObject(payload.levels)) { | ||
| errors.push('levels must be an object'); | ||
| } else { | ||
| const factors = Object.keys(payload.levels); | ||
| if (factors.length === 0) errors.push('levels must price at least one factor'); | ||
| const VALID_UNITS = new Set(['price', 'yield_pct', 'fx_rate', 'spread_bp']); | ||
| for (const f of factors) { | ||
| const lv = payload.levels[f]; | ||
| if (!isObject(lv)) { errors.push(`level ${f} must be an object`); continue; } | ||
| if (!isNumber(lv.level)) errors.push(`level ${f} must carry a numeric level`); | ||
| if (!isString(lv.instrument)) errors.push(`level ${f} must name an instrument`); | ||
| if (!VALID_UNITS.has(lv.unit)) errors.push(`level ${f} has invalid unit`); | ||
| if (lv.multiplier !== undefined && (!isNumber(lv.multiplier) || lv.multiplier <= 0)) errors.push(`level ${f} multiplier must be a positive number`); | ||
| } | ||
| // coverage must list exactly the priced factors (sorted), never overstate. | ||
| if (isArray(payload.coverage)) { | ||
| const cov = [...payload.coverage].sort(); | ||
| const keys = [...factors].sort(); | ||
| if (cov.length !== keys.length || cov.some((c, i) => c !== keys[i])) errors.push('coverage must list exactly the priced factors'); | ||
| } | ||
| } | ||
| } else if (schema === MACRO_MANIFEST_SCHEMA) { | ||
| requireFields(payload, ['schema', 'engine_version', 'exported_at', 'model_verification_key', 'period', 'entries', 'snapshot_count', 'journal_count', 'history_head_digest'], errors); | ||
| if (!HEX_64.test(payload.model_verification_key || '')) errors.push('invalid model_verification_key'); | ||
| if (!HEX_64.test(payload.history_head_digest || '')) errors.push('invalid history_head_digest'); | ||
| refErrors(payload.entries, 'entries', errors, { nonEmpty: false }); | ||
| if (payload.sequence !== undefined && (!isInteger(payload.sequence) || payload.sequence < 1)) errors.push('sequence must be a positive integer'); | ||
| } else if (schema === MACRO_ANCHOR_SCHEMA) { | ||
| requireFields(payload, ['schema', 'engine_version', 'anchored_at', 'sequence', 'manifest_digest', 'history_head_digest'], errors); | ||
| if (!isInteger(payload.sequence) || payload.sequence < 1) errors.push('sequence must be a positive integer'); | ||
| if (!HEX_64.test(payload.manifest_digest || '')) errors.push('invalid manifest_digest'); | ||
| if (!HEX_64.test(payload.history_head_digest || '')) errors.push('invalid history_head_digest'); | ||
| if (payload.previous_anchor_digest !== null && !HEX_64.test(payload.previous_anchor_digest || '')) errors.push('invalid previous_anchor_digest'); | ||
| } else if (schema === MACRO_TRANSPARENCY_HEAD_SCHEMA) { | ||
| requireFields(payload, ['schema', 'engine_version', 'log_id', 'tree_size', 'root_hash', 'timestamp'], errors); | ||
| if (!isInteger(payload.tree_size) || payload.tree_size < 0) errors.push('tree_size must be a non-negative integer'); | ||
| if (!HEX_64.test(payload.root_hash || '')) errors.push('invalid root_hash'); | ||
| // previous_root_hash is a required key but is null at the genesis head. | ||
| if (!Object.hasOwn(payload, 'previous_root_hash')) errors.push('missing previous_root_hash'); | ||
| else if (payload.previous_root_hash !== null && !HEX_64.test(payload.previous_root_hash || '')) errors.push('invalid previous_root_hash'); | ||
| } else if (schema === MACRO_TRANSPARENCY_WITNESS_SCHEMA) { | ||
| requireFields(payload, ['schema', 'engine_version', 'head_digest', 'root_hash', 'tree_size', 'witnessed_at', 'note'], errors); | ||
| if (!HEX_64.test(payload.head_digest || '')) errors.push('invalid head_digest'); | ||
| if (!HEX_64.test(payload.root_hash || '')) errors.push('invalid root_hash'); | ||
| if (!isInteger(payload.tree_size) || payload.tree_size < 0) errors.push('tree_size must be a non-negative integer'); | ||
| } | ||
| return errors; | ||
| } | ||
| /** | ||
| * Extract the salient display fields for a recognized macro snapshot. | ||
| * Returns null for unrecognized schemas. | ||
| * | ||
| * @param {Object} payload signed macro payload | ||
| * @returns {Object|null} | ||
| */ | ||
| export function macroSummary(payload) { | ||
| if (!isObject(payload) || !Object.hasOwn(MACRO_SCHEMAS, payload.schema)) return null; | ||
| const schema = payload.schema; | ||
| const base = { schema, description: MACRO_SCHEMAS[schema], as_of: payload.as_of }; | ||
| if (schema === 'scopeblind.macro.market-state/1') { | ||
| return { ...base, classification: payload.classification, pillars: payload.pillars, confidence: payload.confidence }; | ||
| } | ||
| if (schema === 'scopeblind.macro.regime-snapshot/1') { | ||
| return { ...base, regime: payload.regime, candidate_regime: payload.candidate_regime, liquidity_overlay: payload.liquidity_overlay, confidence: payload.confidence }; | ||
| } | ||
| if (schema === 'scopeblind.macro.tape-snapshot/1') { | ||
| return { ...base, tape_type: payload.tape_type, coherence: payload.coherence, material: payload.material, attribution_tier: payload.attribution?.tier }; | ||
| } | ||
| if (schema === 'scopeblind.macro.vulnerability/1') { | ||
| const top = (isArray(payload.vulnerabilities) ? payload.vulnerabilities : []) | ||
| .slice() | ||
| .sort((a, b) => (b?.pain ?? 0) - (a?.pain ?? 0)) | ||
| .slice(0, 3) | ||
| .map((v) => ({ factor: v?.factor, pain: v?.pain })); | ||
| return { ...base, regime: payload.posture?.regime, market_state: payload.posture?.market_state, top_vulnerabilities: top }; | ||
| } | ||
| if (schema === 'scopeblind.macro.alert/1') { | ||
| return { ...base, severity: payload.severity, kind: payload.kind, title: payload.title }; | ||
| } | ||
| if (schema === 'scopeblind.macro.journal-entry/1') { | ||
| return { ...base, author: payload.author, reference_count: isArray(payload.references) ? payload.references.length : 0, tags: payload.tags }; | ||
| } | ||
| if (schema === 'scopeblind.macro.price-snapshot/1') { | ||
| return { | ||
| ...base, | ||
| source: payload.source, | ||
| delay_minutes: payload.delay_minutes, | ||
| priced: isArray(payload.coverage) ? payload.coverage.length : 0, | ||
| missing: isArray(payload.missing) ? payload.missing.length : 0, | ||
| }; | ||
| } | ||
| if (schema === MACRO_MANIFEST_SCHEMA) { | ||
| return { ...base, sequence: payload.sequence ?? 1, snapshot_count: payload.snapshot_count, journal_count: payload.journal_count }; | ||
| } | ||
| if (schema === MACRO_ANCHOR_SCHEMA) { | ||
| return { ...base, sequence: payload.sequence, manifest_digest: payload.manifest_digest, history_head_digest: payload.history_head_digest }; | ||
| } | ||
| if (schema === MACRO_TRANSPARENCY_HEAD_SCHEMA) { | ||
| return { ...base, log_id: payload.log_id, tree_size: payload.tree_size, root_hash: payload.root_hash }; | ||
| } | ||
| if (schema === MACRO_TRANSPARENCY_WITNESS_SCHEMA) { | ||
| return { ...base, head_digest: payload.head_digest, tree_size: payload.tree_size }; | ||
| } | ||
| return base; | ||
| } | ||
| const refsEqual = (a, b) => | ||
| isObject(a) && isObject(b) && a.schema === b.schema && a.as_of === b.as_of && a.digest === b.digest; | ||
| // ── RFC 6962 transparency-log inclusion verification ───────────────── | ||
| // | ||
| // The macro engine signs an RFC 6962 Merkle transparency log over the | ||
| // snapshot/journal record digests. This block reconstructs a leaf's root from | ||
| // its audit path EXACTLY as the engine builds it, so the verifier can confirm | ||
| // that every exported record is committed under the signed head's root_hash. | ||
| // Hashing is domain-separated over RAW digest bytes (not the hex string). | ||
| /** Decode a hex string to a Uint8Array (no prefix handling; lengths are caller-checked). */ | ||
| function hexToBytes(hex) { | ||
| const bytes = new Uint8Array(hex.length / 2); | ||
| for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); | ||
| return bytes; | ||
| } | ||
| /** Concatenate Uint8Arrays into one. */ | ||
| function concatBytes(...arrays) { | ||
| let length = 0; | ||
| for (const a of arrays) length += a.length; | ||
| const out = new Uint8Array(length); | ||
| let offset = 0; | ||
| for (const a of arrays) { out.set(a, offset); offset += a.length; } | ||
| return out; | ||
| } | ||
| /** RFC 6962 leaf hash: sha256(0x00 || raw record-digest bytes). */ | ||
| function leafHash(recordDigestHex) { | ||
| return sha256(concatBytes(Uint8Array.of(0x00), hexToBytes(recordDigestHex))); | ||
| } | ||
| /** RFC 6962 interior node hash: sha256(0x01 || left || right). */ | ||
| function nodeHash(left, right) { | ||
| return sha256(concatBytes(Uint8Array.of(0x01), left, right)); | ||
| } | ||
| /** Largest power of two STRICTLY less than n (RFC 6962 split point). */ | ||
| function splitPoint(n) { | ||
| let k = 1; | ||
| while ((k << 1) < n) k <<= 1; | ||
| return k; | ||
| } | ||
| /** | ||
| * Verify an RFC 6962 inclusion proof: reconstruct the Merkle root for the leaf | ||
| * at proof.leaf_index in a tree of proof.tree_size leaves, consuming the audit | ||
| * path deepest-sibling-first (top sibling LAST, i.e. from the END of the array). | ||
| * | ||
| * @param {string} recordDigestHex 64-hex record digest committed as the leaf | ||
| * @param {Object} proof { leaf_index, tree_size, audit_path: string[] } | ||
| * @param {string} expectedRootHex 64-hex root_hash from the signed head | ||
| * @returns {boolean} | ||
| */ | ||
| export function verifyInclusion(recordDigestHex, proof, expectedRootHex) { | ||
| if (!isObject(proof)) return false; | ||
| const { leaf_index, tree_size, audit_path } = proof; | ||
| if (!isInteger(leaf_index) || !isInteger(tree_size)) return false; | ||
| if (leaf_index < 0 || leaf_index >= tree_size) return false; | ||
| if (!HEX_64.test(recordDigestHex || '')) return false; | ||
| if (!HEX_64.test(expectedRootHex || '')) return false; | ||
| if (!isArray(audit_path) || !audit_path.every((h) => HEX_64.test(h || ''))) return false; | ||
| const nodes = audit_path.map(hexToBytes); | ||
| // hi = number of path nodes still available; the top sibling is nodes[hi-1]. | ||
| function root(m, size, hi) { | ||
| if (size === 1) return hi === 0 ? leafHash(recordDigestHex) : null; | ||
| if (hi < 1) return null; | ||
| const k = splitPoint(size); | ||
| const sib = nodes[hi - 1]; | ||
| if (m < k) { | ||
| const left = root(m, k, hi - 1); | ||
| return left && nodeHash(left, sib); | ||
| } | ||
| const right = root(m - k, size - k, hi - 1); | ||
| return right && nodeHash(sib, right); | ||
| } | ||
| const r = root(leaf_index, tree_size, nodes.length); | ||
| return r !== null && bytesToHex(r) === expectedRootHex; | ||
| } | ||
| /** | ||
| * Verify a track-record bundle's transparency evidence: the signed log head, | ||
| * every record's inclusion under that head's root, and (optionally) an | ||
| * independent witness co-signature over the same head. | ||
| * | ||
| * @param {Object} evidence bundle.transparency: { head, witness, inclusions } | ||
| * @param {string[]} recordDigests every snapshot + journal record digest that | ||
| * must be committed in the log | ||
| * @returns {{ head_valid: boolean, all_included: boolean, witness_present: boolean, | ||
| * witness_independent: boolean, anchor: 'none'|'self_signed'|'witnessed', | ||
| * tree_size: number|null, root_hash: string|null, errors: string[] }} | ||
| */ | ||
| export function verifyTransparencyEvidence(evidence, recordDigests = []) { | ||
| const result = { | ||
| head_valid: false, | ||
| all_included: false, | ||
| witness_present: false, | ||
| witness_independent: false, | ||
| anchor: 'none', | ||
| tree_size: null, | ||
| root_hash: null, | ||
| errors: [], | ||
| }; | ||
| if (!isObject(evidence) || !isObject(evidence.head)) { | ||
| result.errors.push('transparency evidence is missing a signed head'); | ||
| return result; | ||
| } | ||
| const head = evidence.head; | ||
| const headResult = verifyGateTuple(head); | ||
| const headPayload = isObject(head.payload) ? head.payload : {}; | ||
| const root = headPayload.root_hash; | ||
| result.tree_size = isInteger(headPayload.tree_size) ? headPayload.tree_size : null; | ||
| result.root_hash = isString(root) ? root : null; | ||
| if (!headResult.valid) { | ||
| result.errors.push(`transparency head signature invalid: ${headResult.error}`); | ||
| } else if (headPayload.schema !== MACRO_TRANSPARENCY_HEAD_SCHEMA) { | ||
| result.errors.push('transparency head is not a transparency-head schema'); | ||
| } else if (!HEX_64.test(root || '')) { | ||
| result.errors.push('transparency head root_hash is not 64-hex'); | ||
| } else { | ||
| result.head_valid = true; | ||
| } | ||
| // Inclusion of every checked record under the signed root. | ||
| if (result.head_valid) { | ||
| const inclusions = isArray(evidence.inclusions) ? evidence.inclusions : []; | ||
| const byDigest = new Map(); | ||
| for (const inc of inclusions) { | ||
| if (isObject(inc) && isString(inc.digest)) byDigest.set(inc.digest.toLowerCase(), inc); | ||
| } | ||
| let allIncluded = recordDigests.length > 0 || inclusions.length > 0; | ||
| for (const digest of recordDigests) { | ||
| const inc = byDigest.get(String(digest).toLowerCase()); | ||
| if (!inc || !verifyInclusion(digest, inc.proof, root)) { | ||
| allIncluded = false; | ||
| result.errors.push(`record ${String(digest).slice(0, 16)}... is not provably included in the signed log`); | ||
| } | ||
| } | ||
| result.all_included = allIncluded; | ||
| } | ||
| // Optional independent witness co-signature over the same head. | ||
| if (isObject(evidence.witness)) { | ||
| result.witness_present = true; | ||
| const witness = evidence.witness; | ||
| const witnessResult = verifyGateTuple(witness); | ||
| const witnessPayload = isObject(witness.payload) ? witness.payload : {}; | ||
| const witnessValid = witnessResult.valid | ||
| && witnessPayload.schema === MACRO_TRANSPARENCY_WITNESS_SCHEMA | ||
| && witnessPayload.head_digest === head.digest | ||
| && witnessPayload.root_hash === root; | ||
| if (!witnessValid) { | ||
| result.errors.push('transparency witness does not co-sign this head'); | ||
| } else { | ||
| result.witness_independent = isString(witness.verification_key) | ||
| && isString(head.verification_key) | ||
| && witness.verification_key.toLowerCase() !== head.verification_key.toLowerCase(); | ||
| } | ||
| } | ||
| result.anchor = (!result.head_valid || !result.all_included) | ||
| ? 'none' | ||
| : result.witness_independent ? 'witnessed' : 'self_signed'; | ||
| return result; | ||
| } | ||
| /** | ||
| * Verify a macro track-record bundle. | ||
| * | ||
| * Mirrors verifyGateBundle: it checks each record's signature, that every | ||
| * record (including the manifest) is signed by the single declared model | ||
| * key, that the manifest entries exactly enumerate the snapshots followed by | ||
| * the journal entries in order, the snapshot/journal counts, and that the | ||
| * history_head_digest recomputes over the ordered record digests. | ||
| * | ||
| * @param {Object} bundle parsed track-record bundle | ||
| * @param {Object} [opts] { publicKey } optional pinned model key | ||
| * @returns {Object} result with the same error-shape conventions as gate-receipt.js | ||
| */ | ||
| export function verifyMacroTrackRecord(bundle, opts = {}) { | ||
| const results = { | ||
| valid: true, | ||
| format: 'macro-track-record', | ||
| schema: bundle?.schema, | ||
| exportedAt: bundle?.exported_at, | ||
| period: isObject(bundle?.period) ? bundle.period : null, | ||
| custody: isString(bundle?.custody) ? bundle.custody : null, | ||
| modelVerificationKey: isString(bundle?.model_verification_key) ? bundle.model_verification_key : null, | ||
| snapshotCount: 0, | ||
| journalCount: 0, | ||
| total: 0, | ||
| passed: 0, | ||
| failed: 0, | ||
| cryptoFailed: 0, | ||
| chainChecks: 0, | ||
| chainFailed: 0, | ||
| errors: [], | ||
| records: [], | ||
| signers: [], | ||
| singleSigner: true, | ||
| manifestValid: null, | ||
| signerPinned: isString(opts.publicKey), | ||
| identityStatus: isString(opts.publicKey) ? 'pinned_operator_key' : 'embedded_key_only', | ||
| historyChainValid: null, | ||
| historyAnchored: false, | ||
| historyHeadPinned: false, | ||
| anchorHeadPinned: false, | ||
| sequence: 1, | ||
| transparencyAnchor: null, | ||
| proves: MACRO_BUNDLE_PROVES, | ||
| limitations: MACRO_BUNDLE_LIMITATIONS, | ||
| }; | ||
| if (!isObject(bundle) || bundle.schema !== MACRO_BUNDLE_SCHEMA || !isArray(bundle.snapshots) || !isObject(bundle.manifest)) { | ||
| return { ...results, valid: false, error: 'unknown_format', detail: 'unsupported macro bundle schema or missing snapshots/manifest' }; | ||
| } | ||
| const snapshots = bundle.snapshots; | ||
| const journal = isArray(bundle.journal) ? bundle.journal : []; | ||
| const priorManifests = isArray(bundle.prior_manifests) ? bundle.prior_manifests : []; | ||
| const anchors = isArray(bundle.anchor_chain) ? bundle.anchor_chain : []; | ||
| results.snapshotCount = snapshots.length; | ||
| results.journalCount = journal.length; | ||
| if (!HEX_64.test(results.modelVerificationKey || '')) { | ||
| return { ...results, valid: false, error: 'no_public_key', detail: 'bundle requires model_verification_key' }; | ||
| } | ||
| const modelKey = results.modelVerificationKey.toLowerCase(); | ||
| if (isString(opts.publicKey) && opts.publicKey.toLowerCase() !== modelKey) { | ||
| return { ...results, valid: false, error: 'key_mismatch', detail: 'bundle model_verification_key does not match --key' }; | ||
| } | ||
| const signerRoles = new Map(); | ||
| let firstError = null; | ||
| const add = (tuple, role, index) => { | ||
| results.total++; | ||
| const r = verifyGateTuple(tuple, { publicKey: modelKey }); | ||
| const key = isString(tuple?.verification_key) ? tuple.verification_key.toLowerCase() : null; | ||
| if (key) { | ||
| if (!signerRoles.has(key)) signerRoles.set(key, new Set()); | ||
| signerRoles.get(key).add(role); | ||
| if (key !== modelKey) results.singleSigner = false; | ||
| } else { | ||
| results.singleSigner = false; | ||
| } | ||
| const record = { | ||
| index: index + 1, | ||
| role, | ||
| schema: r.schema, | ||
| schemaRecognized: r.schemaRecognized || isMacroSchema(r.schema), | ||
| cryptoValid: r.valid, | ||
| cryptoError: r.valid ? undefined : r.error, | ||
| signer: tuple?.verification_key, | ||
| digest: tuple?.digest, | ||
| }; | ||
| results.records.push(record); | ||
| if (!r.valid) { | ||
| results.cryptoFailed++; | ||
| results.valid = false; | ||
| firstError ||= r.error; | ||
| results.errors.push(`[crypto] ${role} ${index + 1}: ${r.error}${r.detail ? ` (${r.detail})` : ''}`); | ||
| results.failed++; | ||
| } else { | ||
| results.passed++; | ||
| } | ||
| return r; | ||
| }; | ||
| for (let i = 0; i < snapshots.length; i++) add(snapshots[i], 'snapshot', i); | ||
| for (let i = 0; i < journal.length; i++) add(journal[i], 'journal', i); | ||
| for (let i = 0; i < priorManifests.length; i++) add(priorManifests[i], 'prior-manifest', i); | ||
| const mr = add(bundle.manifest, 'manifest', -1); | ||
| for (let i = 0; i < anchors.length; i++) add(anchors[i], 'history-anchor', i); | ||
| results.manifestValid = mr.valid; | ||
| // Single-signer custody check (every record, including the manifest, shares | ||
| // the declared model key). | ||
| results.chainChecks++; | ||
| if (!results.singleSigner) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| results.errors.push('[chain] not all records are signed by model_verification_key (single-signer custody violated)'); | ||
| } | ||
| // Manifest inventory + counts + history head. | ||
| if (mr.valid) { | ||
| const p = bundle.manifest.payload; | ||
| const ordered = [...snapshots, ...journal]; | ||
| const expectedEntries = ordered.map((t) => ({ schema: t?.payload?.schema, as_of: t?.payload?.as_of, digest: t?.digest })); | ||
| const historyHead = bytesToHex(sha256(utf8ToBytes(canonicalGateJSON(ordered.map((t) => t?.digest))))); | ||
| results.chainChecks++; | ||
| const entries = isArray(p.entries) ? p.entries : []; | ||
| let manifestError = null; | ||
| if (p.model_verification_key?.toLowerCase() !== modelKey) manifestError = 'manifest model_verification_key mismatch'; | ||
| else if (p.exported_at !== bundle.exported_at) manifestError = 'manifest exported_at mismatch'; | ||
| else if (entries.length !== expectedEntries.length) manifestError = `manifest entries count (${entries.length}) does not match exported records (${expectedEntries.length})`; | ||
| else if (!entries.every((e, idx) => refsEqual(e, expectedEntries[idx]))) manifestError = 'manifest entries do not exactly enumerate the exported records in order'; | ||
| else if (p.snapshot_count !== snapshots.length) manifestError = `manifest snapshot_count (${p.snapshot_count}) does not match (${snapshots.length})`; | ||
| else if (p.journal_count !== journal.length) manifestError = `manifest journal_count (${p.journal_count}) does not match (${journal.length})`; | ||
| else if (p.history_head_digest !== historyHead) manifestError = 'manifest history_head_digest mismatch'; | ||
| else if (p.sequence !== undefined && (!isInteger(p.sequence) || p.sequence < 1)) manifestError = 'manifest sequence is not a positive integer'; | ||
| if (manifestError) { | ||
| results.valid = false; | ||
| results.manifestValid = false; | ||
| results.chainFailed++; | ||
| results.errors.push(`[chain] Manifest: ${manifestError}`); | ||
| } | ||
| results.sequence = p.sequence ?? 1; | ||
| results.chainChecks++; | ||
| if (isString(opts.historyHead)) { | ||
| results.historyHeadPinned = opts.historyHead.toLowerCase() === String(p.history_head_digest).toLowerCase(); | ||
| if (!results.historyHeadPinned) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| results.errors.push('[chain] current history head does not match --history-head'); | ||
| } | ||
| } | ||
| const manifests = [...priorManifests, bundle.manifest]; | ||
| let historyChainError = null; | ||
| if (priorManifests.length > 0 || p.sequence !== undefined) { | ||
| const retained = new Set(expectedEntries.map((entry) => entry.digest)); | ||
| for (let i = 0; i < manifests.length && !historyChainError; i++) { | ||
| const current = manifests[i]; | ||
| const payload = current?.payload; | ||
| const expectedSequence = i + 1; | ||
| if ((payload?.sequence ?? expectedSequence) !== expectedSequence) { | ||
| historyChainError = `manifest sequence mismatch at ${expectedSequence}`; | ||
| break; | ||
| } | ||
| if (i > 0) { | ||
| const previous = manifests[i - 1]; | ||
| if (payload?.previous_manifest_digest !== previous?.digest | ||
| || payload?.previous_history_head_digest !== previous?.payload?.history_head_digest) { | ||
| historyChainError = `manifest previous-head mismatch at sequence ${expectedSequence}`; | ||
| break; | ||
| } | ||
| } | ||
| for (const entry of (isArray(payload?.entries) ? payload.entries : [])) { | ||
| if (!retained.has(entry.digest)) { | ||
| historyChainError = `current export omits prior record ${entry.digest}`; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| results.historyChainValid = historyChainError === null; | ||
| results.chainChecks++; | ||
| if (historyChainError) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| results.errors.push(`[chain] History: ${historyChainError}`); | ||
| } | ||
| } | ||
| if (anchors.length > 0) { | ||
| results.historyAnchored = true; | ||
| const offset = manifests.length - anchors.length; | ||
| let anchorError = null; | ||
| for (let i = 0; i < anchors.length && !anchorError; i++) { | ||
| const anchor = anchors[i]; | ||
| const anchorPayload = anchor?.payload; | ||
| const manifest = manifests[offset + i]; | ||
| if (!manifest) { | ||
| anchorError = `anchor ${i + 1} has no corresponding manifest`; | ||
| break; | ||
| } | ||
| const previousAnchor = i > 0 ? anchors[i - 1]?.digest : null; | ||
| if (anchorPayload?.schema !== MACRO_ANCHOR_SCHEMA | ||
| || anchorPayload?.sequence !== manifest.payload?.sequence | ||
| || anchorPayload?.manifest_digest !== manifest.digest | ||
| || anchorPayload?.history_head_digest !== manifest.payload?.history_head_digest | ||
| || anchorPayload?.previous_anchor_digest !== previousAnchor) { | ||
| anchorError = `anchor mismatch at sequence ${anchorPayload?.sequence ?? i + 1}`; | ||
| } | ||
| } | ||
| results.chainChecks++; | ||
| if (anchorError) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| results.errors.push(`[chain] Anchor: ${anchorError}`); | ||
| } | ||
| const anchorHead = anchors.at(-1)?.digest; | ||
| if (isString(opts.anchorHead)) { | ||
| results.anchorHeadPinned = opts.anchorHead.toLowerCase() === String(anchorHead).toLowerCase(); | ||
| results.chainChecks++; | ||
| if (!results.anchorHeadPinned) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| results.errors.push('[chain] current anchor head does not match --anchor-head'); | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| // Manifest signature failed; inventory cannot be trusted. | ||
| results.chainChecks++; | ||
| results.chainFailed++; | ||
| results.valid = false; | ||
| results.errors.push('[chain] Manifest signature invalid; inventory cannot be verified'); | ||
| } | ||
| // Optional RFC 6962 transparency evidence: confirm every exported record | ||
| // digest is committed under a signed (and optionally witnessed) log head. | ||
| // A bundle WITHOUT a transparency field stays valid (backward compatible). | ||
| if (isObject(bundle.transparency)) { | ||
| const recordDigests = [...snapshots, ...journal] | ||
| .map((t) => (isString(t?.digest) ? t.digest : null)) | ||
| .filter(Boolean); | ||
| const transparency = verifyTransparencyEvidence(bundle.transparency, recordDigests); | ||
| results.transparencyAnchor = transparency; | ||
| results.chainChecks++; | ||
| if (!transparency.head_valid || !transparency.all_included) { | ||
| results.valid = false; | ||
| results.chainFailed++; | ||
| const why = !transparency.head_valid | ||
| ? 'transparency head is not a valid signed log head' | ||
| : 'one or more exported records are not provably included in the signed log'; | ||
| results.errors.push(`[chain] Transparency: ${why}${transparency.errors.length ? ` (${transparency.errors[0]})` : ''}`); | ||
| } | ||
| } | ||
| results.signers = [...signerRoles.entries()].map(([key, roles]) => ({ key, roles: [...roles].sort(), isModelKey: key === modelKey })); | ||
| if (!results.valid) results.error = firstError || 'chain_link_mismatch'; | ||
| return results; | ||
| } |
| /** | ||
| * Trusted Context Pack verifier (TCB v1). | ||
| * | ||
| * A fund's desktop runtime turns an approved file (a positions export, a blotter, | ||
| * a mandate, a research folder) into a SIGNED TrustedContextPack before any agent | ||
| * or gate operates over it. This engine lets a third party (an allocator, an LP, | ||
| * an auditor) re-verify that pack offline, holding only this open tool and no | ||
| * ScopeBlind code: the digest is recomputed over the canonical payload and the | ||
| * Ed25519 signature is checked over that digest against the embedded (or pinned) | ||
| * key, exactly like a Gate receipt tuple. | ||
| * | ||
| * The TCB-specific check is the relabel guard: gate_status MUST equal the status | ||
| * derived from the signed confidence and freshness. A pack cannot be re-signed to | ||
| * claim a low-confidence or stale parse is `usable`. This is what makes the | ||
| * confidence-gating trustworthy to someone who did not produce the pack. | ||
| * | ||
| * What a verified pack proves: these exact bytes, with this file hash, parsed to | ||
| * this context at this confidence and freshness, signed by this key. What it does | ||
| * NOT prove: that the source file is authentic, complete, or the fund's true book. | ||
| * That requires a custodian-signed feed or a DKIM/PAdES source (a stronger tier). | ||
| * | ||
| * @module verify-cli/src/engines/trusted-context-pack | ||
| * @license Apache-2.0 | ||
| */ | ||
| import { ed25519 } from '@noble/curves/ed25519'; | ||
| import { sha256 } from '@noble/hashes/sha256'; | ||
| import { utf8ToBytes } from '@noble/hashes/utils'; | ||
| import { sortKeysDeep } from '../util/canonical.js'; | ||
| import { hexToBytes, bytesToHex } from '../util/hex.js'; | ||
| export const TCB_SCHEMA = 'scopeblind.trusted_context_pack.v1'; | ||
| const TCB_USABLE_THRESHOLD = 0.75; | ||
| const HEX_64 = /^[0-9a-f]{64}$/; | ||
| const SOURCE_TYPES = new Set(['book_positions', 'nav_account', 'blotter', 'risk_report', 'mandate_limits', 'research', 'operations', 'unknown']); | ||
| const GATE_STATUSES = new Set(['usable', 'needs_approval', 'blocked']); | ||
| const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v); | ||
| const isString = (v) => typeof v === 'string' && v.length > 0; | ||
| const isNumber = (v) => typeof v === 'number' && Number.isFinite(v); | ||
| export function canonicalTcbJSON(payload) { | ||
| return JSON.stringify(sortKeysDeep(payload)); | ||
| } | ||
| /** The single source of truth for the gate decision (kept in sync with | ||
| * scopeblind-pm/src/trusted-context.ts:gateStatusFor). */ | ||
| export function gateStatusFor(confidence, freshness) { | ||
| if (confidence <= 0) return 'blocked'; | ||
| if (confidence < TCB_USABLE_THRESHOLD || freshness?.stale === true) return 'needs_approval'; | ||
| return 'usable'; | ||
| } | ||
| export const TCB_PROVES = [ | ||
| 'Authenticity: the pack was signed by the Ed25519 key it carries (the fund runtime that built it).', | ||
| 'Integrity: the file hash, parsed context, confidence, freshness, and gate decision are bound by the signature and unmodified.', | ||
| 'Honest gating: gate_status matches the confidence and freshness, so a low-confidence or stale parse cannot be relabeled usable.', | ||
| ]; | ||
| export const TCB_LIMITATIONS = [ | ||
| 'That the source file is authentic, complete, or the fund\'s true book; a pack attests the parse, not the provenance.', | ||
| 'Correctness of the original file behind file_hash unless its bytes are separately disclosed and re-hashed.', | ||
| 'Whether the embedded verification_key is the runtime you expect; pin it with --key to bind trust to a known signer.', | ||
| 'A stronger source tier (custodian-signed feed, DKIM or PAdES) is needed to attest where the data came from.', | ||
| ]; | ||
| function collectFields(payload) { | ||
| const out = {}; | ||
| for (const k of ['source_type', 'source_format', 'source_lineage', 'file_name', 'file_hash', 'parser_version', 'confidence', 'gate_status', 'workspace_id', 'source_id']) { | ||
| if (payload[k] !== undefined && payload[k] !== null) out[k] = payload[k]; | ||
| } | ||
| if (isObject(payload.freshness)) out.freshness = { as_of: payload.freshness.as_of ?? null, stale: payload.freshness.stale === true }; | ||
| if (Array.isArray(payload.warnings)) out.warnings = payload.warnings; | ||
| if (isObject(payload.summary)) out.summary = payload.summary; | ||
| return out; | ||
| } | ||
| function schemaErrors(payload) { | ||
| const errors = []; | ||
| for (const f of ['workspace_id', 'source_id', 'source_format', 'file_hash', 'parser_version']) { | ||
| if (!isString(payload[f])) errors.push(`missing or empty ${f}`); | ||
| } | ||
| if (!SOURCE_TYPES.has(payload.source_type)) errors.push(`invalid source_type ${JSON.stringify(payload.source_type)}`); | ||
| if (!GATE_STATUSES.has(payload.gate_status)) errors.push(`invalid gate_status ${JSON.stringify(payload.gate_status)}`); | ||
| if (!HEX_64.test(payload.file_hash || '')) errors.push('file_hash must be 64 lowercase hex characters'); | ||
| if (!isNumber(payload.confidence) || payload.confidence < 0 || payload.confidence > 1) errors.push('confidence must be a number in [0,1]'); | ||
| if (!Array.isArray(payload.warnings)) errors.push('warnings must be an array'); | ||
| if (!isObject(payload.parsed_artifacts)) errors.push('parsed_artifacts must be an object'); | ||
| if (!isObject(payload.freshness) || !isString(payload.freshness.ingested_at)) errors.push('freshness.ingested_at is required'); | ||
| else if (typeof payload.freshness.stale !== 'boolean') errors.push('freshness.stale must be a boolean'); | ||
| // The relabel guard: gate_status must follow from the signed confidence/freshness. | ||
| if (errors.length === 0) { | ||
| const expected = gateStatusFor(payload.confidence, payload.freshness); | ||
| if (payload.gate_status !== expected) errors.push(`gate_status ${JSON.stringify(payload.gate_status)} does not match the signed confidence and freshness (expected ${expected})`); | ||
| } | ||
| return errors; | ||
| } | ||
| /** | ||
| * Verify a TrustedContextPack tuple { payload, digest, signature, verification_key }. | ||
| * | ||
| * @param {object} tuple | ||
| * @param {{ publicKey?: string }} [opts] | ||
| */ | ||
| export function verifyTrustedContextPack(tuple, opts = {}) { | ||
| const base = { format: 'trusted-context-pack', schema: null, schemaRecognized: false, algorithm: 'ed25519' }; | ||
| if (!isObject(tuple)) return { valid: false, error: 'unknown_format', ...base, detail: 'pack is not an object' }; | ||
| const payload = tuple.payload; | ||
| if (!isObject(payload)) return { valid: false, error: 'missing_payload', ...base }; | ||
| const schema = isString(payload.schema) ? payload.schema : null; | ||
| base.schema = schema; | ||
| base.schemaRecognized = schema === TCB_SCHEMA; | ||
| base.payloadFields = collectFields(payload); | ||
| if (schema !== TCB_SCHEMA) return { valid: false, error: 'unknown_format', ...base, detail: `expected schema ${TCB_SCHEMA}` }; | ||
| if (!isString(tuple.signature)) return { valid: false, error: 'missing_signature', ...base }; | ||
| if (!isString(tuple.digest) || !HEX_64.test(tuple.digest)) return { valid: false, error: 'malformed_hex', ...base, detail: 'digest must be 64 lowercase hex characters' }; | ||
| if (!isString(tuple.verification_key)) return { valid: false, error: 'no_public_key', ...base }; | ||
| const pinned = isString(opts.publicKey); | ||
| if (pinned && opts.publicKey.toLowerCase() !== tuple.verification_key.toLowerCase()) { | ||
| return { valid: false, error: 'key_mismatch', ...base, publicKey: tuple.verification_key, expectedKey: opts.publicKey }; | ||
| } | ||
| const recomputed = bytesToHex(sha256(utf8ToBytes(canonicalTcbJSON(payload)))); | ||
| if (recomputed !== tuple.digest) return { valid: false, error: 'digest_mismatch', ...base, digest: tuple.digest, recomputedDigest: recomputed, publicKey: tuple.verification_key }; | ||
| let ok = false; | ||
| try { | ||
| ok = ed25519.verify(hexToBytes(tuple.signature), hexToBytes(tuple.digest), hexToBytes(tuple.verification_key)); | ||
| } catch (e) { | ||
| return { valid: false, error: 'malformed_hex', ...base, digest: tuple.digest, detail: e.message, publicKey: tuple.verification_key }; | ||
| } | ||
| if (!ok) return { valid: false, error: 'invalid_signature', ...base, digest: tuple.digest, publicKey: tuple.verification_key }; | ||
| const semanticErrors = schemaErrors(payload); | ||
| if (semanticErrors.length) return { valid: false, error: 'schema_invalid', ...base, digest: tuple.digest, publicKey: tuple.verification_key, detail: semanticErrors.join('; '), semanticErrors }; | ||
| return { | ||
| valid: true, | ||
| ...base, | ||
| digest: tuple.digest, | ||
| publicKey: tuple.verification_key, | ||
| keySource: pinned ? 'embedded-tuple (pinned via --key)' : 'embedded-tuple', | ||
| gateStatus: payload.gate_status, | ||
| confidence: payload.confidence, | ||
| sourceType: payload.source_type, | ||
| proves: TCB_PROVES, | ||
| limitations: TCB_LIMITATIONS, | ||
| }; | ||
| } |
| /** | ||
| * Known-issuer labels. | ||
| * | ||
| * Verifying a signature proves the bytes were signed by a given key, not WHO holds | ||
| * it (the engines say this in their limitations). A known-issuers map turns a raw | ||
| * hex key into a human label in the output ("Signer: Meridian Global Macro Desk") | ||
| * so a reader who has pinned trust to a known desk sees the name, not just hex. It | ||
| * is a display aid layered on top of `--key` pinning, never a trust shortcut: an | ||
| * unlabeled key still verifies, and a labeled key is only as trustworthy as the map | ||
| * the verifier chose to load. | ||
| * | ||
| * @module verify-cli/src/util/known-issuers | ||
| * @license Apache-2.0 | ||
| */ | ||
| import { readFileSync } from 'node:fs'; | ||
| /** | ||
| * Load and merge issuer label maps (hex public key -> label). A bundled default is | ||
| * overlaid with an optional user file (the user file wins). Keys are normalized to | ||
| * lowercase hex. Unreadable or malformed files are ignored (labels are non-critical). | ||
| * | ||
| * @param {string|undefined} userPath path passed via --known-issuers | ||
| * @param {string|undefined} bundledPath path to the package's known-issuers.json | ||
| * @returns {Record<string,string>} lowercase-hex key -> label | ||
| */ | ||
| export function loadKnownIssuers(userPath, bundledPath) { | ||
| const merged = {}; | ||
| for (const p of [bundledPath, userPath]) { | ||
| if (!p) continue; | ||
| try { | ||
| const obj = JSON.parse(readFileSync(p, 'utf8')); | ||
| if (obj && typeof obj === 'object') { | ||
| for (const [k, v] of Object.entries(obj)) { | ||
| if (k.startsWith('_')) continue; // _comment / _note keys are documentation | ||
| if (typeof v === 'string' && /^[0-9a-fA-F]+$/.test(k)) merged[k.toLowerCase()] = v; | ||
| } | ||
| } | ||
| } catch { /* labels are non-critical; ignore unreadable/malformed files */ } | ||
| } | ||
| return merged; | ||
| } | ||
| /** Resolve a label for a hex key, or null. Case-insensitive. */ | ||
| export function labelFor(key, issuers) { | ||
| if (typeof key !== 'string' || !issuers) return null; | ||
| return issuers[key.toLowerCase()] || null; | ||
| } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
28
-3.45%485884
-17.61%60
-17.81%10493
-18.78%340
-5.29%