@kontourai/flow-agents 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.github/CODEOWNERS +29 -0
  2. package/.github/actions/trust-verify/action.yml +145 -0
  3. package/.github/workflows/ci.yml +11 -4
  4. package/.github/workflows/kit-gates-demo.yml +2 -2
  5. package/.github/workflows/publish-npm.yml +10 -2
  6. package/.github/workflows/release-please.yml +1 -1
  7. package/.github/workflows/trust-reconcile.yml +113 -0
  8. package/AGENTS.md +13 -0
  9. package/CHANGELOG.md +95 -0
  10. package/CONTRIBUTING.md +4 -4
  11. package/README.md +1 -0
  12. package/agents/tool-planner.json +1 -1
  13. package/build/src/cli/init.js +242 -20
  14. package/build/src/cli/validate-workflow-artifacts.js +19 -2
  15. package/build/src/cli/verify.d.ts +1 -0
  16. package/build/src/cli/verify.js +90 -0
  17. package/build/src/cli/workflow-sidecar.d.ts +300 -8
  18. package/build/src/cli/workflow-sidecar.js +1934 -83
  19. package/build/src/cli.js +2 -3
  20. package/build/src/lib/flow-resolver.d.ts +82 -0
  21. package/build/src/lib/flow-resolver.js +237 -0
  22. package/build/src/tools/build-universal-bundles.js +34 -22
  23. package/build/src/tools/generate-context-map.js +3 -16
  24. package/build/src/tools/validate-source-tree.d.ts +1 -1
  25. package/build/src/tools/validate-source-tree.js +42 -162
  26. package/context/contracts/artifact-contract.md +10 -0
  27. package/context/contracts/delivery-contract.md +1 -0
  28. package/context/contracts/review-contract.md +1 -0
  29. package/context/contracts/verification-contract.md +2 -0
  30. package/context/gate-awareness.md +39 -0
  31. package/context/scripts/hooks/stop-goal-fit.js +632 -70
  32. package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
  33. package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
  34. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  35. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  36. package/docs/adr/0007-skill-audit.md +1 -1
  37. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
  38. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
  39. package/docs/adr/0011-mcp-posture.md +100 -0
  40. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
  41. package/docs/adr/0013-context-lifecycle.md +151 -0
  42. package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
  43. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
  44. package/docs/adr/0016-three-hard-boundary-model.md +71 -0
  45. package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
  46. package/docs/agent-system-guidebook.md +5 -12
  47. package/docs/context-map.md +4 -10
  48. package/docs/index.md +3 -2
  49. package/docs/integrations/framework-adapter.md +19 -6
  50. package/docs/integrations/index.md +2 -2
  51. package/docs/north-star.md +4 -4
  52. package/docs/operating-layers.md +3 -3
  53. package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
  54. package/docs/repository-structure.md +2 -2
  55. package/docs/skills-map.md +1 -0
  56. package/docs/spec/runtime-hook-surface.md +62 -9
  57. package/docs/standards-register.md +3 -3
  58. package/docs/survey-utterance-check.md +1 -1
  59. package/docs/trust-anchor-adoption.md +197 -0
  60. package/docs/verifiable-trust.md +95 -0
  61. package/docs/veritas-integration.md +2 -2
  62. package/docs/workflow-usage-guide.md +69 -0
  63. package/evals/acceptance/DEMO-false-completion.md +144 -0
  64. package/evals/acceptance/demo-cast.sh +92 -0
  65. package/evals/acceptance/demo-false-completion.sh +72 -0
  66. package/evals/acceptance/demo-real-evidence.sh +104 -0
  67. package/evals/acceptance/demo.tape +29 -0
  68. package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
  69. package/evals/acceptance/prove-capture-teeth.sh +114 -0
  70. package/evals/acceptance/prove-teeth.sh +105 -0
  71. package/evals/ci/antigaming-suite.sh +54 -0
  72. package/evals/ci/run-baseline.sh +2 -0
  73. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
  74. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
  75. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
  76. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
  77. package/evals/integration/test_builder_step_producers.sh +379 -0
  78. package/evals/integration/test_bundle_install.sh +35 -71
  79. package/evals/integration/test_bundle_lifecycle.sh +39 -2
  80. package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
  81. package/evals/integration/test_checkpoint_signing.sh +489 -0
  82. package/evals/integration/test_claim_lookup.sh +352 -0
  83. package/evals/integration/test_command_log_integrity.sh +275 -0
  84. package/evals/integration/test_context_map.sh +0 -2
  85. package/evals/integration/test_dual_emit_flow_step.sh +278 -0
  86. package/evals/integration/test_enforcer_expects_driven.sh +281 -0
  87. package/evals/integration/test_evidence_capture_hook.sh +185 -0
  88. package/evals/integration/test_flow_kit_repository.sh +2 -0
  89. package/evals/integration/test_flowdef_session_activation.sh +273 -0
  90. package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
  91. package/evals/integration/test_gate_bypass_chain.sh +448 -0
  92. package/evals/integration/test_gate_lockdown.sh +1137 -0
  93. package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
  94. package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
  95. package/evals/integration/test_goal_fit_hook.sh +69 -4
  96. package/evals/integration/test_goal_fit_rederive.sh +263 -0
  97. package/evals/integration/test_install_merge.sh +1176 -0
  98. package/evals/integration/test_mint_attestation.sh +373 -0
  99. package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
  100. package/evals/integration/test_publish_delivery.sh +269 -0
  101. package/evals/integration/test_reconcile_soundness.sh +528 -0
  102. package/evals/integration/test_resolvefirststep_security.sh +208 -0
  103. package/evals/integration/test_session_resume_roundtrip.sh +286 -0
  104. package/evals/integration/test_trust_checkpoint.sh +325 -0
  105. package/evals/integration/test_trust_reconcile.sh +293 -0
  106. package/evals/integration/test_verify_cli.sh +208 -0
  107. package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
  108. package/evals/lib/node.sh +0 -6
  109. package/evals/run.sh +45 -0
  110. package/evals/static/test_workflow_skills.sh +6 -13
  111. package/install.sh +0 -7
  112. package/integrations/strands-ts/README.md +25 -15
  113. package/integrations/veritas/flow-agents.adapter.json +1 -2
  114. package/kits/builder/flows/build.flow.json +59 -12
  115. package/kits/builder/kit.json +85 -15
  116. package/kits/builder/skills/continue-work/SKILL.md +116 -0
  117. package/kits/builder/skills/deliver/SKILL.md +36 -6
  118. package/kits/builder/skills/design-probe/SKILL.md +28 -0
  119. package/kits/builder/skills/execute-plan/SKILL.md +9 -1
  120. package/kits/builder/skills/gate-review/SKILL.md +234 -0
  121. package/kits/builder/skills/learning-review/SKILL.md +30 -0
  122. package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
  123. package/kits/builder/skills/plan-work/SKILL.md +13 -1
  124. package/kits/builder/skills/pull-work/SKILL.md +19 -0
  125. package/kits/knowledge/adapters/default-store/index.js +38 -0
  126. package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
  127. package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
  128. package/kits/knowledge/docs/store-contract.md +314 -0
  129. package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
  130. package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
  131. package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
  132. package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
  133. package/kits/knowledge/evals/entities/suite.test.js +40 -0
  134. package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
  135. package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
  136. package/kits/knowledge/evals/retirement/suite.test.js +145 -0
  137. package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
  138. package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
  139. package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
  140. package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
  141. package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
  142. package/kits/knowledge/kit.json +51 -1
  143. package/package.json +4 -4
  144. package/packaging/conformance/README.md +10 -2
  145. package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
  146. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
  147. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
  148. package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
  149. package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
  150. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
  151. package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
  152. package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
  153. package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
  154. package/packaging/conformance/run-conformance.js +1 -1
  155. package/scripts/README.md +2 -1
  156. package/scripts/build-universal-bundles.js +0 -1
  157. package/scripts/ci/mint-attestation.js +221 -0
  158. package/scripts/ci/trust-reconcile.js +545 -0
  159. package/scripts/hooks/config-protection.js +423 -1
  160. package/scripts/hooks/evidence-capture.js +348 -0
  161. package/scripts/hooks/lib/liveness-read.js +113 -0
  162. package/scripts/hooks/run-hook.js +6 -1
  163. package/scripts/hooks/stop-goal-fit.js +1471 -79
  164. package/scripts/hooks/workflow-steering.js +135 -5
  165. package/scripts/install-codex-home.sh +39 -0
  166. package/scripts/install-merge.js +330 -0
  167. package/src/cli/init.ts +218 -20
  168. package/src/cli/validate-workflow-artifacts.ts +18 -2
  169. package/src/cli/verify.ts +100 -0
  170. package/src/cli/workflow-sidecar.ts +2064 -77
  171. package/src/cli.ts +2 -3
  172. package/src/lib/flow-resolver.ts +284 -0
  173. package/src/tools/build-universal-bundles.ts +34 -21
  174. package/src/tools/generate-context-map.ts +3 -17
  175. package/src/tools/validate-source-tree.ts +44 -104
  176. package/build/src/tools/filter-installed-packs.d.ts +0 -2
  177. package/build/src/tools/filter-installed-packs.js +0 -135
  178. package/packaging/packs.json +0 -49
  179. package/scripts/filter-installed-packs.js +0 -2
  180. package/src/tools/filter-installed-packs.ts +0 -132
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env bash
2
+ # test_checkpoint_signing.sh — Integration eval for Increment B1: terminal trust checkpoint signing.
3
+ #
4
+ # Proves that:
5
+ # 1. STATEMENT-WITH-SUBJECT: after record-release, an in-toto statement file exists with
6
+ # the correct predicateType "https://hachure.org/v1/bundle" and subject digest matching
7
+ # sha256(trust.checkpoint.json). The checkpoint envelope carries attestation.status.
8
+ # 2. FAIL-OPEN-LOCAL: signStatementWithSigstore returns null locally (no OIDC);
9
+ # the unsigned statement is written and the seal still succeeds (exit 0).
10
+ # attestation.status == "unsigned".
11
+ # 3. DSSE-ROUND-TRIP: toDsseEnvelope(statement, mockSigner) produces an envelope whose:
12
+ # - payloadType == "application/vnd.in-toto+json"
13
+ # - base64 payload round-trips via parseDssePayload back to the statement
14
+ # - PAE bytes match buildPaeBytes(payloadType, statementJson)
15
+ # This proves the signing PATH is structurally correct without needing real OIDC.
16
+ # 4. ADDITIVE: all existing gating tests still pass (record-release, advance-state-delivered,
17
+ # seal-checkpoint, record-evidence, record-critique all unaffected).
18
+ #
19
+ # Deterministic, no model spend, no network, self-cleaning.
20
+ # Usage: bash evals/integration/test_checkpoint_signing.sh
21
+
22
+ set -uo pipefail
23
+
24
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
25
+ source "$ROOT/evals/lib/node.sh"
26
+
27
+ WRITER="workflow-sidecar"
28
+ TMP="$(mktemp -d)"
29
+ errors=0
30
+
31
+ _pass() { echo " ✓ $1"; }
32
+ _fail() { echo " ✗ $1"; errors=$((errors + 1)); }
33
+
34
+ cleanup() { rm -rf "$TMP"; }
35
+ trap cleanup EXIT
36
+
37
+ # ─────────────────────────────────────────────────────────────────────────────
38
+ echo ""
39
+ echo "=== TEST 1: Statement produced with correct subject (sha256 of checkpoint) ==="
40
+
41
+ AROOT1="$TMP/test1/.flow-agents"
42
+ SLUG1="sign-test-statement"
43
+ SESSION_DIR1="$AROOT1/$SLUG1"
44
+ mkdir -p "$AROOT1"
45
+
46
+ flow_agents_node "$WRITER" ensure-session \
47
+ --artifact-root "$AROOT1" \
48
+ --task-slug "$SLUG1" \
49
+ --title "Checkpoint Signing Statement Test" \
50
+ --summary "Verify in-toto statement subject matches sha256 of trust.checkpoint.json." \
51
+ --criterion "Statement subject digest matches checkpoint sha256" \
52
+ --timestamp "2026-06-26T10:00:00Z" >/dev/null 2>&1
53
+
54
+ flow_agents_node "$WRITER" init-plan "$SESSION_DIR1/${SLUG1}--deliver.md" \
55
+ --source-request "Test" --summary "Test" \
56
+ --timestamp "2026-06-26T10:01:00Z" >/dev/null 2>&1
57
+
58
+ flow_agents_node "$WRITER" record-evidence "$SESSION_DIR1" \
59
+ --verdict pass \
60
+ --check-json '{"id":"build","kind":"build","status":"pass","summary":"build passed"}' \
61
+ --check-json '{"id":"types","kind":"types","status":"pass","summary":"types ok"}' \
62
+ --timestamp "2026-06-26T10:02:00Z" >/dev/null 2>&1
63
+
64
+ flow_agents_node "$WRITER" record-critique "$SESSION_DIR1" \
65
+ --verdict pass \
66
+ --summary "Review passed." \
67
+ --timestamp "2026-06-26T10:03:00Z" >/dev/null 2>&1
68
+
69
+ flow_agents_node "$WRITER" record-release "$SESSION_DIR1" \
70
+ --decision merge \
71
+ --gate-json '{"name":"merge","status":"pass","summary":"Ready to merge."}' \
72
+ --summary "Release recorded." \
73
+ --timestamp "2026-06-26T10:04:00Z" >/dev/null 2>&1
74
+
75
+ # Checkpoint must exist (Increment A prerequisite)
76
+ if [[ -f "$SESSION_DIR1/trust.checkpoint.json" ]]; then
77
+ _pass "record-release writes trust.checkpoint.json (Increment A prerequisite)"
78
+ else
79
+ _fail "trust.checkpoint.json absent — Increment A not working"
80
+ fi
81
+
82
+ # Increment B1: unsigned in-toto statement must exist locally (no OIDC in test env)
83
+ INTOTO_FILE="$SESSION_DIR1/trust.checkpoint.intoto.json"
84
+ SIG_FILE="$SESSION_DIR1/trust.checkpoint.sig.json"
85
+
86
+ if [[ -f "$INTOTO_FILE" ]]; then
87
+ _pass "trust.checkpoint.intoto.json written (unsigned statement for local env)"
88
+ elif [[ -f "$SIG_FILE" ]]; then
89
+ _pass "trust.checkpoint.sig.json written (OIDC signing succeeded — CI environment)"
90
+ else
91
+ _fail "no attestation file found (neither trust.checkpoint.intoto.json nor trust.checkpoint.sig.json)"
92
+ fi
93
+
94
+ # Verify the in-toto statement has correct predicateType and subject digest.
95
+ # ROUND-TRIP ASSERTION: after the fix, trust.checkpoint.json is the exact artifact
96
+ # that was signed — no post-digest mutation. sha256(on-disk checkpoint) must equal
97
+ # the subject digest in the in-toto statement.
98
+ node - "$SESSION_DIR1" << 'NODE'
99
+ const fs = require("fs");
100
+ const path = require("path");
101
+ const crypto = require("crypto");
102
+
103
+ const dir = process.argv[2];
104
+ const checkpointPath = path.join(dir, "trust.checkpoint.json");
105
+ const intotoPath = path.join(dir, "trust.checkpoint.intoto.json");
106
+ const sigPath = path.join(dir, "trust.checkpoint.sig.json");
107
+
108
+ const errors = [];
109
+
110
+ // Determine which attestation statement file exists
111
+ let statement;
112
+ if (fs.existsSync(intotoPath)) {
113
+ statement = JSON.parse(fs.readFileSync(intotoPath, "utf8"));
114
+ } else if (fs.existsSync(sigPath)) {
115
+ // Parse from DSSE envelope payload
116
+ const envelope = JSON.parse(fs.readFileSync(sigPath, "utf8"));
117
+ const payloadJson = Buffer.from(envelope.payload, "base64").toString("utf8");
118
+ statement = JSON.parse(payloadJson);
119
+ } else {
120
+ errors.push("no attestation statement file found (neither intoto.json nor sig.json)");
121
+ console.error("ERRORS:\n" + errors.join("\n"));
122
+ process.exit(1);
123
+ }
124
+
125
+ // Verify predicateType
126
+ if (statement.predicateType !== "https://hachure.org/v1/bundle") {
127
+ errors.push("predicateType expected 'https://hachure.org/v1/bundle', got " + statement.predicateType);
128
+ }
129
+
130
+ // Verify _type
131
+ if (statement._type !== "https://in-toto.io/Statement/v1") {
132
+ errors.push("_type expected 'https://in-toto.io/Statement/v1', got " + statement._type);
133
+ }
134
+
135
+ // Verify subject array
136
+ if (!Array.isArray(statement.subject) || statement.subject.length === 0) {
137
+ errors.push("subject must be a non-empty array");
138
+ } else {
139
+ const sub = statement.subject[0];
140
+ if (sub.name !== "trust.checkpoint.json") {
141
+ errors.push("subject[0].name expected 'trust.checkpoint.json', got " + sub.name);
142
+ }
143
+ // ROUND-TRIP ASSERTION: trust.checkpoint.json must be byte-identical to what was signed.
144
+ // No post-digest mutation is allowed, so the on-disk sha256 == signed subject digest.
145
+ const checkpointBytes = fs.readFileSync(checkpointPath);
146
+ const onDiskSha256 = crypto.createHash("sha256").update(checkpointBytes).digest("hex");
147
+ if (!sub.digest || sub.digest.sha256 !== onDiskSha256) {
148
+ errors.push("ROUND-TRIP FAIL: signed subject digest " + (sub.digest && sub.digest.sha256) +
149
+ " != sha256(on-disk trust.checkpoint.json) " + onDiskSha256 +
150
+ " — checkpoint was mutated after signing");
151
+ } else {
152
+ console.log("ROUND-TRIP PASS: sha256(on-disk trust.checkpoint.json) == signed subject digest = " + onDiskSha256.slice(0, 16) + "...");
153
+ }
154
+
155
+ // REGRESSION GUARD: trust.checkpoint.json must NOT contain an attestation field.
156
+ const checkpointEnv = JSON.parse(checkpointBytes);
157
+ if ("attestation" in checkpointEnv) {
158
+ errors.push("trust.checkpoint.json must NOT contain attestation field — it breaks the digest");
159
+ } else {
160
+ console.log("trust.checkpoint.json has no attestation field (correct — digest is stable)");
161
+ }
162
+ }
163
+
164
+ // Verify predicate is the trust bundle (has schemaVersion and claims)
165
+ if (!statement.predicate || typeof statement.predicate !== "object") {
166
+ errors.push("predicate missing or not an object");
167
+ } else {
168
+ if (statement.predicate.schemaVersion === undefined) {
169
+ errors.push("predicate.schemaVersion missing (expected trust bundle)");
170
+ }
171
+ if (!Array.isArray(statement.predicate.claims)) {
172
+ errors.push("predicate.claims must be an array (expected trust bundle)");
173
+ }
174
+ }
175
+
176
+ if (errors.length > 0) {
177
+ console.error("STATEMENT ERRORS:\n" + errors.join("\n"));
178
+ process.exit(1);
179
+ }
180
+ console.log("in-toto statement valid: predicateType=" + statement.predicateType + " subject=" + statement.subject[0].name);
181
+ NODE
182
+ if [[ $? -eq 0 ]]; then
183
+ _pass "in-toto statement: correct predicateType, subject name, ROUND-TRIP digest match, no attestation field in checkpoint"
184
+ else
185
+ _fail "in-toto statement or round-trip digest assertion failed"
186
+ fi
187
+
188
+ # Verify the companion attestation file exists with correct shape.
189
+ # trust.checkpoint.attestation.json carries the attestation pointer/status.
190
+ # trust.checkpoint.json must NOT contain an attestation field (digest stability).
191
+ node - "$SESSION_DIR1" << 'NODE'
192
+ const fs = require("fs");
193
+ const path = require("path");
194
+ const dir = process.argv[2];
195
+ const attestationPath = path.join(dir, "trust.checkpoint.attestation.json");
196
+ const checkpointPath = path.join(dir, "trust.checkpoint.json");
197
+ const errors = [];
198
+
199
+ // Companion file must exist
200
+ if (!fs.existsSync(attestationPath)) {
201
+ errors.push("trust.checkpoint.attestation.json missing — attestation companion file not written");
202
+ } else {
203
+ const att = JSON.parse(fs.readFileSync(attestationPath, "utf8"));
204
+ if (!["signed", "unsigned"].includes(att.status)) {
205
+ errors.push("attestation.status must be 'signed' or 'unsigned', got " + att.status);
206
+ }
207
+ if (typeof att.path !== "string" || !att.path) {
208
+ errors.push("attestation.path must be a non-empty string");
209
+ }
210
+ if (att.status === "unsigned" && att.reason !== "no ambient signing identity") {
211
+ errors.push("attestation.reason expected 'no ambient signing identity', got " + att.reason);
212
+ }
213
+ if (errors.length === 0) {
214
+ console.log("trust.checkpoint.attestation.json: status=" + att.status + " path=" + att.path);
215
+ }
216
+ }
217
+
218
+ // trust.checkpoint.json must NOT carry attestation (that would break the digest)
219
+ const env = JSON.parse(fs.readFileSync(checkpointPath, "utf8"));
220
+ if ("attestation" in env) {
221
+ errors.push("trust.checkpoint.json must NOT contain attestation field (breaks digest verification)");
222
+ }
223
+
224
+ if (errors.length > 0) {
225
+ console.error("ATTESTATION COMPANION ERRORS:\n" + errors.join("\n"));
226
+ process.exit(1);
227
+ }
228
+ console.log("attestation companion file correct; trust.checkpoint.json has no attestation field");
229
+ NODE
230
+ if [[ $? -eq 0 ]]; then
231
+ _pass "trust.checkpoint.attestation.json has correct shape; trust.checkpoint.json has no attestation field"
232
+ else
233
+ _fail "trust.checkpoint.attestation.json missing/malformed or trust.checkpoint.json has attestation field"
234
+ fi
235
+
236
+ # ─────────────────────────────────────────────────────────────────────────────
237
+ echo ""
238
+ echo "=== TEST 2: Fail-open local — unsigned path, seal still succeeds ==="
239
+
240
+ AROOT2="$TMP/test2/.flow-agents"
241
+ SLUG2="sign-test-failopen"
242
+ SESSION_DIR2="$AROOT2/$SLUG2"
243
+ mkdir -p "$AROOT2"
244
+
245
+ flow_agents_node "$WRITER" ensure-session \
246
+ --artifact-root "$AROOT2" \
247
+ --task-slug "$SLUG2" \
248
+ --title "Checkpoint Signing Fail-Open Test" \
249
+ --summary "Verify that signing fail-open produces unsigned statement and seal succeeds." \
250
+ --timestamp "2026-06-26T11:00:00Z" >/dev/null 2>&1
251
+
252
+ flow_agents_node "$WRITER" init-plan "$SESSION_DIR2/${SLUG2}--deliver.md" \
253
+ --source-request "Test" --summary "Test" \
254
+ --timestamp "2026-06-26T11:01:00Z" >/dev/null 2>&1
255
+
256
+ flow_agents_node "$WRITER" record-evidence "$SESSION_DIR2" \
257
+ --verdict pass \
258
+ --check-json '{"id":"build","kind":"build","status":"pass","summary":"build passed"}' \
259
+ --timestamp "2026-06-26T11:02:00Z" >/dev/null 2>&1
260
+
261
+ # advance-state to delivered: this is the other code path that seals the checkpoint
262
+ SEAL_EXIT=0
263
+ flow_agents_node "$WRITER" advance-state "$SESSION_DIR2" \
264
+ --status delivered \
265
+ --phase release \
266
+ --summary "Delivered." \
267
+ --timestamp "2026-06-26T11:03:00Z" >/dev/null 2>&1 || SEAL_EXIT=$?
268
+
269
+ if [[ "$SEAL_EXIT" -eq 0 ]]; then
270
+ _pass "advance-state --status delivered exits 0 (seal succeeds, signing is fail-open)"
271
+ else
272
+ _fail "advance-state --status delivered exited $SEAL_EXIT (signing must not break the seal)"
273
+ fi
274
+
275
+ if [[ -f "$SESSION_DIR2/trust.checkpoint.json" ]]; then
276
+ _pass "trust.checkpoint.json written even when signing is fail-open"
277
+ else
278
+ _fail "trust.checkpoint.json absent — seal did not complete"
279
+ fi
280
+
281
+ # In local (no OIDC), the unsigned path must be taken.
282
+ # Attestation is now in the companion file, not in trust.checkpoint.json.
283
+ node - "$SESSION_DIR2" << 'NODE'
284
+ const fs = require("fs");
285
+ const path = require("path");
286
+ const dir = process.argv[2];
287
+ const attestationPath = path.join(dir, "trust.checkpoint.attestation.json");
288
+ if (!fs.existsSync(attestationPath)) {
289
+ console.error("trust.checkpoint.attestation.json missing from fail-open seal");
290
+ process.exit(1);
291
+ }
292
+ const att = JSON.parse(fs.readFileSync(attestationPath, "utf8"));
293
+ // Local: either unsigned OR signed (if OIDC happens to be available in the test env)
294
+ if (!["signed", "unsigned"].includes(att.status)) {
295
+ console.error("attestation.status must be signed or unsigned, got: " + att.status);
296
+ process.exit(1);
297
+ }
298
+ console.log("fail-open seal: attestation companion: status=" + att.status);
299
+ NODE
300
+ if [[ $? -eq 0 ]]; then
301
+ _pass "trust.checkpoint.attestation.json has valid status after fail-open seal"
302
+ else
303
+ _fail "trust.checkpoint.attestation.json missing or invalid after fail-open seal"
304
+ fi
305
+
306
+ # Verify the SPECIFIC local behavior: unsigned path produces intoto.json when no OIDC
307
+ # (This is the primary fail-open proof; if OIDC IS available in CI the signed path is also OK)
308
+ UNSIGNED_PATH="$SESSION_DIR2/trust.checkpoint.intoto.json"
309
+ SIGNED_PATH="$SESSION_DIR2/trust.checkpoint.sig.json"
310
+
311
+ if [[ -f "$UNSIGNED_PATH" ]]; then
312
+ UNSIGNED_STATEMENT_STATUS="$(node -e "const s=JSON.parse(require('fs').readFileSync('$UNSIGNED_PATH','utf8')); console.log(s.predicateType);" 2>/dev/null || echo "error")"
313
+ if [[ "$UNSIGNED_STATEMENT_STATUS" == "https://hachure.org/v1/bundle" ]]; then
314
+ _pass "unsigned in-toto statement has correct predicateType (fail-open local path confirmed)"
315
+ else
316
+ _fail "unsigned in-toto statement has wrong predicateType: $UNSIGNED_STATEMENT_STATUS"
317
+ fi
318
+ elif [[ -f "$SIGNED_PATH" ]]; then
319
+ _pass "signed envelope exists (OIDC available in this env — fail-open also proved by non-error exit)"
320
+ else
321
+ _fail "neither trust.checkpoint.intoto.json nor trust.checkpoint.sig.json present"
322
+ fi
323
+
324
+ # ─────────────────────────────────────────────────────────────────────────────
325
+ echo ""
326
+ echo "=== TEST 3: DSSE structure — mock signer round-trip via toDsseEnvelope/parseDssePayload ==="
327
+
328
+ node - "$SESSION_DIR1" << 'NODE'
329
+ // This test directly exercises the Surface DSSE primitives with a deterministic mock signer:
330
+ // toDsseEnvelope(statement, mockSigner) → envelope
331
+ // parseDssePayload(envelope) → statement (round-trip)
332
+ // buildPaeBytes(payloadType, statementJson) == paeReceived in mock signer
333
+ //
334
+ // Proves the signing PATH is correct without needing real OIDC.
335
+
336
+ const fs = require("fs");
337
+ const path = require("path");
338
+
339
+ // Load the surface module's interop exports directly (same path the production code uses)
340
+ async function run() {
341
+ const { toDsseEnvelope, parseDssePayload, buildPaeBytes, toInTotoStatement } = await import(
342
+ "@kontourai/surface"
343
+ );
344
+
345
+ const errors = [];
346
+
347
+ // Load the in-toto statement that was produced by the actual seal
348
+ const dir = process.argv[2];
349
+ const intotoPath = path.join(dir, "trust.checkpoint.intoto.json");
350
+ const sigPath = path.join(dir, "trust.checkpoint.sig.json");
351
+
352
+ let statement;
353
+ if (fs.existsSync(intotoPath)) {
354
+ statement = JSON.parse(fs.readFileSync(intotoPath, "utf8"));
355
+ } else if (fs.existsSync(sigPath)) {
356
+ // In CI/OIDC env, use signed envelope's payload
357
+ const envelope = JSON.parse(fs.readFileSync(sigPath, "utf8"));
358
+ statement = JSON.parse(Buffer.from(envelope.payload, "base64").toString("utf8"));
359
+ } else {
360
+ // Construct a minimal statement for the round-trip test
361
+ const bundle = JSON.parse(fs.readFileSync(path.join(dir, "trust.bundle"), "utf8"));
362
+ statement = toInTotoStatement(bundle, {
363
+ subjects: [{ name: "trust.checkpoint.json", digest: { sha256: "a".repeat(64) } }]
364
+ });
365
+ }
366
+
367
+ // Mock signer: deterministic, captures the PAE bytes it receives
368
+ let capturedPaeBytes = null;
369
+ const mockSigner = {
370
+ keyid: "test-mock-key-b1",
371
+ sign: async (paeBytes) => {
372
+ capturedPaeBytes = paeBytes;
373
+ // Return a deterministic base64-encoded "signature"
374
+ return Buffer.from("mock-signature-for-b1-test").toString("base64");
375
+ },
376
+ };
377
+
378
+ // Build the DSSE envelope
379
+ const envelope = await toDsseEnvelope(statement, mockSigner);
380
+
381
+ // Assert 1: payloadType is correct
382
+ if (envelope.payloadType !== "application/vnd.in-toto+json") {
383
+ errors.push("envelope.payloadType expected 'application/vnd.in-toto+json', got " + envelope.payloadType);
384
+ }
385
+
386
+ // Assert 2: payload round-trips back to the statement via parseDssePayload
387
+ let roundTripped;
388
+ try {
389
+ roundTripped = parseDssePayload(envelope);
390
+ } catch (e) {
391
+ errors.push("parseDssePayload threw: " + e.message);
392
+ roundTripped = null;
393
+ }
394
+ if (roundTripped) {
395
+ if (roundTripped._type !== statement._type) {
396
+ errors.push("round-trip _type mismatch: " + roundTripped._type + " vs " + statement._type);
397
+ }
398
+ if (roundTripped.predicateType !== statement.predicateType) {
399
+ errors.push("round-trip predicateType mismatch: " + roundTripped.predicateType);
400
+ }
401
+ if (!Array.isArray(roundTripped.subject) || roundTripped.subject.length !== statement.subject.length) {
402
+ errors.push("round-trip subject length mismatch");
403
+ }
404
+ console.log("parseDssePayload round-trip: predicateType=" + roundTripped.predicateType + " subjects=" + roundTripped.subject.length);
405
+ }
406
+
407
+ // Assert 3: PAE bytes match buildPaeBytes(payloadType, statementJson)
408
+ const statementJson = JSON.stringify(statement);
409
+ const expectedPae = buildPaeBytes("application/vnd.in-toto+json", statementJson);
410
+ if (capturedPaeBytes === null) {
411
+ errors.push("mock signer was not called (sign() never invoked)");
412
+ } else {
413
+ // Compare Uint8Arrays
414
+ const match = capturedPaeBytes.length === expectedPae.length &&
415
+ capturedPaeBytes.every((b, i) => b === expectedPae[i]);
416
+ if (!match) {
417
+ errors.push("PAE bytes mismatch: signer received different bytes than buildPaeBytes produced");
418
+ } else {
419
+ const paeStr = Buffer.from(capturedPaeBytes).toString("utf8").slice(0, 40);
420
+ console.log("PAE bytes match buildPaeBytes: " + paeStr + "...");
421
+ }
422
+ }
423
+
424
+ // Assert 4: signatures carry the mock keyid and sig
425
+ if (!Array.isArray(envelope.signatures) || envelope.signatures.length === 0) {
426
+ errors.push("envelope.signatures is empty");
427
+ } else {
428
+ if (envelope.signatures[0].keyid !== "test-mock-key-b1") {
429
+ errors.push("signatures[0].keyid expected 'test-mock-key-b1', got " + envelope.signatures[0].keyid);
430
+ }
431
+ if (typeof envelope.signatures[0].sig !== "string" || !envelope.signatures[0].sig) {
432
+ errors.push("signatures[0].sig must be a non-empty string");
433
+ }
434
+ console.log("mock signature: keyid=" + envelope.signatures[0].keyid + " sig=" + envelope.signatures[0].sig);
435
+ }
436
+
437
+ if (errors.length > 0) {
438
+ console.error("DSSE ROUND-TRIP ERRORS:\n" + errors.join("\n"));
439
+ process.exit(1);
440
+ }
441
+ console.log("DSSE round-trip: all assertions passed");
442
+ }
443
+
444
+ run().catch((e) => { console.error("DSSE test threw: " + e.message); process.exit(1); });
445
+ NODE
446
+ if [[ $? -eq 0 ]]; then
447
+ _pass "toDsseEnvelope/parseDssePayload/buildPaeBytes round-trip correct with mock signer"
448
+ else
449
+ _fail "DSSE round-trip or PAE structure assertion failed"
450
+ fi
451
+
452
+ # ─────────────────────────────────────────────────────────────────────────────
453
+ echo ""
454
+ echo "=== TEST 4: Additive — existing checkpoint behavior unaffected ==="
455
+
456
+ # Re-check that the TEST 1 session has a valid checkpoint envelope (Increment A shape unchanged)
457
+ node - "$SESSION_DIR1/trust.checkpoint.json" << 'NODE'
458
+ const fs = require("fs");
459
+ const env = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
460
+ const errors = [];
461
+ if (env.schema_version !== "1.0") errors.push("schema_version expected '1.0'");
462
+ if (typeof env.slug !== "string" || !env.slug) errors.push("slug missing");
463
+ if (env.status !== "delivered") errors.push("status expected 'delivered'");
464
+ if (env.phase !== "release") errors.push("phase expected 'release'");
465
+ if (!env.checkpoint || typeof env.checkpoint !== "object") errors.push("checkpoint missing");
466
+ if (!env.checkpoint.statusByClaimId) errors.push("checkpoint.statusByClaimId missing");
467
+ if (errors.length > 0) {
468
+ console.error("ADDITIVE SHAPE ERRORS:\n" + errors.join("\n"));
469
+ process.exit(1);
470
+ }
471
+ const claimCount = Object.keys(env.checkpoint.statusByClaimId || {}).length;
472
+ console.log("Increment A shape preserved: status=" + env.status + " claims=" + claimCount + " (attestation in companion file)");
473
+ NODE
474
+ if [[ $? -eq 0 ]]; then
475
+ _pass "Increment A checkpoint envelope shape preserved (additive — no regression)"
476
+ else
477
+ _fail "Increment A checkpoint envelope shape broken (regression)"
478
+ fi
479
+
480
+ # ─────────────────────────────────────────────────────────────────────────────
481
+ echo ""
482
+ echo "────────────────────────────────────────────"
483
+ if [[ $errors -eq 0 ]]; then
484
+ echo "test_checkpoint_signing: all checks passed."
485
+ exit 0
486
+ else
487
+ echo "test_checkpoint_signing: $errors check(s) failed."
488
+ exit 1
489
+ fi