@kontourai/flow-agents 2.1.0 → 2.1.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/workflows/ci.yml +4 -4
- package/.github/workflows/docs-pages.yml +1 -1
- package/.github/workflows/kit-gates-demo.yml +2 -2
- package/.github/workflows/publish-npm.yml +2 -2
- package/.github/workflows/runtime-compat.yml +2 -2
- package/.github/workflows/trust-reconcile.yml +1 -1
- package/CHANGELOG.md +7 -0
- package/evals/integration/test_gate_lockdown.sh +21 -6
- package/package.json +1 -1
- package/scripts/ci/trust-reconcile.js +7 -23
- package/scripts/hooks/evidence-capture.js +10 -37
- package/scripts/hooks/stop-goal-fit.js +18 -45
- package/scripts/lib/command-log-chain.js +73 -0
- package/scripts/repair-command-log.js +8 -15
package/.github/workflows/ci.yml
CHANGED
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
|
|
34
34
|
steps:
|
|
35
35
|
- name: Checkout
|
|
36
|
-
uses: actions/checkout@
|
|
36
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
37
37
|
|
|
38
38
|
- name: Set up Node.js
|
|
39
39
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -99,7 +99,7 @@ jobs:
|
|
|
99
99
|
|
|
100
100
|
steps:
|
|
101
101
|
- name: Checkout
|
|
102
|
-
uses: actions/checkout@
|
|
102
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
103
103
|
|
|
104
104
|
- name: Set up Node.js
|
|
105
105
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -160,7 +160,7 @@ jobs:
|
|
|
160
160
|
|
|
161
161
|
steps:
|
|
162
162
|
- name: Checkout
|
|
163
|
-
uses: actions/checkout@
|
|
163
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
164
164
|
|
|
165
165
|
- name: Set up Node.js
|
|
166
166
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -292,7 +292,7 @@ jobs:
|
|
|
292
292
|
|
|
293
293
|
steps:
|
|
294
294
|
- name: Checkout
|
|
295
|
-
uses: actions/checkout@
|
|
295
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
296
296
|
|
|
297
297
|
- name: Set up Node.js
|
|
298
298
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
24
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
26
26
|
|
|
27
27
|
- name: Configure Pages
|
|
28
28
|
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
|
|
38
38
|
steps:
|
|
39
39
|
- name: Checkout
|
|
40
|
-
uses: actions/checkout@
|
|
40
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
41
41
|
|
|
42
42
|
- name: Set up Node.js
|
|
43
43
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -103,7 +103,7 @@ jobs:
|
|
|
103
103
|
|
|
104
104
|
steps:
|
|
105
105
|
- name: Checkout
|
|
106
|
-
uses: actions/checkout@
|
|
106
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
107
107
|
|
|
108
108
|
- name: Set up Node.js
|
|
109
109
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
24
|
- name: Check out repository
|
|
25
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
26
26
|
with:
|
|
27
27
|
fetch-depth: 0
|
|
28
28
|
|
|
@@ -58,7 +58,7 @@ jobs:
|
|
|
58
58
|
id-token: write
|
|
59
59
|
steps:
|
|
60
60
|
- name: Check out repository
|
|
61
|
-
uses: actions/checkout@
|
|
61
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
62
62
|
with:
|
|
63
63
|
fetch-depth: 0
|
|
64
64
|
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
version: pi --version
|
|
38
38
|
steps:
|
|
39
39
|
- name: Checkout
|
|
40
|
-
uses: actions/checkout@
|
|
40
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
41
41
|
|
|
42
42
|
- name: Set up Node.js
|
|
43
43
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -67,7 +67,7 @@ jobs:
|
|
|
67
67
|
timeout-minutes: 20
|
|
68
68
|
steps:
|
|
69
69
|
- name: Checkout
|
|
70
|
-
uses: actions/checkout@
|
|
70
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
71
71
|
|
|
72
72
|
- name: Set up Node.js
|
|
73
73
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
@@ -62,7 +62,7 @@ jobs:
|
|
|
62
62
|
|
|
63
63
|
steps:
|
|
64
64
|
- name: Checkout
|
|
65
|
-
uses: actions/checkout@
|
|
65
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
66
66
|
|
|
67
67
|
- name: Set up Node.js
|
|
68
68
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.1](https://github.com/kontourai/flow-agents/compare/v2.1.0...v2.1.1) (2026-06-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Refactoring
|
|
7
|
+
|
|
8
|
+
* **flow-agents:** one shared module for command-log chain helpers (ops[#20](https://github.com/kontourai/flow-agents/issues/20)) ([#249](https://github.com/kontourai/flow-agents/issues/249)) ([67af85f](https://github.com/kontourai/flow-agents/commit/67af85f5010dace3f33b36b86245e0c7aad95f77))
|
|
9
|
+
|
|
3
10
|
## [2.1.0](https://github.com/kontourai/flow-agents/compare/v2.0.1...v2.1.0) (2026-06-29)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -790,6 +790,15 @@ echo "=== AC3.1 — Surface unavailable fail-closed ==="
|
|
|
790
790
|
echo ""
|
|
791
791
|
echo "--- AC3.1a: Isolated (no @kontourai/surface) with high-impact claim → BLOCKS ---"
|
|
792
792
|
|
|
793
|
+
# The gate imports the shared scripts/lib/command-log-chain.js helpers. A real
|
|
794
|
+
# install rsyncs the whole tree, so the lib always sits beside the hooks. Mirror that:
|
|
795
|
+
# the isolated gates live at "$TMP/surface-iso*/stop-goal-fit.js", so "../lib" resolves
|
|
796
|
+
# to "$TMP/lib" for both. This keeps the test exercising surface-unavailable fail-closed
|
|
797
|
+
# (not a spurious module-not-found crash).
|
|
798
|
+
ISO_LIBDIR="$TMP/lib"
|
|
799
|
+
mkdir -p "$ISO_LIBDIR"
|
|
800
|
+
cp "$ROOT/scripts/lib/command-log-chain.js" "$ISO_LIBDIR/"
|
|
801
|
+
|
|
793
802
|
# Create isolated node context that can't find @kontourai/surface
|
|
794
803
|
ISO_DIR="$TMP/surface-iso"
|
|
795
804
|
mkdir -p "$ISO_DIR/repo/.flow-agents/surftest"
|
|
@@ -1014,13 +1023,19 @@ else
|
|
|
1014
1023
|
fi
|
|
1015
1024
|
|
|
1016
1025
|
echo ""
|
|
1017
|
-
echo "--- AC3.3c:
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1026
|
+
echo "--- AC3.3c: genesis is single-sourced and imported by writer + verifier (cannot diverge) ---"
|
|
1027
|
+
# Stronger than the old "two literals match" check: the genesis literal now lives in
|
|
1028
|
+
# exactly ONE module, and both the writer and verifier import it — so divergence is
|
|
1029
|
+
# structurally impossible rather than merely currently-equal.
|
|
1030
|
+
genesis_lib=$(grep "const CHAIN_GENESIS = " "$ROOT/scripts/lib/command-log-chain.js" | sed "s/.*= '//;s/'.*//")
|
|
1031
|
+
if [ -n "$genesis_lib" ] \
|
|
1032
|
+
&& grep -q "require.*command-log-chain" "$ROOT/scripts/hooks/evidence-capture.js" \
|
|
1033
|
+
&& grep -q "require.*command-log-chain" "$ROOT/scripts/hooks/stop-goal-fit.js" \
|
|
1034
|
+
&& ! grep -qE "const CHAIN_GENESIS = '" "$ROOT/scripts/hooks/evidence-capture.js" \
|
|
1035
|
+
&& ! grep -qE "const CHAIN_GENESIS_VERIFY = '" "$ROOT/scripts/hooks/stop-goal-fit.js"; then
|
|
1036
|
+
_pass "AC3.3: genesis single-sourced in scripts/lib/command-log-chain.js ($genesis_lib); writer + verifier import it, no divergent literal"
|
|
1022
1037
|
else
|
|
1023
|
-
_fail "AC3.3:
|
|
1038
|
+
_fail "AC3.3: genesis not single-sourced (lib='$genesis_lib') or a divergent literal remains in a consumer"
|
|
1024
1039
|
fi
|
|
1025
1040
|
|
|
1026
1041
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -61,6 +61,10 @@ const { spawnSync } = require('child_process');
|
|
|
61
61
|
const fs = require('fs');
|
|
62
62
|
const os = require('os');
|
|
63
63
|
const path = require('path');
|
|
64
|
+
// One normative definition shared with scripts/hooks/stop-goal-fit.js — the local
|
|
65
|
+
// copy here had drifted (it was missing the trailing `/bin/true` check), which is
|
|
66
|
+
// exactly why this is now imported rather than duplicated.
|
|
67
|
+
const { hasLaunderingOperator } = require('../lib/command-log-chain.js');
|
|
64
68
|
|
|
65
69
|
// ---------------------------------------------------------------------------
|
|
66
70
|
// Helpers
|
|
@@ -80,29 +84,9 @@ function isPassingValue(v) {
|
|
|
80
84
|
return v === true || v === 1 || v === 'true' || v === 'pass';
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
* Rules (applied to claimed verification commands only):
|
|
88
|
-
* - ANY || operator — verify commands must not contain ||. This catches:
|
|
89
|
-
* || exit 0, || echo ok, || /bin/true, || true, || :, etc.
|
|
90
|
-
* - ; or newline followed by true / : / exit 0 — trailing success injection
|
|
91
|
-
*
|
|
92
|
-
* NOTE: Logic must stay identical to scripts/hooks/stop-goal-fit.js hasLaunderingOperator.
|
|
93
|
-
* Centralize into a shared module as a follow-up (coordinate-free duplication for now).
|
|
94
|
-
*/
|
|
95
|
-
function hasLaunderingOperator(cmd) {
|
|
96
|
-
// Flag ANY || operator — masks the exit code of the left-hand command.
|
|
97
|
-
if (/\|\|/.test(cmd)) return true;
|
|
98
|
-
// Flag ; or newline followed by true / : / exit 0
|
|
99
|
-
if (/[;\n]\s*true\b/.test(cmd)) return true;
|
|
100
|
-
if (/[;\n]\s*:\s*(?:$|\s|;|\n)/.test(cmd)) return true;
|
|
101
|
-
if (/[;\n]\s*exit\s+0\b/.test(cmd)) return true;
|
|
102
|
-
// Flag pipe to true (pipeline absorbs exit code)
|
|
103
|
-
if (/\|\s*true\b/.test(cmd)) return true;
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
87
|
+
// hasLaunderingOperator is imported from ../lib/command-log-chain.js (above) so this
|
|
88
|
+
// CI reconciler and the stop-goal-fit verifier apply the identical exit-code-mask
|
|
89
|
+
// heuristic — see that module for the rules.
|
|
106
90
|
|
|
107
91
|
/**
|
|
108
92
|
* Run a single shell command under bash, capturing exit code.
|
|
@@ -62,43 +62,16 @@ const COMMAND_TOOL_NAME = /(^|[^a-z])(bash|shell|sh|exec|run|command|terminal|cm
|
|
|
62
62
|
|
|
63
63
|
// ─── Hash-chain integrity (tamper-EVIDENCE) ───────────────────────────────────
|
|
64
64
|
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// HONEST FRAMING: this makes alteration DETECTABLE, not impossible. An agent
|
|
76
|
-
// that rewrites all hashes can still forge the chain. The real tamper-proof
|
|
77
|
-
// boundary is the signed checkpoint (B1). We do not oversell this boundary.
|
|
78
|
-
const CHAIN_GENESIS = 'a3f9e2b7d5c84f1e6a0d2c3b9f7e1a4d8c6b5f2e9a0d3c7b1f4e8a2d6c0b9f3';
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Stable canonical JSON for the chain input: the record WITHOUT the `_chain`
|
|
82
|
-
* field, keys sorted alphabetically. This ensures the hash is independent of
|
|
83
|
-
* key insertion order and that `_chain` itself does not contribute to its own
|
|
84
|
-
* hash (circular dependency).
|
|
85
|
-
*/
|
|
86
|
-
function canonicalJsonForChain(record) {
|
|
87
|
-
// Strip _chain if present (should not be, but defensive).
|
|
88
|
-
const keys = Object.keys(record).filter(k => k !== '_chain').sort();
|
|
89
|
-
const obj = {};
|
|
90
|
-
for (const k of keys) obj[k] = record[k];
|
|
91
|
-
return JSON.stringify(obj);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Compute the sha256 hex hash for this chain link.
|
|
96
|
-
* hash = sha256(prevHash + canonicalJson(record))
|
|
97
|
-
*/
|
|
98
|
-
function computeChainHash(prevHash, record) {
|
|
99
|
-
const input = prevHash + canonicalJsonForChain(record);
|
|
100
|
-
return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
|
|
101
|
-
}
|
|
65
|
+
// CHAIN_GENESIS is a fixed arbitrary sentinel — NOT the SHA256 of any specific
|
|
66
|
+
// input string (a previous comment incorrectly claimed sha256("…:genesis")). The
|
|
67
|
+
// writer here and the verifier in stop-goal-fit.js MUST canonicalize and seed
|
|
68
|
+
// identically, so the genesis constant and the canonicalJson/hash helpers live in
|
|
69
|
+
// ONE shared module that both import — divergence is structurally impossible.
|
|
70
|
+
const {
|
|
71
|
+
CHAIN_GENESIS,
|
|
72
|
+
canonicalJsonForChain,
|
|
73
|
+
computeChainHash,
|
|
74
|
+
} = require('../lib/command-log-chain.js');
|
|
102
75
|
|
|
103
76
|
/**
|
|
104
77
|
* Read the last entry from command-log.jsonl that has a `_chain` block.
|
|
@@ -29,6 +29,16 @@ const path = require('path');
|
|
|
29
29
|
const { spawnSync } = require('child_process');
|
|
30
30
|
const crypto = require('crypto');
|
|
31
31
|
|
|
32
|
+
// Hash-chain primitives + the exit-code-laundering heuristic come from ONE shared
|
|
33
|
+
// module, so this verifier can never drift from the writer (evidence-capture.js).
|
|
34
|
+
// CHAIN_GENESIS is re-aliased to CHAIN_GENESIS_VERIFY to preserve the long-standing
|
|
35
|
+
// export name consumed by repair-command-log.js and the fork-classification eval.
|
|
36
|
+
const {
|
|
37
|
+
CHAIN_GENESIS: CHAIN_GENESIS_VERIFY,
|
|
38
|
+
canonicalJsonForChain,
|
|
39
|
+
hasLaunderingOperator,
|
|
40
|
+
} = require('../lib/command-log-chain.js');
|
|
41
|
+
|
|
32
42
|
const MAX_STDIN = 1024 * 1024;
|
|
33
43
|
const ACTIVE_STATUSES = new Set([
|
|
34
44
|
'planning',
|
|
@@ -733,36 +743,10 @@ function claimAcknowledgesFailure(status, value) {
|
|
|
733
743
|
|| v === 'fail' || v === 'failed' || v === 'not_verified' || v === 'failing';
|
|
734
744
|
}
|
|
735
745
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
*
|
|
741
|
-
* R6 extended logic (identical patterns used by scripts/ci/trust-reconcile.js — centralize
|
|
742
|
-
* as a follow-up if drift becomes a maintenance concern):
|
|
743
|
-
* - ANY || operator is flagged. A legitimate verification command never needs || — its
|
|
744
|
-
* only purpose in a verification command is to mask the real exit code (e.g.
|
|
745
|
-
* `npm test || exit 0`, `npm test || echo ok`, `npm test || /bin/true`, `npm test || (exit 0)`).
|
|
746
|
-
* - | true (single pipe into true — always exits 0)
|
|
747
|
-
* - Trailing ; or newline followed by: true : exit 0 /bin/true
|
|
748
|
-
*
|
|
749
|
-
* Fix D: applied in captureCrossReference's satisfied path and capturedFailReconciliation.
|
|
750
|
-
*/
|
|
751
|
-
function hasLaunderingOperator(cmd) {
|
|
752
|
-
// ANY || in a claimed verification command is an exit-code mask.
|
|
753
|
-
// Legitimate verification commands never need || — its only purpose there is to
|
|
754
|
-
// suppress the real exit code (|| exit 0, || echo ok, || /bin/true, || (exit 0), etc.).
|
|
755
|
-
if (/\|\|/.test(cmd)) return true;
|
|
756
|
-
// | true — single-pipe into true: `cmd | true` always exits 0 regardless of left-side exit code.
|
|
757
|
-
if (/\|\s*true\b/.test(cmd)) return true;
|
|
758
|
-
// Trailing ; or \n followed by exit-neutralizing commands (same threat, appended after the real cmd):
|
|
759
|
-
// ; true ; : ; exit 0 ; /bin/true (and \n variants)
|
|
760
|
-
if (/[;\n]\s*true\b/.test(cmd)) return true;
|
|
761
|
-
if (/[;\n]\s*:\s*(?:$|\s|;)/.test(cmd)) return true;
|
|
762
|
-
if (/[;\n]\s*exit\s+0\b/.test(cmd)) return true;
|
|
763
|
-
if (/[;\n]\s*\/bin\/true\b/.test(cmd)) return true;
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
746
|
+
// hasLaunderingOperator (the exit-code-mask heuristic) is imported from
|
|
747
|
+
// ../lib/command-log-chain.js so this verifier and scripts/ci/trust-reconcile.js
|
|
748
|
+
// share one normative definition. Applied in captureCrossReference's satisfied
|
|
749
|
+
// path and capturedFailReconciliation.
|
|
766
750
|
|
|
767
751
|
// ─── Hash-chain integrity verification (Increment B2, tamper-EVIDENCE) ────────
|
|
768
752
|
//
|
|
@@ -786,20 +770,9 @@ function hasLaunderingOperator(cmd) {
|
|
|
786
770
|
// The genesis prevHash is a fixed arbitrary sentinel — NOT the SHA256 of any
|
|
787
771
|
// specific input string. The comment in evidence-capture.js previously (and
|
|
788
772
|
// incorrectly) claimed it was sha256("flow-agents:command-log:genesis"); it is not.
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Canonical JSON for chain verification: record WITHOUT `_chain`, keys sorted.
|
|
795
|
-
* Must be byte-identical to canonicalJsonForChain() in evidence-capture.js.
|
|
796
|
-
*/
|
|
797
|
-
function canonicalJsonForVerify(record) {
|
|
798
|
-
const keys = Object.keys(record).filter(k => k !== '_chain').sort();
|
|
799
|
-
const obj = {};
|
|
800
|
-
for (const k of keys) obj[k] = record[k];
|
|
801
|
-
return JSON.stringify(obj);
|
|
802
|
-
}
|
|
773
|
+
// Both the genesis (CHAIN_GENESIS_VERIFY, imported above) and the canonical-JSON
|
|
774
|
+
// helper (canonicalJsonForChain) come from ../lib/command-log-chain.js, the single
|
|
775
|
+
// source the writer in evidence-capture.js imports too — so they cannot diverge.
|
|
803
776
|
|
|
804
777
|
/**
|
|
805
778
|
* Verify the hash chain of command-log.jsonl.
|
|
@@ -870,7 +843,7 @@ function verifyCommandLogChain(artifactDir) {
|
|
|
870
843
|
// (a) Self-consistency. A content edit without rehashing fails here.
|
|
871
844
|
if (typeof chain.prevHash !== 'string') return { status: 'broken', brokenAt: i, forkAt: null };
|
|
872
845
|
const selfHash = crypto.createHash('sha256')
|
|
873
|
-
.update(chain.prevHash +
|
|
846
|
+
.update(chain.prevHash + canonicalJsonForChain(entry), 'utf8')
|
|
874
847
|
.digest('hex');
|
|
875
848
|
if (chain.hash !== selfHash) return { status: 'broken', brokenAt: i, forkAt: null };
|
|
876
849
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
//
|
|
3
|
+
// Single normative source for the command-log hash-chain primitives and the
|
|
4
|
+
// exit-code-laundering heuristic.
|
|
5
|
+
//
|
|
6
|
+
// These were previously copy-pasted across the writer (hooks/evidence-capture.js),
|
|
7
|
+
// the verifier (hooks/stop-goal-fit.js), the repair tool (repair-command-log.js),
|
|
8
|
+
// and CI reconcile (ci/trust-reconcile.js) under "keep byte-identical" comments —
|
|
9
|
+
// the most security-sensitive path in the bundle, since the chain's integrity
|
|
10
|
+
// claim rests on writer and verifier canonicalizing identically. The copies had
|
|
11
|
+
// ALREADY drifted (ci/trust-reconcile's hasLaunderingOperator was missing the
|
|
12
|
+
// trailing `/bin/true` check), which is exactly the failure mode duplication
|
|
13
|
+
// invites. Importing from one module makes that divergence structurally impossible.
|
|
14
|
+
//
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
// The genesis prevHash is a FIXED ARBITRARY SENTINEL — NOT the SHA256 of any
|
|
18
|
+
// specific input string. (An earlier comment incorrectly claimed it was
|
|
19
|
+
// sha256("flow-agents:command-log:genesis"); that is wrong.) Writer and verifier
|
|
20
|
+
// MUST share this exact value — existing chained logs depend on it.
|
|
21
|
+
//
|
|
22
|
+
// HONEST FRAMING: this makes alteration DETECTABLE, not impossible. An agent that
|
|
23
|
+
// rewrites all hashes can still forge the chain. The real tamper-proof boundary is
|
|
24
|
+
// the signed checkpoint (B1). We do not oversell this boundary.
|
|
25
|
+
const CHAIN_GENESIS = 'a3f9e2b7d5c84f1e6a0d2c3b9f7e1a4d8c6b5f2e9a0d3c7b1f4e8a2d6c0b9f3';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stable canonical JSON for a chain link: the record WITHOUT its `_chain` field,
|
|
29
|
+
* keys sorted alphabetically. This makes the hash independent of key insertion
|
|
30
|
+
* order and keeps `_chain` from contributing to its own hash.
|
|
31
|
+
*/
|
|
32
|
+
function canonicalJsonForChain(record) {
|
|
33
|
+
const keys = Object.keys(record).filter((k) => k !== '_chain').sort();
|
|
34
|
+
const obj = {};
|
|
35
|
+
for (const k of keys) obj[k] = record[k];
|
|
36
|
+
return JSON.stringify(obj);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Chain link hash: sha256(prevHash + canonicalJsonForChain(record)), hex. */
|
|
40
|
+
function computeChainHash(prevHash, record) {
|
|
41
|
+
return crypto
|
|
42
|
+
.createHash('sha256')
|
|
43
|
+
.update(prevHash + canonicalJsonForChain(record), 'utf8')
|
|
44
|
+
.digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when a claimed verification command contains an exit-code-laundering
|
|
49
|
+
* operator. Legitimate verification commands never need these — their only
|
|
50
|
+
* purpose is to suppress a real non-zero exit:
|
|
51
|
+
* - ANY `||` (e.g. `npm test || exit 0`, `|| echo ok`, `|| /bin/true`)
|
|
52
|
+
* - `| true` (pipe into true — the pipeline absorbs the exit code)
|
|
53
|
+
* - trailing `; true` / `; :` / `; exit 0` / `; /bin/true` (and `\n` variants)
|
|
54
|
+
*/
|
|
55
|
+
function hasLaunderingOperator(cmd) {
|
|
56
|
+
// ANY || in a claimed verification command is an exit-code mask.
|
|
57
|
+
if (/\|\|/.test(cmd)) return true;
|
|
58
|
+
// | true — single-pipe into true always exits 0 regardless of the left side.
|
|
59
|
+
if (/\|\s*true\b/.test(cmd)) return true;
|
|
60
|
+
// Trailing ; or \n followed by an exit-neutralizing command:
|
|
61
|
+
if (/[;\n]\s*true\b/.test(cmd)) return true;
|
|
62
|
+
if (/[;\n]\s*:\s*(?:$|\s|;)/.test(cmd)) return true;
|
|
63
|
+
if (/[;\n]\s*exit\s+0\b/.test(cmd)) return true;
|
|
64
|
+
if (/[;\n]\s*\/bin\/true\b/.test(cmd)) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
CHAIN_GENESIS,
|
|
70
|
+
canonicalJsonForChain,
|
|
71
|
+
computeChainHash,
|
|
72
|
+
hasLaunderingOperator,
|
|
73
|
+
};
|
|
@@ -26,17 +26,10 @@ const path = require('path');
|
|
|
26
26
|
const crypto = require('crypto');
|
|
27
27
|
|
|
28
28
|
const gate = require(path.join(__dirname, 'hooks', 'stop-goal-fit.js'));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const obj = {};
|
|
34
|
-
for (const k of keys) obj[k] = rec[k];
|
|
35
|
-
return JSON.stringify(obj);
|
|
36
|
-
}
|
|
37
|
-
function hashLink(prev, rec) {
|
|
38
|
-
return crypto.createHash('sha256').update(prev + canon(rec), 'utf8').digest('hex');
|
|
39
|
-
}
|
|
29
|
+
// Genesis + canonicalization/hash come from the single shared module, so a repaired
|
|
30
|
+
// chain is re-linked byte-identically to how the writer/verifier compute it.
|
|
31
|
+
const { CHAIN_GENESIS, canonicalJsonForChain, computeChainHash } = require('./lib/command-log-chain.js');
|
|
32
|
+
const GENESIS = CHAIN_GENESIS;
|
|
40
33
|
|
|
41
34
|
function main() {
|
|
42
35
|
const dir = process.argv[2];
|
|
@@ -77,8 +70,8 @@ function main() {
|
|
|
77
70
|
records.sort((a, b) => {
|
|
78
71
|
const ta = String(a.capturedAt || ''), tb = String(b.capturedAt || '');
|
|
79
72
|
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
80
|
-
const ha = crypto.createHash('sha256').update(
|
|
81
|
-
const hb = crypto.createHash('sha256').update(
|
|
73
|
+
const ha = crypto.createHash('sha256').update(canonicalJsonForChain(a)).digest('hex');
|
|
74
|
+
const hb = crypto.createHash('sha256').update(canonicalJsonForChain(b)).digest('hex');
|
|
82
75
|
return ha < hb ? -1 : ha > hb ? 1 : 0;
|
|
83
76
|
});
|
|
84
77
|
|
|
@@ -87,7 +80,7 @@ function main() {
|
|
|
87
80
|
let prev = GENESIS;
|
|
88
81
|
let seq = 0;
|
|
89
82
|
for (const rec of records) {
|
|
90
|
-
const h =
|
|
83
|
+
const h = computeChainHash(prev, rec);
|
|
91
84
|
out.push(JSON.stringify({ ...rec, _chain: { seq, prevHash: prev, hash: h } }));
|
|
92
85
|
prev = h; seq += 1;
|
|
93
86
|
}
|
|
@@ -100,7 +93,7 @@ function main() {
|
|
|
100
93
|
source: 'chain-repair',
|
|
101
94
|
repair: { reason, entries: records.length, forkAt: verdict.forkAt },
|
|
102
95
|
};
|
|
103
|
-
const mh =
|
|
96
|
+
const mh = computeChainHash(prev, marker);
|
|
104
97
|
out.push(JSON.stringify({ ...marker, _chain: { seq, prevHash: prev, hash: mh } }));
|
|
105
98
|
|
|
106
99
|
fs.copyFileSync(file, file + '.prebackup-repair');
|