@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,20 @@
1
+ import { setResponsibilityContract } from "../core/responsibility.js";
2
+
3
+ export async function runResponsibilitySet(options = {}) {
4
+ return setResponsibilityContract(options.target, {
5
+ ...options,
6
+ allowedPaths: options.responsibilityAllowedPaths?.length ? options.responsibilityAllowedPaths : undefined,
7
+ readOnlyPaths: options.responsibilityReadOnlyPaths?.length ? options.responsibilityReadOnlyPaths : undefined,
8
+ requiredCheckIds: options.responsibilityRequiredChecks?.length ? options.responsibilityRequiredChecks : undefined,
9
+ label: options.responsibilityLabel,
10
+ frozenInputs: {
11
+ contract: options.responsibilityFreezeContract,
12
+ route: options.responsibilityFreezeRoute,
13
+ claims: options.responsibilityFreezeClaims,
14
+ },
15
+ });
16
+ }
17
+
18
+ export function formatResponsibilitySetResult(result) {
19
+ return `FORGELOOP RESPONSIBILITY SET\ntask: ${result.taskId}\nlabel: ${result.responsibility.label}\nfingerprint: ${result.fingerprint}\npath: ${result.path}\n\n`;
20
+ }
@@ -0,0 +1,12 @@
1
+ import { resolveResponsibilityStatus } from "../core/responsibility.js";
2
+
3
+ export async function runResponsibilityStatus({ target, packageRoot, taskId } = {}) {
4
+ return resolveResponsibilityStatus(target, { packageRoot, taskId });
5
+ }
6
+
7
+ export function formatResponsibilityStatusResult(result) {
8
+ const lines = [`FORGELOOP RESPONSIBILITY: ${result.status}`, `task: ${result.taskId}`];
9
+ if (result.responsibility) lines.push(`label: ${result.responsibility.label}`);
10
+ for (const error of result.errors ?? []) lines.push(`${error.code}: ${error.message}`);
11
+ return `${lines.join("\n")}\n`;
12
+ }
@@ -4,6 +4,8 @@ import {
4
4
  } from "../core/completion-artifacts.js";
5
5
  import { runCommandExecution } from "../core/execution.js";
6
6
  import { withTaskMutation } from "../core/task-command.js";
7
+ import { readVerificationScope, validateVerificationScopeFreshness } from "../core/verification-scope.js";
8
+ import { bindVerificationScopeCommand } from "../core/verification-scope-capability.js";
7
9
 
8
10
  export async function runCheck({
9
11
  target,
@@ -13,6 +15,7 @@ export async function runCheck({
13
15
  argv,
14
16
  details,
15
17
  timeoutMs,
18
+ scopeRef,
16
19
  authorityContext,
17
20
  runtimeContext,
18
21
  taskId,
@@ -25,6 +28,43 @@ export async function runCheck({
25
28
  }
26
29
  return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "run-check", async (ctx) => {
27
30
  const effectiveTaskId = ctx?.taskId ?? null;
31
+ let executionDetails = details;
32
+ let commandArgv = [...argv];
33
+ if (scopeRef) {
34
+ const scope = await readVerificationScope(target, {
35
+ packageRoot,
36
+ taskId: effectiveTaskId,
37
+ scopePath: scopeRef,
38
+ });
39
+ const freshness = await validateVerificationScopeFreshness(target, {
40
+ packageRoot,
41
+ taskId: effectiveTaskId,
42
+ scope: scope.value,
43
+ });
44
+ const binding = await bindVerificationScopeCommand({
45
+ target,
46
+ packageRoot,
47
+ checkId: id,
48
+ argv: commandArgv,
49
+ scope: scope.value,
50
+ capabilities: freshness.current.checkerCapabilities,
51
+ });
52
+ commandArgv = binding.argv;
53
+ executionDetails = {
54
+ ...(details ?? {}),
55
+ verificationScope: {
56
+ ref: scope.path,
57
+ fingerprint: scope.fingerprint,
58
+ mode: scope.value.resolvedMode,
59
+ selectedPaths: scope.value.selectedPaths,
60
+ argv: [...commandArgv],
61
+ ...(binding.checker ? {
62
+ checkerId: binding.checker.checkId,
63
+ checkerCapabilityFingerprint: binding.capabilityFingerprint,
64
+ } : {}),
65
+ },
66
+ };
67
+ }
28
68
  const ready = await assertRecordCheckPrerequisites({
29
69
  target,
30
70
  packageRoot,
@@ -43,8 +83,8 @@ export async function runCheck({
43
83
  checkId: id,
44
84
  requirement,
45
85
  verificationCycle,
46
- argv,
47
- details,
86
+ argv: commandArgv,
87
+ details: executionDetails,
48
88
  timeoutMs,
49
89
  authorityContext,
50
90
  runtimeContext,
@@ -62,7 +102,7 @@ export async function runCheck({
62
102
  command: execution.execution.argv.join(" "),
63
103
  result: execution.result,
64
104
  ...(execution.execution.exitCode === null ? {} : { exitCode: execution.execution.exitCode }),
65
- details,
105
+ details: executionDetails,
66
106
  executionRef: execution.execution.executionId,
67
107
  provenance: "FORGELOOP_EXECUTED",
68
108
  authorityContext,
@@ -195,7 +195,6 @@ export async function runTaskRepairLegacyRecovery({ target, packageRoot, taskId,
195
195
 
196
196
  const existingRecovery = await readTaskRecovery(target, { taskId: effectiveTaskId, packageRoot });
197
197
  const locked = await inspectLegacyRecoveryMigration(target, { taskId: effectiveTaskId, packageRoot });
198
- let candidate;
199
198
  if (locked.alreadyMigrated) {
200
199
  return verifyAlreadyRepaired(target, {
201
200
  taskId: effectiveTaskId,
@@ -204,7 +203,7 @@ export async function runTaskRepairLegacyRecovery({ target, packageRoot, taskId,
204
203
  existingRecovery,
205
204
  });
206
205
  }
207
- candidate = requireSingleTailCandidate({
206
+ const candidate = requireSingleTailCandidate({
208
207
  taskId: effectiveTaskId,
209
208
  events: locked.events,
210
209
  candidates: locked.candidates,
@@ -0,0 +1,9 @@
1
+ import { captureVerificationScope } from "../core/verification-scope.js";
2
+
3
+ export async function runVerifyScope({ target, packageRoot, taskId, verificationScopeMode } = {}) {
4
+ return captureVerificationScope(target, { packageRoot, taskId, mode: verificationScopeMode ?? "AUTO" });
5
+ }
6
+
7
+ export function formatVerifyScopeResult(result) {
8
+ return `FORGELOOP VERIFICATION SCOPE: ${result.scope.resolvedMode}\ntask: ${result.scope.taskId}\nselected paths: ${result.scope.selectedPaths.length}\npath: ${result.path}\n\n`;
9
+ }
@@ -0,0 +1,17 @@
1
+ import { bindTaskWorkspace } from "../core/workspace-binding.js";
2
+
3
+ export async function runWorkspaceBind({ target, packageRoot, taskId, now } = {}) {
4
+ return bindTaskWorkspace(target, { taskId, packageRoot, now });
5
+ }
6
+
7
+ export function formatWorkspaceBindResult(result) {
8
+ return [
9
+ `FORGELOOP WORKSPACE BIND: ${result.status}`,
10
+ `task: ${result.taskId}`,
11
+ `binding: ${result.path}`,
12
+ `workspace: ${result.binding.workspaceIdentity}`,
13
+ `repository: ${result.binding.repositoryIdentity}`,
14
+ `already bound: ${result.alreadyBound ? "yes" : "no"}`,
15
+ "",
16
+ ].join("\n");
17
+ }
@@ -0,0 +1,19 @@
1
+ import { resolveWorkspaceBindingStatus } from "../core/workspace-binding.js";
2
+
3
+ export async function runWorkspaceStatus({ target, packageRoot, taskId } = {}) {
4
+ return resolveWorkspaceBindingStatus(target, { taskId, packageRoot });
5
+ }
6
+
7
+ export function formatWorkspaceStatusResult(result) {
8
+ const lines = [
9
+ `FORGELOOP WORKSPACE: ${result.status}`,
10
+ `task: ${result.taskId}`,
11
+ `binding: ${result.path}`,
12
+ ];
13
+ if (result.binding) {
14
+ lines.push(`workspace: ${result.binding.workspaceIdentity}`);
15
+ lines.push(`repository: ${result.binding.repositoryIdentity}`);
16
+ }
17
+ if (result.error) lines.push(`${result.error.code}: ${result.error.message}`);
18
+ return `${lines.join("\n")}\n`;
19
+ }
@@ -283,4 +283,88 @@ export const ARTIFACT_REGISTRY = Object.freeze({
283
283
  isPersisted: true,
284
284
  description: "Immutable trajectory evaluation results compiled from the canonical trace against a local reference scenario.",
285
285
  }),
286
+ workspaceBinding: Object.freeze({
287
+ key: "workspaceBinding",
288
+ scope: "TASK",
289
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.workspaceBinding}`,
290
+ schema: "workspace-binding",
291
+ owner: "PROTOCOL_GENERATED",
292
+ mutability: "IMMUTABLE_AFTER_BIND",
293
+ trustRole: "WORKSPACE_IDENTITY_BINDING",
294
+ isPublic: true,
295
+ isPersisted: true,
296
+ description: "Optional task binding to the exact Git worktree identity used for ForgeLoop mutation and verification.",
297
+ }),
298
+ handoffs: Object.freeze({
299
+ key: "handoffs",
300
+ scope: "TASK",
301
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.handoffs}/handoff-<id>.json`,
302
+ schema: "handoff-envelope",
303
+ owner: "PROTOCOL_COMPILED",
304
+ mutability: "IMMUTABLE_ONCE_WRITTEN",
305
+ trustRole: "CANONICAL_HANDOFF_SNAPSHOT",
306
+ isPublic: true,
307
+ isPersisted: true,
308
+ description: "Immutable protocol-derived handoff snapshots; not delegation, review, completion, or authority evidence.",
309
+ }),
310
+ responsibility: Object.freeze({
311
+ key: "responsibility",
312
+ scope: "TASK",
313
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.responsibility}`,
314
+ schema: "responsibility",
315
+ owner: "AGENT_OR_HARNESS_CONSTRAINED",
316
+ mutability: "IMMUTABLE_DURING_PASS",
317
+ trustRole: "RESPONSIBILITY_CONSTRAINT",
318
+ isPublic: true,
319
+ isPersisted: true,
320
+ description: "Optional mechanically verifiable pass constraints for allowed, read-only, frozen, and required-check paths.",
321
+ }),
322
+ verificationScope: Object.freeze({
323
+ key: "verificationScope",
324
+ scope: "TASK",
325
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.verificationScope}`,
326
+ schema: "verification-scope",
327
+ owner: "PROTOCOL_COMPILED",
328
+ mutability: "RECOMPUTED_ON_SCOPE_CHANGE",
329
+ trustRole: "VERIFICATION_SCOPE_PLAN",
330
+ isPublic: true,
331
+ isPersisted: true,
332
+ description: "Read-only planning boundary for changed, claimed, full, or explicitly unresolved verification.",
333
+ }),
334
+ codeManifest: Object.freeze({
335
+ key: "codeManifest",
336
+ scope: "TASK",
337
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.attestations}/code-manifest.json`,
338
+ schema: "code-manifest",
339
+ owner: "PROTOCOL_GENERATED",
340
+ mutability: "IMMUTABLE_ONCE_WRITTEN",
341
+ trustRole: "CONTENT_INTEGRITY_SNAPSHOT",
342
+ isPublic: true,
343
+ isPersisted: true,
344
+ description: "ForgeLoop SHA-256 source-content snapshot bound to final completion evidence.",
345
+ }),
346
+ attestationStatement: Object.freeze({
347
+ key: "attestationStatement",
348
+ scope: "TASK",
349
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.attestations}/statement.json`,
350
+ schema: "in-toto-statement",
351
+ owner: "PROTOCOL_COMPILED",
352
+ mutability: "IMMUTABLE_ONCE_WRITTEN",
353
+ trustRole: "CODE_ATTESTATION_STATEMENT",
354
+ isPublic: true,
355
+ isPersisted: true,
356
+ description: "Deterministic in-toto Statement v1 predicate binding source content to ForgeLoop evidence.",
357
+ }),
358
+ attestationBundle: Object.freeze({
359
+ key: "attestationBundle",
360
+ scope: "TASK",
361
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.attestations}/statement.sigstore.json`,
362
+ schema: null,
363
+ owner: "EXTERNAL_SIGNING_PROVIDER",
364
+ mutability: "EXTERNAL_IMMUTABLE",
365
+ trustRole: "EXTERNAL_SIGNATURE_BUNDLE",
366
+ isPublic: true,
367
+ isPersisted: true,
368
+ description: "Optional external signing-provider bundle; existence alone never establishes a valid signature.",
369
+ }),
286
370
  });
@@ -0,0 +1,134 @@
1
+ import { getPackageRoot } from "./templates.js";
2
+ import { discoverTasks } from "./task-discovery.js";
3
+ import { readCodeManifest, validateCodeManifestBindings } from "./code-manifest.js";
4
+ import { readAttestationStatement } from "./attestation.js";
5
+ import { resolveRevisionProvider } from "./revision/provider.js";
6
+ import { REVISION_PROVIDERS } from "./revision/registry.js";
7
+ import { assertAttestationStatementBindings, verifyCodeManifestContent } from "./attestation-verifier.js";
8
+ import { taskAttestationBundlePath } from "./task-paths.js";
9
+
10
+ function pathMatchesExclusion(pathValue, exclusions) {
11
+ return exclusions.some((pattern) => {
12
+ const normalized = String(pattern).replaceAll("\\", "/");
13
+ if (normalized.endsWith("/**")) return pathValue === normalized.slice(0, -3) || pathValue.startsWith(normalized.slice(0, -2));
14
+ return pathValue === normalized;
15
+ });
16
+ }
17
+
18
+ function changedPathNames(entry) {
19
+ return [entry.path, entry.sourcePath].filter(Boolean);
20
+ }
21
+
22
+ function isProtocolMetadataPath(value) {
23
+ const normalized = String(value).replaceAll("\\", "/").replace(/^\.\//u, "");
24
+ return normalized === ".forgeloop" || normalized.startsWith(".forgeloop/");
25
+ }
26
+
27
+ export async function evaluateAttestationCoverage({
28
+ target,
29
+ packageRoot = getPackageRoot(),
30
+ revisionProvider = "git",
31
+ baseRevision,
32
+ headRevision,
33
+ requireCompleteCoverage = true,
34
+ requireSignature = false,
35
+ signingProvider = null,
36
+ signerPolicy = {},
37
+ exclusions = [],
38
+ } = {}) {
39
+ const provider = typeof revisionProvider === "string"
40
+ ? await resolveRevisionProvider({ target, providerName: revisionProvider, registry: REVISION_PROVIDERS })
41
+ : revisionProvider;
42
+ const changedEntries = await provider.getChangedEntries({ target, baseRevision, headRevision });
43
+ const changedPaths = [...new Set(changedEntries.flatMap(changedPathNames)
44
+ .filter((value) => !isProtocolMetadataPath(value)))].sort();
45
+ const validAttestations = [];
46
+ const errors = [];
47
+ const tasks = await discoverTasks(target, packageRoot);
48
+ for (const task of tasks.filter((item) => item.healthy !== false && item.taskId)) {
49
+ try {
50
+ const statement = await readAttestationStatement({ target, packageRoot, taskId: task.taskId });
51
+ const manifest = await readCodeManifest({ target, packageRoot, taskId: task.taskId });
52
+ await validateCodeManifestBindings({ target, packageRoot, taskId: task.taskId, manifest: manifest.value, revisionProvider: provider });
53
+ assertAttestationStatementBindings(statement.value, manifest.value, task.taskId, manifest.fingerprint);
54
+ await verifyCodeManifestContent({ target, manifest: manifest.value, revisionProvider: provider, revision: headRevision });
55
+ let signatureStatus = "UNSIGNED";
56
+ if (requireSignature) {
57
+ const { verifyAttestation } = await import("./attestation-verifier.js");
58
+ const verified = await verifyAttestation({
59
+ target,
60
+ packageRoot,
61
+ taskId: task.taskId,
62
+ revision: headRevision,
63
+ revisionProvider: provider,
64
+ requireSignature: true,
65
+ signingProvider,
66
+ bundlePath: taskAttestationBundlePath(task.taskId),
67
+ identity: signerPolicy.identity,
68
+ issuer: signerPolicy.issuer,
69
+ trustedRoot: signerPolicy.trustedRoot,
70
+ });
71
+ signatureStatus = verified.signature.status;
72
+ if (verified.status !== "VALID") {
73
+ const error = new Error(verified.errors[0]?.message ?? "Signature verification failed");
74
+ error.code = verified.errors[0]?.code ?? "E_ATTESTATION_SIGNATURE_INVALID";
75
+ throw error;
76
+ }
77
+ }
78
+ validAttestations.push({
79
+ taskId: task.taskId,
80
+ statement,
81
+ manifest: manifest.value,
82
+ signatureStatus,
83
+ });
84
+ } catch (error) {
85
+ if (error.code !== "E_ATTESTATION_STATEMENT_MISSING" && error.code !== "E_ATTESTATION_MANIFEST_MISSING") {
86
+ errors.push({ taskId: task.taskId, code: error.code ?? "E_ATTESTATION_INVALID", message: error.message });
87
+ }
88
+ }
89
+ }
90
+ const coveredByPath = new Map();
91
+ const overlaps = [];
92
+ for (const attestation of validAttestations) {
93
+ for (const entry of attestation.manifest.entries) {
94
+ for (const name of changedPathNames(entry)) {
95
+ const digest = entry.kind === "DELETED" ? "DELETED" : entry.sha256 ?? entry.providerContentId ?? "UNKNOWN";
96
+ const existing = coveredByPath.get(name);
97
+ if (existing && existing.digest !== digest) {
98
+ errors.push({ code: "E_ATTESTATION_COVERAGE_CONFLICT", message: `Conflicting attestation content for ${name}`, path: name, tasks: [existing.taskId, attestation.taskId] });
99
+ } else if (existing) {
100
+ overlaps.push({ path: name, tasks: [...new Set([existing.taskId, attestation.taskId])] });
101
+ } else {
102
+ coveredByPath.set(name, { taskId: attestation.taskId, digest });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ const excluded = changedPaths.filter((item) => pathMatchesExclusion(item, exclusions));
108
+ const uncoveredPaths = changedPaths.filter((item) => !pathMatchesExclusion(item, exclusions) && !coveredByPath.has(item));
109
+ if (requireCompleteCoverage && uncoveredPaths.length > 0) errors.push({ code: "E_ATTESTATION_COVERAGE_GAP", message: `Changed source paths are not covered by valid attestations: ${uncoveredPaths.join(", ")}`, paths: uncoveredPaths });
110
+ const status = errors.length === 0 && (!requireCompleteCoverage || uncoveredPaths.length === 0) ? "VALID" : "INVALID";
111
+ const signatureStatus = requireSignature
112
+ ? (validAttestations.every((item) => item.signatureStatus === "VALID") && (validAttestations.length > 0 || changedPaths.length === 0) ? "VALID" : "INVALID")
113
+ : "UNSIGNED";
114
+ return {
115
+ schemaVersion: 1,
116
+ status,
117
+ level: status === "VALID" ? (requireSignature ? "ATTESTED" : "VERIFIED") : "PROCESSED",
118
+ revisionProvider: provider.name ?? "unknown",
119
+ baseRevision,
120
+ headRevision,
121
+ changedPaths: changedPaths.length,
122
+ coveredPaths: [...coveredByPath.keys()].filter((item) => !excluded.includes(item)).length,
123
+ uncoveredPaths,
124
+ tasks: validAttestations.length,
125
+ overlaps,
126
+ errors,
127
+ signature: {
128
+ required: requireSignature,
129
+ status: signatureStatus,
130
+ ...(signerPolicy.identity ? { identity: signerPolicy.identity } : {}),
131
+ ...(signerPolicy.issuer ? { issuer: signerPolicy.issuer } : {}),
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,227 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { canonicalFingerprint } from "./artifacts.js";
4
+ import { getPackageRoot } from "./templates.js";
5
+ import { readCodeManifest, validateCodeManifestBindings } from "./code-manifest.js";
6
+ import { readAttestationStatement, validateAttestationStatement } from "./attestation.js";
7
+ import { assertRevisionProvider, resolveRevisionProvider } from "./revision/provider.js";
8
+ import { REVISION_PROVIDERS } from "./revision/registry.js";
9
+ import { assertSigningProvider, resolveSigningProvider } from "./signing/provider.js";
10
+ import { SIGNING_PROVIDERS } from "./signing/registry.js";
11
+ import { taskAttestationBundlePath, taskAttestationStatementPath } from "./task-paths.js";
12
+ import { ensureWithin, fileExists } from "./filesystem.js";
13
+ import { E_ATTESTATION_CONFIGURATION_INVALID } from "./error-codes.js";
14
+
15
+ function verifierError(code, message, artifacts = []) {
16
+ const error = new Error(message);
17
+ error.name = "AttestationVerificationError";
18
+ error.code = code;
19
+ error.artifacts = artifacts;
20
+ return error;
21
+ }
22
+
23
+ function sha256(bytes) {
24
+ return createHash("sha256").update(bytes).digest("hex");
25
+ }
26
+
27
+ export function assertAttestationStatementBindings(statement, manifest, taskId, manifestFingerprint = canonicalFingerprint(manifest)) {
28
+ const predicate = statement.predicate;
29
+ if (predicate.task.taskId !== taskId || predicate.task.verificationCycle !== manifest.verificationCycle) {
30
+ throw verifierError("E_ATTESTATION_SCOPE_MISMATCH", "Attestation statement task or verification cycle does not match the manifest");
31
+ }
32
+ if (predicate.content.manifestFingerprint !== manifestFingerprint) {
33
+ throw verifierError("E_ATTESTATION_SUBJECT_MISMATCH", "Statement does not bind the current code manifest");
34
+ }
35
+ if (predicate.content.contentDigest !== manifest.contentDigest) {
36
+ throw verifierError("E_ATTESTATION_SUBJECT_MISMATCH", "Statement subject does not bind the manifest content digest");
37
+ }
38
+ const expectedPaths = [...new Set(manifest.entries.flatMap((entry) => [entry.path, entry.sourcePath].filter(Boolean)))].sort();
39
+ const actualPaths = [...new Set(predicate.content.coveredPaths)].sort();
40
+ if (JSON.stringify(expectedPaths) !== JSON.stringify(actualPaths)) {
41
+ throw verifierError("E_ATTESTATION_SCOPE_MISMATCH", "Statement covered paths do not match the manifest");
42
+ }
43
+ const bindingPairs = [
44
+ ["contractFingerprint", "E_ATTESTATION_CONTRACT_MISMATCH"],
45
+ ["routeFingerprint", "E_ATTESTATION_ROUTE_MISMATCH"],
46
+ ["stateFingerprint", "E_ATTESTATION_STATE_MISMATCH"],
47
+ ["receiptFingerprint", "E_ATTESTATION_RECEIPT_MISMATCH"],
48
+ ["ledgerSeq", "E_ATTESTATION_LEDGER_MISMATCH"],
49
+ ["ledgerHash", "E_ATTESTATION_LEDGER_MISMATCH"],
50
+ ];
51
+ for (const [key, code] of bindingPairs) {
52
+ if (predicate.evidence[key] !== manifest.bindings[key]) {
53
+ throw verifierError(code, `Statement evidence does not match the manifest binding: ${key}`);
54
+ }
55
+ }
56
+ }
57
+
58
+ async function verifyEntryContent(provider, { target, revision }, entry) {
59
+ if (entry.kind === "DELETED" || entry.operation === "DELETED") {
60
+ try {
61
+ await provider.readContent({ target, revision, path: entry.path });
62
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Deleted path exists at revision ${revision}: ${entry.path}`);
63
+ } catch (error) {
64
+ if (error.code === "E_ATTESTATION_CONTENT_MISMATCH") throw error;
65
+ if (error.notFound === true) return { valid: true };
66
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Unable to prove that deleted path is absent at revision ${revision}: ${entry.path}`, [entry.path]);
67
+ }
68
+ }
69
+ if (entry.kind === "GITLINK") {
70
+ let identity;
71
+ try {
72
+ identity = await provider.getContentIdentity({ target, revision, path: entry.path });
73
+ } catch (error) {
74
+ if (error.notFound === true) throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Gitlink is absent at revision ${revision}: ${entry.path}`, [entry.path]);
75
+ throw error;
76
+ }
77
+ if (identity !== entry.providerContentId) {
78
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Gitlink identity differs from the attested snapshot: ${entry.path}`, [entry.path]);
79
+ }
80
+ return { valid: true, actualIdentity: identity };
81
+ }
82
+ let bytes;
83
+ try {
84
+ bytes = await provider.readContent({ target, revision, path: entry.path });
85
+ } catch (error) {
86
+ if (error.code?.startsWith("E_REVISION_") && error.notFound !== true) throw error;
87
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Attested path cannot be read at revision ${revision}: ${entry.path}`, [entry.path]);
88
+ }
89
+ const actualDigest = sha256(bytes);
90
+ if (actualDigest !== entry.sha256) {
91
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Current source content differs from the attested snapshot: ${entry.path}`, [entry.path]);
92
+ }
93
+ if (entry.providerContentId) {
94
+ let actualIdentity;
95
+ try {
96
+ actualIdentity = await provider.getContentIdentity({ target, revision, path: entry.path });
97
+ } catch (error) {
98
+ if (error.code?.startsWith("E_REVISION_") && error.notFound !== true) throw error;
99
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Attested provider identity cannot be read at revision ${revision}: ${entry.path}`, [entry.path]);
100
+ }
101
+ if (actualIdentity !== entry.providerContentId) {
102
+ throw verifierError("E_ATTESTATION_CONTENT_MISMATCH", `Provider content identity differs from the attested snapshot: ${entry.path}`, [entry.path]);
103
+ }
104
+ }
105
+ return { valid: true, actualDigest };
106
+ }
107
+
108
+ export async function verifyCodeManifestContent({ target, manifest, revisionProvider, revision = null } = {}) {
109
+ const provider = typeof revisionProvider === "string"
110
+ ? await resolveRevisionProvider({ target, providerName: revisionProvider, registry: REVISION_PROVIDERS })
111
+ : revisionProvider;
112
+ assertRevisionProvider(provider);
113
+ const targetRevision = revision ?? (manifest.capture.mode === "WORKTREE" ? "WORKTREE" : manifest.capture.observedRevision);
114
+ const results = [];
115
+ for (const entry of manifest.entries) {
116
+ results.push({ path: entry.path, ...(await verifyEntryContent(provider, { target, revision: targetRevision }, entry)) });
117
+ }
118
+ return { valid: true, revision: targetRevision, results, provider };
119
+ }
120
+
121
+ export async function verifyAttestation({
122
+ target,
123
+ packageRoot = getPackageRoot(),
124
+ taskId,
125
+ revision = null,
126
+ bundlePath = null,
127
+ identity = null,
128
+ issuer = null,
129
+ revisionProvider = null,
130
+ signingProvider = null,
131
+ trustedRoot = null,
132
+ requireSignature = false,
133
+ } = {}) {
134
+ let effectiveRequireSignature = requireSignature === true;
135
+ let effectiveSigningProvider = signingProvider;
136
+ let effectiveBundlePath = bundlePath;
137
+ let configuredPolicy = {};
138
+ let configurationError = null;
139
+ try {
140
+ const { readConfig } = await import("./config.js");
141
+ const config = await readConfig(target, packageRoot);
142
+ const signing = config.attestation?.signing ?? {};
143
+ effectiveRequireSignature ||= signing.required === true;
144
+ effectiveSigningProvider ??= signing.provider;
145
+ configuredPolicy = signing.policy ?? {};
146
+ } catch (error) {
147
+ if (error.code !== "ARTIFACT_MISSING") configurationError = error;
148
+ }
149
+ if (effectiveRequireSignature && !effectiveBundlePath) effectiveBundlePath = taskAttestationBundlePath(taskId);
150
+ const result = {
151
+ schemaVersion: 1,
152
+ status: "INVALID",
153
+ level: "PROCESSED",
154
+ taskId,
155
+ revisionProvider: typeof revisionProvider === "string" ? revisionProvider : "git",
156
+ revision: revision ?? null,
157
+ content: { status: "NOT_CHECKED", checkedPaths: 0 },
158
+ receipt: "NOT_CHECKED",
159
+ statement: "NOT_CHECKED",
160
+ ledger: "NOT_CHECKED",
161
+ signature: { required: effectiveRequireSignature, status: effectiveRequireSignature ? "UNAVAILABLE" : "NOT_CHECKED" },
162
+ errors: [],
163
+ };
164
+ if (configurationError) {
165
+ result.errors.push({ code: E_ATTESTATION_CONFIGURATION_INVALID, message: configurationError.message });
166
+ return result;
167
+ }
168
+ try {
169
+ const statementArtifact = await readAttestationStatement({ target, packageRoot, taskId });
170
+ const manifestArtifact = await readCodeManifest({ target, packageRoot, taskId });
171
+ await validateAttestationStatement(statementArtifact.value, packageRoot);
172
+ result.statement = "VALID";
173
+ result.ledger = "VALID";
174
+ result.receipt = "VALID";
175
+ assertAttestationStatementBindings(
176
+ statementArtifact.value,
177
+ manifestArtifact.value,
178
+ taskId,
179
+ manifestArtifact.fingerprint,
180
+ );
181
+ const provider = typeof revisionProvider === "string"
182
+ ? await resolveRevisionProvider({ target, providerName: revisionProvider, registry: REVISION_PROVIDERS })
183
+ : revisionProvider ?? await resolveRevisionProvider({ target, providerName: manifestArtifact.value.capture.revisionProvider, registry: REVISION_PROVIDERS });
184
+ result.revisionProvider = provider.name ?? result.revisionProvider;
185
+ await validateCodeManifestBindings({ target, packageRoot, taskId, manifest: manifestArtifact.value, revisionProvider: provider });
186
+ const content = await verifyCodeManifestContent({ target, manifest: manifestArtifact.value, revisionProvider: provider, revision });
187
+ result.revision = content.revision;
188
+ result.content = { status: "VALID", checkedPaths: content.results.length };
189
+ const signingPolicy = {
190
+ ...configuredPolicy,
191
+ identity,
192
+ issuer,
193
+ ...(trustedRoot ? { trustedRoot } : {}),
194
+ };
195
+ if (effectiveRequireSignature && !(await fileExists(ensureWithin(target, effectiveBundlePath)))) {
196
+ throw verifierError("E_ATTESTATION_UNSIGNED", `Required attestation signature bundle is missing: ${effectiveBundlePath}`);
197
+ }
198
+ const signer = typeof effectiveSigningProvider === "object"
199
+ ? assertSigningProvider(effectiveSigningProvider)
200
+ : await resolveSigningProvider({
201
+ providerName: effectiveSigningProvider ?? (effectiveBundlePath ? "sigstore" : "none"),
202
+ registry: SIGNING_PROVIDERS,
203
+ });
204
+ const signature = await signer.verify({
205
+ target,
206
+ statementPath: taskAttestationStatementPath(taskId),
207
+ bundlePath: effectiveBundlePath,
208
+ policy: signingPolicy,
209
+ });
210
+ result.signature = {
211
+ required: effectiveRequireSignature,
212
+ status: signature.status,
213
+ provider: signer.name,
214
+ ...(identity ? { identity } : {}),
215
+ ...(issuer ? { issuer } : {}),
216
+ };
217
+ if (effectiveRequireSignature && signature.status !== "VALID") {
218
+ throw verifierError(signature.code ?? "E_ATTESTATION_SIGNATURE_INVALID", signature.message ?? "Required signature is not valid");
219
+ }
220
+ result.status = "VALID";
221
+ result.level = signature.status === "VALID" ? "ATTESTED" : "VERIFIED";
222
+ return result;
223
+ } catch (error) {
224
+ result.errors.push({ code: error.code ?? "E_ATTESTATION_STATEMENT_INVALID", message: error.message, ...(error.artifacts ? { artifacts: error.artifacts } : {}) });
225
+ return result;
226
+ }
227
+ }