@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.
Files changed (127) hide show
  1. package/DOCS_INDEX.md +26 -4
  2. package/EXECUTION_STATE.md +7 -0
  3. package/LOOP_ENGINEERING.md +58 -0
  4. package/LOOP_SYSTEM_DESIGN.md +54 -0
  5. package/ORCHESTRATOR_INTEGRATION.md +23 -0
  6. package/PROTOCOL_INTEGRATION.md +36 -0
  7. package/QUALITY_SCORECARD.md +21 -0
  8. package/README.md +57 -2
  9. package/TERMINOLOGY.md +11 -0
  10. package/THREAT_MODEL.md +7 -0
  11. package/completions/_forgeloop +85 -0
  12. package/completions/forgeloop.bash +173 -0
  13. package/completions/forgeloop.fish +544 -0
  14. package/docs/AGENT_PROTOCOL_SUMMARY.md +281 -0
  15. package/docs/ARTIFACT_REFERENCE.md +226 -0
  16. package/docs/CLI_REFERENCE.md +308 -2
  17. package/docs/CODE_ATTESTATION.md +141 -0
  18. package/docs/CROSS_HARNESS_CONTINUITY.md +22 -2
  19. package/docs/DOCUMENTATION_GUIDE.md +13 -11
  20. package/docs/GETTING_STARTED.md +98 -1
  21. package/docs/MCP.md +8 -0
  22. package/docs/PLATFORM_ADAPTERS.md +69 -0
  23. package/docs/RECIPES.md +138 -0
  24. package/docs/RELEASE_CHECKLIST.md +54 -0
  25. package/docs/REVISION_PROVIDERS.md +136 -0
  26. package/docs/SIGNING_PROVIDERS.md +83 -0
  27. package/docs/TROUBLESHOOTING.md +184 -0
  28. package/docs/UNIVERSAL_INTEGRATION.md +11 -0
  29. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +13841 -0
  30. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +37 -0
  31. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +5046 -0
  32. package/docs/assets/diagrams/forgeloop-engineering-flow.html +46 -43
  33. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  34. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +41 -41
  35. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +13783 -0
  36. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +37 -0
  37. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +4988 -0
  38. package/docs/diagrams/README.md +30 -22
  39. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +125 -0
  40. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +11 -8
  41. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +116 -0
  42. package/docs/diagrams/manifest.json +42 -0
  43. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +20 -0
  44. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +3 -3
  45. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +20 -0
  46. package/integrations/generic-ci/verify.sh +14 -0
  47. package/package.json +31 -9
  48. package/schemas/attestation-verification-result.schema.json +34 -0
  49. package/schemas/code-attestation.schema.json +61 -0
  50. package/schemas/code-manifest.schema.json +57 -0
  51. package/schemas/config.schema.json +57 -1
  52. package/schemas/handoff-envelope.schema.json +57 -0
  53. package/schemas/in-toto-statement.schema.json +32 -0
  54. package/schemas/responsibility.schema.json +37 -0
  55. package/schemas/verification-scope.schema.json +26 -0
  56. package/schemas/workspace-binding.schema.json +20 -0
  57. package/scripts/CI_VALIDATORS.md +3 -0
  58. package/scripts/benchmark-cli-startup.mjs +60 -0
  59. package/scripts/check-changelog-freshness.mjs +88 -0
  60. package/scripts/check-critical-coverage.mjs +58 -0
  61. package/scripts/generate-agent-protocol-summary.mjs +141 -0
  62. package/scripts/generate-shell-completions.mjs +132 -0
  63. package/scripts/write-forgeloop-attestation-summary.mjs +93 -0
  64. package/src/cli.js +114 -6
  65. package/src/commands/attestation-create.js +30 -0
  66. package/src/commands/attestation-status.js +12 -0
  67. package/src/commands/attestation-verify-range.js +32 -0
  68. package/src/commands/attestation-verify.js +23 -0
  69. package/src/commands/handoff-create.js +9 -0
  70. package/src/commands/handoff-list.js +12 -0
  71. package/src/commands/handoff-show.js +10 -0
  72. package/src/commands/responsibility-set.js +20 -0
  73. package/src/commands/responsibility-status.js +12 -0
  74. package/src/commands/run-check.js +43 -3
  75. package/src/commands/task-repair-legacy-recovery.js +1 -2
  76. package/src/commands/verify-scope.js +9 -0
  77. package/src/commands/workspace-bind.js +17 -0
  78. package/src/commands/workspace-status.js +19 -0
  79. package/src/core/artifact-registry.js +84 -0
  80. package/src/core/attestation-coverage.js +134 -0
  81. package/src/core/attestation-verifier.js +227 -0
  82. package/src/core/attestation.js +263 -0
  83. package/src/core/audit.js +67 -1
  84. package/src/core/bundles.js +210 -9
  85. package/src/core/cli-command-definitions.js +196 -0
  86. package/src/core/code-manifest.js +293 -0
  87. package/src/core/command-executors.js +79 -0
  88. package/src/core/command-input.js +69 -18
  89. package/src/core/command-runtime.js +2 -1
  90. package/src/core/completion.js +170 -8
  91. package/src/core/config.js +60 -2
  92. package/src/core/continuity-cli-options.js +9 -3
  93. package/src/core/continuity-reconciliation.js +20 -0
  94. package/src/core/diagnostic-projection.js +3 -1
  95. package/src/core/error-codes.js +146 -0
  96. package/src/core/events.js +109 -13
  97. package/src/core/exit-codes.js +22 -0
  98. package/src/core/handoff.js +208 -0
  99. package/src/core/information-gain-projection.js +3 -3
  100. package/src/core/integration-invocation-policy.js +40 -0
  101. package/src/core/integration-resources.js +54 -0
  102. package/src/core/next-action-phases.js +968 -0
  103. package/src/core/next-action.js +32 -951
  104. package/src/core/phase.js +29 -1
  105. package/src/core/protocol-info.js +41 -1
  106. package/src/core/repository.js +256 -11
  107. package/src/core/responsibility.js +265 -0
  108. package/src/core/revision/git.js +207 -0
  109. package/src/core/revision/provider.js +90 -0
  110. package/src/core/revision/registry.js +5 -0
  111. package/src/core/route-artifact.js +1 -1
  112. package/src/core/schema-validation.js +8 -0
  113. package/src/core/signing/none.js +23 -0
  114. package/src/core/signing/provider.js +21 -0
  115. package/src/core/signing/registry.js +7 -0
  116. package/src/core/signing/sigstore.js +158 -0
  117. package/src/core/task-command.js +9 -1
  118. package/src/core/task-paths.js +72 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/verification-scope-capability.js +179 -0
  121. package/src/core/verification-scope.js +271 -0
  122. package/src/core/workspace-binding.js +211 -0
  123. package/src/integration.d.ts +91 -0
  124. package/AGENT_COMPATIBILITY.md +0 -11
  125. package/docs/RELEASE_CHECKLIST_1_4.md +0 -38
  126. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +0 -78
  127. 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([...completion.errors, ...readyConsistencyErrors, ...ownershipErrors]);
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,
@@ -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
- let copied = null;
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, {