yay-layer 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ratify.js ADDED
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+ // Shared ratification helpers. The "reviewed bundle" is the exact set + content a human sees
3
+ // before ratifying delegated (Autopilot) work; its hash lets `yay ratify --sign` prove it signs
4
+ // PRECISELY what was reviewed (TOCTOU protection). One implementation, used by both the CLI and
5
+ // the dashboard, so the render-time hash and the sign-time hash are computed identically.
6
+ const { sha256 } = require('./crypto');
7
+ const { canonical } = require('./util');
8
+
9
+ // Cells whose current effective approval is a grant auto-approval (delegated, awaiting ratification).
10
+ function autoCellIds(verified) {
11
+ const results = (verified && verified.results) || {};
12
+ return Object.keys(results).filter((id) => { const t = results[id].trust; return !!(t && t.auto); });
13
+ }
14
+
15
+ // A stable hash over {each delegated Cell's specHash + code hash + grant}. Any material change —
16
+ // spec edited, code changed, the delegated set changed, a different grant — yields a different hash.
17
+ function ratifyBundle(manifest, verified, autoIds) {
18
+ const ids = (autoIds || autoCellIds(verified)).slice().sort();
19
+ const per = {};
20
+ for (const id of ids) {
21
+ const c = (manifest.cells || {})[id] || {};
22
+ const t = ((verified.results || {})[id] || {}).trust || {};
23
+ per[id] = { spec: c.specHash || null, code: sha256(String(c.unitBody || '')), grant: t.grant || null };
24
+ }
25
+ return { ids, per, hash: sha256(canonical({ v: 1, cells: per })) };
26
+ }
27
+
28
+ module.exports = { autoCellIds, ratifyBundle };
package/src/record.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ // A recording stand-in for an EFFECT SURFACE (a canvas 2D context, a DOM node, a
3
+ // logger…). Every method call and property assignment on it is appended to a shared
4
+ // `trace`, so a spec can assert on WHAT a side-effecting function DID — e.g. which
5
+ // `fillStyle` was set before which `fillRect` — instead of a return value it doesn't
6
+ // have. Property READS return another recorder, so chains like `ctx.canvas.width`
7
+ // don't crash; used as a value a recorder coerces to 0 / ''.
8
+ //
9
+ // trace entries: { type:'call', name, args } | { type:'set', name, value }
10
+ function makeRecorder() {
11
+ const trace = [];
12
+ function node(prefix) {
13
+ return new Proxy(function () {}, {
14
+ get(_t, k) {
15
+ if (k === '__trace') return trace;
16
+ if (k === Symbol.toPrimitive) return () => 0;
17
+ if (k === 'then') return undefined; // not a thenable
18
+ if (typeof k === 'symbol') return undefined;
19
+ return node(prefix ? prefix + '.' + String(k) : String(k));
20
+ },
21
+ set(_t, k, v) { trace.push({ type: 'set', name: (prefix ? prefix + '.' : '') + String(k), value: v }); return true; },
22
+ apply(_t, _this, args) { trace.push({ type: 'call', name: prefix || '(call)', args: args }); return node(prefix); },
23
+ construct() { return node(prefix); },
24
+ has() { return true; },
25
+ });
26
+ }
27
+ return { proxy: node(''), trace };
28
+ }
29
+
30
+ module.exports = { makeRecorder };
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+ // Reverify engine — P1: re-run TODAY's verifier against a reconstructed HISTORICAL snapshot and report
3
+ // the per-Cell verdict as the machine sees it NOW. This is the "replay the tape through today's better
4
+ // reader" primitive. It assesses VERIFICATION only (code⇔spec) — the snapshot was an already-approved
5
+ // state, and re-verification is the machine re-judging, independent of the human-authority axis (the
6
+ // papers keep those two chains separate). P2/P3 will sweep every snapshot and diff against the original
7
+ // attestation to produce the upgrade report; this is the single-state core they stand on.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const D = require('./durable');
13
+ const A = require('./attest');
14
+ const { buildManifest } = require('./manifest');
15
+ const { verifyManifest } = require('./verify');
16
+ const cap = require('./capability');
17
+
18
+ // Reconstruct `snap` into a throwaway dir and verify it under the current verifier.
19
+ // Returns { codeTreeHash, capability, fingerprint, counts, passed, cells:{id:{state,proven,predicate}} }
20
+ // or { error } if the tree can't be faithfully rebuilt. `key` is the resolved Durable archive key.
21
+ function reverifyState(p, snap, key, config, opts) {
22
+ opts = opts || {};
23
+ let dir = null;
24
+ try {
25
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'yay-reverify-'));
26
+ D.reconstructSnapshot(p, snap, key, dir);
27
+ // Fidelity gate: a re-verdict is only meaningful if the rebuilt tree IS the recorded one.
28
+ const manifest = buildManifest(dir);
29
+ const rebuilt = A.codeTreeHashOf(manifest);
30
+ if (snap.codeTreeHash && rebuilt !== snap.codeTreeHash) {
31
+ return { error: 'reconstruction mismatch (rebuilt ' + rebuilt.slice(0, 12) + '… ≠ recorded ' + String(snap.codeTreeHash).slice(0, 12) + '…)' };
32
+ }
33
+ // Verification-only: assumeSigned (the snapshot was approved) so the state reflects the machine's
34
+ // code⇔spec judgment under today's checks, not the UNSIGNED gate (there is no lock in the temp dir).
35
+ const verified = verifyManifest(manifest, { approvals: [] }, config, { mutate: opts.mutate !== false, assumeSigned: true });
36
+ const cells = {};
37
+ for (const id of Object.keys(verified.results)) {
38
+ const r = verified.results[id];
39
+ cells[id] = {
40
+ state: r.state,
41
+ proven: !!r.proven,
42
+ predicate: !!(r.predicate && r.predicate.findings && r.predicate.findings.length),
43
+ coverage: (r.coverage && r.coverage.total) ? { exercised: r.coverage.exercised, total: r.coverage.total } : null,
44
+ };
45
+ }
46
+ // The signable verification object for this reconstructed historical tree, judged under TODAY's
47
+ // verifier. `yay reverify --attest` turns this into a signed, append-only reverification record
48
+ // (see reverificationObj). Built from the reconstructed manifest, so its code/spec hashes describe
49
+ // the HISTORICAL state, while its capability/fingerprint describe today's verifier.
50
+ const verObj = A.buildVerification(manifest, verified, { config });
51
+ return {
52
+ codeTreeHash: rebuilt,
53
+ capability: cap.CAPABILITY || null,
54
+ fingerprint: cap.capabilityFingerprint ? cap.capabilityFingerprint() : null,
55
+ counts: verified.counts,
56
+ passed: verified.passed,
57
+ cells,
58
+ verObj,
59
+ };
60
+ } catch (e) {
61
+ return { error: (e && e.message) || String(e) };
62
+ } finally {
63
+ if (dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) {} }
64
+ }
65
+ }
66
+
67
+ // ── P2: the sweep ───────────────────────────────────────────────────────────────────────────────
68
+ // Replay every preserved snapshot through TODAY's verifier and diff each Cell against its ORIGINAL
69
+ // recorded verdict, so a verifier upgrade turns into an "what changed" report. Never rewrites history —
70
+ // the originals stand; this only computes deltas (and P3 will append reverification attestations).
71
+ const RANK = { GREEN: 0, YELLOW: 1, RED: 2, PINK: 2, UNSIGNED: 2 };
72
+ function classify(from, to) {
73
+ if (from == null) return 'no-baseline';
74
+ if (from === to) return 'unchanged';
75
+ const rf = RANK[from] != null ? RANK[from] : 2;
76
+ const rt = RANK[to] != null ? RANK[to] : 2;
77
+ return rt > rf ? 'regressed' : (rt < rf ? 'improved' : 'changed');
78
+ }
79
+ // The baseline for a snapshot = the per-Cell evidence + capability of its linked (signature-verified)
80
+ // attestation. Pluggable so callers/tests can supply a baseline; default reads the real attestation.
81
+ function defaultBaselineOf(p, config) {
82
+ return (snap) => {
83
+ if (!snap || !snap.attest) return null;
84
+ try { const att = A.loadAttestation(p, config, snap.attest); return att ? { capability: att.capability || null, evidence: att.evidence || null } : null; }
85
+ catch (_) { return null; }
86
+ };
87
+ }
88
+ // Sweep all snapshots (deduped by codeTreeHash — identical trees re-verified once). Returns a report:
89
+ // { total, checked, deduped, errors, unchanged, improved, regressed, noBaseline, capability, fingerprint,
90
+ // regressions:[…], improvements:[…], states:[…] }.
91
+ function reverifySweep(p, key, config, opts) {
92
+ opts = opts || {};
93
+ const baselineOf = opts.baselineOf || defaultBaselineOf(p, config);
94
+ const nowCap = cap.CAPABILITY || null;
95
+ const allSnaps = D.listSnapshots(p);
96
+ const cache = {};
97
+ const report = {
98
+ total: allSnaps.length, filtered: 0, checked: 0, deduped: 0, errors: 0,
99
+ unchanged: 0, improved: 0, regressed: 0, noBaseline: 0,
100
+ capability: nowCap, fingerprint: cap.capabilityFingerprint ? cap.capabilityFingerprint() : null,
101
+ regressions: [], improvements: [], states: [],
102
+ };
103
+ for (const snap of allSnaps) {
104
+ // Cheap pre-filters (no reconstruction needed): --since keeps snapshots at/after a date; --eligible
105
+ // keeps only those a capability change could actually re-judge (baseline capability ≠ today's).
106
+ if (opts.since && snap.at && String(snap.at) < String(opts.since)) { report.filtered++; continue; }
107
+ if (opts.eligibleOnly) {
108
+ const b = baselineOf(snap);
109
+ if (b && b.capability && b.capability === nowCap) { report.filtered++; continue; }
110
+ }
111
+ const ck = snap.codeTreeHash;
112
+ let rv;
113
+ if (ck && cache[ck]) { rv = cache[ck]; report.deduped++; }
114
+ else { rv = reverifyState(p, snap, key, config, opts); if (ck) cache[ck] = rv; }
115
+ if (rv.error) { report.errors++; report.states.push({ at: snap.at, error: rv.error }); continue; }
116
+ report.checked++;
117
+ const base = baselineOf(snap);
118
+ const baseCap = (base && base.capability) || null;
119
+ const baseEv = (base && base.evidence) || null;
120
+ const perCell = {};
121
+ for (const id of Object.keys(rv.cells)) {
122
+ const to = rv.cells[id].state;
123
+ const from = baseEv && baseEv[id] ? baseEv[id].state : null;
124
+ const verdict = classify(from, to);
125
+ perCell[id] = { from, to, verdict, predicate: rv.cells[id].predicate };
126
+ if (verdict === 'regressed') { report.regressed++; report.regressions.push({ at: snap.at, cell: id, from, to, fromCapability: baseCap, toCapability: rv.capability, predicate: rv.cells[id].predicate }); }
127
+ else if (verdict === 'improved') { report.improved++; report.improvements.push({ at: snap.at, cell: id, from, to, fromCapability: baseCap, toCapability: rv.capability }); }
128
+ else if (verdict === 'unchanged') report.unchanged++;
129
+ else if (verdict === 'no-baseline') report.noBaseline++;
130
+ }
131
+ // A state "changed" if any Cell moved against its baseline — this is what --attest mints a record for.
132
+ const changed = Object.keys(perCell).some((id) => perCell[id].verdict === 'regressed' || perCell[id].verdict === 'improved');
133
+ const st = { at: snap.at, codeTreeHash: ck, attest: snap.attest || null, fromCapability: baseCap, toCapability: rv.capability, changed, cells: perCell };
134
+ if (opts.keepVerObj) st.verObj = rv.verObj;
135
+ report.states.push(st);
136
+ }
137
+ return report;
138
+ }
139
+
140
+ // Turn a reconstructed state's verification object into a signable REVERIFICATION record: the same
141
+ // canonical verification bytes (today's capability judging the historical code/spec), tagged as a
142
+ // reverification and carrying a reference to the ORIGINAL attestation it re-assesses (Paper 3: append
143
+ // a new immutable record beside the old, never rewrite). The caller signs it with the verifier key and
144
+ // appends it to the same chain (A.signAttestation → A.appendAttestation).
145
+ function reverificationObj(verObj, snap, baseline) {
146
+ return Object.assign({}, verObj, {
147
+ kind: 'reverification',
148
+ reassesses: (snap && snap.attest) || null, // hash of the original attestation for this state
149
+ reassessedAt: (snap && snap.at) || null, // when the original state was archived
150
+ originalCapability: (baseline && baseline.capability) || null,
151
+ });
152
+ }
153
+
154
+ // ── P4: the reverification posture (grandfathering) ──────────────────────────────────────────────
155
+ // A project-level gate control — off / guarded / strict — in the same spirit as the foundation seal
156
+ // posture (and, like it, NOT a per-Cell capability policy kind: it changes gate ENFORCEMENT of record
157
+ // existence, never how any Cell's verdict is computed, so it stays out of the capability fingerprint).
158
+ // off — preserved history is grandfathered (the default). A better verifier never blocks old work.
159
+ // guarded — `yay verify` WARNS when preserved history predates the current verifier capability.
160
+ // strict — the gate BLOCKS until each such state has a signed reverification under the current
161
+ // capability (`yay reverify --all --attest`). Enforces the EXISTENCE of the signed record —
162
+ // never key-possession at view time (the keyless report is always available).
163
+ function reverifyPosture(config) {
164
+ const m = String((config && config.reverification) || 'off').toLowerCase();
165
+ return (m === 'guarded' || m === 'strict') ? m : 'off';
166
+ }
167
+
168
+ // Consult the posture against preserved history. Returns { posture, capability, subject, pending, satisfied }
169
+ // where `pending` lists distinct preserved states whose ORIGINAL attestation predates the current
170
+ // capability and that no reverification at the current capability yet covers. `opts.capability` overrides
171
+ // the current capability (used in tests to simulate a bump without a live version change).
172
+ function reverifyGate(p, config, opts) {
173
+ opts = opts || {};
174
+ const posture = opts.posture || reverifyPosture(config);
175
+ const cur = opts.capability || cap.CAPABILITY || null;
176
+ const out = { posture, capability: cur, subject: posture !== 'off', pending: [], satisfied: true };
177
+ if (posture === 'off') return out;
178
+ // Which original attestations already have a reverification at the CURRENT capability?
179
+ const led = A.loadLedger(p, config);
180
+ const covered = {};
181
+ for (const e of led.entries) {
182
+ const a = A.loadAttestation(p, config, e.hash);
183
+ if (a && a.kind === 'reverification' && a.capability === cur && a.reassesses) covered[a.reassesses] = true;
184
+ }
185
+ // Optional per-Cell scoping (P4b): when opts.scoped is set, only states that contained at least one
186
+ // in-scope Cell (an owner-signed `reverify: latest` match — opts.scopedIds is that Cell-id set) are
187
+ // subject; the rest stay grandfathered. Membership is read from the state's own attestation evidence,
188
+ // so scoping stays KEYLESS (no reconstruction needed to consult the posture).
189
+ const scoped = !!opts.scoped;
190
+ const scopedIds = opts.scopedIds || null;
191
+ if (scoped) out.scoped = true;
192
+ const seen = new Set();
193
+ for (const s of D.listSnapshots(p)) {
194
+ if (!s.attest) continue; // never attested — no baseline capability to compare
195
+ if (s.codeTreeHash && seen.has(s.codeTreeHash)) continue; // dedup identical trees
196
+ if (s.codeTreeHash) seen.add(s.codeTreeHash);
197
+ const orig = A.loadAttestation(p, config, s.attest);
198
+ const origCap = orig ? orig.capability : null;
199
+ if (!origCap || origCap === cur) continue; // already at the current capability
200
+ if (covered[s.attest]) continue; // a current-capability reverification covers it
201
+ if (scoped) {
202
+ const ev = (orig && orig.evidence) || {};
203
+ const inScope = Object.keys(ev).some((id) => scopedIds && scopedIds.has(id));
204
+ if (!inScope) continue; // no in-scope Cell in this state — grandfathered
205
+ }
206
+ out.pending.push({ at: s.at, attest: s.attest, fromCapability: origCap, codeTreeHash: s.codeTreeHash });
207
+ }
208
+ out.satisfied = out.pending.length === 0;
209
+ return out;
210
+ }
211
+
212
+ module.exports = { reverifyState, reverifySweep, reverificationObj, classify, reverifyPosture, reverifyGate };
package/src/roster.js ADDED
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+ // The SIGNED roster — who is allowed to sign, as a tamper-evident chain.
3
+ //
4
+ // Without this, anyone (a malignant AI) with repo write could add their own key
5
+ // to the roster file and then "approve" their own code — defeating the whole
6
+ // protocol. So the roster is not a plain file you trust: it is an append-only log
7
+ // of events, each SIGNED by an existing OWNER's key (which lives on a phone, not
8
+ // on the AI's machine). `deriveRoster` rebuilds the set of trusted signers using
9
+ // ONLY events whose signatures check out — so an unsigned edit has no effect.
10
+ //
11
+ // Semantics: 1-of-N. Any enrolled signer may approve a Cell ALONE. Enrolling a
12
+ // signer is itself an owner-signed act; it does not require co-approval.
13
+ //
14
+ // Bootstrapping truth: the first event (genesis) is self-signed — it IS the trust
15
+ // root. If the AI can rewrite every file it could swap genesis too, so the root
16
+ // must be anchored outside the repo (a fingerprint pinned in CI, or your eyes).
17
+ // `deriveRoster` exposes rootFp and enforces an optional pin.
18
+
19
+ const C = require('./crypto');
20
+ const { canonical } = require('./util');
21
+
22
+ // Bytes an event is signed over: the event minus its own signature.
23
+ function eventBytes(event) {
24
+ const { signature, ...body } = event; // eslint-disable-line no-unused-vars
25
+ return canonical(body);
26
+ }
27
+
28
+ // Human-verifiable fingerprint of a public key (for pinning / eyeballing).
29
+ function fingerprint(pub) {
30
+ const h = C.sha256(pub).slice(0, 16).toUpperCase();
31
+ return h.replace(/(.{4})(?=.)/g, '$1-'); // 3F2A-9C11-8D0E-1B44
32
+ }
33
+
34
+ // Replay the log into an effective roster, trusting only validly-signed events.
35
+ // opts.root (optional) pins the expected genesis fingerprint.
36
+ // Returns { roster:{name:[pub]}, roles:{name:'owner'|'signer'}, rootFp, problems, ok }.
37
+ function deriveRoster(log, opts) {
38
+ const events = (log && log.events) || [];
39
+ const roster = {};
40
+ const roles = {};
41
+ const problems = [];
42
+ let rootFp = null;
43
+ let policy = { rules: [] }; // signing policy — set by owner-signed `policy` events (latest wins)
44
+ let foundation = null; // foundation seal payload — set by owner-signed `foundation` events (latest wins)
45
+ let foundationMode = 'off'; // off | guarded | strict
46
+ let rootMeta = null; // reroot provenance stamped into genesis (supersedes/rerootedAt/rerootReason)
47
+
48
+ const ownerKeys = () => {
49
+ const out = [];
50
+ for (const n of Object.keys(roles)) if (roles[n] === 'owner') out.push(...(roster[n] || []));
51
+ return out;
52
+ };
53
+ const addKey = (name, pub) => { (roster[name] = roster[name] || []); if (!roster[name].includes(pub)) roster[name].push(pub); };
54
+
55
+ if (!events.length) return { roster, roles, rootFp, problems: ['roster log is empty — no trust root established'], ok: false };
56
+
57
+ events.forEach((e, i) => {
58
+ if (i === 0) {
59
+ if (e.type !== 'genesis') { problems.push('first roster event must be genesis'); return; }
60
+ if (!e.name || !e.pub || !e.signature || !C.verify(eventBytes(e), e.signature, e.pub)) { problems.push('genesis signature invalid — trust root not established'); return; }
61
+ addKey(e.name, e.pub); roles[e.name] = 'owner'; rootFp = fingerprint(e.pub);
62
+ // A reroot stamps the rotation into the (signed) genesis — surfaced for the audit.
63
+ if (e.supersedes || e.rerootReason || e.rerootedAt) rootMeta = { supersedes: e.supersedes || null, rerootedAt: e.rerootedAt || e.at || null, rerootReason: e.rerootReason || null };
64
+ return;
65
+ }
66
+ if (!rootFp) { problems.push(`event ${e.id || i}: no valid trust root, ignored`); return; }
67
+ // Authority: the event must be signed by a CURRENT owner's key.
68
+ const authorized = e.signature && ownerKeys().some((k) => C.verify(eventBytes(e), e.signature, k));
69
+ if (!authorized) { problems.push(`event ${e.id || i} (${e.type} ${e.name || ''}) is not signed by an owner — REJECTED`); return; }
70
+ if (e.type === 'add-signer') {
71
+ if (!e.name || !e.pub) { problems.push(`event ${e.id || i}: add-signer missing name/pub`); return; }
72
+ addKey(e.name, e.pub);
73
+ if (!roles[e.name]) roles[e.name] = (e.role === 'owner' ? 'owner' : 'signer');
74
+ } else if (e.type === 'add-key') {
75
+ if (!roster[e.name]) { problems.push(`event ${e.id || i}: add-key for unknown identity "${e.name}"`); return; }
76
+ addKey(e.name, e.pub);
77
+ } else if (e.type === 'revoke-key') {
78
+ // Remove one compromised/rotated key; the identity survives if it has others.
79
+ // (Past seals stay attributed — this only governs who can sign going forward.)
80
+ if (!e.name || !e.pub) { problems.push(`event ${e.id || i}: revoke-key missing name/pub`); return; }
81
+ if (roster[e.name]) { roster[e.name] = roster[e.name].filter((k) => k !== e.pub); if (!roster[e.name].length) { delete roster[e.name]; delete roles[e.name]; } }
82
+ } else if (e.type === 'remove-signer') {
83
+ // Remove an identity entirely (all its keys). History remains attributed.
84
+ if (!e.name) { problems.push(`event ${e.id || i}: remove-signer missing name`); return; }
85
+ delete roster[e.name]; delete roles[e.name];
86
+ } else if (e.type === 'policy') {
87
+ // Owner-signed signing policy. The rules are inside eventBytes, so they're
88
+ // tamper-evident and chained like every other governance event. Latest wins;
89
+ // an empty ruleset returns to neutral.
90
+ policy = { rules: Array.isArray(e.rules) ? e.rules : [] };
91
+ } else if (e.type === 'foundation') {
92
+ // Owner-signed FOUNDATION SEAL — the baseline of the fixed core files (see foundation.js).
93
+ // Being an owner-signed, chained roster event makes it un-removable: deleting it breaks
94
+ // the pinned chain and is itself flagged. Latest wins; mode 'off' retires the seal.
95
+ foundationMode = ['guarded', 'strict', 'off'].includes(e.mode) ? e.mode : 'guarded';
96
+ foundation = (foundationMode === 'off') ? null : (e.seal || null);
97
+ } else {
98
+ problems.push(`event ${e.id || i}: unknown type "${e.type}"`);
99
+ }
100
+ });
101
+
102
+ // Safety: never let a revocation leave the project with no owner (governance lockout).
103
+ if (rootFp && !ownerKeys().length) problems.push('roster would have NO owner key left — governance locked out (revocation refused)');
104
+
105
+ const pinned = opts && opts.root ? String(opts.root).toUpperCase() : null;
106
+ if (pinned && rootFp && pinned !== rootFp) problems.push(`TRUST-ROOT MISMATCH: expected ${pinned}, found ${rootFp} — the roster may have been swapped`);
107
+
108
+ return { roster, roles, rootFp, problems, ok: problems.length === 0, policy, foundation, foundationMode, rootMeta };
109
+ }
110
+
111
+ // Next event id given a log.
112
+ function nextEventId(log) {
113
+ const n = ((log && log.events) || []).length + 1;
114
+ return 'R-' + String(n).padStart(4, '0');
115
+ }
116
+
117
+ module.exports = { deriveRoster, eventBytes, fingerprint, nextEventId };