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/CONSTITUTION.md +55 -0
- package/LICENSE +21 -0
- package/README.md +383 -0
- package/bin/yay.js +3550 -0
- package/package.json +55 -0
- package/src/adopt.js +181 -0
- package/src/adversary.js +119 -0
- package/src/analyze.js +270 -0
- package/src/assurance.js +122 -0
- package/src/attest.js +216 -0
- package/src/capability.js +59 -0
- package/src/constitution.js +162 -0
- package/src/coverage.js +77 -0
- package/src/crypto.js +78 -0
- package/src/dashboard.js +463 -0
- package/src/durable.js +152 -0
- package/src/e2e.js +67 -0
- package/src/extract.js +179 -0
- package/src/foundation.js +143 -0
- package/src/gate.js +252 -0
- package/src/grants.js +249 -0
- package/src/history.js +77 -0
- package/src/ids.js +40 -0
- package/src/manifest.js +303 -0
- package/src/map.js +1742 -0
- package/src/mutate.js +192 -0
- package/src/objects.js +31 -0
- package/src/phone.js +188 -0
- package/src/plan.js +141 -0
- package/src/policy.js +0 -0
- package/src/predicate.js +143 -0
- package/src/prove.js +876 -0
- package/src/ratify.js +28 -0
- package/src/record.js +30 -0
- package/src/reverify.js +212 -0
- package/src/roster.js +117 -0
- package/src/signer-page.js +603 -0
- package/src/specdiff.js +69 -0
- package/src/tags.js +67 -0
- package/src/testrun.js +41 -0
- package/src/util.js +234 -0
- package/src/vendor/recovery.js +217 -0
- package/src/vendor/tweetnacl.min.js +1 -0
- package/src/verify.js +560 -0
- package/standard/STANDARD.md +135 -0
package/src/verify.js
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The gate. For every Cell it answers two orthogonal questions:
|
|
3
|
+
// TRUST — is this spec covered by a valid signature in the lock? (who signed)
|
|
4
|
+
// VERIFY — does the code match the spec? (static checks)
|
|
5
|
+
// and combines them into GREEN / YELLOW / RED / UNSIGNED.
|
|
6
|
+
//
|
|
7
|
+
// MVP scope: VERIFY runs *static-lite* checks (unit exists, declared purity
|
|
8
|
+
// holds, effects are declared). Deep behavioural proof — property tests generated
|
|
9
|
+
// from `ensures`, real AST effect analysis, mutation scoring — is the per-language
|
|
10
|
+
// adapter milestone on the roadmap (see standard/STANDARD.md §Verification tiers).
|
|
11
|
+
|
|
12
|
+
const { canonical, pubKeysOf, isJsLang, normLangName } = require('./util');
|
|
13
|
+
const { verify: sigVerify } = require('./crypto');
|
|
14
|
+
const { proveManifest } = require('./prove');
|
|
15
|
+
const { requiredSigners, inertLevel, ignoreAllowed, coverageRequired, predicateRequired } = require('./policy');
|
|
16
|
+
const { deriveRoster } = require('./roster');
|
|
17
|
+
const G = require('./grants');
|
|
18
|
+
const F = require('./foundation');
|
|
19
|
+
|
|
20
|
+
// Shallow side-effect signals used for the MVP purity / minimality checks — per
|
|
21
|
+
// LANGUAGE, so a `pure: yes` claim is policed in every language we can run, not just
|
|
22
|
+
// JS. (Signal-based, not a proof of purity: an evader can dodge a regex; the typical
|
|
23
|
+
// AI-written effect gets caught.)
|
|
24
|
+
const EFFECT_SIGNALS = [
|
|
25
|
+
['localStorage', /\blocalStorage\b/], ['sessionStorage', /\bsessionStorage\b/],
|
|
26
|
+
['console', /\bconsole\s*\./], ['network', /\bfetch\s*\(|\bXMLHttpRequest\b/],
|
|
27
|
+
['require', /\brequire\s*\(/], ['process', /\bprocess\s*\./],
|
|
28
|
+
['dom', /\bdocument\b|\bwindow\b/], ['filesystem', /\bfs\s*\./],
|
|
29
|
+
['nondeterminism', /\bMath\.random\b|\bDate\.now\b|\bnew Date\b/],
|
|
30
|
+
];
|
|
31
|
+
const EFFECT_SIGNALS_PY = [
|
|
32
|
+
['stdout', /\bprint\s*\(/], ['stdin', /\binput\s*\(/],
|
|
33
|
+
['filesystem', /\bopen\s*\(|\bshutil\s*\.|\bpathlib\b.*\.(write|unlink|mkdir)/],
|
|
34
|
+
['os', /\bos\s*\./], ['sys', /\bsys\s*\./], ['subprocess', /\bsubprocess\s*\./],
|
|
35
|
+
['network', /\bsocket\s*\.|\brequests\s*\.|\burllib\b|\bhttpx\s*\./],
|
|
36
|
+
['nondeterminism', /\brandom\s*\.|\btime\s*\.\s*time\s*\(|\bdatetime\s*\.\s*(datetime\s*\.\s*)?now\s*\(/],
|
|
37
|
+
['global-state', /^\s*global\s+[A-Za-z_]/],
|
|
38
|
+
['import', /\b__import__\s*\(/],
|
|
39
|
+
];
|
|
40
|
+
// Per-language nets for the five P1.5 languages, so a `pure: yes` claim is policed in each with
|
|
41
|
+
// its OWN effect idioms (before this, non-JS/Python fell through to no net — a missed Red).
|
|
42
|
+
// Conservative: match common effect calls, not every token (a false Red is worse than a gap).
|
|
43
|
+
const EFFECT_SIGNALS_RUBY = [
|
|
44
|
+
['stdout', /\b(puts|print|pp)\b|\$stdout\b|\bSTDOUT\b/],
|
|
45
|
+
['filesystem', /\bFile\s*\.|\bIO\s*\.|\bDir\s*\.|\bFileUtils\b/],
|
|
46
|
+
['network', /\bNet::HTTP\b|\bURI\s*\.\s*open\b|\bopen-uri\b|\bTCPSocket\b|\bSocket\b|\bHTTParty\b/],
|
|
47
|
+
['exec', /\bsystem\s*\(|\bexec\s*\(|\bIO\.popen\b|\bProcess\s*\.|`[^`]*`|%x[({[]/],
|
|
48
|
+
['env', /\bENV\b/],
|
|
49
|
+
['nondeterminism', /\brand\b|\bTime\s*\.\s*now\b|\bDateTime\b|\bSecureRandom\b/],
|
|
50
|
+
['global-state', /(^|[^@\w])\$[a-zA-Z_]\w*/],
|
|
51
|
+
];
|
|
52
|
+
const EFFECT_SIGNALS_PHP = [
|
|
53
|
+
['stdout', /\becho\b|\bprint\b|\bprintf\s*\(|\bvar_dump\s*\(/],
|
|
54
|
+
['filesystem', /\bfile_get_contents\s*\(|\bfile_put_contents\s*\(|\bfopen\s*\(|\bunlink\s*\(|\bmkdir\s*\(|\brename\s*\(/],
|
|
55
|
+
['network', /\bcurl_exec\s*\(|\bfsockopen\s*\(|\bstream_socket_client\s*\(/],
|
|
56
|
+
['exec', /\bexec\s*\(|\bshell_exec\s*\(|\bsystem\s*\(|\bpassthru\s*\(|\bproc_open\s*\(|`[^`]*`/],
|
|
57
|
+
['superglobal', /\$_(GET|POST|REQUEST|SESSION|COOKIE|SERVER|ENV|FILES)\b/],
|
|
58
|
+
['db', /\bnew\s+PDO\b|\bmysqli?_(query|connect)\b/],
|
|
59
|
+
['nondeterminism', /\brand\s*\(|\bmt_rand\s*\(|\brandom_int\s*\(|\btime\s*\(|\bmicrotime\s*\(|\bdate\s*\(|\buniqid\s*\(/],
|
|
60
|
+
];
|
|
61
|
+
const EFFECT_SIGNALS_SOLIDITY = [
|
|
62
|
+
['state', /\bstorage\b|\bselfdestruct\s*\(/],
|
|
63
|
+
['event', /\bemit\s+\w/],
|
|
64
|
+
['external-call', /\.\s*call\s*[({]|\.\s*delegatecall\s*\(|\.\s*transfer\s*\(|\.\s*send\s*\(/],
|
|
65
|
+
['context', /\bblock\s*\.|\bmsg\s*\.|\btx\s*\.|\bblockhash\s*\(|\bnow\b/],
|
|
66
|
+
];
|
|
67
|
+
const EFFECT_SIGNALS_RUST = [
|
|
68
|
+
['stdout', /\bprintln!|\bprint!|\beprintln!|\beprint!|\bio::stdout\b/],
|
|
69
|
+
['filesystem', /\bstd::fs\b|\bFile::(open|create)\b|\bfs::(read|write|remove|create)/],
|
|
70
|
+
['network', /\bTcpStream\b|\bTcpListener\b|\bstd::net\b|\breqwest\b/],
|
|
71
|
+
['process', /\bstd::process\b|\bCommand::new\b/],
|
|
72
|
+
['nondeterminism', /\brand::|\bInstant::now\b|\bSystemTime::now\b|\bthread_rng\b/],
|
|
73
|
+
['global-mut', /\bstatic\s+mut\b/],
|
|
74
|
+
['unsafe', /\bunsafe\b/],
|
|
75
|
+
];
|
|
76
|
+
const EFFECT_SIGNALS_CSHARP = [
|
|
77
|
+
['stdout', /\bConsole\s*\.\s*(Write|WriteLine|Read|ReadLine)\b/],
|
|
78
|
+
['filesystem', /\bFile\s*\.|\bDirectory\s*\.|\bStreamReader\b|\bStreamWriter\b|\bSystem\s*\.\s*IO\b/],
|
|
79
|
+
['network', /\bHttpClient\b|\bWebClient\b|\bSocket\b|\bSystem\s*\.\s*Net\b/],
|
|
80
|
+
['process', /\bProcess\s*\.\s*Start\b|\bSystem\s*\.\s*Diagnostics\s*\.\s*Process\b/],
|
|
81
|
+
['env', /\bEnvironment\s*\./],
|
|
82
|
+
['nondeterminism', /\bnew\s+Random\b|\bDateTime\s*\.\s*(Now|UtcNow|Today)\b|\bGuid\s*\.\s*NewGuid\b|\bStopwatch\b/],
|
|
83
|
+
];
|
|
84
|
+
const isPyLang = (l) => /^py(thon)?w?$/i.test(String(l || ''));
|
|
85
|
+
// A machine-readable descriptor of the per-language effect nets: { lang: [signal labels] }. Used by
|
|
86
|
+
// the verifier-capability fingerprint (src/capability.js) so adding/removing a signal or a whole net
|
|
87
|
+
// changes the fingerprint — forcing a capability-version bump instead of a silent expansion.
|
|
88
|
+
function effectNetDescriptor() {
|
|
89
|
+
const labels = (arr) => arr.map((s) => s[0]).sort();
|
|
90
|
+
return {
|
|
91
|
+
js: labels(EFFECT_SIGNALS), python: labels(EFFECT_SIGNALS_PY), ruby: labels(EFFECT_SIGNALS_RUBY),
|
|
92
|
+
php: labels(EFFECT_SIGNALS_PHP), solidity: labels(EFFECT_SIGNALS_SOLIDITY), rust: labels(EFFECT_SIGNALS_RUST),
|
|
93
|
+
csharp: labels(EFFECT_SIGNALS_CSHARP),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function effectSignalsFor(lang) {
|
|
98
|
+
if (!lang || isJsLang(lang)) return EFFECT_SIGNALS;
|
|
99
|
+
if (isPyLang(lang)) return EFFECT_SIGNALS_PY;
|
|
100
|
+
switch (normLangName(lang)) {
|
|
101
|
+
case 'ruby': return EFFECT_SIGNALS_RUBY;
|
|
102
|
+
case 'php': return EFFECT_SIGNALS_PHP;
|
|
103
|
+
case 'solidity': return EFFECT_SIGNALS_SOLIDITY;
|
|
104
|
+
case 'rust': return EFFECT_SIGNALS_RUST;
|
|
105
|
+
case 'csharp': return EFFECT_SIGNALS_CSHARP;
|
|
106
|
+
default: return null; // languages with no net yet — don't guess (a false Red is worse)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const SEV = { GREEN: 0, YELLOW: 1, UNSIGNED: 2, RED: 3 };
|
|
111
|
+
const worst = (a, b) => (SEV[a] >= SEV[b] ? a : b);
|
|
112
|
+
|
|
113
|
+
function trustOf(cell, lock, roster, grants, policy, rejections) {
|
|
114
|
+
let match = null;
|
|
115
|
+
let violation = null; // a delegated approval whose grant-key signature is real but broke its envelope
|
|
116
|
+
let firstAt = null; // earliest approval that ever covered this Cell → "created in the system"
|
|
117
|
+
for (const ap of lock.approvals || []) {
|
|
118
|
+
if (!ap.items || !(cell.id in ap.items)) continue;
|
|
119
|
+
if (ap.at && (!firstAt || Date.parse(ap.at) < Date.parse(firstAt))) firstAt = ap.at;
|
|
120
|
+
if (ap.items[cell.id] !== cell.specHash) continue;
|
|
121
|
+
const { signature, ...rest } = ap;
|
|
122
|
+
if (ap.autoApproved && ap.grant) {
|
|
123
|
+
// Autopilot: signed by the machine-held GRANT key, valid only if a good grant
|
|
124
|
+
// covers it (owner-signed, unexpired, unrevoked-before-this, in-envelope, in-count).
|
|
125
|
+
const g = (grants || {})[ap.grant];
|
|
126
|
+
const chk = g && G.autoApprovalOk(g, ap, cell.id, cell, { policy });
|
|
127
|
+
const sigOk = g && sigVerify(canonical(rest), signature, g.grantPub);
|
|
128
|
+
if (g && chk.ok && sigOk) {
|
|
129
|
+
match = { signed: true, signer: ap.signer || null, auto: true, grant: ap.grant, at: ap.at || null };
|
|
130
|
+
} else if (sigOk && g && g.ownerOk) {
|
|
131
|
+
// BACKSTOP (P3): the grant is a VALID, owner-authorized delegation and its key really signed
|
|
132
|
+
// this — but the target is OUTSIDE the envelope (wrong path/cell/risk, or an owner-signed
|
|
133
|
+
// non-delegable area). That's an attempt to widen scope, not a lapsed grant, so we don't drop
|
|
134
|
+
// it silently: flag a GRANT VIOLATION that blocks the gate loudly, agent behaviour aside.
|
|
135
|
+
// (A merely expired/revoked/count-exhausted grant is a lapse → falls through to UNSIGNED.)
|
|
136
|
+
const cov = G.grantCoversCellR(g, cell.id, cell, { policy });
|
|
137
|
+
if (!cov.ok) violation = { grant: ap.grant, reason: cov.reason };
|
|
138
|
+
}
|
|
139
|
+
continue; // a human ratification (a later normal approval) will supersede this
|
|
140
|
+
}
|
|
141
|
+
const pubs = pubKeysOf(roster[ap.signer]); // an identity may hold several keys
|
|
142
|
+
if (!pubs.length) continue;
|
|
143
|
+
if (pubs.some((pub) => sigVerify(canonical(rest), signature, pub))) {
|
|
144
|
+
// Keep the most recent valid signature over the current spec as "signed at".
|
|
145
|
+
match = { signed: true, signer: ap.signer, auto: false, grant: null, at: ap.at || null };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Rejection withdraws approval (P3). A human turning down delegated work is like removing a
|
|
149
|
+
// signature: the Cell drops behind the gate until it's reworked and re-approved. A reject targets
|
|
150
|
+
// an EXACT specHash, so reworking the code (which changes the specHash) naturally clears the old
|
|
151
|
+
// reject; and a later HUMAN approval over that same spec — the human changing their mind — also
|
|
152
|
+
// supersedes it. A delegated (grant-key) re-approval never clears a human rejection. Forging a
|
|
153
|
+
// rejection can only BLOCK a Cell (fail-safe), never approve one, so a signature check isn't
|
|
154
|
+
// required for soundness here.
|
|
155
|
+
let rejected = null;
|
|
156
|
+
for (const ev of (rejections && rejections.events) || []) {
|
|
157
|
+
if (!ev || ev.type !== 'reject' || !ev.cells || !ev.cells.includes(cell.id)) continue;
|
|
158
|
+
if (!ev.specHashes || ev.specHashes[cell.id] !== cell.specHash) continue; // only the exact reviewed spec
|
|
159
|
+
if (!rejected || Date.parse(ev.at || 0) > Date.parse(rejected.at || 0)) rejected = ev;
|
|
160
|
+
}
|
|
161
|
+
if (rejected) {
|
|
162
|
+
const humanAfter = match && match.auto === false && match.at && Date.parse(match.at) > Date.parse(rejected.at || 0);
|
|
163
|
+
if (!humanAfter) {
|
|
164
|
+
return { signed: false, rejected: true, rejectReason: rejected.reason || '', rejectBy: rejected.signer || rejected.by || null, firstAt, violation };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (match) return { ...match, firstAt };
|
|
168
|
+
return { signed: false, firstAt, violation };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function staticChecks(cell) {
|
|
172
|
+
const notes = [];
|
|
173
|
+
let red = false, yellow = false;
|
|
174
|
+
const spec = cell.spec || {};
|
|
175
|
+
const isModule = !!(cell.contains && cell.contains.length);
|
|
176
|
+
|
|
177
|
+
// A module (container Cell) governs composition, not a code unit — so it is not
|
|
178
|
+
// expected to have a function body or the machine fields a leaf Cell needs.
|
|
179
|
+
if (!isModule && cell.unitName && !cell.unitFound) {
|
|
180
|
+
red = true; notes.push({ level: 'red', text: `code missing: no unit "${cell.unitName}" found below the spec` });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const declaredEffects = (spec.effects || '').toLowerCase();
|
|
184
|
+
const pure = /^yes\b/i.test(spec.pure || '');
|
|
185
|
+
let badLines = [];
|
|
186
|
+
// Effect / purity signals per LANGUAGE (JS tokens for JS/TS, Python tokens for
|
|
187
|
+
// Python). Languages with no reliable net get none — a false red on a function
|
|
188
|
+
// that merely shares a name would be worse than an honest gap.
|
|
189
|
+
// (Missing lang ⇒ JS, so legacy/synthetic cells behave exactly as before.)
|
|
190
|
+
const sigSet = effectSignalsFor(cell.langName || cell.lang);
|
|
191
|
+
if (cell.unitBody && sigSet) {
|
|
192
|
+
const base = cell.unitBodyStart || 0;
|
|
193
|
+
const found = []; // { signal, line, text } — the exact offending source lines
|
|
194
|
+
cell.unitBody.split('\n').forEach((ln, k) => {
|
|
195
|
+
for (const [sig, re] of sigSet) {
|
|
196
|
+
if (re.test(ln)) { found.push({ signal: sig, line: base + k + 1, text: ln.trim() }); break; }
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const signals = [...new Set(found.map((f) => f.signal))];
|
|
200
|
+
const renders = /^yes\b/i.test(spec.renders || '');
|
|
201
|
+
if (pure && found.length && renders) {
|
|
202
|
+
// A component's inline handlers (onClick={() => fetch(…)}) run AFTER render, so a
|
|
203
|
+
// signal here isn't proof the RENDER is impure — a line net can't tell handler from
|
|
204
|
+
// body. Honest middle: Yellow, with the fix spelled out (never a silent pass, never
|
|
205
|
+
// a false Red on a legitimately pure render that attaches effectful handlers).
|
|
206
|
+
yellow = true; badLines = found.map((f) => f.text);
|
|
207
|
+
for (const f of found) notes.push({ level: 'yellow', text: `effect signal in a \`renders:\` component (line ${f.line}): ${f.signal} → ${f.text} — if it's in a handler, declare it in \`effects:\` (handlers run outside the render) or drop \`pure: yes\`; if it runs during render, the render is not pure` });
|
|
208
|
+
} else if (pure && found.length) {
|
|
209
|
+
red = true;
|
|
210
|
+
badLines = found.map((f) => f.text);
|
|
211
|
+
for (const f of found) notes.push({ level: 'red', text: `purity violated (line ${f.line}): \`pure: yes\` but uses ${f.signal} → ${f.text}` });
|
|
212
|
+
} else if (!pure && found.length) {
|
|
213
|
+
const undeclared = found.filter((f) => !declaredEffects.includes(f.signal) && !declaredEffects.includes('any'));
|
|
214
|
+
if (declaredEffects && undeclared.length) {
|
|
215
|
+
yellow = true; badLines = undeclared.map((f) => f.text);
|
|
216
|
+
for (const f of undeclared) notes.push({ level: 'yellow', text: `undeclared effect (line ${f.line}): ${f.signal} → ${f.text}` });
|
|
217
|
+
} else if (!declaredEffects) {
|
|
218
|
+
yellow = true; badLines = found.map((f) => f.text);
|
|
219
|
+
notes.push({ level: 'yellow', text: `has effects (${signals.join(', ')}) but none declared in \`effects:\`` });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// NOTE the non-JS honesty cap ("signed-only, capped at Yellow") no longer lives here:
|
|
225
|
+
// some non-JS Cells ARE machine-provable now (pure Python via the subprocess prover),
|
|
226
|
+
// so the cap is applied in verifyManifest AFTER the prover pass — only Cells the
|
|
227
|
+
// prover did NOT prove get capped. Same honesty, without punishing a real proof.
|
|
228
|
+
|
|
229
|
+
// vague / prose-only spec caps at YELLOW: a leaf Cell needs at least one machine field.
|
|
230
|
+
const machineFields = ['in', 'out', 'ensures', 'pure', 'throws'].some((k) => spec[k]);
|
|
231
|
+
if (!isModule && !machineFields) { yellow = true; notes.push({ level: 'yellow', text: 'prose-only spec (no in/out/ensures/pure/throws) — capped at Yellow' }); }
|
|
232
|
+
if (!spec.intent) { yellow = true; notes.push({ level: 'yellow', text: 'no `intent:` line' }); }
|
|
233
|
+
|
|
234
|
+
// unit-name mismatch: the spec's `unit:` names a different function than the code
|
|
235
|
+
// actually defines below it (a rename, or the block attached to the wrong function).
|
|
236
|
+
const short = (n) => String(n || '').split('.').pop();
|
|
237
|
+
if (!isModule && spec.unit && cell.detectedUnit && short(spec.unit) !== short(cell.detectedUnit)) {
|
|
238
|
+
yellow = true;
|
|
239
|
+
notes.push({ level: 'yellow', text: `unit name mismatch: spec says \`unit: ${spec.unit}\` but the code defines \`${cell.detectedUnit}\` — update the spec, or the block may be on the wrong function` });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// dangling references: this Cell calls a bare name defined nowhere in the project
|
|
243
|
+
// (renamed/removed function, typo, or an import the analyzer couldn't see).
|
|
244
|
+
if (cell.unresolved && cell.unresolved.length) {
|
|
245
|
+
yellow = true;
|
|
246
|
+
const names = cell.unresolved.map((n) => '`' + n + '`').join(', ');
|
|
247
|
+
notes.push({ level: 'yellow', text: `calls undefined name${cell.unresolved.length > 1 ? 's' : ''} ${names} — not defined anywhere in the project (renamed/removed/typo, or an unlisted import)` });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { red, yellow, notes, badLines };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function verifyManifest(manifest, lock, config, opts) {
|
|
254
|
+
opts = opts || {};
|
|
255
|
+
// Trusted signers come from the SIGNED roster log when present (authoritative),
|
|
256
|
+
// not from the plain config file — so an unsigned edit to who-can-sign has no
|
|
257
|
+
// effect. Legacy projects with no signed log fall back to config.signers.
|
|
258
|
+
let roster, rosterProblems = [], rootFp = null, rosterOk = true, signedRoster = false;
|
|
259
|
+
let ownerPubs = [];
|
|
260
|
+
let rosterPolicy = { rules: [] }; // the ENFORCED policy comes from the owner-signed roster (tamper-evident)
|
|
261
|
+
let foundationSeal = null, foundationMode = 'off', rootMeta = null;
|
|
262
|
+
if (opts.roster && opts.roster.events) {
|
|
263
|
+
const d = deriveRoster(opts.roster, { root: opts.root });
|
|
264
|
+
roster = d.roster; rosterProblems = d.problems; rootFp = d.rootFp; rosterOk = d.ok; signedRoster = true;
|
|
265
|
+
rosterPolicy = d.policy || { rules: [] };
|
|
266
|
+
foundationSeal = d.foundation || null; foundationMode = d.foundationMode || 'off'; rootMeta = d.rootMeta || null;
|
|
267
|
+
ownerPubs = Object.keys(d.roles || {}).filter((n) => d.roles[n] === 'owner').reduce((a, n) => a.concat(roster[n] || []), []);
|
|
268
|
+
} else {
|
|
269
|
+
roster = (config && config.signers) || {};
|
|
270
|
+
ownerPubs = ((config && config.owners) || []).reduce((a, n) => a.concat(pubKeysOf(roster[n])), []);
|
|
271
|
+
}
|
|
272
|
+
// Autopilot: validated delegation grants (empty when the project doesn't use them).
|
|
273
|
+
const grants = opts.grants ? G.deriveGrants(opts.grants, ownerPubs, lock.approvals) : {};
|
|
274
|
+
const results = {};
|
|
275
|
+
|
|
276
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
277
|
+
const cell = manifest.cells[id];
|
|
278
|
+
const trust = trustOf(cell, lock, roster, grants, rosterPolicy, opts.rejections);
|
|
279
|
+
const sc = staticChecks(cell);
|
|
280
|
+
|
|
281
|
+
let state;
|
|
282
|
+
// assumeSigned: reverify re-judges a reconstructed HISTORICAL (already-approved) tree on code⇔spec
|
|
283
|
+
// alone — there's no lock in the temp dir, so we skip the UNSIGNED gate and report the machine verdict.
|
|
284
|
+
if (trust.violation) state = 'RED'; // grant-envelope violation → gate-blocking backstop
|
|
285
|
+
else if (!trust.signed && !opts.assumeSigned) state = 'UNSIGNED';
|
|
286
|
+
else if (sc.red) state = 'RED';
|
|
287
|
+
else if (sc.yellow) state = 'YELLOW';
|
|
288
|
+
else state = 'GREEN';
|
|
289
|
+
|
|
290
|
+
const isModule = !!(cell.contains && cell.contains.length);
|
|
291
|
+
// Influence overlay (computed from the call graph, not asserted). It does not
|
|
292
|
+
// change color — it's an oversight signal — except the honest bloat *note*.
|
|
293
|
+
if (!isModule && cell.bloat) {
|
|
294
|
+
sc.notes.push({ level: 'info', text: 'no static callers found — possible dead code / bloat candidate (or an entry point called dynamically)' });
|
|
295
|
+
}
|
|
296
|
+
if (trust.auto) {
|
|
297
|
+
sc.notes.push({ level: 'info', text: `DELEGATED under grant ${trust.grant} (Autopilot) — awaiting ratification, not human-reviewed. Run \`yay ratify\` to sign it for real.` });
|
|
298
|
+
}
|
|
299
|
+
if (trust.violation) {
|
|
300
|
+
sc.notes.push({ level: 'red', text: `GRANT VIOLATION — a delegated (Autopilot) approval under grant ${trust.violation.grant} covered this Cell, but it is outside that grant's envelope: ${trust.violation.reason}. The agent cannot widen its own grant; this needs a real human signature. (verifier backstop)` });
|
|
301
|
+
}
|
|
302
|
+
if (trust.rejected) {
|
|
303
|
+
sc.notes.push({ level: 'red', text: `🚫 rejected by ${trust.rejectBy || 'a human'}${trust.rejectReason ? ` — ${String(trust.rejectReason).replace(/[.\s]+$/, '')}` : ''}. The delegated approval was withdrawn (like removing a signature); rework and re-approve to clear it. (yay ratify --reject)` });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
results[id] = {
|
|
307
|
+
id, state, trust, notes: sc.notes, badLines: sc.badLines || [], file: cell.file, line: cell.line,
|
|
308
|
+
blast: cell.blast || 0, dependents: cell.directCallers || 0, isEntry: !!cell.isEntry, bloat: !!cell.bloat,
|
|
309
|
+
// legibility: is the promise actually machine-proven, or just signed?
|
|
310
|
+
hasEnsures: !!(cell.spec && cell.spec.ensures), proven: false,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Behavioural proof: run each pure Cell against its `ensures` (spec-derived
|
|
315
|
+
// tests). A counterexample ⇒ Red (code contradicts its promise); a claim we
|
|
316
|
+
// can't check ⇒ Yellow (unproven), never a fake pass.
|
|
317
|
+
const proofs = proveManifest(manifest, opts);
|
|
318
|
+
for (const id of Object.keys(proofs)) {
|
|
319
|
+
if (!results[id]) continue;
|
|
320
|
+
const pr = proofs[id];
|
|
321
|
+
if (pr.status === 'fail') {
|
|
322
|
+
results[id].state = worst(results[id].state, 'RED');
|
|
323
|
+
results[id].notes.push({ level: 'red', text: 'ensures FAILED — ' + pr.counterexample });
|
|
324
|
+
const body = manifest.cells[id] && manifest.cells[id].unitBody;
|
|
325
|
+
if (body) results[id].badLines = body.split('\n').map((l) => l.trim()).filter((l) => /\breturn\b/.test(l));
|
|
326
|
+
} else if (pr.status === 'pass') {
|
|
327
|
+
results[id].proven = true;
|
|
328
|
+
const mu = pr.mutation;
|
|
329
|
+
let text = `ensures proven over ${pr.cases} generated case(s)`;
|
|
330
|
+
// Honesty: a proven non-JS Cell (Python subprocess prover) isn't yet graded by
|
|
331
|
+
// mutation testing or the inertness check — say so, so its green reads correctly.
|
|
332
|
+
const prLang = manifest.cells[id] && manifest.cells[id].lang;
|
|
333
|
+
if ((!mu || !mu.total) && prLang && !isJsLang(prLang)) text += ` (mutation grading + inertness check not yet available for ${prLang})`;
|
|
334
|
+
if (mu && mu.total) {
|
|
335
|
+
text += `; mutation score ${Math.round(mu.score * 100)}% (${mu.killed}/${mu.total} killed)`;
|
|
336
|
+
if (mu.score < 0.5) {
|
|
337
|
+
results[id].state = worst(results[id].state, 'YELLOW');
|
|
338
|
+
results[id].notes.push({ level: 'yellow', text: `weak ensures — ${mu.survived} mutant(s) survived (e.g. ${mu.survivor || 'a code change'}); the promise passes even when the code is broken` });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
results[id].notes.push({ level: 'info', text });
|
|
342
|
+
// Branch-exercise honesty: show the coverage boundary of the proof instead of hiding it.
|
|
343
|
+
// A badge only — it never downgrades the green here (a legit `throws:` guard is often
|
|
344
|
+
// unexercised on purpose); policy (`coverage: full`) decides if that's gate-blocking.
|
|
345
|
+
const cov = pr.coverage;
|
|
346
|
+
if (cov && cov.total) {
|
|
347
|
+
results[id].coverage = cov;
|
|
348
|
+
if (cov.missed && cov.missed.length) {
|
|
349
|
+
const where = cov.missed.slice(0, 3).map((m) => 'line ' + m.line).join(', ') + (cov.missed.length > 3 ? '…' : '');
|
|
350
|
+
results[id].notes.push({ level: 'info', text: `proven — but ${cov.exercised}/${cov.total} branches exercised by spec-derived inputs; unexercised: ${where}. A dormant branch is an unexercised branch — check these are intended (guards/edge cases), spec them, or require full exercise via policy.` });
|
|
351
|
+
} else {
|
|
352
|
+
results[id].notes.push({ level: 'info', text: `proven — all ${cov.total} branches exercised by spec-derived inputs (full branch coverage)` });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} else if (pr.status === 'skip') {
|
|
356
|
+
// Couldn't check it (prose, exotic type, won't load) — say so, but don't
|
|
357
|
+
// punish honest code for the prover's limits. Only a real contradiction is Red.
|
|
358
|
+
results[id].notes.push({ level: 'info', text: 'ensures not machine-verified — ' + pr.reason });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Honesty cap for non-JS Cells, applied AFTER the prover so a genuinely proven Cell
|
|
363
|
+
// (e.g. a pure Python function proved by the subprocess prover) keeps its Green.
|
|
364
|
+
// Everything non-JS the prover did NOT prove stays capped at Yellow — no silent
|
|
365
|
+
// false green for signed-but-unverified code in other languages.
|
|
366
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
367
|
+
const cell = manifest.cells[id];
|
|
368
|
+
if (!results[id]) continue;
|
|
369
|
+
const isModule = !!(cell.contains && cell.contains.length);
|
|
370
|
+
if (isModule || !cell.lang || isJsLang(cell.lang)) continue;
|
|
371
|
+
if (results[id].proven) continue; // machine-proven out-of-VM → the proof stands
|
|
372
|
+
results[id].state = worst(results[id].state, 'YELLOW');
|
|
373
|
+
results[id].notes.push({ level: 'yellow', text: `signed, but not machine-verified — ${cell.lang} code⇔spec isn't checked yet (pure Python functions with an ensures ARE provable); capped at Yellow` });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Higher-order: broken feeds edges, and roll-up color for container Cells.
|
|
377
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
378
|
+
const cell = manifest.cells[id];
|
|
379
|
+
for (const t of cell.feeds || []) {
|
|
380
|
+
if (!manifest.cells[t]) {
|
|
381
|
+
results[id].notes.push({ level: 'yellow', text: `broken edge: feeds → ${t} (no such Cell)` });
|
|
382
|
+
results[id].state = worst(results[id].state, 'YELLOW');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ── Signing policy (optional; neutral when there are no rules) ──
|
|
387
|
+
// A Cell matching a rule MUST be signed by a required signer with a real (non-AUTO)
|
|
388
|
+
// seal, else it can't ship: downgrade it to RED so it blocks the gate and shows on the
|
|
389
|
+
// map, with a note naming who must sign. No policy / no match → unconstrained, as before.
|
|
390
|
+
const policy = opts.policy || rosterPolicy || { rules: [] };
|
|
391
|
+
let policyBlocked = 0;
|
|
392
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
393
|
+
const cell = manifest.cells[id];
|
|
394
|
+
const r = results[id];
|
|
395
|
+
if (!r || (cell.contains && cell.contains.length)) continue; // leaves only
|
|
396
|
+
const req = requiredSigners(policy, cell);
|
|
397
|
+
if (!req.length) continue;
|
|
398
|
+
const okBy = r.trust && r.trust.signed && !r.trust.auto && req.includes(r.trust.signer);
|
|
399
|
+
if (!okBy) {
|
|
400
|
+
r.state = worst(r.state, 'RED');
|
|
401
|
+
r.policyOk = false;
|
|
402
|
+
r.notes.push({ level: 'red', text: `policy: requires ${req.join(' or ')} to sign — ${r.trust && r.trust.signed ? 'currently signed by ' + (r.trust.signer || '?') : 'not yet signed by them'}` });
|
|
403
|
+
policyBlocked++;
|
|
404
|
+
} else { r.policyOk = true; }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Inertness verdicts (built-in security feature) ──
|
|
408
|
+
// The prover flags branches removable with every spec-derived test still passing —
|
|
409
|
+
// unpromised behaviour riding under a signature (dead weight, ahead-of-spec
|
|
410
|
+
// scaffolding, or a dormant payload). Default: Yellow cap. Policy escalates
|
|
411
|
+
// (inert: block → gate-blocking) or relaxes (inert: note) per path/tag/module.
|
|
412
|
+
for (const id of Object.keys(proofs)) {
|
|
413
|
+
const pr = proofs[id];
|
|
414
|
+
const r = results[id];
|
|
415
|
+
if (!r || !pr || !pr.inertness) continue;
|
|
416
|
+
const inert = pr.inertness;
|
|
417
|
+
if (inert.exempt) { r.notes.push({ level: 'info', text: `inertness: Cell exempt — ${inert.exempt} (signed declaration)` }); continue; }
|
|
418
|
+
if (!inert.flagged || !inert.flagged.length) continue;
|
|
419
|
+
const level = inertLevel(policy, manifest.cells[id]);
|
|
420
|
+
const ex = inert.flagged[0];
|
|
421
|
+
// Sharpen with branch-exercise data: an inert branch that was ALSO never exercised by any
|
|
422
|
+
// spec-derived input is the highest-confidence dormancy signal — call it out explicitly.
|
|
423
|
+
const missedLines = new Set(((r.coverage && r.coverage.missed) || []).map((m) => m.line));
|
|
424
|
+
const alsoUnexercised = (inert.flagged || []).some((f) => missedLines.has(f.line));
|
|
425
|
+
const msg = `inert code — ${inert.flagged.length} branch(es) removable with every spec-derived test still passing (e.g. line ${ex.line}: \`${ex.snippet}\`)${alsoUnexercised ? ' — and never exercised by any spec-derived input (strongest dormancy signal)' : ''}: unpromised behaviour riding under the signature. Prune it, spec it (add the ensures case or its own Cell), or declare it (\`throws:\` for guards, \`perf:\` for optimizations).`;
|
|
426
|
+
if (level === 'note') {
|
|
427
|
+
r.notes.push({ level: 'info', text: msg + ' (policy: inert → note)' });
|
|
428
|
+
} else if (level === 'block') {
|
|
429
|
+
r.state = worst(r.state, 'RED');
|
|
430
|
+
r.notes.push({ level: 'red', text: msg + ' (policy: inert → block — this Cell is gate-blocked until resolved)' });
|
|
431
|
+
} else {
|
|
432
|
+
r.state = worst(r.state, 'YELLOW');
|
|
433
|
+
r.notes.push({ level: 'yellow', text: msg });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Coverage strictness (owner-signed policy `coverage: full`) ──
|
|
438
|
+
// Default is a badge, never a downgrade. But for crown-jewel scopes a policy rule can DEMAND that
|
|
439
|
+
// every branch of a proven Cell was exercised — an unexercised branch (where a dormant payload
|
|
440
|
+
// hides) then blocks the gate until it's exercised, spec'd, or pruned.
|
|
441
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
442
|
+
const cell = manifest.cells[id];
|
|
443
|
+
const r = results[id];
|
|
444
|
+
if (!r || (cell.contains && cell.contains.length) || !r.coverage) continue;
|
|
445
|
+
if (!coverageRequired(policy, cell)) continue;
|
|
446
|
+
const missed = (r.coverage.missed || []);
|
|
447
|
+
if (missed.length) {
|
|
448
|
+
r.state = worst(r.state, 'RED');
|
|
449
|
+
r.notes.push({ level: 'red', text: `policy: coverage full — ${missed.length} branch(es) never exercised by spec-derived inputs (${missed.slice(0, 3).map((m) => 'line ' + m.line).join(', ')}${missed.length > 3 ? '…' : ''}); this scope requires every branch exercised. Exercise them (strengthen the ensures/inputs), spec them, or prune them.` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Undeclared-input predicate provenance (built-in; the 3rd prong of the pincer) ──
|
|
454
|
+
// A branch that keys off a parameter the Cell's `in:` never declares — inertness (branch does
|
|
455
|
+
// nothing), coverage (was it exercised?), and literal-seeding (magic constant) all miss this shape.
|
|
456
|
+
// Default Yellow (advisory, non-blocking, like inertness); owner-signed policy (`predicate: declared`)
|
|
457
|
+
// escalates it to gate-blocking for sensitive scopes. The fix is to DECLARE the input in `in:` (or a
|
|
458
|
+
// `throws:`/`ensures` case) — the spec-strengthening loop — after which nothing flags; or prune the branch.
|
|
459
|
+
for (const id of Object.keys(proofs)) {
|
|
460
|
+
const pr = proofs[id];
|
|
461
|
+
const r = results[id];
|
|
462
|
+
if (!r || !pr || !pr.predicate || !pr.predicate.findings || !pr.predicate.findings.length) continue;
|
|
463
|
+
const f = pr.predicate.findings;
|
|
464
|
+
const ex = f[0];
|
|
465
|
+
const names = Array.from(new Set(f.map((x) => x.name)));
|
|
466
|
+
const msg = `undeclared-input predicate — ${f.length} branch(es) key off ${names.map((n) => '\`' + n + '\`').join(', ')}, which this Cell's \`in:\` never declares (e.g. line ${ex.line}: \`${ex.snippet}\`). This branch's behaviour depends on an input the spec never signed. Declare it in \`in:\` (or add a \`throws:\`/\`ensures\` case), or prune the branch.`;
|
|
467
|
+
r.predicate = { findings: f, undeclared: pr.predicate.undeclared };
|
|
468
|
+
if (predicateRequired(policy, manifest.cells[id])) {
|
|
469
|
+
r.state = worst(r.state, 'RED');
|
|
470
|
+
r.notes.push({ level: 'red', text: msg + ' (policy: predicate → declared — gate-blocked until every branch predicate traces to the spec)' });
|
|
471
|
+
} else {
|
|
472
|
+
r.state = worst(r.state, 'YELLOW');
|
|
473
|
+
r.notes.push({ level: 'yellow', text: msg });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for (const id of Object.keys(manifest.cells)) {
|
|
478
|
+
const cell = manifest.cells[id];
|
|
479
|
+
if (cell.contains && cell.contains.length) {
|
|
480
|
+
let rolled = results[id].state;
|
|
481
|
+
for (const child of cell.contains) {
|
|
482
|
+
if (results[child]) rolled = worst(rolled, results[child].state);
|
|
483
|
+
}
|
|
484
|
+
if (rolled !== results[id].state) results[id].notes.push({ level: 'info', text: `rolled up to ${rolled} from contained Cells` });
|
|
485
|
+
results[id].state = rolled;
|
|
486
|
+
results[id].isModule = true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// PINK: code with no spec block at all — untracked, never described or signed.
|
|
491
|
+
// The most dangerous state, so it BLOCKS the gate: everything must be covered.
|
|
492
|
+
for (const u of manifest.untracked || []) {
|
|
493
|
+
let id = `«${u.name}»`;
|
|
494
|
+
if (results[id]) id += ` @${u.file}:${u.line}`;
|
|
495
|
+
const wrapHint = isJsLang(u.lang)
|
|
496
|
+
? 'wrap it in an IIFE around a spec\'d function — `(function(){ function init(){…} init(); })()` — or run `yay adopt`'
|
|
497
|
+
: 'move it under an entry guard (e.g. `if __name__ == "__main__":`) or into a function an explicit caller runs';
|
|
498
|
+
const text = u.kind === 'loose'
|
|
499
|
+
? `top-level code runs at load with no spec (${u.count || 1} statement${(u.count || 1) > 1 ? 's' : ''}): ${wrapHint}`
|
|
500
|
+
: `no formal specification (${u.kind || 'unit'}) — never described or signed (run \`yay adopt\`)`;
|
|
501
|
+
results[id] = {
|
|
502
|
+
id, state: 'PINK', trust: { signed: false }, untracked: true,
|
|
503
|
+
name: u.name, file: u.file, line: u.line, lang: u.lang, module: u.module, group: u.group,
|
|
504
|
+
notes: [{ level: 'red', text }],
|
|
505
|
+
badLines: [],
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Source code hidden from the gate by .yaylayerignore → gate-blocking Pink, unless an
|
|
510
|
+
// OWNER-SIGNED policy `ignore: source` rule authorises it. Closes the bypass where a
|
|
511
|
+
// bare .yaylayerignore line removes a file from judgment entirely.
|
|
512
|
+
for (const ig of manifest.ignoredSource || []) {
|
|
513
|
+
if (ignoreAllowed(policy, ig.file)) continue; // whitelisted (vendored/generated), signed
|
|
514
|
+
const id = `«ignored: ${ig.file}»`;
|
|
515
|
+
results[id] = {
|
|
516
|
+
id, state: 'PINK', trust: { signed: false }, untracked: true, ignoredSource: true,
|
|
517
|
+
name: 'ignored source', file: ig.file, line: 1, lang: ig.lang, module: ig.file, group: 'hidden',
|
|
518
|
+
notes: [{ level: 'red', text: `SOURCE file hidden from the gate by .yaylayerignore — code here is never scanned, signed, or verified. Un-ignore it, or authorise it with an owner-signed policy rule { "match": { "path": "${ig.file}" }, "ignore": "source" } (yay policy → --set).` }],
|
|
519
|
+
badLines: [],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const counts = { GREEN: 0, YELLOW: 0, RED: 0, UNSIGNED: 0, PINK: 0 };
|
|
524
|
+
for (const r of Object.values(results)) counts[r.state]++;
|
|
525
|
+
counts.policyBlocked = policyBlocked; // Cells blocked by the signing policy (already RED)
|
|
526
|
+
// Legibility: of the GREEN Cells, how many are machine-PROVEN vs signed-but-unproven
|
|
527
|
+
// (a promise stated but never machine-checked — the specs to strengthen next).
|
|
528
|
+
let proven = 0, unproven = 0;
|
|
529
|
+
for (const r of Object.values(results)) if (r.state === 'GREEN') { if (r.proven) proven++; else if (r.hasEnsures) unproven++; }
|
|
530
|
+
counts.proven = proven; counts.unproven = unproven;
|
|
531
|
+
// Autopilot: of the signed Cells, how many are AUTO (delegated, awaiting ratification).
|
|
532
|
+
let auto = 0;
|
|
533
|
+
for (const r of Object.values(results)) if (r.trust && r.trust.auto && r.state !== 'UNSIGNED') auto++;
|
|
534
|
+
counts.auto = auto;
|
|
535
|
+
// Foundation seal (owner-signed baseline of the fixed core files) — reveal any drift. The
|
|
536
|
+
// posture is authoritative from the signed roster event; config.foundation mirrors it and
|
|
537
|
+
// survives a reroot, so if it EXPECTS a seal but none exists under the current root, that's
|
|
538
|
+
// itself flagged (e.g. after a reroot, until the new owner re-seals).
|
|
539
|
+
let foundation = null;
|
|
540
|
+
const expectMode = foundationMode !== 'off' ? foundationMode
|
|
541
|
+
: ((config && config.foundation && config.foundation !== 'off') ? config.foundation : 'off');
|
|
542
|
+
if (expectMode !== 'off') {
|
|
543
|
+
if (!foundationSeal) {
|
|
544
|
+
foundation = { mode: expectMode, clean: false, expectedButMissing: true, changed: [], missing: [], addedFiles: [], removedFiles: [] };
|
|
545
|
+
} else {
|
|
546
|
+
const cmp = F.compareSeal(manifest.root, foundationSeal, Array.isArray(opts.tracked) ? opts.tracked : null);
|
|
547
|
+
foundation = { mode: expectMode, expectedButMissing: false, ...cmp };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Strict foundation posture blocks the gate on drift; Guarded reveals (warns) without blocking.
|
|
551
|
+
const foundationBlocks = !!(foundation && foundation.mode === 'strict' && !foundation.clean);
|
|
552
|
+
// A tampered / unauthorized / root-mismatched roster blocks the gate: if we can't
|
|
553
|
+
// trust WHO may sign, we can't trust any signature.
|
|
554
|
+
const passed = counts.RED === 0 && counts.UNSIGNED === 0 && counts.PINK === 0 && rosterOk && !foundationBlocks;
|
|
555
|
+
// `policy` is the EFFECTIVE ruleset the verdicts were produced under — exposed so an attestation
|
|
556
|
+
// (P2) can hash exactly what the verifier used (rulesetHash), not re-guess it.
|
|
557
|
+
return { results, counts, passed, grants, rosterProblems, rootFp, rosterOk, signedRoster, policy, foundation, rootMeta };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
module.exports = { verifyManifest, worst, effectNetDescriptor };
|