@cassiomc1/forgeloop 1.6.2 → 1.6.4
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/DOCS_INDEX.md +26 -4
- package/EXECUTION_STATE.md +7 -0
- package/LOOP_ENGINEERING.md +58 -0
- package/LOOP_SYSTEM_DESIGN.md +54 -0
- package/ORCHESTRATOR_INTEGRATION.md +23 -0
- package/PROTOCOL_INTEGRATION.md +36 -0
- package/QUALITY_SCORECARD.md +21 -0
- package/README.md +57 -2
- package/TERMINOLOGY.md +11 -0
- package/THREAT_MODEL.md +7 -0
- package/completions/_forgeloop +85 -0
- package/completions/forgeloop.bash +173 -0
- package/completions/forgeloop.fish +544 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
- package/docs/ARTIFACT_REFERENCE.md +226 -0
- package/docs/CLI_REFERENCE.md +308 -2
- package/docs/CODE_ATTESTATION.md +141 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
- package/docs/DOCUMENTATION_GUIDE.md +13 -11
- package/docs/GETTING_STARTED.md +98 -1
- package/docs/MCP.md +8 -0
- package/docs/PLATFORM_ADAPTERS.md +69 -0
- package/docs/RECIPES.md +138 -0
- package/docs/RELEASE_CHECKLIST.md +54 -0
- package/docs/REVISION_PROVIDERS.md +136 -0
- package/docs/SIGNING_PROVIDERS.md +83 -0
- package/docs/TROUBLESHOOTING.md +184 -0
- package/docs/UNIVERSAL_INTEGRATION.md +11 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
- package/docs/diagrams/README.md +30 -22
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
- package/integrations/generic-ci/verify.sh +14 -0
- package/package.json +31 -9
- package/schemas/attestation-verification-result.schema.json +34 -0
- package/schemas/code-attestation.schema.json +61 -0
- package/schemas/code-manifest.schema.json +57 -0
- package/schemas/config.schema.json +57 -1
- package/schemas/handoff-envelope.schema.json +57 -0
- package/schemas/in-toto-statement.schema.json +32 -0
- package/schemas/responsibility.schema.json +37 -0
- package/schemas/verification-scope.schema.json +26 -0
- package/schemas/workspace-binding.schema.json +20 -0
- package/scripts/CI_VALIDATORS.md +3 -0
- package/scripts/benchmark-cli-startup.mjs +60 -0
- package/scripts/check-changelog-freshness.mjs +88 -0
- package/scripts/check-critical-coverage.mjs +58 -0
- package/scripts/generate-agent-protocol-summary.mjs +141 -0
- package/scripts/generate-shell-completions.mjs +132 -0
- package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
- package/src/cli.js +114 -6
- package/src/commands/attestation-create.js +30 -0
- package/src/commands/attestation-status.js +12 -0
- package/src/commands/attestation-verify-range.js +32 -0
- package/src/commands/attestation-verify.js +23 -0
- package/src/commands/handoff-create.js +9 -0
- package/src/commands/handoff-list.js +12 -0
- package/src/commands/handoff-show.js +10 -0
- package/src/commands/responsibility-set.js +20 -0
- package/src/commands/responsibility-status.js +12 -0
- package/src/commands/run-check.js +43 -3
- package/src/commands/task-repair-legacy-recovery.js +1 -2
- package/src/commands/verify-scope.js +9 -0
- package/src/commands/workspace-bind.js +17 -0
- package/src/commands/workspace-status.js +19 -0
- package/src/core/artifact-registry.js +84 -0
- package/src/core/attestation-coverage.js +134 -0
- package/src/core/attestation-verifier.js +227 -0
- package/src/core/attestation.js +263 -0
- package/src/core/audit.js +67 -1
- package/src/core/bundles.js +210 -9
- package/src/core/cli-command-definitions.js +196 -0
- package/src/core/code-manifest.js +293 -0
- package/src/core/command-executors.js +79 -0
- package/src/core/command-input.js +69 -18
- package/src/core/command-runtime.js +2 -1
- package/src/core/completion.js +170 -8
- package/src/core/config.js +60 -2
- package/src/core/continuity-cli-options.js +9 -3
- package/src/core/continuity-reconciliation.js +20 -0
- package/src/core/diagnostic-projection.js +3 -1
- package/src/core/error-codes.js +146 -0
- package/src/core/events.js +109 -13
- package/src/core/exit-codes.js +22 -0
- package/src/core/handoff.js +208 -0
- package/src/core/information-gain-projection.js +3 -3
- package/src/core/integration-invocation-policy.js +40 -0
- package/src/core/integration-resources.js +54 -0
- package/src/core/next-action-phases.js +968 -0
- package/src/core/next-action.js +32 -951
- package/src/core/phase.js +29 -1
- package/src/core/protocol-info.js +41 -1
- package/src/core/repository.js +256 -11
- package/src/core/responsibility.js +265 -0
- package/src/core/revision/git.js +207 -0
- package/src/core/revision/provider.js +90 -0
- package/src/core/revision/registry.js +5 -0
- package/src/core/route-artifact.js +1 -1
- package/src/core/schema-validation.js +8 -0
- package/src/core/signing/none.js +23 -0
- package/src/core/signing/provider.js +21 -0
- package/src/core/signing/registry.js +7 -0
- package/src/core/signing/sigstore.js +158 -0
- package/src/core/task-command.js +9 -1
- package/src/core/task-paths.js +72 -0
- package/src/core/templates.js +8 -0
- package/src/core/verification-scope-capability.js +179 -0
- package/src/core/verification-scope.js +271 -0
- package/src/core/workspace-binding.js +211 -0
- package/src/integration.d.ts +91 -0
- package/AGENT_COMPATIBILITY.md +0 -11
- package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
- package/docs/RELEASE_CHECKLIST_1_6_1.md +0 -121
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
3
|
+
import { assertSecretFree } from "./receipt.js";
|
|
4
|
+
import { getPackageRoot } from "./templates.js";
|
|
5
|
+
import { readCodeManifest, validateCodeManifestBindings } from "./code-manifest.js";
|
|
6
|
+
import {
|
|
7
|
+
taskAttestationStatementPath,
|
|
8
|
+
taskAttestationStatementHistoryPath,
|
|
9
|
+
taskAttestationBundlePath,
|
|
10
|
+
} from "./task-paths.js";
|
|
11
|
+
import { appendProtocolEvent } from "./events.js";
|
|
12
|
+
import { ensureWithin, fileExists } from "./filesystem.js";
|
|
13
|
+
import { E_ATTESTATION_CONFIGURATION_INVALID } from "./error-codes.js";
|
|
14
|
+
|
|
15
|
+
const STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
16
|
+
const PREDICATE_TYPE = "https://forgeloop.dev/attestation/v1";
|
|
17
|
+
|
|
18
|
+
function attestationError(code, message, artifacts = []) {
|
|
19
|
+
const error = new Error(message);
|
|
20
|
+
error.name = "AttestationError";
|
|
21
|
+
error.code = code;
|
|
22
|
+
error.artifacts = artifacts;
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function validateAttestationPredicate(predicate, packageRoot = getPackageRoot()) {
|
|
27
|
+
try {
|
|
28
|
+
assertSchema(predicate, await readSchema("code-attestation", packageRoot), "ForgeLoop attestation predicate");
|
|
29
|
+
assertSecretFree(predicate);
|
|
30
|
+
return predicate;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw attestationError("E_ATTESTATION_STATEMENT_INVALID", error.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function validateAttestationStatement(statement, packageRoot = getPackageRoot()) {
|
|
37
|
+
try {
|
|
38
|
+
assertSchema(statement, await readSchema("in-toto-statement", packageRoot), "in-toto statement");
|
|
39
|
+
if (statement._type !== STATEMENT_TYPE || statement.predicateType !== PREDICATE_TYPE) {
|
|
40
|
+
throw attestationError("E_ATTESTATION_STATEMENT_INVALID", "Statement type or predicate type is not the ForgeLoop v1 contract");
|
|
41
|
+
}
|
|
42
|
+
await validateAttestationPredicate(statement.predicate, packageRoot);
|
|
43
|
+
const subject = statement.subject[0];
|
|
44
|
+
if (subject.name !== `forgeloop-task:${statement.predicate.task.taskId}`) {
|
|
45
|
+
throw attestationError("E_ATTESTATION_SUBJECT_MISMATCH", "Statement subject does not match the attested task");
|
|
46
|
+
}
|
|
47
|
+
if (subject.digest.sha256 !== statement.predicate.content.contentDigest) {
|
|
48
|
+
throw attestationError("E_ATTESTATION_SUBJECT_MISMATCH", "Statement subject digest does not match the predicate content digest");
|
|
49
|
+
}
|
|
50
|
+
assertSecretFree(statement);
|
|
51
|
+
return statement;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code?.startsWith("E_ATTESTATION_")) throw error;
|
|
54
|
+
throw attestationError("E_ATTESTATION_STATEMENT_INVALID", error.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function buildAttestationStatement({
|
|
59
|
+
target,
|
|
60
|
+
packageRoot = getPackageRoot(),
|
|
61
|
+
taskId,
|
|
62
|
+
completionResult = null,
|
|
63
|
+
auditResult = null,
|
|
64
|
+
revisionProvider = null,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const manifestArtifact = await readCodeManifest({ target, packageRoot, taskId });
|
|
67
|
+
const binding = await validateCodeManifestBindings({
|
|
68
|
+
target,
|
|
69
|
+
packageRoot,
|
|
70
|
+
taskId,
|
|
71
|
+
manifest: manifestArtifact.value,
|
|
72
|
+
revisionProvider: revisionProvider ?? manifestArtifact.value.capture.revisionProvider,
|
|
73
|
+
});
|
|
74
|
+
let completion = completionResult;
|
|
75
|
+
if (!completion) {
|
|
76
|
+
const { evaluateCompletion } = await import("./completion.js");
|
|
77
|
+
completion = await evaluateCompletion({ target, packageRoot, taskId });
|
|
78
|
+
}
|
|
79
|
+
if (completion.status !== "VALID") throw attestationError("E_ATTESTATION_STATEMENT_INVALID", "A VALID completion result is required before creating an attestation statement");
|
|
80
|
+
let audit = auditResult;
|
|
81
|
+
if (!audit) {
|
|
82
|
+
const { evaluateAudit } = await import("./audit.js");
|
|
83
|
+
audit = await evaluateAudit({ target, packageRoot, taskId });
|
|
84
|
+
}
|
|
85
|
+
if (audit.status !== "VALID") throw attestationError("E_ATTESTATION_STATEMENT_INVALID", "A VALID audit result is required before creating an attestation statement");
|
|
86
|
+
const manifest = binding.manifest;
|
|
87
|
+
const evidence = manifest.bindings;
|
|
88
|
+
const coveredPaths = [...new Set(manifest.entries.flatMap((entry) => [entry.path, entry.sourcePath].filter(Boolean)))].sort();
|
|
89
|
+
const predicate = await validateAttestationPredicate({
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
protocol: { name: "ForgeLoop", protocolVersion: 1 },
|
|
92
|
+
task: { taskId, verificationCycle: manifest.verificationCycle },
|
|
93
|
+
content: {
|
|
94
|
+
manifestFingerprint: manifestArtifact.fingerprint,
|
|
95
|
+
contentDigest: manifest.contentDigest,
|
|
96
|
+
coveredPaths,
|
|
97
|
+
},
|
|
98
|
+
evidence: {
|
|
99
|
+
contractFingerprint: evidence.contractFingerprint,
|
|
100
|
+
routeFingerprint: evidence.routeFingerprint,
|
|
101
|
+
stateFingerprint: evidence.stateFingerprint,
|
|
102
|
+
receiptFingerprint: evidence.receiptFingerprint,
|
|
103
|
+
ledgerSeq: evidence.ledgerSeq,
|
|
104
|
+
ledgerHash: evidence.ledgerHash,
|
|
105
|
+
},
|
|
106
|
+
verification: { completion: "VALID", audit: "VALID" },
|
|
107
|
+
}, packageRoot);
|
|
108
|
+
const statement = await validateAttestationStatement({
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
_type: STATEMENT_TYPE,
|
|
111
|
+
subject: [{ name: `forgeloop-task:${taskId}`, digest: { sha256: manifest.contentDigest } }],
|
|
112
|
+
predicateType: PREDICATE_TYPE,
|
|
113
|
+
predicate,
|
|
114
|
+
}, packageRoot);
|
|
115
|
+
return {
|
|
116
|
+
statement,
|
|
117
|
+
fingerprint: canonicalFingerprint(statement),
|
|
118
|
+
manifest: manifestArtifact,
|
|
119
|
+
completion,
|
|
120
|
+
audit,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function readAttestationStatement({ target, packageRoot = getPackageRoot(), taskId } = {}) {
|
|
125
|
+
const relativePath = taskAttestationStatementPath(taskId);
|
|
126
|
+
try {
|
|
127
|
+
const artifact = await readJsonArtifact(target, relativePath, "in-toto-statement", packageRoot);
|
|
128
|
+
await validateAttestationStatement(artifact.value, packageRoot);
|
|
129
|
+
return artifact;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.code === "ARTIFACT_MISSING") throw attestationError("E_ATTESTATION_STATEMENT_MISSING", `Attestation statement is missing: ${relativePath}`, [relativePath]);
|
|
132
|
+
if (error.code?.startsWith("E_ATTESTATION_")) throw error;
|
|
133
|
+
throw attestationError("E_ATTESTATION_STATEMENT_INVALID", error.message, [relativePath]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function writeAttestationStatement({ target, packageRoot = getPackageRoot(), taskId, statement } = {}) {
|
|
138
|
+
await validateAttestationStatement(statement, packageRoot);
|
|
139
|
+
const relativePath = taskAttestationStatementPath(taskId);
|
|
140
|
+
if (await fileExists(ensureWithin(target, relativePath))) {
|
|
141
|
+
let previous;
|
|
142
|
+
try {
|
|
143
|
+
previous = await readAttestationStatement({ target, packageRoot, taskId });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw attestationError("E_ATTESTATION_STATEMENT_INVALID", `Existing attestation statement cannot be versioned safely: ${error.message}`, [relativePath]);
|
|
146
|
+
}
|
|
147
|
+
const previousCycle = previous.value.predicate.task.verificationCycle;
|
|
148
|
+
const nextCycle = statement.predicate.task.verificationCycle;
|
|
149
|
+
if (nextCycle <= previousCycle) {
|
|
150
|
+
throw attestationError("E_ATTESTATION_STATEMENT_INVALID", "Attestation statement is immutable within a verification cycle", [relativePath]);
|
|
151
|
+
}
|
|
152
|
+
const historyPath = taskAttestationStatementHistoryPath(taskId, previousCycle);
|
|
153
|
+
if (!(await fileExists(ensureWithin(target, historyPath)))) {
|
|
154
|
+
await writeJsonArtifact(target, historyPath, previous.value, "in-toto-statement", packageRoot, { taskId, operation: "attestation-statement-history" });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const artifact = await writeJsonArtifact(target, relativePath, statement, "in-toto-statement", packageRoot, { taskId, operation: "attestation-create" });
|
|
158
|
+
await appendProtocolEvent(target, {
|
|
159
|
+
taskId,
|
|
160
|
+
event: "ATTESTATION_STATEMENT_CREATED",
|
|
161
|
+
fingerprint: artifact.fingerprint,
|
|
162
|
+
details: { statementFingerprint: artifact.fingerprint, manifestFingerprint: statement.predicate.content.manifestFingerprint },
|
|
163
|
+
}, packageRoot, { taskId });
|
|
164
|
+
return { path: relativePath, fingerprint: artifact.fingerprint, statement };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function resolveAttestationStatus({ target, packageRoot = getPackageRoot(), taskId, revisionProvider = null, bundlePath = null, requireSignature = false, signingProvider = null, signerPolicy = {} } = {}) {
|
|
168
|
+
const result = {
|
|
169
|
+
taskId,
|
|
170
|
+
status: "MISSING",
|
|
171
|
+
level: "PROCESSED",
|
|
172
|
+
content: "NOT_CHECKED",
|
|
173
|
+
receipt: "NOT_CHECKED",
|
|
174
|
+
ledger: "NOT_CHECKED",
|
|
175
|
+
signature: requireSignature ? "REQUIRED" : "NOT_CHECKED",
|
|
176
|
+
signer: null,
|
|
177
|
+
files: 0,
|
|
178
|
+
subject: null,
|
|
179
|
+
errors: [],
|
|
180
|
+
};
|
|
181
|
+
let effectiveRequireSignature = requireSignature === true;
|
|
182
|
+
let effectiveSigningProvider = signingProvider;
|
|
183
|
+
let effectiveSignerPolicy = { ...signerPolicy };
|
|
184
|
+
let effectiveBundlePath = bundlePath;
|
|
185
|
+
try {
|
|
186
|
+
const { readConfig } = await import("./config.js");
|
|
187
|
+
const config = await readConfig(target, packageRoot);
|
|
188
|
+
const configuredSigning = config.attestation?.signing ?? {};
|
|
189
|
+
effectiveRequireSignature ||= configuredSigning.required === true;
|
|
190
|
+
effectiveSigningProvider ??= configuredSigning.provider;
|
|
191
|
+
effectiveSignerPolicy = {
|
|
192
|
+
...(configuredSigning.policy ?? {}),
|
|
193
|
+
...signerPolicy,
|
|
194
|
+
};
|
|
195
|
+
if (effectiveRequireSignature && !effectiveBundlePath) effectiveBundlePath = taskAttestationBundlePath(taskId);
|
|
196
|
+
result.signature = effectiveRequireSignature ? "REQUIRED" : "NOT_CHECKED";
|
|
197
|
+
if ((config.attestation?.mode ?? "off") === "off" && !bundlePath && !effectiveRequireSignature) {
|
|
198
|
+
result.status = "DISABLED";
|
|
199
|
+
result.level = "PROCESSED";
|
|
200
|
+
result.signature = "NOT_CHECKED";
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error.code !== "ARTIFACT_MISSING") {
|
|
205
|
+
result.status = "INVALID";
|
|
206
|
+
result.errors.push({ code: E_ATTESTATION_CONFIGURATION_INVALID, message: error.message });
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
result.signature = effectiveRequireSignature ? "REQUIRED" : "NOT_CHECKED";
|
|
211
|
+
if (effectiveRequireSignature && !effectiveBundlePath) effectiveBundlePath = taskAttestationBundlePath(taskId);
|
|
212
|
+
try {
|
|
213
|
+
const statement = await readAttestationStatement({ target, packageRoot, taskId });
|
|
214
|
+
const manifest = await readCodeManifest({ target, packageRoot, taskId });
|
|
215
|
+
const provider = revisionProvider ?? manifest.value.capture.revisionProvider;
|
|
216
|
+
await validateCodeManifestBindings({ target, packageRoot, taskId, manifest: manifest.value, revisionProvider: provider });
|
|
217
|
+
const { assertAttestationStatementBindings, verifyCodeManifestContent } = await import("./attestation-verifier.js");
|
|
218
|
+
assertAttestationStatementBindings(statement.value, manifest.value, taskId, manifest.fingerprint);
|
|
219
|
+
await verifyCodeManifestContent({
|
|
220
|
+
target,
|
|
221
|
+
manifest: manifest.value,
|
|
222
|
+
revisionProvider: provider,
|
|
223
|
+
revision: manifest.value.capture.mode === "WORKTREE" ? "WORKTREE" : manifest.value.capture.observedRevision,
|
|
224
|
+
});
|
|
225
|
+
result.status = "VALID";
|
|
226
|
+
result.level = "VERIFIED";
|
|
227
|
+
result.content = "VALID";
|
|
228
|
+
result.receipt = "VALID";
|
|
229
|
+
result.ledger = "VALID";
|
|
230
|
+
result.files = statement.value.predicate.content.coveredPaths.length;
|
|
231
|
+
result.subject = `sha256:${statement.value.subject[0].digest.sha256}`;
|
|
232
|
+
if (effectiveRequireSignature && !(await fileExists(ensureWithin(target, effectiveBundlePath)))) {
|
|
233
|
+
result.signature = "UNSIGNED";
|
|
234
|
+
result.status = "INVALID";
|
|
235
|
+
result.errors.push({ code: "E_ATTESTATION_UNSIGNED", message: `Required attestation signature bundle is missing: ${effectiveBundlePath}` });
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
if (effectiveBundlePath) {
|
|
239
|
+
const { assertSigningProvider, resolveSigningProvider } = await import("./signing/provider.js");
|
|
240
|
+
const { SIGNING_PROVIDERS } = await import("./signing/registry.js");
|
|
241
|
+
const signer = typeof effectiveSigningProvider === "object"
|
|
242
|
+
? assertSigningProvider(effectiveSigningProvider)
|
|
243
|
+
: await resolveSigningProvider({ providerName: effectiveSigningProvider ?? effectiveSignerPolicy.provider ?? (effectiveBundlePath ? "sigstore" : "none"), registry: SIGNING_PROVIDERS });
|
|
244
|
+
const verified = await signer.verify({ target, statementPath: taskAttestationStatementPath(taskId), bundlePath: effectiveBundlePath, policy: effectiveSignerPolicy });
|
|
245
|
+
result.signature = verified.status;
|
|
246
|
+
result.signer = verified.signer ?? null;
|
|
247
|
+
if (verified.status === "VALID") result.level = "ATTESTED";
|
|
248
|
+
else if (effectiveRequireSignature) {
|
|
249
|
+
result.status = "INVALID";
|
|
250
|
+
result.errors.push({ code: verified.code ?? (verified.status === "UNSIGNED" ? "E_ATTESTATION_UNSIGNED" : "E_ATTESTATION_SIGNATURE_INVALID"), message: verified.message ?? "Signature verification failed" });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (["E_ATTESTATION_STATEMENT_MISSING", "E_ATTESTATION_MANIFEST_MISSING"].includes(error.code)) {
|
|
255
|
+
result.status = "MISSING";
|
|
256
|
+
result.level = "PROCESSED";
|
|
257
|
+
} else {
|
|
258
|
+
result.status = "INVALID";
|
|
259
|
+
}
|
|
260
|
+
result.errors.push({ code: error.code ?? "E_ATTESTATION_STATEMENT_INVALID", message: error.message });
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
package/src/core/audit.js
CHANGED
|
@@ -8,6 +8,9 @@ import { validateReadyProtocolConsistency } from "./preflight.js";
|
|
|
8
8
|
import { taskArtifactPath } from "./task-paths.js";
|
|
9
9
|
import { findTaskById } from "./task-discovery.js";
|
|
10
10
|
import { validateActionLedgerConsistency } from "./actions.js";
|
|
11
|
+
import { readCodeManifest } from "./code-manifest.js";
|
|
12
|
+
import { readAttestationStatement, validateAttestationStatement } from "./attestation.js";
|
|
13
|
+
import { assertAttestationStatementBindings, verifyCodeManifestContent } from "./attestation-verifier.js";
|
|
11
14
|
|
|
12
15
|
function sortErrors(errors) {
|
|
13
16
|
return [...errors].sort((left, right) => left.code.localeCompare(right.code)
|
|
@@ -55,6 +58,55 @@ async function compareChangedPaths(target, packageRoot, options = {}) {
|
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
async function evaluateLocalAttestation({ target, packageRoot, taskId, completion }) {
|
|
62
|
+
const configured = completion.attestation ?? { mode: "off" };
|
|
63
|
+
const base = {
|
|
64
|
+
mode: configured.mode ?? "off",
|
|
65
|
+
manifest: configured.mode === "off" ? "NOT_CHECKED" : configured.status === "CAPTURED" ? "VALID" : configured.status ?? "MISSING",
|
|
66
|
+
statement: "NOT_CHECKED",
|
|
67
|
+
signature: "NOT_CHECKED",
|
|
68
|
+
coverage: "NOT_CHECKED",
|
|
69
|
+
errors: [],
|
|
70
|
+
};
|
|
71
|
+
if (configured.mode === "off") return { ...base, status: "DISABLED" };
|
|
72
|
+
if (configured.status !== "CAPTURED") {
|
|
73
|
+
return { ...base, status: configured.status === "UNAVAILABLE" ? "UNAVAILABLE" : "MISSING" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let manifest;
|
|
77
|
+
try {
|
|
78
|
+
manifest = await readCodeManifest({ target, packageRoot, taskId });
|
|
79
|
+
await verifyCodeManifestContent({
|
|
80
|
+
target,
|
|
81
|
+
manifest: manifest.value,
|
|
82
|
+
revisionProvider: manifest.value.capture.revisionProvider,
|
|
83
|
+
revision: manifest.value.capture.mode === "WORKTREE" ? "WORKTREE" : manifest.value.capture.observedRevision,
|
|
84
|
+
});
|
|
85
|
+
base.coverage = "VALID";
|
|
86
|
+
} catch (error) {
|
|
87
|
+
base.manifest = "INVALID";
|
|
88
|
+
base.status = "INVALID";
|
|
89
|
+
base.errors.push({ code: error.code ?? "E_ATTESTATION_CONTENT_MISMATCH", message: error.message });
|
|
90
|
+
return base;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const statement = await readAttestationStatement({ target, packageRoot, taskId });
|
|
95
|
+
await validateAttestationStatement(statement.value, packageRoot);
|
|
96
|
+
assertAttestationStatementBindings(statement.value, manifest.value, taskId, manifest.fingerprint);
|
|
97
|
+
base.statement = "VALID";
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error.code === "E_ATTESTATION_STATEMENT_MISSING") {
|
|
100
|
+
return { ...base, status: "VALID" };
|
|
101
|
+
}
|
|
102
|
+
base.statement = "INVALID";
|
|
103
|
+
base.status = "INVALID";
|
|
104
|
+
base.errors.push({ code: error.code ?? "E_ATTESTATION_STATEMENT_INVALID", message: error.message });
|
|
105
|
+
return base;
|
|
106
|
+
}
|
|
107
|
+
return { ...base, status: "VALID" };
|
|
108
|
+
}
|
|
109
|
+
|
|
58
110
|
export async function evaluateAudit({
|
|
59
111
|
target,
|
|
60
112
|
packageRoot,
|
|
@@ -83,6 +135,7 @@ export async function evaluateAudit({
|
|
|
83
135
|
eventsPath,
|
|
84
136
|
preflightPath,
|
|
85
137
|
});
|
|
138
|
+
const attestation = await evaluateLocalAttestation({ target, packageRoot, taskId, completion });
|
|
86
139
|
const preflightRel = preflightPath ?? (taskId ? taskArtifactPath(taskId, "preflight") : ARTIFACT_PATHS.preflight);
|
|
87
140
|
const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
|
|
88
141
|
const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
|
|
@@ -118,7 +171,12 @@ export async function evaluateAudit({
|
|
|
118
171
|
artifacts: error.artifacts ?? [taskArtifactPath(taskId, "recovery"), taskArtifactPath(taskId, "events")],
|
|
119
172
|
}))
|
|
120
173
|
: [];
|
|
121
|
-
const errors = sortErrors([
|
|
174
|
+
const errors = sortErrors([
|
|
175
|
+
...completion.errors,
|
|
176
|
+
...readyConsistencyErrors,
|
|
177
|
+
...ownershipErrors,
|
|
178
|
+
...attestation.errors,
|
|
179
|
+
]);
|
|
122
180
|
if (taskId) {
|
|
123
181
|
for (const actionError of await validateActionLedgerConsistency(target, { packageRoot, taskId })) {
|
|
124
182
|
errors.push({ ...actionError, artifacts: [taskArtifactPath(taskId, "actions"), taskArtifactPath(taskId, "events")] });
|
|
@@ -199,6 +257,14 @@ export async function evaluateAudit({
|
|
|
199
257
|
},
|
|
200
258
|
policy: policyStatus,
|
|
201
259
|
completion,
|
|
260
|
+
attestation: {
|
|
261
|
+
mode: attestation.mode,
|
|
262
|
+
manifest: attestation.manifest,
|
|
263
|
+
statement: attestation.statement,
|
|
264
|
+
signature: attestation.signature,
|
|
265
|
+
coverage: attestation.coverage,
|
|
266
|
+
status: attestation.status,
|
|
267
|
+
},
|
|
202
268
|
recovery: taskInfo?.recovery ?? null,
|
|
203
269
|
claims: taskInfo ? {
|
|
204
270
|
state: taskInfo.claimState,
|
package/src/core/bundles.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
|
+
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
4
4
|
import { validateContract } from "./contract.js";
|
|
5
5
|
import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
|
|
6
6
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
7
|
import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
8
8
|
import { readExecutionArtifact } from "./execution.js";
|
|
9
9
|
import { assertContinuitySemantics } from "./continuity.js";
|
|
10
|
+
import { validateEventLedger } from "./events.js";
|
|
10
11
|
import { taskArtifactPath, taskDirectory } from "./task-paths.js";
|
|
11
12
|
import { resolveTaskClaimState } from "./task-claim-state.js";
|
|
12
13
|
import { E_TASK_CLAIM_OWNERSHIP_INCONSISTENT } from "./error-codes.js";
|
|
13
14
|
import { listActions } from "./actions.js";
|
|
15
|
+
import { validateCanonicalHandoff } from "./handoff.js";
|
|
16
|
+
import { validateVerificationScope } from "./verification-scope.js";
|
|
17
|
+
import { validateResponsibilityContract } from "./responsibility.js";
|
|
18
|
+
import { validateWorkspaceBinding } from "./workspace-binding.js";
|
|
19
|
+
import { validateCodeManifest, validateCodeManifestBindings } from "./code-manifest.js";
|
|
20
|
+
import { assertAttestationStatementBindings } from "./attestation-verifier.js";
|
|
21
|
+
import { validateAttestationStatement } from "./attestation.js";
|
|
14
22
|
|
|
15
23
|
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
16
24
|
const BUNDLE_ROOT = ".forgeloop/tasks";
|
|
@@ -28,6 +36,34 @@ function bundleDirectory(taskId) {
|
|
|
28
36
|
return `${BUNDLE_ROOT}/${safeTaskId(taskId)}`;
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
function bundleBindingError(code, message) {
|
|
40
|
+
const error = new Error(message);
|
|
41
|
+
error.code = code;
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertBundledCodeManifestBindings({ loaded, ledger, taskId }) {
|
|
46
|
+
const manifest = loaded.codeManifest;
|
|
47
|
+
if (!manifest) return;
|
|
48
|
+
const comparisons = [
|
|
49
|
+
["taskId", manifest.taskId, taskId, "E_BUNDLE_TASK_MISMATCH"],
|
|
50
|
+
["contractFingerprint", manifest.bindings.contractFingerprint, loaded.contract ? canonicalFingerprint(loaded.contract) : null, "E_ATTESTATION_CONTRACT_MISMATCH"],
|
|
51
|
+
["routeFingerprint", manifest.bindings.routeFingerprint, loaded.route ? canonicalFingerprint(loaded.route) : null, "E_ATTESTATION_ROUTE_MISMATCH"],
|
|
52
|
+
["stateFingerprint", manifest.bindings.stateFingerprint, loaded.state ? canonicalFingerprint(loaded.state) : null, "E_ATTESTATION_STATE_MISMATCH"],
|
|
53
|
+
["receiptFingerprint", manifest.bindings.receiptFingerprint, loaded.receipt ? canonicalFingerprint(loaded.receipt) : null, "E_ATTESTATION_RECEIPT_MISMATCH"],
|
|
54
|
+
];
|
|
55
|
+
for (const [name, expected, actual, code] of comparisons) {
|
|
56
|
+
if (expected !== actual) throw bundleBindingError(code, `Bundled code manifest binding does not match ${name}`);
|
|
57
|
+
}
|
|
58
|
+
if (!ledger?.valid) {
|
|
59
|
+
throw bundleBindingError("E_ATTESTATION_LEDGER_MISMATCH", "A valid bundled event ledger is required by the code manifest");
|
|
60
|
+
}
|
|
61
|
+
const completion = ledger.events.findLast((event) => event.taskId === taskId && event.event === "COMPLETION_VALIDATED");
|
|
62
|
+
if (!completion || completion.seq !== manifest.bindings.ledgerSeq || completion.hash !== manifest.bindings.ledgerHash) {
|
|
63
|
+
throw bundleBindingError("E_ATTESTATION_LEDGER_MISMATCH", "Bundled code manifest does not match the COMPLETION_VALIDATED ledger checkpoint");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
async function copyJson(target, sourcePath, destinationPath, schemaName, packageRoot, artifacts, relativeName) {
|
|
32
68
|
try {
|
|
33
69
|
const value = await readJsonArtifact(target, sourcePath, schemaName, packageRoot);
|
|
@@ -40,6 +76,23 @@ async function copyJson(target, sourcePath, destinationPath, schemaName, package
|
|
|
40
76
|
}
|
|
41
77
|
}
|
|
42
78
|
|
|
79
|
+
async function copyOptionalJson(target, taskPath, legacyPath, destinationPath, schemaName, packageRoot, artifacts, relativeName) {
|
|
80
|
+
const candidates = [taskPath, legacyPath].filter(Boolean);
|
|
81
|
+
for (const sourcePath of candidates) {
|
|
82
|
+
if (!(await fileExists(ensureWithin(target, sourcePath)))) continue;
|
|
83
|
+
return copyJson(target, sourcePath, destinationPath, schemaName, packageRoot, artifacts, relativeName);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function copyRawFile(target, sourcePath, destinationPath, artifacts, relativeName) {
|
|
89
|
+
if (!(await fileExists(ensureWithin(target, sourcePath)))) return false;
|
|
90
|
+
await assertSafePath(target, destinationPath);
|
|
91
|
+
await writeFileAtomic(ensureWithin(target, destinationPath), await readBytes(ensureWithin(target, sourcePath)));
|
|
92
|
+
artifacts.push(relativeName);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
43
96
|
async function tryReadJson(target, taskPath, legacyPath, schemaName, packageRoot) {
|
|
44
97
|
try {
|
|
45
98
|
return await readJsonArtifact(target, taskPath, schemaName, packageRoot);
|
|
@@ -117,19 +170,89 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
117
170
|
[ARTIFACT_PATHS.config, null, "config.json", "config"],
|
|
118
171
|
[taskArtifactPath(taskId, "continuity"), ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
|
|
119
172
|
[taskArtifactPath(taskId, "recovery"), null, "recovery.json", "task-recovery"],
|
|
173
|
+
[taskArtifactPath(taskId, "workspaceBinding"), null, "workspace-binding.json", "workspace-binding"],
|
|
174
|
+
[taskArtifactPath(taskId, "responsibility"), null, "responsibility.json", "responsibility"],
|
|
175
|
+
[taskArtifactPath(taskId, "verificationScope"), null, "verification-scope.json", "verification-scope"],
|
|
176
|
+
[taskArtifactPath(taskId, "attestations") + "/code-manifest.json", null, "attestations/code-manifest.json", "code-manifest"],
|
|
177
|
+
[taskArtifactPath(taskId, "attestations") + "/statement.json", null, "attestations/statement.json", "in-toto-statement"],
|
|
120
178
|
];
|
|
179
|
+
let exportedWorkspaceBinding = null;
|
|
180
|
+
let exportedResponsibility = null;
|
|
181
|
+
let exportedVerificationScope = null;
|
|
182
|
+
let exportedManifest = null;
|
|
183
|
+
let exportedStatement = null;
|
|
121
184
|
for (const [taskRel, legacyRel, destinationName, schemaName] of optional) {
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
copied = await copyJson(target, taskRel, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
125
|
-
} catch {
|
|
126
|
-
if (legacyRel) {
|
|
127
|
-
copied = await copyJson(target, legacyRel, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
185
|
+
const copied = await copyOptionalJson(target, taskRel, legacyRel, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
|
|
130
186
|
if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
|
|
187
|
+
if (destinationName === "attestations/code-manifest.json") exportedManifest = copied;
|
|
188
|
+
if (destinationName === "attestations/statement.json") exportedStatement = copied;
|
|
189
|
+
if (destinationName === "workspace-binding.json") exportedWorkspaceBinding = copied;
|
|
190
|
+
if (destinationName === "responsibility.json") exportedResponsibility = copied;
|
|
191
|
+
if (destinationName === "verification-scope.json") exportedVerificationScope = copied;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (exportedWorkspaceBinding?.value) {
|
|
195
|
+
exportedWorkspaceBinding.value = await validateWorkspaceBinding(exportedWorkspaceBinding.value, packageRoot);
|
|
196
|
+
if (exportedWorkspaceBinding.value.taskId !== taskId) {
|
|
197
|
+
const error = new Error("Workspace binding taskId does not match its bundle task");
|
|
198
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (exportedResponsibility?.value) {
|
|
203
|
+
exportedResponsibility.value = await validateResponsibilityContract(exportedResponsibility.value, packageRoot);
|
|
204
|
+
if (exportedResponsibility.value.taskId !== taskId) {
|
|
205
|
+
const error = new Error("Responsibility taskId does not match its bundle task");
|
|
206
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (exportedVerificationScope?.value) {
|
|
211
|
+
exportedVerificationScope.value = await validateVerificationScope(exportedVerificationScope.value, packageRoot);
|
|
212
|
+
if (exportedVerificationScope.value.taskId !== taskId) {
|
|
213
|
+
const error = new Error("Verification scope taskId does not match its bundle task");
|
|
214
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const handoffDirectory = taskArtifactPath(taskId, "handoffs");
|
|
220
|
+
if (await fileExists(ensureWithin(target, handoffDirectory))) {
|
|
221
|
+
const handoffEntries = await readdir(ensureWithin(target, handoffDirectory), { withFileTypes: true });
|
|
222
|
+
for (const entry of handoffEntries
|
|
223
|
+
.filter((item) => item.isFile() && /^handoff-[A-Za-z0-9_-]+\.json$/u.test(item.name))
|
|
224
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
225
|
+
const sourcePath = `${handoffDirectory}/${entry.name}`;
|
|
226
|
+
const handoff = await readJsonArtifact(target, sourcePath, "handoff-envelope", packageRoot);
|
|
227
|
+
await validateCanonicalHandoff(target, handoff.value, { taskId, packageRoot });
|
|
228
|
+
const destination = `handoffs/${entry.name}`;
|
|
229
|
+
await writeJsonArtifact(target, `${directory}/${destination}`, handoff.value, "handoff-envelope", packageRoot);
|
|
230
|
+
artifacts.push(destination);
|
|
231
|
+
}
|
|
131
232
|
}
|
|
132
233
|
|
|
234
|
+
const rawAttestationBundle = `${taskArtifactPath(taskId, "attestations")}/statement.sigstore.json`;
|
|
235
|
+
await copyRawFile(target, rawAttestationBundle, `${directory}/attestations/statement.sigstore.json`, artifacts, "attestations/statement.sigstore.json");
|
|
236
|
+
if (exportedManifest?.value) {
|
|
237
|
+
exportedManifest.value = await validateCodeManifest(exportedManifest.value, packageRoot);
|
|
238
|
+
if (exportedManifest.value.taskId !== taskId) {
|
|
239
|
+
const error = new Error("Code manifest taskId does not match its bundle task");
|
|
240
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
await validateCodeManifestBindings({ target, packageRoot, taskId, manifest: exportedManifest.value });
|
|
244
|
+
}
|
|
245
|
+
if (exportedStatement?.value) {
|
|
246
|
+
await validateAttestationStatement(exportedStatement.value, packageRoot);
|
|
247
|
+
if (exportedStatement.value.predicate.task.taskId !== taskId) {
|
|
248
|
+
const error = new Error("Attestation statement taskId does not match its bundle task");
|
|
249
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (exportedManifest?.value && exportedStatement?.value) {
|
|
254
|
+
assertAttestationStatementBindings(exportedStatement.value, exportedManifest.value, taskId, canonicalFingerprint(exportedManifest.value));
|
|
255
|
+
}
|
|
133
256
|
const actionArtifacts = await listActions(target, { packageRoot, taskId });
|
|
134
257
|
for (const action of actionArtifacts) {
|
|
135
258
|
const destination = `${directory}/actions/${action.actionId}.json`;
|
|
@@ -203,6 +326,11 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
203
326
|
"continuity.json": ["continuity", "continuity"],
|
|
204
327
|
"task.json": ["descriptor", "task-descriptor"],
|
|
205
328
|
"recovery.json": ["recovery", "task-recovery"],
|
|
329
|
+
"workspace-binding.json": ["workspaceBinding", "workspace-binding"],
|
|
330
|
+
"responsibility.json": ["responsibility", "responsibility"],
|
|
331
|
+
"verification-scope.json": ["verificationScope", "verification-scope"],
|
|
332
|
+
"attestations/code-manifest.json": ["codeManifest", "code-manifest"],
|
|
333
|
+
"attestations/statement.json": ["attestationStatement", "in-toto-statement"],
|
|
206
334
|
...Object.fromEntries(manifest.value.artifacts.filter((artifact) => artifact.startsWith("actions/")).map((artifact) => [artifact, ["action", "action"]])),
|
|
207
335
|
};
|
|
208
336
|
const executions = {};
|
|
@@ -218,6 +346,24 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
218
346
|
loaded.actions[action.value.actionId] = action.value;
|
|
219
347
|
continue;
|
|
220
348
|
}
|
|
349
|
+
if (artifact.startsWith("handoffs/") && artifact.endsWith(".json")) {
|
|
350
|
+
const handoff = await readJsonArtifact(target, `${directory}/${artifact}`, "handoff-envelope", packageRoot);
|
|
351
|
+
await validateCanonicalHandoff(target, handoff.value, { taskId, packageRoot });
|
|
352
|
+
loaded.handoffs ??= [];
|
|
353
|
+
loaded.handoffs.push(handoff.value);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (artifact === "attestations/statement.sigstore.json") {
|
|
357
|
+
const raw = await readBytes(ensureWithin(target, `${directory}/${artifact}`));
|
|
358
|
+
try {
|
|
359
|
+
loaded.attestationBundle = JSON.parse(raw.toString("utf8"));
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const invalid = new Error(`Attestation signature bundle is not valid JSON: ${error.message}`);
|
|
362
|
+
invalid.code = "E_ATTESTATION_SIGNATURE_INVALID";
|
|
363
|
+
throw invalid;
|
|
364
|
+
}
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
221
367
|
const mapping = mappings[artifact];
|
|
222
368
|
if (!mapping) continue;
|
|
223
369
|
const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
|
|
@@ -227,8 +373,63 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
227
373
|
if (mapping[1] === "continuity") {
|
|
228
374
|
assertContinuitySemantics(loadedArtifact.value);
|
|
229
375
|
}
|
|
376
|
+
if (mapping[1] === "workspace-binding" && loadedArtifact.value.taskId !== taskId) {
|
|
377
|
+
const error = new Error("Workspace binding taskId does not match its bundle task");
|
|
378
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (mapping[1] === "workspace-binding") {
|
|
382
|
+
loadedArtifact.value = await validateWorkspaceBinding(loadedArtifact.value, packageRoot);
|
|
383
|
+
}
|
|
384
|
+
if (mapping[1] === "responsibility") {
|
|
385
|
+
loadedArtifact.value = await validateResponsibilityContract(loadedArtifact.value, packageRoot);
|
|
386
|
+
if (loadedArtifact.value.taskId !== taskId) {
|
|
387
|
+
const error = new Error("Responsibility taskId does not match its bundle task");
|
|
388
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (mapping[1] === "verification-scope") {
|
|
393
|
+
loadedArtifact.value = await validateVerificationScope(loadedArtifact.value, packageRoot);
|
|
394
|
+
if (loadedArtifact.value.taskId !== taskId) {
|
|
395
|
+
const error = new Error("Verification scope taskId does not match its bundle task");
|
|
396
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (mapping[1] === "code-manifest") {
|
|
401
|
+
loadedArtifact.value = await validateCodeManifest(loadedArtifact.value, packageRoot);
|
|
402
|
+
if (loadedArtifact.value.taskId !== taskId) {
|
|
403
|
+
const error = new Error("Code manifest taskId does not match its bundle task");
|
|
404
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (mapping[1] === "in-toto-statement") {
|
|
409
|
+
await validateAttestationStatement(loadedArtifact.value, packageRoot);
|
|
410
|
+
if (loadedArtifact.value.predicate.task.taskId !== taskId) {
|
|
411
|
+
const error = new Error("Attestation statement taskId does not match its bundle task");
|
|
412
|
+
error.code = "E_BUNDLE_TASK_MISMATCH";
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
230
416
|
loaded[mapping[0]] = loadedArtifact.value;
|
|
231
417
|
}
|
|
418
|
+
const bundledLedger = loaded.codeManifest && manifest.value.artifacts.includes("events.ndjson")
|
|
419
|
+
? await validateEventLedger(target, packageRoot, { taskId, eventsPath: `${directory}/events.ndjson` })
|
|
420
|
+
: null;
|
|
421
|
+
assertBundledCodeManifestBindings({ loaded, ledger: bundledLedger, taskId });
|
|
422
|
+
if (loaded.attestationStatement && !loaded.codeManifest) {
|
|
423
|
+
throw bundleBindingError("E_ATTESTATION_MANIFEST_MISSING", "A bundled attestation statement requires its code manifest");
|
|
424
|
+
}
|
|
425
|
+
if (loaded.codeManifest && loaded.attestationStatement) {
|
|
426
|
+
assertAttestationStatementBindings(
|
|
427
|
+
loaded.attestationStatement,
|
|
428
|
+
loaded.codeManifest,
|
|
429
|
+
taskId,
|
|
430
|
+
canonicalFingerprint(loaded.codeManifest),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
232
433
|
if (Object.keys(executions).length > 0) loaded.executions = executions;
|
|
233
434
|
if (loaded.state?.checks) {
|
|
234
435
|
await validateChecksExecutionProvenance(loaded.state.checks, {
|