@kontourai/flow-agents 1.4.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +29 -0
- package/.github/actions/trust-verify/action.yml +145 -0
- package/.github/workflows/ci.yml +11 -4
- package/.github/workflows/kit-gates-demo.yml +2 -2
- package/.github/workflows/publish-npm.yml +10 -2
- package/.github/workflows/release-please.yml +1 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/.github/workflows/trust-reconcile.yml +113 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +103 -0
- package/CONTRIBUTING.md +4 -4
- package/README.md +1 -0
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/init.js +242 -20
- package/build/src/cli/validate-workflow-artifacts.js +19 -2
- package/build/src/cli/verify.d.ts +1 -0
- package/build/src/cli/verify.js +90 -0
- package/build/src/cli/workflow-sidecar.d.ts +316 -8
- package/build/src/cli/workflow-sidecar.js +1996 -91
- package/build/src/cli.js +2 -3
- package/build/src/lib/flow-resolver.d.ts +111 -0
- package/build/src/lib/flow-resolver.js +308 -0
- package/build/src/tools/build-universal-bundles.js +34 -22
- package/build/src/tools/generate-context-map.js +3 -16
- package/build/src/tools/validate-source-tree.d.ts +1 -1
- package/build/src/tools/validate-source-tree.js +42 -162
- package/context/contracts/artifact-contract.md +10 -0
- package/context/contracts/delivery-contract.md +1 -0
- package/context/contracts/review-contract.md +1 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/gate-awareness.md +39 -0
- package/context/scripts/hooks/stop-goal-fit.js +632 -70
- package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
- package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0007-skill-audit.md +1 -1
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
- package/docs/adr/0011-mcp-posture.md +100 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
- package/docs/adr/0013-context-lifecycle.md +151 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
- package/docs/adr/0016-three-hard-boundary-model.md +71 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
- package/docs/agent-system-guidebook.md +5 -12
- package/docs/context-map.md +4 -10
- package/docs/index.md +3 -2
- package/docs/integrations/framework-adapter.md +19 -6
- package/docs/integrations/index.md +2 -2
- package/docs/north-star.md +4 -4
- package/docs/operating-layers.md +3 -3
- package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
- package/docs/repository-structure.md +2 -2
- package/docs/skills-map.md +1 -0
- package/docs/spec/runtime-hook-surface.md +62 -9
- package/docs/standards-register.md +3 -3
- package/docs/survey-utterance-check.md +1 -1
- package/docs/trust-anchor-adoption.md +197 -0
- package/docs/verifiable-trust.md +95 -0
- package/docs/veritas-integration.md +2 -2
- package/docs/workflow-usage-guide.md +69 -0
- package/evals/acceptance/DEMO-false-completion.md +144 -0
- package/evals/acceptance/demo-cast.sh +92 -0
- package/evals/acceptance/demo-false-completion.sh +72 -0
- package/evals/acceptance/demo-real-evidence.sh +104 -0
- package/evals/acceptance/demo.tape +29 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
- package/evals/acceptance/prove-capture-teeth.sh +114 -0
- package/evals/acceptance/prove-teeth.sh +105 -0
- package/evals/ci/antigaming-suite.sh +55 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
- package/evals/integration/test_builder_step_producers.sh +379 -0
- package/evals/integration/test_bundle_install.sh +35 -71
- package/evals/integration/test_bundle_lifecycle.sh +39 -2
- package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
- package/evals/integration/test_checkpoint_signing.sh +489 -0
- package/evals/integration/test_claim_lookup.sh +352 -0
- package/evals/integration/test_command_log_fork_classification.sh +134 -0
- package/evals/integration/test_command_log_integrity.sh +275 -0
- package/evals/integration/test_context_map.sh +0 -2
- package/evals/integration/test_dual_emit_flow_step.sh +278 -0
- package/evals/integration/test_enforcer_expects_driven.sh +281 -0
- package/evals/integration/test_evidence_capture_hook.sh +185 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +273 -0
- package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
- package/evals/integration/test_gate_bypass_chain.sh +448 -0
- package/evals/integration/test_gate_lockdown.sh +1137 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
- package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
- package/evals/integration/test_goal_fit_hook.sh +69 -4
- package/evals/integration/test_goal_fit_rederive.sh +263 -0
- package/evals/integration/test_install_merge.sh +1176 -0
- package/evals/integration/test_kit_identity_trust.sh +393 -0
- package/evals/integration/test_mint_attestation.sh +373 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
- package/evals/integration/test_publish_delivery.sh +269 -0
- package/evals/integration/test_reconcile_soundness.sh +528 -0
- package/evals/integration/test_resolvefirststep_security.sh +208 -0
- package/evals/integration/test_session_resume_roundtrip.sh +286 -0
- package/evals/integration/test_trust_checkpoint.sh +325 -0
- package/evals/integration/test_trust_reconcile.sh +293 -0
- package/evals/integration/test_verify_cli.sh +208 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
- package/evals/lib/node.sh +0 -6
- package/evals/run.sh +47 -0
- package/evals/static/test_workflow_skills.sh +6 -13
- package/install.sh +0 -7
- package/integrations/strands-ts/README.md +25 -15
- package/integrations/veritas/flow-agents.adapter.json +1 -2
- package/kits/builder/flows/build.flow.json +59 -12
- package/kits/builder/kit.json +85 -15
- package/kits/builder/skills/continue-work/SKILL.md +116 -0
- package/kits/builder/skills/deliver/SKILL.md +36 -6
- package/kits/builder/skills/design-probe/SKILL.md +28 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -1
- package/kits/builder/skills/gate-review/SKILL.md +234 -0
- package/kits/builder/skills/learning-review/SKILL.md +30 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
- package/kits/builder/skills/plan-work/SKILL.md +13 -1
- package/kits/builder/skills/pull-work/SKILL.md +19 -0
- package/kits/knowledge/adapters/default-store/index.js +38 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
- package/kits/knowledge/docs/store-contract.md +314 -0
- package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
- package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
- package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
- package/kits/knowledge/evals/entities/suite.test.js +40 -0
- package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
- package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
- package/kits/knowledge/evals/retirement/suite.test.js +145 -0
- package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
- package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
- package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
- package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
- package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
- package/kits/knowledge/kit.json +51 -1
- package/package.json +6 -6
- package/packaging/conformance/README.md +10 -2
- package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
- package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
- package/packaging/conformance/run-conformance.js +1 -1
- package/scripts/README.md +2 -1
- package/scripts/build-universal-bundles.js +0 -1
- package/scripts/ci/mint-attestation.js +221 -0
- package/scripts/ci/trust-reconcile.js +545 -0
- package/scripts/hooks/config-protection.js +423 -1
- package/scripts/hooks/evidence-capture.js +348 -0
- package/scripts/hooks/lib/liveness-read.js +113 -0
- package/scripts/hooks/run-hook.js +6 -1
- package/scripts/hooks/stop-goal-fit.js +1524 -79
- package/scripts/hooks/workflow-steering.js +135 -5
- package/scripts/install-codex-home.sh +39 -0
- package/scripts/install-merge.js +330 -0
- package/scripts/repair-command-log.js +115 -0
- package/src/cli/init.ts +218 -20
- package/src/cli/validate-workflow-artifacts.ts +18 -2
- package/src/cli/verify.ts +100 -0
- package/src/cli/workflow-sidecar.ts +2127 -84
- package/src/cli.ts +2 -3
- package/src/lib/flow-resolver.ts +369 -0
- package/src/tools/build-universal-bundles.ts +34 -21
- package/src/tools/generate-context-map.ts +3 -17
- package/src/tools/validate-source-tree.ts +44 -104
- package/build/src/tools/filter-installed-packs.d.ts +0 -2
- package/build/src/tools/filter-installed-packs.js +0 -135
- package/packaging/packs.json +0 -49
- package/scripts/filter-installed-packs.js +0 -2
- 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
|