loadout-ai 0.1.0
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/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { createHash, createPublicKey } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { benchmarkCampaignSha256, buildBenchmarkSchedule, parseBenchmarkCampaign, } from "./benchmark-campaign.js";
|
|
4
|
+
import { reduceBenchmarkEvidence, } from "./benchmark-evidence.js";
|
|
5
|
+
import { signPayload, verifyEnvelope } from "./signing.js";
|
|
6
|
+
export const BENCHMARK_TRUST_EVIDENCE_VERSION = "loadout-benchmark-trust-evidence-v1";
|
|
7
|
+
export const TRUST_DECISION_POLICY_VERSION = "loadout-trust-decision-v1";
|
|
8
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
9
|
+
const COMMIT = /^[a-f0-9]{40}$/;
|
|
10
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
|
11
|
+
const PACKAGE_ID = /^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$/;
|
|
12
|
+
const TASK_FAMILIES = new Set([
|
|
13
|
+
"workflow-adherence",
|
|
14
|
+
"code-review-coverage",
|
|
15
|
+
"documentation-retrieval",
|
|
16
|
+
"browser-test-planning",
|
|
17
|
+
]);
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function exactKeys(value, expected, context) {
|
|
22
|
+
const allowed = new Set(expected);
|
|
23
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
24
|
+
if (unknown.length)
|
|
25
|
+
throw new Error(`${context} has unknown field(s): ${unknown.join(", ")}`);
|
|
26
|
+
const missing = expected.filter((key) => !(key in value));
|
|
27
|
+
if (missing.length)
|
|
28
|
+
throw new Error(`${context} is missing field(s): ${missing.join(", ")}`);
|
|
29
|
+
}
|
|
30
|
+
function text(value, context, maximum = 256) {
|
|
31
|
+
if (typeof value !== "string" ||
|
|
32
|
+
!value.length ||
|
|
33
|
+
value !== value.trim() ||
|
|
34
|
+
value.length > maximum ||
|
|
35
|
+
/(?:sk-[A-Za-z0-9_-]{8,}|bearer\s+\S+|password\s*[=:])/i.test(value))
|
|
36
|
+
throw new Error(`${context} must be non-sensitive, trimmed text`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function identifier(value, context) {
|
|
40
|
+
const result = text(value, context, 128);
|
|
41
|
+
if (!ID.test(result) || result.includes("..") || result.includes("//"))
|
|
42
|
+
throw new Error(`${context} is invalid`);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function packageId(value, context) {
|
|
46
|
+
if (typeof value !== "string" || !PACKAGE_ID.test(value))
|
|
47
|
+
throw new Error(`${context} is invalid`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function sha256(value, context) {
|
|
51
|
+
if (typeof value !== "string" || !SHA256.test(value))
|
|
52
|
+
throw new Error(`${context} must be a lowercase SHA-256`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function commit(value, context) {
|
|
56
|
+
if (typeof value !== "string" || !COMMIT.test(value))
|
|
57
|
+
throw new Error(`${context} must be a full lowercase Git commit`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function timestamp(value, context) {
|
|
61
|
+
const result = text(value, context, 64);
|
|
62
|
+
let normalized;
|
|
63
|
+
try {
|
|
64
|
+
normalized = new Date(result).toISOString();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`${context} must be an ISO-8601 UTC timestamp`);
|
|
68
|
+
}
|
|
69
|
+
if (normalized !== result)
|
|
70
|
+
throw new Error(`${context} must be an ISO-8601 UTC timestamp`);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function boundedNumber(value, context, minimum, maximum) {
|
|
74
|
+
if (typeof value !== "number" ||
|
|
75
|
+
!Number.isFinite(value) ||
|
|
76
|
+
value < minimum ||
|
|
77
|
+
value > maximum)
|
|
78
|
+
throw new Error(`${context} must be from ${minimum} to ${maximum}`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function integer(value, context, minimum, maximum) {
|
|
82
|
+
const result = boundedNumber(value, context, minimum, maximum);
|
|
83
|
+
if (!Number.isInteger(result))
|
|
84
|
+
throw new Error(`${context} must be an integer`);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
function canonical(value) {
|
|
88
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
if (typeof value === "number") {
|
|
91
|
+
if (!Number.isFinite(value))
|
|
92
|
+
throw new Error("Cannot hash non-finite data");
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value))
|
|
96
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
97
|
+
if (isRecord(value))
|
|
98
|
+
return `{${Object.entries(value)
|
|
99
|
+
.filter(([, entry]) => entry !== undefined)
|
|
100
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
101
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`)
|
|
102
|
+
.join(",")}}`;
|
|
103
|
+
throw new Error(`Cannot hash value of type ${typeof value}`);
|
|
104
|
+
}
|
|
105
|
+
function digest(value) {
|
|
106
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
function rounded(value) {
|
|
109
|
+
return Math.round(value * 100) / 100;
|
|
110
|
+
}
|
|
111
|
+
function parseJudgments(value) {
|
|
112
|
+
if (!Array.isArray(value))
|
|
113
|
+
throw new Error("Benchmark judgments must be an array");
|
|
114
|
+
const result = value.map((entry, index) => {
|
|
115
|
+
const context = `Benchmark judgment ${index + 1}`;
|
|
116
|
+
if (!isRecord(entry))
|
|
117
|
+
throw new Error(`${context} must be an object`);
|
|
118
|
+
exactKeys(entry, ["requestId", "outputSha256", "score", "blockingSafetyFailure"], context);
|
|
119
|
+
const requestId = identifier(entry.requestId, `${context}.requestId`);
|
|
120
|
+
const outputSha256 = sha256(entry.outputSha256, `${context}.outputSha256`);
|
|
121
|
+
const score = boundedNumber(entry.score, `${context}.score`, 0, 100);
|
|
122
|
+
if (rounded(score) !== score)
|
|
123
|
+
throw new Error(`${context}.score supports at most two decimals`);
|
|
124
|
+
if (typeof entry.blockingSafetyFailure !== "boolean")
|
|
125
|
+
throw new Error(`${context}.blockingSafetyFailure must be boolean`);
|
|
126
|
+
if (entry.blockingSafetyFailure && score !== 0)
|
|
127
|
+
throw new Error(`${context} with a blocking safety failure must score zero`);
|
|
128
|
+
return {
|
|
129
|
+
requestId,
|
|
130
|
+
outputSha256,
|
|
131
|
+
score,
|
|
132
|
+
blockingSafetyFailure: entry.blockingSafetyFailure,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
if (new Set(result.map((item) => item.requestId)).size !== result.length)
|
|
136
|
+
throw new Error("Benchmark judgments must have unique request ids");
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function computeTrustEvidence(campaignValue, eventValues, judgmentValues, createdAtValue) {
|
|
140
|
+
const campaign = parseBenchmarkCampaign(campaignValue);
|
|
141
|
+
const state = reduceBenchmarkEvidence(eventValues, campaign);
|
|
142
|
+
const judgments = parseJudgments(judgmentValues);
|
|
143
|
+
const createdAt = timestamp(createdAtValue, "Trust evidence timestamp");
|
|
144
|
+
const schedule = buildBenchmarkSchedule(campaign);
|
|
145
|
+
const scheduled = new Map(schedule.map((request) => [request.requestId, request]));
|
|
146
|
+
const completions = new Map(state.completions.map((completion) => [completion.requestId, completion]));
|
|
147
|
+
const judgmentByRequest = new Map(judgments.map((judgment) => [judgment.requestId, judgment]));
|
|
148
|
+
for (const judgment of judgments) {
|
|
149
|
+
const request = scheduled.get(judgment.requestId);
|
|
150
|
+
const completion = completions.get(judgment.requestId);
|
|
151
|
+
if (!request || !completion || completion.outcome !== "succeeded")
|
|
152
|
+
throw new Error("Benchmark judgment has no successful scheduled completion");
|
|
153
|
+
if (completion.outputSha256 !== judgment.outputSha256)
|
|
154
|
+
throw new Error("Benchmark judgment output hash does not match completion");
|
|
155
|
+
}
|
|
156
|
+
const paired = [];
|
|
157
|
+
for (let pairIndex = 0; pairIndex < campaign.trials.pairs; pairIndex++) {
|
|
158
|
+
const requests = schedule.filter((request) => request.pairIndex === pairIndex);
|
|
159
|
+
const baselineRequest = requests.find((request) => request.role === "baseline");
|
|
160
|
+
const candidateRequest = requests.find((request) => request.role === "candidate");
|
|
161
|
+
const baseline = baselineRequest
|
|
162
|
+
? judgmentByRequest.get(baselineRequest.requestId)
|
|
163
|
+
: undefined;
|
|
164
|
+
const candidate = candidateRequest
|
|
165
|
+
? judgmentByRequest.get(candidateRequest.requestId)
|
|
166
|
+
: undefined;
|
|
167
|
+
if (baseline && candidate)
|
|
168
|
+
paired.push({ baseline, candidate });
|
|
169
|
+
}
|
|
170
|
+
const mean = (rows, role) => rows.length
|
|
171
|
+
? rounded(rows.reduce((total, row) => total + row[role].score, 0) / rows.length)
|
|
172
|
+
: 0;
|
|
173
|
+
const baselineMeanScore = mean(paired, "baseline");
|
|
174
|
+
const candidateMeanScore = mean(paired, "candidate");
|
|
175
|
+
const scoreDelta = rounded(candidateMeanScore - baselineMeanScore);
|
|
176
|
+
const candidateBlockingSafetyFailures = paired.filter((row) => row.candidate.blockingSafetyFailure).length;
|
|
177
|
+
const protocolConformant = state.status === "completed" &&
|
|
178
|
+
paired.length >= campaign.decision.minimumSuccessfulPairs;
|
|
179
|
+
const meaningfulGain = protocolConformant &&
|
|
180
|
+
scoreDelta >= campaign.decision.minimumPracticalScoreDelta;
|
|
181
|
+
const unacceptableRegression = candidateBlockingSafetyFailures > 0 ||
|
|
182
|
+
(protocolConformant &&
|
|
183
|
+
scoreDelta <= -campaign.decision.minimumPracticalScoreDelta);
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: 1,
|
|
186
|
+
evidenceVersion: BENCHMARK_TRUST_EVIDENCE_VERSION,
|
|
187
|
+
createdAt,
|
|
188
|
+
campaign,
|
|
189
|
+
events: state.events,
|
|
190
|
+
judgments,
|
|
191
|
+
summary: {
|
|
192
|
+
taskFamily: campaign.category,
|
|
193
|
+
successfulPairs: paired.length,
|
|
194
|
+
requiredSuccessfulPairs: campaign.decision.minimumSuccessfulPairs,
|
|
195
|
+
baselineMeanScore,
|
|
196
|
+
candidateMeanScore,
|
|
197
|
+
scoreDelta,
|
|
198
|
+
minimumPracticalScoreDelta: campaign.decision.minimumPracticalScoreDelta,
|
|
199
|
+
candidateBlockingSafetyFailures,
|
|
200
|
+
protocolConformant,
|
|
201
|
+
meaningfulGain,
|
|
202
|
+
unacceptableRegression,
|
|
203
|
+
},
|
|
204
|
+
boundary: "Signed synthetic task-family evidence is scoped to this exact protocol, fixture, model, package commit, and instruction hash; popularity is not quality evidence.",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function createBenchmarkTrustEvidence(campaign, events, judgments, createdAt = new Date().toISOString()) {
|
|
208
|
+
return computeTrustEvidence(campaign, events, judgments, createdAt);
|
|
209
|
+
}
|
|
210
|
+
export function signBenchmarkTrustEvidence(evidenceValue, privateKeyPem) {
|
|
211
|
+
const evidence = parseBenchmarkTrustEvidence(evidenceValue);
|
|
212
|
+
return signPayload(evidence, privateKeyPem, evidence.createdAt);
|
|
213
|
+
}
|
|
214
|
+
export function parseBenchmarkTrustEvidence(value) {
|
|
215
|
+
if (!isRecord(value))
|
|
216
|
+
throw new Error("Benchmark trust evidence must be an object");
|
|
217
|
+
exactKeys(value, [
|
|
218
|
+
"schemaVersion",
|
|
219
|
+
"evidenceVersion",
|
|
220
|
+
"createdAt",
|
|
221
|
+
"campaign",
|
|
222
|
+
"events",
|
|
223
|
+
"judgments",
|
|
224
|
+
"summary",
|
|
225
|
+
"boundary",
|
|
226
|
+
], "Benchmark trust evidence");
|
|
227
|
+
if (value.schemaVersion !== 1 ||
|
|
228
|
+
value.evidenceVersion !== BENCHMARK_TRUST_EVIDENCE_VERSION)
|
|
229
|
+
throw new Error("Unsupported benchmark trust evidence version");
|
|
230
|
+
if (!Array.isArray(value.events))
|
|
231
|
+
throw new Error("Benchmark trust evidence events must be an array");
|
|
232
|
+
const rebuilt = computeTrustEvidence(value.campaign, value.events, value.judgments, value.createdAt);
|
|
233
|
+
if (!isDeepStrictEqual(value, rebuilt))
|
|
234
|
+
throw new Error("Benchmark trust evidence does not match recomputed protocol data");
|
|
235
|
+
return rebuilt;
|
|
236
|
+
}
|
|
237
|
+
export function verifyBenchmarkTrustEvidence(envelope, publicKeyPem) {
|
|
238
|
+
if (!verifyEnvelope(envelope, publicKeyPem).valid)
|
|
239
|
+
throw new Error("Benchmark trust evidence signature is invalid");
|
|
240
|
+
const evidence = parseBenchmarkTrustEvidence(envelope.payload);
|
|
241
|
+
if (envelope.createdAt !== evidence.createdAt)
|
|
242
|
+
throw new Error("Benchmark trust evidence envelope timestamp is inconsistent");
|
|
243
|
+
return evidence;
|
|
244
|
+
}
|
|
245
|
+
function evidenceReference(envelope, evidence) {
|
|
246
|
+
const candidate = evidence.campaign.candidates.find((item) => item.role === "candidate");
|
|
247
|
+
return {
|
|
248
|
+
evidenceSha256: digest(envelope),
|
|
249
|
+
publicKeyFingerprint: envelope.publicKeyFingerprint,
|
|
250
|
+
createdAt: evidence.createdAt,
|
|
251
|
+
campaignId: evidence.campaign.campaignId,
|
|
252
|
+
campaignSha256: benchmarkCampaignSha256(evidence.campaign),
|
|
253
|
+
runId: evidence.events[0]?.runId ?? "missing-run",
|
|
254
|
+
packageId: candidate.packageId,
|
|
255
|
+
reviewedCommit: candidate.reviewedCommit,
|
|
256
|
+
instructionSha256: candidate.instructionSha256,
|
|
257
|
+
taskFamily: evidence.summary.taskFamily,
|
|
258
|
+
successfulPairs: evidence.summary.successfulPairs,
|
|
259
|
+
requiredSuccessfulPairs: evidence.summary.requiredSuccessfulPairs,
|
|
260
|
+
scoreDelta: evidence.summary.scoreDelta,
|
|
261
|
+
minimumPracticalScoreDelta: evidence.summary.minimumPracticalScoreDelta,
|
|
262
|
+
protocolConformant: evidence.summary.protocolConformant,
|
|
263
|
+
meaningfulGain: evidence.summary.meaningfulGain,
|
|
264
|
+
unacceptableRegression: evidence.summary.unacceptableRegression,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function parseReview(value) {
|
|
268
|
+
if (value === null)
|
|
269
|
+
return null;
|
|
270
|
+
if (!isRecord(value))
|
|
271
|
+
throw new Error("Human trust review must be an object");
|
|
272
|
+
exactKeys(value, [
|
|
273
|
+
"attestation",
|
|
274
|
+
"reviewId",
|
|
275
|
+
"reviewedBy",
|
|
276
|
+
"reviewedAt",
|
|
277
|
+
"packageId",
|
|
278
|
+
"reviewedCommit",
|
|
279
|
+
"licenseDecision",
|
|
280
|
+
"trustDecision",
|
|
281
|
+
"reviewEvidenceSha256",
|
|
282
|
+
], "Human trust review");
|
|
283
|
+
if (value.attestation !== "human-reviewed")
|
|
284
|
+
throw new Error("Human trust review requires an explicit attestation");
|
|
285
|
+
const licenseDecision = value.licenseDecision;
|
|
286
|
+
const trustDecision = value.trustDecision;
|
|
287
|
+
if (licenseDecision !== "approved" && licenseDecision !== "rejected")
|
|
288
|
+
throw new Error("Human license decision is invalid");
|
|
289
|
+
if (trustDecision !== "approved" && trustDecision !== "rejected")
|
|
290
|
+
throw new Error("Human trust decision is invalid");
|
|
291
|
+
return {
|
|
292
|
+
attestation: "human-reviewed",
|
|
293
|
+
reviewId: identifier(value.reviewId, "Human review id"),
|
|
294
|
+
reviewedBy: identifier(value.reviewedBy, "Human reviewer"),
|
|
295
|
+
reviewedAt: timestamp(value.reviewedAt, "Human review timestamp"),
|
|
296
|
+
packageId: packageId(value.packageId, "Human review package id"),
|
|
297
|
+
reviewedCommit: commit(value.reviewedCommit, "Human reviewed commit"),
|
|
298
|
+
licenseDecision,
|
|
299
|
+
trustDecision,
|
|
300
|
+
reviewEvidenceSha256: sha256(value.reviewEvidenceSha256, "Human review evidence hash"),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function identifierArray(value, context) {
|
|
304
|
+
if (!Array.isArray(value))
|
|
305
|
+
throw new Error(`${context} must be an array`);
|
|
306
|
+
const result = value.map((item, index) => identifier(item, `${context}[${index}]`));
|
|
307
|
+
if (new Set(result).size !== result.length)
|
|
308
|
+
throw new Error(`${context} must not contain duplicates`);
|
|
309
|
+
return [...result].sort();
|
|
310
|
+
}
|
|
311
|
+
function parseSecurity(value) {
|
|
312
|
+
if (!isRecord(value))
|
|
313
|
+
throw new Error("Security review must be an object");
|
|
314
|
+
exactKeys(value, [
|
|
315
|
+
"scannerVersion",
|
|
316
|
+
"scannedCommit",
|
|
317
|
+
"blockingFindingIds",
|
|
318
|
+
"warningFindingIds",
|
|
319
|
+
], "Security review");
|
|
320
|
+
return {
|
|
321
|
+
scannerVersion: identifier(value.scannerVersion, "Security scanner version"),
|
|
322
|
+
scannedCommit: commit(value.scannedCommit, "Security scanned commit"),
|
|
323
|
+
blockingFindingIds: identifierArray(value.blockingFindingIds, "Blocking security findings"),
|
|
324
|
+
warningFindingIds: identifierArray(value.warningFindingIds, "Security warning findings"),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function decisionReasons(status, metrics, review) {
|
|
328
|
+
return [
|
|
329
|
+
metrics.conformantEvidenceRecords
|
|
330
|
+
? `${metrics.conformantEvidenceRecords} signed protocol-conformant evidence record(s) establish benchmarked status.`
|
|
331
|
+
: "No signed protocol-conformant evidence is present; missing evidence contributes zero.",
|
|
332
|
+
metrics.taskFamiliesWithMeaningfulGain
|
|
333
|
+
? `Meaningful gain exists in ${metrics.taskFamiliesWithMeaningfulGain} declared task family/families.`
|
|
334
|
+
: "No declared task family currently has a meaningful measured gain.",
|
|
335
|
+
metrics.unacceptableRegressions
|
|
336
|
+
? `${metrics.unacceptableRegressions} unacceptable regression record(s) block recommendation.`
|
|
337
|
+
: "No unacceptable regression appears in the current conformant evidence.",
|
|
338
|
+
metrics.blockingSecurityFindings
|
|
339
|
+
? `${metrics.blockingSecurityFindings} blocking security finding(s) block recommendation.`
|
|
340
|
+
: "No blocking security finding is recorded for the reviewed commit.",
|
|
341
|
+
review?.licenseDecision === "approved" &&
|
|
342
|
+
review.trustDecision === "approved"
|
|
343
|
+
? `Human license/trust review '${review.reviewId}' is approved for the reviewed commit.`
|
|
344
|
+
: "An approved human license and trust review is missing.",
|
|
345
|
+
`Result: ${status}. Stars, installs, telemetry, and popularity are not quality evidence.`,
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
function evidenceKey(reference) {
|
|
349
|
+
return reference.evidenceSha256;
|
|
350
|
+
}
|
|
351
|
+
function qualityStatus(metrics, review) {
|
|
352
|
+
if (!metrics.conformantEvidenceRecords)
|
|
353
|
+
return "unbenchmarked";
|
|
354
|
+
const humanApproved = review?.licenseDecision === "approved" &&
|
|
355
|
+
review.trustDecision === "approved";
|
|
356
|
+
if (humanApproved &&
|
|
357
|
+
metrics.taskFamiliesWithMeaningfulGain > 0 &&
|
|
358
|
+
metrics.unacceptableRegressions === 0 &&
|
|
359
|
+
metrics.blockingSecurityFindings === 0)
|
|
360
|
+
return "recommended";
|
|
361
|
+
return "benchmarked";
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Evaluate and sign one immutable transition. `humanReview` records a human's
|
|
365
|
+
* attestation; software can validate its shape and binding but cannot prove a
|
|
366
|
+
* person actually performed the review.
|
|
367
|
+
*/
|
|
368
|
+
export function createSignedTrustDecision(options) {
|
|
369
|
+
if (!isRecord(options))
|
|
370
|
+
throw new Error("Trust decision input must be an object");
|
|
371
|
+
exactKeys(options, [
|
|
372
|
+
"packageId",
|
|
373
|
+
"reviewedCommit",
|
|
374
|
+
"instructionSha256",
|
|
375
|
+
"evidence",
|
|
376
|
+
"humanReview",
|
|
377
|
+
"security",
|
|
378
|
+
"privateKeyPem",
|
|
379
|
+
...(options.previousDecision === undefined ? [] : ["previousDecision"]),
|
|
380
|
+
...(options.createdAt === undefined ? [] : ["createdAt"]),
|
|
381
|
+
], "Trust decision input");
|
|
382
|
+
const selectedPackage = packageId(options.packageId, "Trust decision package id");
|
|
383
|
+
const selectedCommit = commit(options.reviewedCommit, "Trust decision commit");
|
|
384
|
+
const selectedInstruction = sha256(options.instructionSha256, "Trust decision instruction hash");
|
|
385
|
+
const createdAt = timestamp(options.createdAt ?? new Date().toISOString(), "Trust decision timestamp");
|
|
386
|
+
if (!Array.isArray(options.evidence))
|
|
387
|
+
throw new Error("Trust decision evidence must be an array");
|
|
388
|
+
const currentEvidence = options.evidence.map((entry, index) => {
|
|
389
|
+
if (!isRecord(entry))
|
|
390
|
+
throw new Error(`Trust decision evidence ${index + 1} is invalid`);
|
|
391
|
+
exactKeys(entry, ["envelope", "publicKeyPem"], `Trust decision evidence ${index + 1}`);
|
|
392
|
+
if (typeof entry.publicKeyPem !== "string")
|
|
393
|
+
throw new Error(`Trust decision evidence ${index + 1} public key is invalid`);
|
|
394
|
+
const evidence = verifyBenchmarkTrustEvidence(entry.envelope, entry.publicKeyPem);
|
|
395
|
+
const reference = evidenceReference(entry.envelope, evidence);
|
|
396
|
+
if (reference.packageId !== selectedPackage ||
|
|
397
|
+
reference.reviewedCommit !== selectedCommit ||
|
|
398
|
+
reference.instructionSha256 !== selectedInstruction)
|
|
399
|
+
throw new Error("Benchmark evidence does not bind the selected package revision");
|
|
400
|
+
return reference;
|
|
401
|
+
});
|
|
402
|
+
if (new Set(currentEvidence.map(evidenceKey)).size !== currentEvidence.length)
|
|
403
|
+
throw new Error("Trust decision evidence must not contain duplicates");
|
|
404
|
+
currentEvidence.sort((left, right) => evidenceKey(left).localeCompare(evidenceKey(right)));
|
|
405
|
+
const review = parseReview(options.humanReview);
|
|
406
|
+
if (review &&
|
|
407
|
+
(review.packageId !== selectedPackage ||
|
|
408
|
+
review.reviewedCommit !== selectedCommit))
|
|
409
|
+
throw new Error("Human review does not bind the selected package revision");
|
|
410
|
+
const security = parseSecurity(options.security);
|
|
411
|
+
if (security.scannedCommit !== selectedCommit)
|
|
412
|
+
throw new Error("Security review does not bind the selected package revision");
|
|
413
|
+
let sequence = 0;
|
|
414
|
+
let previousDecisionSha256 = null;
|
|
415
|
+
let previousStatus = "unbenchmarked";
|
|
416
|
+
let priorEvidence = [];
|
|
417
|
+
if (options.previousDecision) {
|
|
418
|
+
const publicPem = createPublicKey(options.privateKeyPem)
|
|
419
|
+
.export({ type: "spki", format: "pem" })
|
|
420
|
+
.toString();
|
|
421
|
+
const previous = verifyTrustDecisionRecord(options.previousDecision, publicPem);
|
|
422
|
+
if (previous.packageId !== selectedPackage)
|
|
423
|
+
throw new Error("Previous trust decision names a different package");
|
|
424
|
+
if (Date.parse(createdAt) < Date.parse(previous.createdAt))
|
|
425
|
+
throw new Error("Trust decision timestamps must be monotonic");
|
|
426
|
+
sequence = previous.sequence + 1;
|
|
427
|
+
previousDecisionSha256 = digest(options.previousDecision);
|
|
428
|
+
previousStatus = previous.status;
|
|
429
|
+
priorEvidence = previous.retainedEvidence;
|
|
430
|
+
}
|
|
431
|
+
const retainedByHash = new Map([...priorEvidence, ...currentEvidence].map((reference) => [
|
|
432
|
+
evidenceKey(reference),
|
|
433
|
+
reference,
|
|
434
|
+
]));
|
|
435
|
+
const retainedEvidence = [...retainedByHash.values()].sort((left, right) => evidenceKey(left).localeCompare(evidenceKey(right)));
|
|
436
|
+
const conformant = currentEvidence.filter((reference) => reference.protocolConformant);
|
|
437
|
+
const metrics = {
|
|
438
|
+
signedEvidenceRecords: currentEvidence.length,
|
|
439
|
+
conformantEvidenceRecords: conformant.length,
|
|
440
|
+
taskFamiliesWithMeaningfulGain: new Set(conformant
|
|
441
|
+
.filter((reference) => reference.meaningfulGain)
|
|
442
|
+
.map((reference) => reference.taskFamily)).size,
|
|
443
|
+
unacceptableRegressions: conformant.filter((reference) => reference.unacceptableRegression).length,
|
|
444
|
+
blockingSecurityFindings: security.blockingFindingIds.length,
|
|
445
|
+
missingEvidenceContribution: 0,
|
|
446
|
+
};
|
|
447
|
+
const status = qualityStatus(metrics, review);
|
|
448
|
+
const payload = {
|
|
449
|
+
schemaVersion: 1,
|
|
450
|
+
policyVersion: TRUST_DECISION_POLICY_VERSION,
|
|
451
|
+
decisionId: digest({
|
|
452
|
+
selectedPackage,
|
|
453
|
+
selectedCommit,
|
|
454
|
+
sequence,
|
|
455
|
+
createdAt,
|
|
456
|
+
}).slice(0, 32),
|
|
457
|
+
sequence,
|
|
458
|
+
createdAt,
|
|
459
|
+
packageId: selectedPackage,
|
|
460
|
+
reviewedCommit: selectedCommit,
|
|
461
|
+
instructionSha256: selectedInstruction,
|
|
462
|
+
status,
|
|
463
|
+
transition: `${previousStatus}->${status}`,
|
|
464
|
+
previousDecisionSha256,
|
|
465
|
+
currentEvidence,
|
|
466
|
+
retainedEvidence,
|
|
467
|
+
humanReview: review,
|
|
468
|
+
security,
|
|
469
|
+
metrics,
|
|
470
|
+
reasons: decisionReasons(status, metrics, review),
|
|
471
|
+
policyBoundary: "Promotion is scoped evidence, not a universal-best claim. Human attestation remains a real human responsibility and popularity never substitutes for quality evidence.",
|
|
472
|
+
};
|
|
473
|
+
return signPayload(payload, options.privateKeyPem, createdAt);
|
|
474
|
+
}
|
|
475
|
+
function parseEvidenceReference(value, context) {
|
|
476
|
+
if (!isRecord(value))
|
|
477
|
+
throw new Error(`${context} must be an object`);
|
|
478
|
+
exactKeys(value, [
|
|
479
|
+
"evidenceSha256",
|
|
480
|
+
"publicKeyFingerprint",
|
|
481
|
+
"createdAt",
|
|
482
|
+
"campaignId",
|
|
483
|
+
"campaignSha256",
|
|
484
|
+
"runId",
|
|
485
|
+
"packageId",
|
|
486
|
+
"reviewedCommit",
|
|
487
|
+
"instructionSha256",
|
|
488
|
+
"taskFamily",
|
|
489
|
+
"successfulPairs",
|
|
490
|
+
"requiredSuccessfulPairs",
|
|
491
|
+
"scoreDelta",
|
|
492
|
+
"minimumPracticalScoreDelta",
|
|
493
|
+
"protocolConformant",
|
|
494
|
+
"meaningfulGain",
|
|
495
|
+
"unacceptableRegression",
|
|
496
|
+
], context);
|
|
497
|
+
const taskFamily = value.taskFamily;
|
|
498
|
+
if (typeof taskFamily !== "string" ||
|
|
499
|
+
!TASK_FAMILIES.has(taskFamily))
|
|
500
|
+
throw new Error(`${context}.taskFamily is invalid`);
|
|
501
|
+
const protocolConformant = value.protocolConformant;
|
|
502
|
+
const meaningfulGain = value.meaningfulGain;
|
|
503
|
+
const unacceptableRegression = value.unacceptableRegression;
|
|
504
|
+
if (typeof protocolConformant !== "boolean" ||
|
|
505
|
+
typeof meaningfulGain !== "boolean" ||
|
|
506
|
+
typeof unacceptableRegression !== "boolean")
|
|
507
|
+
throw new Error(`${context} boolean state is invalid`);
|
|
508
|
+
const scoreDelta = boundedNumber(value.scoreDelta, `${context}.scoreDelta`, -100, 100);
|
|
509
|
+
const minimumPracticalScoreDelta = boundedNumber(value.minimumPracticalScoreDelta, `${context}.minimumPracticalScoreDelta`, 0, 100);
|
|
510
|
+
if (meaningfulGain !==
|
|
511
|
+
(protocolConformant && scoreDelta >= minimumPracticalScoreDelta))
|
|
512
|
+
throw new Error(`${context}.meaningfulGain is inconsistent`);
|
|
513
|
+
return {
|
|
514
|
+
evidenceSha256: sha256(value.evidenceSha256, `${context}.evidenceSha256`),
|
|
515
|
+
publicKeyFingerprint: (() => {
|
|
516
|
+
const fingerprint = text(value.publicKeyFingerprint, `${context}.publicKeyFingerprint`, 96);
|
|
517
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(fingerprint))
|
|
518
|
+
throw new Error(`${context}.publicKeyFingerprint is invalid`);
|
|
519
|
+
return fingerprint;
|
|
520
|
+
})(),
|
|
521
|
+
createdAt: timestamp(value.createdAt, `${context}.createdAt`),
|
|
522
|
+
campaignId: identifier(value.campaignId, `${context}.campaignId`),
|
|
523
|
+
campaignSha256: sha256(value.campaignSha256, `${context}.campaignSha256`),
|
|
524
|
+
runId: identifier(value.runId, `${context}.runId`),
|
|
525
|
+
packageId: packageId(value.packageId, `${context}.packageId`),
|
|
526
|
+
reviewedCommit: commit(value.reviewedCommit, `${context}.reviewedCommit`),
|
|
527
|
+
instructionSha256: sha256(value.instructionSha256, `${context}.instructionSha256`),
|
|
528
|
+
taskFamily: taskFamily,
|
|
529
|
+
successfulPairs: integer(value.successfulPairs, `${context}.successfulPairs`, 0, 100),
|
|
530
|
+
requiredSuccessfulPairs: integer(value.requiredSuccessfulPairs, `${context}.requiredSuccessfulPairs`, 5, 100),
|
|
531
|
+
scoreDelta,
|
|
532
|
+
minimumPracticalScoreDelta,
|
|
533
|
+
protocolConformant,
|
|
534
|
+
meaningfulGain,
|
|
535
|
+
unacceptableRegression,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function parseReferenceArray(value, context) {
|
|
539
|
+
if (!Array.isArray(value))
|
|
540
|
+
throw new Error(`${context} must be an array`);
|
|
541
|
+
const references = value.map((entry, index) => parseEvidenceReference(entry, `${context}[${index}]`));
|
|
542
|
+
const hashes = references.map(evidenceKey);
|
|
543
|
+
if (new Set(hashes).size !== hashes.length)
|
|
544
|
+
throw new Error(`${context} must not contain duplicate evidence`);
|
|
545
|
+
if (!isDeepStrictEqual(hashes, [...hashes].sort()))
|
|
546
|
+
throw new Error(`${context} must be sorted by evidence hash`);
|
|
547
|
+
return references;
|
|
548
|
+
}
|
|
549
|
+
function parseMetrics(value) {
|
|
550
|
+
if (!isRecord(value))
|
|
551
|
+
throw new Error("Trust decision metrics must be an object");
|
|
552
|
+
exactKeys(value, [
|
|
553
|
+
"signedEvidenceRecords",
|
|
554
|
+
"conformantEvidenceRecords",
|
|
555
|
+
"taskFamiliesWithMeaningfulGain",
|
|
556
|
+
"unacceptableRegressions",
|
|
557
|
+
"blockingSecurityFindings",
|
|
558
|
+
"missingEvidenceContribution",
|
|
559
|
+
], "Trust decision metrics");
|
|
560
|
+
if (value.missingEvidenceContribution !== 0)
|
|
561
|
+
throw new Error("Missing evidence contribution must be zero");
|
|
562
|
+
return {
|
|
563
|
+
signedEvidenceRecords: integer(value.signedEvidenceRecords, "Signed evidence count", 0, 10_000),
|
|
564
|
+
conformantEvidenceRecords: integer(value.conformantEvidenceRecords, "Conformant evidence count", 0, 10_000),
|
|
565
|
+
taskFamiliesWithMeaningfulGain: integer(value.taskFamiliesWithMeaningfulGain, "Meaningful task-family count", 0, TASK_FAMILIES.size),
|
|
566
|
+
unacceptableRegressions: integer(value.unacceptableRegressions, "Unacceptable regression count", 0, 10_000),
|
|
567
|
+
blockingSecurityFindings: integer(value.blockingSecurityFindings, "Blocking security finding count", 0, 10_000),
|
|
568
|
+
missingEvidenceContribution: 0,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
export function parseTrustDecisionRecord(value) {
|
|
572
|
+
if (!isRecord(value))
|
|
573
|
+
throw new Error("Trust decision must be an object");
|
|
574
|
+
exactKeys(value, [
|
|
575
|
+
"schemaVersion",
|
|
576
|
+
"policyVersion",
|
|
577
|
+
"decisionId",
|
|
578
|
+
"sequence",
|
|
579
|
+
"createdAt",
|
|
580
|
+
"packageId",
|
|
581
|
+
"reviewedCommit",
|
|
582
|
+
"instructionSha256",
|
|
583
|
+
"status",
|
|
584
|
+
"transition",
|
|
585
|
+
"previousDecisionSha256",
|
|
586
|
+
"currentEvidence",
|
|
587
|
+
"retainedEvidence",
|
|
588
|
+
"humanReview",
|
|
589
|
+
"security",
|
|
590
|
+
"metrics",
|
|
591
|
+
"reasons",
|
|
592
|
+
"policyBoundary",
|
|
593
|
+
], "Trust decision");
|
|
594
|
+
if (value.schemaVersion !== 1 ||
|
|
595
|
+
value.policyVersion !== TRUST_DECISION_POLICY_VERSION)
|
|
596
|
+
throw new Error("Unsupported trust decision version");
|
|
597
|
+
const status = value.status;
|
|
598
|
+
if (status !== "unbenchmarked" &&
|
|
599
|
+
status !== "benchmarked" &&
|
|
600
|
+
status !== "recommended")
|
|
601
|
+
throw new Error("Trust decision status is invalid");
|
|
602
|
+
const sequence = integer(value.sequence, "Trust decision sequence", 0, 100_000);
|
|
603
|
+
const previousDecisionSha256 = value.previousDecisionSha256 === null
|
|
604
|
+
? null
|
|
605
|
+
: sha256(value.previousDecisionSha256, "Previous trust decision hash");
|
|
606
|
+
if ((sequence === 0) !== (previousDecisionSha256 === null))
|
|
607
|
+
throw new Error("Trust decision predecessor is inconsistent with sequence");
|
|
608
|
+
const currentEvidence = parseReferenceArray(value.currentEvidence, "Current benchmark evidence");
|
|
609
|
+
const retainedEvidence = parseReferenceArray(value.retainedEvidence, "Retained benchmark evidence");
|
|
610
|
+
const retainedHashes = new Set(retainedEvidence.map(evidenceKey));
|
|
611
|
+
if (currentEvidence.some((reference) => !retainedHashes.has(evidenceKey(reference))))
|
|
612
|
+
throw new Error("Current benchmark evidence is missing from retained evidence");
|
|
613
|
+
const review = parseReview(value.humanReview);
|
|
614
|
+
const security = parseSecurity(value.security);
|
|
615
|
+
const metrics = parseMetrics(value.metrics);
|
|
616
|
+
const expectedMetrics = {
|
|
617
|
+
signedEvidenceRecords: currentEvidence.length,
|
|
618
|
+
conformantEvidenceRecords: currentEvidence.filter((reference) => reference.protocolConformant).length,
|
|
619
|
+
taskFamiliesWithMeaningfulGain: new Set(currentEvidence
|
|
620
|
+
.filter((reference) => reference.protocolConformant && reference.meaningfulGain)
|
|
621
|
+
.map((reference) => reference.taskFamily)).size,
|
|
622
|
+
unacceptableRegressions: currentEvidence.filter((reference) => reference.protocolConformant && reference.unacceptableRegression).length,
|
|
623
|
+
blockingSecurityFindings: security.blockingFindingIds.length,
|
|
624
|
+
missingEvidenceContribution: 0,
|
|
625
|
+
};
|
|
626
|
+
if (!isDeepStrictEqual(metrics, expectedMetrics))
|
|
627
|
+
throw new Error("Trust decision metrics do not match retained inputs");
|
|
628
|
+
const expectedStatus = qualityStatus(metrics, review);
|
|
629
|
+
if (status !== expectedStatus)
|
|
630
|
+
throw new Error("Trust decision status does not follow promotion policy");
|
|
631
|
+
if (!Array.isArray(value.reasons))
|
|
632
|
+
throw new Error("Trust decision reasons must be an array");
|
|
633
|
+
const expectedReasons = decisionReasons(status, metrics, review);
|
|
634
|
+
if (!isDeepStrictEqual(value.reasons, expectedReasons))
|
|
635
|
+
throw new Error("Trust decision reasons do not match promotion policy");
|
|
636
|
+
const selectedPackage = packageId(value.packageId, "Trust decision package id");
|
|
637
|
+
const selectedCommit = commit(value.reviewedCommit, "Trust decision commit");
|
|
638
|
+
const selectedInstruction = sha256(value.instructionSha256, "Trust decision instruction hash");
|
|
639
|
+
const createdAt = timestamp(value.createdAt, "Trust decision timestamp");
|
|
640
|
+
const decisionId = text(value.decisionId, "Trust decision id", 64);
|
|
641
|
+
const expectedDecisionId = digest({
|
|
642
|
+
selectedPackage,
|
|
643
|
+
selectedCommit,
|
|
644
|
+
sequence,
|
|
645
|
+
createdAt,
|
|
646
|
+
}).slice(0, 32);
|
|
647
|
+
if (decisionId !== expectedDecisionId)
|
|
648
|
+
throw new Error("Trust decision id does not match its immutable inputs");
|
|
649
|
+
const transition = text(value.transition, "Trust decision transition", 64);
|
|
650
|
+
if ((sequence === 0 && transition !== `unbenchmarked->${status}`) ||
|
|
651
|
+
(sequence > 0 && !transition.endsWith(`->${status}`)))
|
|
652
|
+
throw new Error("Trust decision transition is inconsistent");
|
|
653
|
+
if (currentEvidence.some((reference) => reference.packageId !== selectedPackage ||
|
|
654
|
+
reference.reviewedCommit !== selectedCommit ||
|
|
655
|
+
reference.instructionSha256 !== selectedInstruction) ||
|
|
656
|
+
(review &&
|
|
657
|
+
(review.packageId !== selectedPackage ||
|
|
658
|
+
review.reviewedCommit !== selectedCommit)) ||
|
|
659
|
+
security.scannedCommit !== selectedCommit)
|
|
660
|
+
throw new Error("Trust decision inputs do not bind the selected package revision");
|
|
661
|
+
if (retainedEvidence.some((reference) => reference.packageId !== selectedPackage))
|
|
662
|
+
throw new Error("Retained evidence names a different package");
|
|
663
|
+
if (sequence === 0 &&
|
|
664
|
+
!isDeepStrictEqual(retainedEvidence.map(evidenceKey), currentEvidence.map(evidenceKey)))
|
|
665
|
+
throw new Error("First trust decision cannot contain prior evidence");
|
|
666
|
+
const record = {
|
|
667
|
+
schemaVersion: 1,
|
|
668
|
+
policyVersion: TRUST_DECISION_POLICY_VERSION,
|
|
669
|
+
decisionId,
|
|
670
|
+
sequence,
|
|
671
|
+
createdAt,
|
|
672
|
+
packageId: selectedPackage,
|
|
673
|
+
reviewedCommit: selectedCommit,
|
|
674
|
+
instructionSha256: selectedInstruction,
|
|
675
|
+
status,
|
|
676
|
+
transition,
|
|
677
|
+
previousDecisionSha256,
|
|
678
|
+
currentEvidence,
|
|
679
|
+
retainedEvidence,
|
|
680
|
+
humanReview: review,
|
|
681
|
+
security,
|
|
682
|
+
metrics,
|
|
683
|
+
reasons: expectedReasons,
|
|
684
|
+
policyBoundary: text(value.policyBoundary, "Trust decision policy boundary", 512),
|
|
685
|
+
};
|
|
686
|
+
return record;
|
|
687
|
+
}
|
|
688
|
+
export function verifyTrustDecisionRecord(envelope, publicKeyPem) {
|
|
689
|
+
if (!verifyEnvelope(envelope, publicKeyPem).valid)
|
|
690
|
+
throw new Error("Trust decision signature is invalid");
|
|
691
|
+
const record = parseTrustDecisionRecord(envelope.payload);
|
|
692
|
+
if (record.createdAt !== envelope.createdAt)
|
|
693
|
+
throw new Error("Trust decision envelope timestamp is inconsistent");
|
|
694
|
+
return record;
|
|
695
|
+
}
|
|
696
|
+
export function verifyTrustDecisionChain(envelopes, publicKeyPem) {
|
|
697
|
+
if (!envelopes.length)
|
|
698
|
+
throw new Error("Trust decision chain is empty");
|
|
699
|
+
const records = envelopes.map((envelope) => verifyTrustDecisionRecord(envelope, publicKeyPem));
|
|
700
|
+
for (const [index, record] of records.entries()) {
|
|
701
|
+
if (record.sequence !== index)
|
|
702
|
+
throw new Error(`Trust decision chain is non-contiguous at ${index}`);
|
|
703
|
+
if (index === 0)
|
|
704
|
+
continue;
|
|
705
|
+
const previous = records[index - 1];
|
|
706
|
+
if (record.previousDecisionSha256 !== digest(envelopes[index - 1]) ||
|
|
707
|
+
record.packageId !== previous.packageId ||
|
|
708
|
+
Date.parse(record.createdAt) < Date.parse(previous.createdAt))
|
|
709
|
+
throw new Error(`Trust decision chain is invalid at ${index}`);
|
|
710
|
+
if (record.transition !== `${previous.status}->${record.status}`)
|
|
711
|
+
throw new Error(`Trust decision transition is invalid at ${index}`);
|
|
712
|
+
const retained = new Set(record.retainedEvidence.map(evidenceKey));
|
|
713
|
+
if (previous.retainedEvidence.some((reference) => !retained.has(evidenceKey(reference))))
|
|
714
|
+
throw new Error(`Trust decision dropped prior evidence at ${index}`);
|
|
715
|
+
}
|
|
716
|
+
return records;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Produce complete catalog coverage from verified decision chains. A decision
|
|
720
|
+
* for an older commit remains visible, but is never silently applied to a newer
|
|
721
|
+
* catalog revision.
|
|
722
|
+
*/
|
|
723
|
+
export function buildCatalogTrustCoverage(catalog, chains) {
|
|
724
|
+
const catalogIds = catalog.map((item) => item.id);
|
|
725
|
+
if (new Set(catalogIds).size !== catalogIds.length)
|
|
726
|
+
throw new Error("Catalog trust coverage requires unique package ids");
|
|
727
|
+
const latestByPackage = new Map();
|
|
728
|
+
for (const [index, chain] of chains.entries()) {
|
|
729
|
+
if (!isRecord(chain))
|
|
730
|
+
throw new Error(`Catalog trust chain ${index + 1} must be an object`);
|
|
731
|
+
exactKeys(chain, ["envelopes", "publicKeyPem"], `Catalog trust chain ${index + 1}`);
|
|
732
|
+
if (typeof chain.publicKeyPem !== "string")
|
|
733
|
+
throw new Error(`Catalog trust chain ${index + 1} public key is invalid`);
|
|
734
|
+
const records = verifyTrustDecisionChain(chain.envelopes, chain.publicKeyPem);
|
|
735
|
+
const latest = records.at(-1);
|
|
736
|
+
if (latestByPackage.has(latest.packageId))
|
|
737
|
+
throw new Error(`Catalog has multiple trust chains for '${latest.packageId}'`);
|
|
738
|
+
latestByPackage.set(latest.packageId, latest);
|
|
739
|
+
}
|
|
740
|
+
const packages = [...catalog]
|
|
741
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
742
|
+
.map((item) => {
|
|
743
|
+
const latest = latestByPackage.get(item.id);
|
|
744
|
+
const catalogCommit = item.source?.commit ?? null;
|
|
745
|
+
if (!latest)
|
|
746
|
+
return {
|
|
747
|
+
packageId: item.id,
|
|
748
|
+
catalogCommit,
|
|
749
|
+
status: "unbenchmarked",
|
|
750
|
+
decisionId: null,
|
|
751
|
+
decisionCommit: null,
|
|
752
|
+
transition: "none->unbenchmarked",
|
|
753
|
+
retainedEvidenceSha256: [],
|
|
754
|
+
explanation: [
|
|
755
|
+
"No signed trust decision exists; missing benchmark evidence contributes zero.",
|
|
756
|
+
"Popularity and stars do not establish catalog quality.",
|
|
757
|
+
],
|
|
758
|
+
};
|
|
759
|
+
const exactRevision = catalogCommit !== null && latest.reviewedCommit === catalogCommit;
|
|
760
|
+
return {
|
|
761
|
+
packageId: item.id,
|
|
762
|
+
catalogCommit,
|
|
763
|
+
status: exactRevision ? latest.status : "unbenchmarked",
|
|
764
|
+
decisionId: latest.decisionId,
|
|
765
|
+
decisionCommit: latest.reviewedCommit,
|
|
766
|
+
transition: exactRevision
|
|
767
|
+
? latest.transition
|
|
768
|
+
: `${latest.status}->unbenchmarked`,
|
|
769
|
+
retainedEvidenceSha256: latest.retainedEvidence.map(evidenceKey),
|
|
770
|
+
explanation: exactRevision
|
|
771
|
+
? [...latest.reasons]
|
|
772
|
+
: [
|
|
773
|
+
catalogCommit
|
|
774
|
+
? `Latest signed decision covers ${latest.reviewedCommit}; catalog now pins ${catalogCommit}.`
|
|
775
|
+
: "Catalog package has no immutable reviewed commit.",
|
|
776
|
+
"The prior signed decision and evidence references are retained, but missing evidence for this revision contributes zero.",
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
const counts = {
|
|
781
|
+
unbenchmarked: packages.filter((item) => item.status === "unbenchmarked")
|
|
782
|
+
.length,
|
|
783
|
+
benchmarked: packages.filter((item) => item.status === "benchmarked")
|
|
784
|
+
.length,
|
|
785
|
+
recommended: packages.filter((item) => item.status === "recommended")
|
|
786
|
+
.length,
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
schemaVersion: 1,
|
|
790
|
+
policyVersion: TRUST_DECISION_POLICY_VERSION,
|
|
791
|
+
packages,
|
|
792
|
+
counts,
|
|
793
|
+
policyBoundary: "Coverage reports signed evidence for exact reviewed commits. Missing or stale evidence contributes zero; popularity is never substituted.",
|
|
794
|
+
};
|
|
795
|
+
}
|