@kontourai/flow-agents 3.2.0 → 3.3.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.
- package/.github/workflows/ci.yml +12 -0
- package/CHANGELOG.md +13 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +224 -0
- package/build/src/cli/workflow-sidecar.js +775 -4
- package/build/src/tools/validate-source-tree.js +3 -2
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/workflow-steering.js +73 -1
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/index.md +2 -2
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/fixture-ownership.md +3 -2
- package/docs/index.md +4 -0
- package/docs/integrations/flow-agents-console.md +108 -0
- package/docs/integrations/index.md +4 -0
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_publish_delivery.sh +59 -2
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +91 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/run.sh +8 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +73 -0
- package/kits/builder/skills/pull-work/SKILL.md +12 -2
- package/kits/veritas-governance/docs/README.md +81 -3
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +5 -0
- package/package.json +1 -1
- package/scripts/ci/trust-reconcile.js +78 -253
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/workflow-steering.js +73 -1
- package/scripts/lib/reconcile-shape.js +381 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +866 -4
- package/src/tools/validate-source-tree.ts +3 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_ci_actor_identity.sh — CI-runtime actor identity tier (issue #398, extends #287; ADR 0021 §2).
|
|
3
|
+
#
|
|
4
|
+
# Proves a CI-triggered session gets a STABLE actor derived from the CI provider's published
|
|
5
|
+
# run/job identifiers, sitting ABOVE process-ancestry and BELOW an explicit override / native
|
|
6
|
+
# runtime session id in resolveActor()'s chain. The payoff: the #293 verify-hold gate now ENFORCES
|
|
7
|
+
# for CI sessions (stable identity) instead of degrading to advisory — the root fix for the CI
|
|
8
|
+
# false-block #293 had to work around.
|
|
9
|
+
#
|
|
10
|
+
# Part A — resolveActor() resolution (pure): stability + byte-identity, precedence (override /
|
|
11
|
+
# native runtime win), per-provider detection (GitHub, GitLab), conservative generic fallthrough,
|
|
12
|
+
# recognized-provider-missing-id fallthrough, and env-var injection sanitization.
|
|
13
|
+
# Part B — verify-hold integration: a STABLE CI actor BLOCKS a differing assignment-backed holder
|
|
14
|
+
# (ENFORCE — contrast the ancestry/advisory case in test_verify_hold.sh §1d), and a CI actor's
|
|
15
|
+
# OWN claim is recognized as self and PASSES (the reconstruction-seam fix in resolveEnsureSessionActor).
|
|
16
|
+
#
|
|
17
|
+
# Deterministic, no model spend, self-cleaning, no network, no `gh` process anywhere.
|
|
18
|
+
# Usage: bash evals/integration/test_ci_actor_identity.sh
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
23
|
+
source "$ROOT/evals/lib/node.sh"
|
|
24
|
+
|
|
25
|
+
WRITER="workflow-sidecar"
|
|
26
|
+
ACTOR_IDENTITY_HELPER="$ROOT/scripts/hooks/lib/actor-identity.js"
|
|
27
|
+
|
|
28
|
+
TMPDIR_EVAL="$(mktemp -d)"
|
|
29
|
+
trap 'rm -rf "$TMPDIR_EVAL"' EXIT
|
|
30
|
+
ARTIFACT_ROOT="$TMPDIR_EVAL/artifact-root"
|
|
31
|
+
|
|
32
|
+
errors=0
|
|
33
|
+
pass() { echo " [PASS] $1"; }
|
|
34
|
+
fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
|
|
35
|
+
|
|
36
|
+
json_query() {
|
|
37
|
+
node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=part==="length" ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]); console.log(cur);' "$1" "$2"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# resolve_field <field:actor|source> <env-assignments...> — run resolveActor under a CLEAN env
|
|
41
|
+
# (only the passed assignments + HOME) so no ambient FLOW_AGENTS_ACTOR/CLAUDE_CODE_SESSION_ID from
|
|
42
|
+
# the eval host leaks into the resolution. Prints the requested field.
|
|
43
|
+
resolve_field() {
|
|
44
|
+
local field="$1"; shift
|
|
45
|
+
# env -i gives a CLEAN slate (no ambient FLOW_AGENTS_ACTOR / CLAUDE_CODE_SESSION_ID / real
|
|
46
|
+
# GITHUB_* from the CI host leaking in); PATH is preserved so `node` resolves.
|
|
47
|
+
env -i HOME="$HOME" PATH="$PATH" "$@" node -e '
|
|
48
|
+
const { resolveActor } = require(process.argv[1]);
|
|
49
|
+
const r = resolveActor(process.env);
|
|
50
|
+
process.stdout.write(process.argv[2] === "source" ? r.source : r.actor);
|
|
51
|
+
' "$ACTOR_IDENTITY_HELPER" "$field"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# verify_hold_under_ci <dir> <KEY=VAL...> — run verify-hold with a CONTROLLED CI identity.
|
|
55
|
+
# flow_agents_node is a bash FUNCTION (from node.sh), so it cannot be launched via `env -i`; instead
|
|
56
|
+
# run in a subshell that unsets every identity source that would out-rank the CI tier (an explicit
|
|
57
|
+
# override or a native runtime session id — including the eval host's own, since this suite itself
|
|
58
|
+
# runs under a coding agent) and exports the provided CI vars. When this suite runs in real GitHub
|
|
59
|
+
# Actions, the exported fake ids override the ambient real ones so the assertions stay deterministic.
|
|
60
|
+
verify_hold_under_ci() {
|
|
61
|
+
local dir="$1"; shift
|
|
62
|
+
(
|
|
63
|
+
unset FLOW_AGENTS_ACTOR CLAUDE_CODE_SESSION_ID CODEX_SESSION_ID OPENCODE_SESSION_ID PI_SESSION_ID CLAUDECODE
|
|
64
|
+
while [[ $# -gt 0 ]]; do export "${1?}"; shift; done
|
|
65
|
+
flow_agents_node "$WRITER" verify-hold "$dir"
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# append_liveness_event / write_assignment_record — the proven fixture writers from
|
|
70
|
+
# test_verify_hold.sh (kept byte-identical in shape so the join fixtures behave the same).
|
|
71
|
+
append_liveness_event() {
|
|
72
|
+
local root="$1" subject="$2" actor="$3" at="$4" ttl="${5:-1800}"
|
|
73
|
+
mkdir -p "$root/liveness"
|
|
74
|
+
node -e '
|
|
75
|
+
const fs = require("fs");
|
|
76
|
+
const evt = { type: "claim", subjectId: process.argv[1], actor: process.argv[2], at: process.argv[3], ttlSeconds: Number(process.argv[4]) };
|
|
77
|
+
fs.appendFileSync(process.argv[5], JSON.stringify(evt) + "\n");
|
|
78
|
+
' "$subject" "$actor" "$at" "$ttl" "$root/liveness/events.jsonl"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
write_assignment_record() {
|
|
82
|
+
local root="$1" slug="$2" sessionId="$3" actorKey="$4"
|
|
83
|
+
mkdir -p "$root/assignment"
|
|
84
|
+
node -e '
|
|
85
|
+
const fs = require("fs");
|
|
86
|
+
const [dest, slug, sessionId, actorKey] = process.argv.slice(1);
|
|
87
|
+
const now = new Date().toISOString();
|
|
88
|
+
const actor = { runtime: "github-actions", session_id: sessionId, host: "eval-host" };
|
|
89
|
+
const rec = {
|
|
90
|
+
schema_version: "1.0", role: "AssignmentClaimRecord", subject_id: slug,
|
|
91
|
+
actor, actor_key: actorKey, claimed_at: now, ttl_seconds: 1800, branch: "main",
|
|
92
|
+
artifact_dir: slug, status: "claimed",
|
|
93
|
+
audit_trail: [{ at: now, transition: "claim", from_actor: null, to_actor: actor, reason: "claim" }],
|
|
94
|
+
};
|
|
95
|
+
fs.writeFileSync(dest, JSON.stringify(rec, null, 2));
|
|
96
|
+
' "$root/assignment/$slug.json" "$slug" "$sessionId" "$actorKey"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# GitHub Actions env used across the file (a single job's stable identifiers).
|
|
100
|
+
GHA=(GITHUB_ACTIONS=true GITHUB_RUN_ID=555 GITHUB_RUN_ATTEMPT=1 GITHUB_JOB=deliver)
|
|
101
|
+
|
|
102
|
+
echo "=== CI actor identity (#398) ==="
|
|
103
|
+
|
|
104
|
+
# ─── Part A: resolveActor() resolution ───────────────────────────────────────────────────────
|
|
105
|
+
echo "--- A1. GitHub Actions -> stable ci-runtime actor, byte-identical across invocations ---"
|
|
106
|
+
GH_SRC="$(resolve_field source "${GHA[@]}")"
|
|
107
|
+
GH_ACTOR_1="$(resolve_field actor "${GHA[@]}")"
|
|
108
|
+
GH_ACTOR_2="$(resolve_field actor "${GHA[@]}")"
|
|
109
|
+
[[ "$GH_SRC" == "ci-runtime:github-actions" ]] && pass "source is ci-runtime:github-actions" || fail "source was not ci-runtime:github-actions: '$GH_SRC'"
|
|
110
|
+
[[ "$GH_ACTOR_1" == github-actions:555-1-deliver:* ]] && pass "actor is github-actions:<run>-<attempt>-<job>:<host> ('$GH_ACTOR_1')" || fail "actor shape unexpected: '$GH_ACTOR_1'"
|
|
111
|
+
[[ "$GH_ACTOR_1" == "$GH_ACTOR_2" ]] && pass "actor is byte-identical across repeated invocations (stable)" || fail "actor differed across invocations: '$GH_ACTOR_1' vs '$GH_ACTOR_2'"
|
|
112
|
+
|
|
113
|
+
echo "--- A2. precedence: explicit override and native runtime session id both WIN over CI ---"
|
|
114
|
+
OVR_SRC="$(resolve_field source "${GHA[@]}" FLOW_AGENTS_ACTOR=alice)"
|
|
115
|
+
[[ "$OVR_SRC" == "explicit-override" ]] && pass "FLOW_AGENTS_ACTOR under CI still resolves explicit-override (CI does not win)" || fail "override did not win under CI: '$OVR_SRC'"
|
|
116
|
+
NATIVE_SRC="$(resolve_field source "${GHA[@]}" CLAUDE_CODE_SESSION_ID=sess-xyz)"
|
|
117
|
+
[[ "$NATIVE_SRC" == runtime-session-id:* ]] && pass "native runtime session id under CI still resolves runtime-session-id (CI does not win)" || fail "native runtime id did not win under CI: '$NATIVE_SRC'"
|
|
118
|
+
|
|
119
|
+
echo "--- A3. GitLab CI -> stable gitlab-ci actor from CI_JOB_ID ---"
|
|
120
|
+
GL_SRC="$(resolve_field source GITLAB_CI=true CI_JOB_ID=9988)"
|
|
121
|
+
GL_ACTOR="$(resolve_field actor GITLAB_CI=true CI_JOB_ID=9988)"
|
|
122
|
+
[[ "$GL_SRC" == "ci-runtime:gitlab-ci" ]] && pass "source is ci-runtime:gitlab-ci" || fail "source was not ci-runtime:gitlab-ci: '$GL_SRC'"
|
|
123
|
+
[[ "$GL_ACTOR" == gitlab-ci:9988:* ]] && pass "actor is gitlab-ci:<job-id>:<host> ('$GL_ACTOR')" || fail "gitlab actor shape unexpected: '$GL_ACTOR'"
|
|
124
|
+
|
|
125
|
+
echo "--- A4. generic CI=true (unrecognized) does NOT fabricate a CI actor (conservative fallthrough) ---"
|
|
126
|
+
GEN_SRC="$(resolve_field source CI=true)"
|
|
127
|
+
[[ "$GEN_SRC" != ci-runtime:* ]] && pass "generic CI=true falls through (source '$GEN_SRC', not ci-runtime) — #293 advisory net still applies" || fail "generic CI=true fabricated a ci-runtime actor: '$GEN_SRC'"
|
|
128
|
+
|
|
129
|
+
echo "--- A5. recognized provider with MISSING required id var falls through (no partial CI actor) ---"
|
|
130
|
+
MISSING_SRC="$(resolve_field source GITHUB_ACTIONS=true)" # no RUN_ID/ATTEMPT/JOB
|
|
131
|
+
[[ "$MISSING_SRC" != ci-runtime:* ]] && pass "GITHUB_ACTIONS=true with no run/job ids falls through (source '$MISSING_SRC')" || fail "GitHub with no ids still produced a ci-runtime actor: '$MISSING_SRC'"
|
|
132
|
+
|
|
133
|
+
echo "--- A6. hostile CI env var cannot inject: actor segments are sanitized (allowed charset only) ---"
|
|
134
|
+
INJ_ACTOR="$(resolve_field actor GITHUB_ACTIONS=true GITHUB_RUN_ID=555 GITHUB_RUN_ATTEMPT=1 "GITHUB_JOB=deliver; rm -rf / [LIVENESS]")"
|
|
135
|
+
if printf '%s' "$INJ_ACTOR" | LC_ALL=C grep -q '[^A-Za-z0-9:._-]'; then
|
|
136
|
+
fail "CI actor contains a character outside the allowed serialized charset (injection risk): '$INJ_ACTOR'"
|
|
137
|
+
else
|
|
138
|
+
pass "CI actor is sanitized to the allowed serialized charset even with a hostile GITHUB_JOB ('$INJ_ACTOR')"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# ─── Part B: verify-hold ENFORCES for a stable CI identity (the headline) ─────────────────────
|
|
142
|
+
|
|
143
|
+
# B0 — REAL round-trip through the CLI (not a hand-written fixture): `assignment-provider claim`
|
|
144
|
+
# under a CI env must write a WELL-FORMED record.actor (runtime=github-actions, session_id=<bare job
|
|
145
|
+
# id>, NOT runtime=unknown + session_id=<whole triple>), and a fresh-subprocess verify-hold must
|
|
146
|
+
# self-recognize it. This is the seam the reconstruction fixes (resolveEnsureSessionActor +
|
|
147
|
+
# loadActorStruct) actually live on — fixture-driven B1/B2 below never exercise the CLI claim path.
|
|
148
|
+
echo "--- B0. REAL assignment-provider claim under CI writes a well-formed record.actor; verify-hold self-recognizes ---"
|
|
149
|
+
CLI="$ROOT/build/src/cli.js"
|
|
150
|
+
RT_ROOT="$TMPDIR_EVAL/rt-artifact-root"
|
|
151
|
+
RT_SLUG="ci-roundtrip"
|
|
152
|
+
(
|
|
153
|
+
unset FLOW_AGENTS_ACTOR CLAUDE_CODE_SESSION_ID CODEX_SESSION_ID OPENCODE_SESSION_ID PI_SESSION_ID CLAUDECODE
|
|
154
|
+
export "${GHA[@]}"
|
|
155
|
+
node "$CLI" assignment-provider claim --provider local-file --artifact-root "$RT_ROOT" \
|
|
156
|
+
--subject-id "$RT_SLUG" --branch main --artifact-dir "$RT_SLUG"
|
|
157
|
+
) >"$TMPDIR_EVAL/rt-claim.out" 2>"$TMPDIR_EVAL/rt-claim.err"
|
|
158
|
+
RT_REC="$RT_ROOT/assignment/$RT_SLUG.json"
|
|
159
|
+
if [[ -f "$RT_REC" ]]; then
|
|
160
|
+
pass "assignment-provider claim under CI wrote a record"
|
|
161
|
+
RT_RUNTIME="$(json_query "$RT_REC" "actor.runtime")"
|
|
162
|
+
RT_SESSION="$(json_query "$RT_REC" "actor.session_id")"
|
|
163
|
+
RT_KEY="$(json_query "$RT_REC" "actor_key")"
|
|
164
|
+
[[ "$RT_RUNTIME" == "github-actions" ]] && pass "record.actor.runtime is github-actions (NOT 'unknown' — F1 reconstruction fix)" || fail "record.actor.runtime was '$RT_RUNTIME', expected github-actions (F1 regressed: loadActorStruct not CI-aware)"
|
|
165
|
+
[[ "$RT_SESSION" == "555-1-deliver" ]] && pass "record.actor.session_id is the bare CI job id 555-1-deliver (NOT the whole triple — F1 fix)" || fail "record.actor.session_id was '$RT_SESSION', expected 555-1-deliver (F1 regressed)"
|
|
166
|
+
[[ "$RT_KEY" == github-actions:555-1-deliver:* ]] && pass "record.actor_key is the canonical CI key ('$RT_KEY')" || fail "record.actor_key was '$RT_KEY', expected github-actions:555-1-deliver:<host>"
|
|
167
|
+
else
|
|
168
|
+
fail "assignment-provider claim under CI wrote NO record: $(cat "$TMPDIR_EVAL/rt-claim.out" "$TMPDIR_EVAL/rt-claim.err")"
|
|
169
|
+
fi
|
|
170
|
+
# Fresh-subprocess verify-hold under the same CI env → the actor written by the real claim is self.
|
|
171
|
+
mkdir -p "$RT_ROOT/$RT_SLUG"
|
|
172
|
+
if verify_hold_under_ci "$RT_ROOT/$RT_SLUG" "${GHA[@]}" >"$TMPDIR_EVAL/rt-vh.out" 2>"$TMPDIR_EVAL/rt-vh.err"; then
|
|
173
|
+
pass "verify-hold self-recognizes the CI actor from a REAL claim across subprocesses (end-to-end seam)"
|
|
174
|
+
else
|
|
175
|
+
fail "verify-hold FALSE-BLOCKED the CI actor's own real claim across subprocesses: $(cat "$TMPDIR_EVAL/rt-vh.out" "$TMPDIR_EVAL/rt-vh.err")"
|
|
176
|
+
fi
|
|
177
|
+
[[ "$(json_query "$TMPDIR_EVAL/rt-vh.out" "reason")" == "self_is_holder" ]] && pass "real-round-trip verify-hold reason is self_is_holder" || fail "real-round-trip verify-hold reason was not self_is_holder: $(cat "$TMPDIR_EVAL/rt-vh.out")"
|
|
178
|
+
|
|
179
|
+
echo "--- B1. a stable CI actor BLOCKS a differing, assignment-backed holder (ENFORCE, not advisory) ---"
|
|
180
|
+
BLOCK_SLUG="ci-verify-hold-block"
|
|
181
|
+
BLOCK_DIR="$ARTIFACT_ROOT/$BLOCK_SLUG"
|
|
182
|
+
mkdir -p "$BLOCK_DIR"
|
|
183
|
+
# Held (fresh) by a clearly different actor -> a durable, assignment-backed conflict.
|
|
184
|
+
write_assignment_record "$ARTIFACT_ROOT" "$BLOCK_SLUG" "other-session" "eval-actor-ci-other-holder"
|
|
185
|
+
append_liveness_event "$ARTIFACT_ROOT" "$BLOCK_SLUG" "eval-actor-ci-other-holder" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 1800
|
|
186
|
+
# Current actor: a STABLE CI identity (GitHub env), NO FLOW_AGENTS_ACTOR — before #398 this would
|
|
187
|
+
# have fallen to ancestry (unstable) and the gate would have degraded to advisory/PASS.
|
|
188
|
+
if verify_hold_under_ci "$BLOCK_DIR" "${GHA[@]}" >"$TMPDIR_EVAL/ci-block.out" 2>"$TMPDIR_EVAL/ci-block.err"; then
|
|
189
|
+
fail "verify-hold under a stable CI identity should BLOCK a differing assignment-backed holder (ENFORCE) — got exit 0: $(cat "$TMPDIR_EVAL/ci-block.out" "$TMPDIR_EVAL/ci-block.err")"
|
|
190
|
+
else
|
|
191
|
+
pass "verify-hold under a stable CI identity BLOCKS a differing assignment-backed holder (the #398 enforce payoff)"
|
|
192
|
+
fi
|
|
193
|
+
[[ "$(json_query "$TMPDIR_EVAL/ci-block.out" "ok")" == "false" ]] && pass "CI-block verify-hold JSON reports ok:false" || fail "CI-block verify-hold JSON did not report ok:false: $(cat "$TMPDIR_EVAL/ci-block.out")"
|
|
194
|
+
CI_BLOCK_REASON="$(json_query "$TMPDIR_EVAL/ci-block.out" "reason")"
|
|
195
|
+
[[ "$CI_BLOCK_REASON" != "actor-identity-unstable-advisory-only" ]] && pass "CI-block reason is NOT the unstable-advisory degradation ('$CI_BLOCK_REASON') — the gate genuinely enforces" || fail "CI actor still degraded to advisory (reason=$CI_BLOCK_REASON) — #398 did not make the CI identity stable"
|
|
196
|
+
|
|
197
|
+
echo "--- B2. a CI actor's OWN claim is recognized as self and PASSES (the reconstruction-seam fix) ---"
|
|
198
|
+
SELF_SLUG="ci-verify-hold-self"
|
|
199
|
+
SELF_DIR="$ARTIFACT_ROOT/$SELF_SLUG"
|
|
200
|
+
mkdir -p "$SELF_DIR"
|
|
201
|
+
# The holder's actor_key IS the CI actor resolveActor() produces under the GitHub env — so the
|
|
202
|
+
# claim written in one CI step is recognized as self at publish in a later step. If the
|
|
203
|
+
# resolveEnsureSessionActor reconstruction diverged (rebuilt an ancestry struct), this would
|
|
204
|
+
# FALSE-BLOCK — exactly the bug #398 removes.
|
|
205
|
+
write_assignment_record "$ARTIFACT_ROOT" "$SELF_SLUG" "self-session" "$GH_ACTOR_1"
|
|
206
|
+
if verify_hold_under_ci "$SELF_DIR" "${GHA[@]}" >"$TMPDIR_EVAL/ci-self.out" 2>"$TMPDIR_EVAL/ci-self.err"; then
|
|
207
|
+
pass "verify-hold PASSES when the CI actor is the holder (self recognized across subprocesses — seam fix)"
|
|
208
|
+
else
|
|
209
|
+
fail "verify-hold FALSE-BLOCKED a CI actor on its OWN claim (the reconstruction seam regressed): $(cat "$TMPDIR_EVAL/ci-self.out" "$TMPDIR_EVAL/ci-self.err")"
|
|
210
|
+
fi
|
|
211
|
+
[[ "$(json_query "$TMPDIR_EVAL/ci-self.out" "ok")" == "true" ]] && pass "CI-self verify-hold JSON reports ok:true" || fail "CI-self verify-hold JSON did not report ok:true: $(cat "$TMPDIR_EVAL/ci-self.out")"
|
|
212
|
+
[[ "$(json_query "$TMPDIR_EVAL/ci-self.out" "reason")" == "self_is_holder" ]] && pass "CI-self verify-hold reason is self_is_holder (the CI actor matched the stored actor_key)" || fail "CI-self verify-hold reason was not self_is_holder: $(cat "$TMPDIR_EVAL/ci-self.out")"
|
|
213
|
+
|
|
214
|
+
echo ""
|
|
215
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
216
|
+
echo "ALL CI ACTOR IDENTITY CHECKS PASSED"
|
|
217
|
+
exit 0
|
|
218
|
+
else
|
|
219
|
+
echo "$errors CHECK(S) FAILED"
|
|
220
|
+
exit 1
|
|
221
|
+
fi
|
|
@@ -21,9 +21,9 @@ json_query() {
|
|
|
21
21
|
node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=Array.isArray(cur) ? cur[Number(part)] : cur[part]; console.log(cur);' "$1" "$2"
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
[[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.scanned")" == "
|
|
24
|
+
[[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.scanned")" == "16" ]] && pass "audit scans all fixture groups" || fail "audit scans all fixture groups"
|
|
25
25
|
[[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.retire_candidates")" == "0" ]] && pass "audit finds no unowned retire candidates" || fail "audit finds no unowned retire candidates"
|
|
26
|
-
[[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.kept")" == "
|
|
26
|
+
[[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.kept")" == "16" ]] && pass "audit keeps all owned fixture groups" || fail "audit keeps all owned fixture groups"
|
|
27
27
|
|
|
28
28
|
node - "$TMPDIR_EVAL/audit.json" <<'NODE'
|
|
29
29
|
const fs = require("node:fs");
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
# 4. RECONCILE-MATCHING: delivery trust.bundle (+ matching-sha checkpoint sibling) + CI
|
|
10
10
|
# pass -> exit 0.
|
|
11
11
|
# 5. FAIL-SOFT: no trust.bundle -> publishDelivery skips, record-release exits 0.
|
|
12
|
+
# 6. AC6 SHAPE-INVALID-REFUSED (#356): a shape-invalid trust.bundle (test_output claim
|
|
13
|
+
# naming a non-manifest command) is REFUSED by publishDelivery() -- non-zero exit, loud
|
|
14
|
+
# message, nothing copied to delivery/. Distinct from FAIL-SOFT (bundle absent).
|
|
12
15
|
#
|
|
13
16
|
# Deterministic, no model spend, self-cleaning.
|
|
14
17
|
# Usage: bash evals/integration/test_publish_delivery.sh
|
|
@@ -118,7 +121,14 @@ FIXTURE1="$TMP/fixture1.json"
|
|
|
118
121
|
write_bundle_to "$FIXTURE1" "node --version" "true"
|
|
119
122
|
setup_session "$AROOT1" "$SLUG1" "$FIXTURE1"
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
# #356: the fixture bundle's evidence.execution.label is "node --version" (a real,
|
|
125
|
+
# deterministic command, not a manifest-registered one in this scratch repo) — the new
|
|
126
|
+
# reconcile-preflight gate inside publishDelivery() resolves the manifest the SAME way
|
|
127
|
+
# trust-reconcile.js itself does, whose legacy fallback tier folds TRUST_RECONCILE_COMMANDS
|
|
128
|
+
# into the manifest. Setting it here makes this fixture genuinely shape-valid (a
|
|
129
|
+
# manifest-matched command claim), matching what CI would actually accept for a repo whose
|
|
130
|
+
# canonical verify is "node --version" — not a preflight-specific carve-out.
|
|
131
|
+
rr_out1=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" record-release "$SESSION_DIR1" \
|
|
122
132
|
--decision merge \
|
|
123
133
|
--gate-json '{"name":"merge","status":"pass","summary":"Ready."}' \
|
|
124
134
|
--summary "Release." --repo-root "$REPO1" \
|
|
@@ -168,7 +178,9 @@ FIXTURE2="$TMP/fixture2.json"
|
|
|
168
178
|
write_bundle_to "$FIXTURE2" "node --version" "true"
|
|
169
179
|
setup_session "$AROOT2" "$SLUG2" "$FIXTURE2"
|
|
170
180
|
|
|
171
|
-
|
|
181
|
+
# #356: same rationale as TEST 1 above — make the fixture genuinely shape-valid for the
|
|
182
|
+
# new reconcile-preflight gate inside publishDelivery().
|
|
183
|
+
pd_out=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR2" \
|
|
172
184
|
--repo-root "$REPO2" 2>&1)
|
|
173
185
|
pd_exit=$?
|
|
174
186
|
|
|
@@ -282,6 +294,51 @@ else
|
|
|
282
294
|
_fail "FAIL-SOFT: delivery/trust.bundle was created unexpectedly"
|
|
283
295
|
fi
|
|
284
296
|
|
|
297
|
+
# ==== TEST 6: AC6 SHAPE-INVALID-REFUSED (#356) ======================
|
|
298
|
+
# publishDelivery() now runs the reconcile-preflight shape check BEFORE copying anything
|
|
299
|
+
# into delivery/ (#356 Wave 3, AC6). A bundle whose test_output-evidenced claim names a
|
|
300
|
+
# command that is not in the resolved manifest (no TRUST_RECONCILE_COMMANDS/run-baseline.sh
|
|
301
|
+
# entry naming it here) is shape-invalid and must be REFUSED: publish-delivery exits
|
|
302
|
+
# non-zero, loudly, and nothing is copied to delivery/. This must never be confused with the
|
|
303
|
+
# FAIL-SOFT (bundle-absent) case above, which stays a soft, exit-0 no-op.
|
|
304
|
+
echo ""
|
|
305
|
+
echo "=== TEST 6: AC6 SHAPE-INVALID-REFUSED ==="
|
|
306
|
+
|
|
307
|
+
REPO6="$TMP/repo6"
|
|
308
|
+
AROOT6="$REPO6/.flow-agents"
|
|
309
|
+
SLUG6="pd-shape-invalid-test"
|
|
310
|
+
SESSION_DIR6="$AROOT6/$SLUG6"
|
|
311
|
+
mkdir -p "$REPO6/kits"
|
|
312
|
+
|
|
313
|
+
FIXTURE6="$TMP/fixture6.json"
|
|
314
|
+
write_bundle_to "$FIXTURE6" "node --version" "true"
|
|
315
|
+
setup_session "$AROOT6" "$SLUG6" "$FIXTURE6"
|
|
316
|
+
|
|
317
|
+
# Deliberately NO TRUST_RECONCILE_COMMANDS env var here — "node --version" resolves to no
|
|
318
|
+
# manifest entry in this scratch repo (no run-baseline.sh/package.json manifest source
|
|
319
|
+
# either), so the bundle is shape-invalid (not-run: command not in the reconcile manifest).
|
|
320
|
+
shape_out=$(flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR6" --repo-root "$REPO6" 2>&1)
|
|
321
|
+
shape_exit=$?
|
|
322
|
+
|
|
323
|
+
if [[ $shape_exit -ne 0 ]]; then
|
|
324
|
+
_pass "AC6 SHAPE-INVALID-REFUSED: publish-delivery exits non-zero for a shape-invalid bundle"
|
|
325
|
+
else
|
|
326
|
+
_fail "AC6 SHAPE-INVALID-REFUSED: expected non-zero exit, got 0 -- $shape_out"
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if echo "$shape_out" | grep -q "REFUSING to publish"; then
|
|
330
|
+
_pass "AC6 SHAPE-INVALID-REFUSED: output names the refusal loudly (REFUSING to publish)"
|
|
331
|
+
else
|
|
332
|
+
_fail "AC6 SHAPE-INVALID-REFUSED: expected a loud REFUSING to publish message, got: $shape_out"
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
DELIVERY_BUNDLE6="$REPO6/delivery/$SLUG6/trust.bundle"
|
|
336
|
+
if [[ ! -f "$DELIVERY_BUNDLE6" ]]; then
|
|
337
|
+
_pass "AC6 SHAPE-INVALID-REFUSED: delivery/$SLUG6/trust.bundle NOT created for a shape-invalid bundle"
|
|
338
|
+
else
|
|
339
|
+
_fail "AC6 SHAPE-INVALID-REFUSED: delivery/$SLUG6/trust.bundle was created despite invalid shape"
|
|
340
|
+
fi
|
|
341
|
+
|
|
285
342
|
# ---- Summary ----
|
|
286
343
|
echo ""
|
|
287
344
|
echo "----------------------------------------------"
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_reconcile_preflight.sh — #356 (Local Reconcile-Shape Preflight) proof.
|
|
3
|
+
#
|
|
4
|
+
# Proves `workflow-sidecar reconcile-preflight <artifact-dir>` catches every documented
|
|
5
|
+
# ADR-0020-invalid trust.bundle shape LOCALLY, before git push — reusing (never forking)
|
|
6
|
+
# scripts/lib/reconcile-shape.js's own classification (the same module
|
|
7
|
+
# scripts/ci/trust-reconcile.js requires), so the local check can never silently drift from
|
|
8
|
+
# what CI enforces. Mirrors evals/integration/test_trust_reconcile_negatives.sh's
|
|
9
|
+
# fixture-per-case + run_case() helper shape.
|
|
10
|
+
#
|
|
11
|
+
# 1. non-manifest command-backed claim (reuses trust-reconcile-exploits/no-label-bypass.json)
|
|
12
|
+
# → not-run divergence + fix hint to name the exact manifest command.
|
|
13
|
+
# 2. unwaived assumed claim (reuses trust-reconcile-exploits/skip-assumed-bypass.json)
|
|
14
|
+
# → unwaived-assumed divergence + fix hint (also carries Q2's root-cause hint for shape 5).
|
|
15
|
+
# 3. un-superseded disputed critique (new fixture: disputed-critique-unsuperseded.json)
|
|
16
|
+
# → session-local-failed divergence (a workflow.critique.review claim, no superseded_by).
|
|
17
|
+
# 4. standalone fail/disputed session-local claim, non-critique (#292/#384 variant; new
|
|
18
|
+
# fixture: standalone-disputed-session-local.json) → the SAME session-local-failed
|
|
19
|
+
# divergence type as case 3, proving a disjoint pre-existing failure can never be
|
|
20
|
+
# smuggled in as a standalone claim either way.
|
|
21
|
+
# 5. waiver-voided-by-mixed-call — reproduced via the REAL producer CLI (record-evidence),
|
|
22
|
+
# not a hand-built fixture. Confirms the actual current behavior: the command-backed-
|
|
23
|
+
# waiver guard at src/cli/workflow-sidecar.ts's recordEvidence() dies at RECORD time
|
|
24
|
+
# when --accepted-gap-reason/--waived-by is combined with ANY command-backed check in
|
|
25
|
+
# the same call (even alongside other, session-local checks in the same --check-json
|
|
26
|
+
# set) — so there is no "silently voided" bundle to reconcile in this repo's current
|
|
27
|
+
# producer; this is documented, correct, prevention-at-the-source, not a preflight gap.
|
|
28
|
+
# 6. dropped waiver metadata round-trip (AC2) — a regression PROOF, not a preflight
|
|
29
|
+
# detection: record-evidence (waiver on a session-local check) → record-critique
|
|
30
|
+
# (rebuilds via checksFromBundle) → the bundle's claim STILL has metadata.waiver.
|
|
31
|
+
#
|
|
32
|
+
# Plus: CLEAN-BUNDLE (AC4, reuses trust-reconcile-mixed-bundle/mixed-bundle.json) → exit 0,
|
|
33
|
+
# no issues. AC5 (local/fast, no CI spawn): a --manifest override naming a sentinel-writing
|
|
34
|
+
# command is never invoked by the preflight on the clean bundle.
|
|
35
|
+
#
|
|
36
|
+
# Deterministic, no model spend, self-cleaning.
|
|
37
|
+
# Usage: bash evals/integration/test_reconcile_preflight.sh
|
|
38
|
+
|
|
39
|
+
set -uo pipefail
|
|
40
|
+
|
|
41
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
42
|
+
source "$ROOT/evals/lib/node.sh"
|
|
43
|
+
|
|
44
|
+
WRITER="workflow-sidecar"
|
|
45
|
+
FX_EXPLOITS="$ROOT/evals/fixtures/trust-reconcile-exploits"
|
|
46
|
+
FX_PREFLIGHT="$ROOT/evals/fixtures/reconcile-preflight"
|
|
47
|
+
MIXED_BUNDLE="$ROOT/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json"
|
|
48
|
+
|
|
49
|
+
TMP="$(mktemp -d)"
|
|
50
|
+
errors=0
|
|
51
|
+
|
|
52
|
+
_pass() { echo " PASS: $1"; }
|
|
53
|
+
_fail() { echo " FAIL: $1"; errors=$((errors + 1)); }
|
|
54
|
+
|
|
55
|
+
cleanup() { rm -rf "$TMP"; }
|
|
56
|
+
trap cleanup EXIT
|
|
57
|
+
|
|
58
|
+
# run_case <label> <bundle> <needle...>
|
|
59
|
+
# Copies <bundle> into a fresh session dir as trust.bundle, runs reconcile-preflight, and
|
|
60
|
+
# asserts: exit non-zero AND stdout/stderr contains EVERY needle passed after <bundle>.
|
|
61
|
+
run_case() {
|
|
62
|
+
local label="$1" bundle="$2"
|
|
63
|
+
shift 2
|
|
64
|
+
echo "=== $label ==="
|
|
65
|
+
if [[ ! -f "$bundle" ]]; then _fail "$label: fixture not found at $bundle"; return; fi
|
|
66
|
+
local session="$TMP/case-$RANDOM$RANDOM"
|
|
67
|
+
mkdir -p "$session"
|
|
68
|
+
cp "$bundle" "$session/trust.bundle"
|
|
69
|
+
local out code
|
|
70
|
+
out="$(flow_agents_node "$WRITER" reconcile-preflight "$session" --repo-root "$ROOT" 2>&1)"
|
|
71
|
+
code=$?
|
|
72
|
+
if [[ $code -ne 0 ]]; then
|
|
73
|
+
_pass "$label: reconcile-preflight exits non-zero ($code)"
|
|
74
|
+
else
|
|
75
|
+
_fail "$label: expected non-zero exit, got 0 — output: $out"
|
|
76
|
+
fi
|
|
77
|
+
local needle
|
|
78
|
+
for needle in "$@"; do
|
|
79
|
+
if echo "$out" | grep -qF "$needle"; then
|
|
80
|
+
_pass "$label: output contains \"$needle\""
|
|
81
|
+
else
|
|
82
|
+
_fail "$label: expected \"$needle\" in output — output: $out"
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ==== Case 1: non-manifest command-backed claim (reused fixture) ====================
|
|
88
|
+
run_case "case 1: non-manifest command-backed claim" "$FX_EXPLOITS/no-label-bypass.json" \
|
|
89
|
+
"c-fabricated-test" \
|
|
90
|
+
"no manifest-matched execution.label" \
|
|
91
|
+
"FIX: fold this into a non-command summary"
|
|
92
|
+
|
|
93
|
+
# ==== Case 2: unwaived assumed claim (reused fixture) ================================
|
|
94
|
+
run_case "case 2: unwaived assumed claim" "$FX_EXPLOITS/skip-assumed-bypass.json" \
|
|
95
|
+
"c-skipped" \
|
|
96
|
+
"re-derived status 'assumed' but carries no waiver" \
|
|
97
|
+
"requires a documented waiver (--accepted-gap-reason/--waived-by)"
|
|
98
|
+
|
|
99
|
+
# ==== Case 3: un-superseded disputed critique (new fixture) ==========================
|
|
100
|
+
run_case "case 3: un-superseded disputed critique" "$FX_PREFLIGHT/disputed-critique-unsuperseded.json" \
|
|
101
|
+
"c-disputed-critique" \
|
|
102
|
+
"workflow.critique.review" \
|
|
103
|
+
"has re-derived status 'disputed'" \
|
|
104
|
+
"FIX: a disputed/failing claim always blocks reconcile. Document a disjoint pre-existing failure as prose in a WAIVED non-command summary, not as a standalone claim."
|
|
105
|
+
|
|
106
|
+
# ==== Case 4: standalone fail/disputed session-local claim, non-critique (#292/#384) =
|
|
107
|
+
run_case "case 4: standalone disputed session-local claim (non-critique)" "$FX_PREFLIGHT/standalone-disputed-session-local.json" \
|
|
108
|
+
"c-standalone-disputed" \
|
|
109
|
+
"has re-derived status 'disputed'" \
|
|
110
|
+
"FIX: a disputed/failing claim always blocks reconcile. Document a disjoint pre-existing failure as prose in a WAIVED non-command summary, not as a standalone claim."
|
|
111
|
+
|
|
112
|
+
# ==== Case 5: waiver-voided-by-mixed-call — REAL PRODUCER CLI reproduction ===========
|
|
113
|
+
# Q2 resolution: shape #5 is NOT a distinct predicate — per the plan, confirm the ACTUAL
|
|
114
|
+
# current behavior first. This repo's guard at record-evidence time (the command-backed-
|
|
115
|
+
# waiver check ahead of any bundle write) dies loudly BEFORE a bundle is ever written when
|
|
116
|
+
# --accepted-gap-reason/--waived-by is combined with a command-backed check in the same
|
|
117
|
+
# call — including when a session-local check is ALSO present in the same call. That is the
|
|
118
|
+
# correct prevention (fail at the source, not a silently-voided bundle downstream) — assert
|
|
119
|
+
# the die message is clear and that no trust.bundle is produced.
|
|
120
|
+
echo "=== case 5: waiver-voided-by-mixed-call (real producer CLI reproduction) ==="
|
|
121
|
+
|
|
122
|
+
AROOT5="$TMP/repo5/.flow-agents"
|
|
123
|
+
SLUG5="case5-mixed-call"
|
|
124
|
+
SESSION_DIR5="$AROOT5/$SLUG5"
|
|
125
|
+
mkdir -p "$TMP/repo5/kits" "$AROOT5"
|
|
126
|
+
|
|
127
|
+
flow_agents_node "$WRITER" ensure-session --artifact-root "$AROOT5" --task-slug "$SLUG5" \
|
|
128
|
+
--title "Case 5" --summary "waiver-voided-by-mixed-call repro" --criterion "x" \
|
|
129
|
+
--timestamp "2026-07-04T10:00:00Z" >/dev/null 2>&1
|
|
130
|
+
flow_agents_node "$WRITER" init-plan "$SESSION_DIR5/${SLUG5}--deliver.md" \
|
|
131
|
+
--source-request "t" --summary "t" --timestamp "2026-07-04T10:01:00Z" >/dev/null 2>&1
|
|
132
|
+
|
|
133
|
+
# 5a. Single command-backed check + waiver flags in the SAME call → dies at record time.
|
|
134
|
+
case5a_out="$(flow_agents_node "$WRITER" record-evidence "$SESSION_DIR5" --verdict pass \
|
|
135
|
+
--check-json '{"id":"cmdcheck","kind":"test","status":"pass","summary":"cmd ran","command":"npm run validate:source --"}' \
|
|
136
|
+
--accepted-gap-reason "load test env unavailable" --waived-by "brian" \
|
|
137
|
+
--timestamp "2026-07-04T10:02:00Z" 2>&1)"
|
|
138
|
+
case5a_code=$?
|
|
139
|
+
if [[ $case5a_code -ne 0 ]]; then
|
|
140
|
+
_pass "case 5a: record-evidence dies at RECORD time for a mixed waiver+command-backed call ($case5a_code)"
|
|
141
|
+
else
|
|
142
|
+
_fail "case 5a: expected record-evidence to die (non-zero exit), got 0 — output: $case5a_out"
|
|
143
|
+
fi
|
|
144
|
+
if echo "$case5a_out" | grep -qF "cannot be applied to a command-backed check"; then
|
|
145
|
+
_pass "case 5a: die message clearly names the command-backed-waiver rejection"
|
|
146
|
+
else
|
|
147
|
+
_fail "case 5a: expected the command-backed-waiver die message — output: $case5a_out"
|
|
148
|
+
fi
|
|
149
|
+
if [[ ! -f "$SESSION_DIR5/trust.bundle" ]]; then
|
|
150
|
+
_pass "case 5a: no trust.bundle written (dies BEFORE any bundle write — no silently-voided waiver bundle downstream)"
|
|
151
|
+
else
|
|
152
|
+
_fail "case 5a: a trust.bundle was written despite the mixed-call guard firing"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# 5b. TWO checks in the SAME call (one command-backed, one session-local) + waiver flags →
|
|
156
|
+
# the guard scans ALL checks in the call, so this ALSO dies at record time (proves the
|
|
157
|
+
# guard is not scoped only to a single-check call — the exact "mixed call" shape).
|
|
158
|
+
case5b_out="$(flow_agents_node "$WRITER" record-evidence "$SESSION_DIR5" --verdict pass \
|
|
159
|
+
--check-json '{"id":"cmdcheck2","kind":"test","status":"pass","summary":"cmd ran","command":"npm run validate:source --"}' \
|
|
160
|
+
--check-json '{"id":"sessioncheck2","kind":"external","status":"skip","summary":"manual load test skip"}' \
|
|
161
|
+
--accepted-gap-reason "load test env unavailable" --waived-by "brian" \
|
|
162
|
+
--timestamp "2026-07-04T10:03:00Z" 2>&1)"
|
|
163
|
+
case5b_code=$?
|
|
164
|
+
if [[ $case5b_code -ne 0 ]]; then
|
|
165
|
+
_pass "case 5b: record-evidence ALSO dies for a two-check mixed call (command-backed + session-local, one waiver) ($case5b_code)"
|
|
166
|
+
else
|
|
167
|
+
_fail "case 5b: expected record-evidence to die (non-zero exit), got 0 — output: $case5b_out"
|
|
168
|
+
fi
|
|
169
|
+
if echo "$case5b_out" | grep -qF "cannot be applied to a command-backed check"; then
|
|
170
|
+
_pass "case 5b: die message clearly names the command-backed-waiver rejection"
|
|
171
|
+
else
|
|
172
|
+
_fail "case 5b: expected the command-backed-waiver die message — output: $case5b_out"
|
|
173
|
+
fi
|
|
174
|
+
if [[ ! -f "$SESSION_DIR5/trust.bundle" ]]; then
|
|
175
|
+
_pass "case 5b: no trust.bundle written"
|
|
176
|
+
else
|
|
177
|
+
_fail "case 5b: a trust.bundle was written despite the mixed-call guard firing"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# 5c. Sanity: the Q2 root-cause hint IS reachable via reconcile-preflight's message for an
|
|
181
|
+
# honest (non-mixed-call) unwaived-assumed divergence — i.e. an agent who DOES end up
|
|
182
|
+
# with an unwaived-assumed claim (by whatever path) gets pointed at the mixed-call guard
|
|
183
|
+
# as a likely root cause. Reuses case 2's fixture; restated here for traceability to
|
|
184
|
+
# shape #5 per Q2's resolution (shape #5 is #2 enriched with a root-cause hint, not a
|
|
185
|
+
# distinct predicate).
|
|
186
|
+
Q2_SESSION="$TMP/q2-check"
|
|
187
|
+
mkdir -p "$Q2_SESSION"
|
|
188
|
+
cp "$FX_EXPLOITS/skip-assumed-bypass.json" "$Q2_SESSION/trust.bundle"
|
|
189
|
+
q2_out="$(flow_agents_node "$WRITER" reconcile-preflight "$Q2_SESSION" --repo-root "$ROOT" 2>&1)"
|
|
190
|
+
if echo "$q2_out" | grep -qF "voids/rejects the waiver"; then
|
|
191
|
+
_pass "case 5c (Q2 traceability): unwaived-assumed's preflight message carries the mixed-call root-cause hint"
|
|
192
|
+
else
|
|
193
|
+
_fail "case 5c (Q2 traceability): expected the mixed-call root-cause hint on an unwaived-assumed divergence — output: $q2_out"
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# ==== Case 6 (AC2): dropped waiver metadata round-trip — regression PROOF, not detection ==
|
|
197
|
+
echo "=== case 6 (AC2): waiver metadata round-trip survives record-critique rebuild ==="
|
|
198
|
+
|
|
199
|
+
AROOT6="$TMP/repo6/.flow-agents"
|
|
200
|
+
SLUG6="case6-waiver-roundtrip"
|
|
201
|
+
SESSION_DIR6="$AROOT6/$SLUG6"
|
|
202
|
+
mkdir -p "$TMP/repo6/kits" "$AROOT6"
|
|
203
|
+
|
|
204
|
+
flow_agents_node "$WRITER" ensure-session --artifact-root "$AROOT6" --task-slug "$SLUG6" \
|
|
205
|
+
--title "Case 6" --summary "waiver round-trip repro" --criterion "x" \
|
|
206
|
+
--timestamp "2026-07-04T10:00:00Z" >/dev/null 2>&1
|
|
207
|
+
flow_agents_node "$WRITER" init-plan "$SESSION_DIR6/${SLUG6}--deliver.md" \
|
|
208
|
+
--source-request "t" --summary "t" --timestamp "2026-07-04T10:01:00Z" >/dev/null 2>&1
|
|
209
|
+
|
|
210
|
+
flow_agents_node "$WRITER" record-evidence "$SESSION_DIR6" --verdict pass \
|
|
211
|
+
--check-json '{"id":"loadtest","kind":"external","status":"skip","summary":"load test skip"}' \
|
|
212
|
+
--accepted-gap-reason "load test env unavailable" --waived-by "brian" \
|
|
213
|
+
--timestamp "2026-07-04T10:02:00Z" >/dev/null 2>&1
|
|
214
|
+
|
|
215
|
+
flow_agents_node "$WRITER" record-critique "$SESSION_DIR6" --verdict pass --summary "ok." \
|
|
216
|
+
--timestamp "2026-07-04T10:03:00Z" >/dev/null 2>&1
|
|
217
|
+
|
|
218
|
+
if [[ -f "$SESSION_DIR6/trust.bundle" ]]; then
|
|
219
|
+
_pass "case 6: trust.bundle exists after record-evidence -> record-critique rebuild"
|
|
220
|
+
bundle_json="$(cat "$SESSION_DIR6/trust.bundle")"
|
|
221
|
+
if echo "$bundle_json" | grep -qF '"waiver"'; then
|
|
222
|
+
_pass "case 6: rebuilt trust.bundle's claim STILL carries metadata.waiver (AC2 fix holds — pre-fix this silently dropped)"
|
|
223
|
+
else
|
|
224
|
+
_fail "case 6: rebuilt trust.bundle's claim LOST metadata.waiver — AC2 round-trip regressed. bundle: $bundle_json"
|
|
225
|
+
fi
|
|
226
|
+
if echo "$bundle_json" | grep -qF '"approved_by": "brian"'; then
|
|
227
|
+
_pass "case 6: waiver's approved_by ('brian') survives the rebuild"
|
|
228
|
+
else
|
|
229
|
+
_fail "case 6: expected waiver.approved_by 'brian' to survive the rebuild — bundle: $bundle_json"
|
|
230
|
+
fi
|
|
231
|
+
else
|
|
232
|
+
_fail "case 6: trust.bundle was not found at $SESSION_DIR6/trust.bundle after the round-trip sequence"
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# Reconcile-preflight must ALSO pass cleanly on the rebuilt (waiver-intact) bundle —
|
|
236
|
+
# an accepted-gap waiver on a session-local check is a shape-clean, WAIVED divergence-free
|
|
237
|
+
# bundle by ADR 0020's own rules.
|
|
238
|
+
preflight6_out="$(flow_agents_node "$WRITER" reconcile-preflight "$SESSION_DIR6" --repo-root "$ROOT" 2>&1)"
|
|
239
|
+
preflight6_code=$?
|
|
240
|
+
if [[ $preflight6_code -eq 0 ]]; then
|
|
241
|
+
_pass "case 6: reconcile-preflight exits 0 on the round-tripped, waiver-intact bundle"
|
|
242
|
+
else
|
|
243
|
+
_fail "case 6: expected reconcile-preflight exit 0 on the round-tripped bundle, got $preflight6_code — output: $preflight6_out"
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# ==== CLEAN-BUNDLE (AC4) ==============================================================
|
|
247
|
+
echo "=== CLEAN-BUNDLE (AC4): a valid ADR-0020-conformant bundle passes ==="
|
|
248
|
+
|
|
249
|
+
CLEAN_SESSION="$TMP/clean-bundle"
|
|
250
|
+
mkdir -p "$CLEAN_SESSION"
|
|
251
|
+
cp "$MIXED_BUNDLE" "$CLEAN_SESSION/trust.bundle"
|
|
252
|
+
|
|
253
|
+
clean_out="$(flow_agents_node "$WRITER" reconcile-preflight "$CLEAN_SESSION" --repo-root "$ROOT" 2>&1)"
|
|
254
|
+
clean_code=$?
|
|
255
|
+
if [[ $clean_code -eq 0 ]]; then
|
|
256
|
+
_pass "CLEAN-BUNDLE: reconcile-preflight exits 0 for a valid bundle"
|
|
257
|
+
else
|
|
258
|
+
_fail "CLEAN-BUNDLE: expected exit 0, got $clean_code — output: $clean_out"
|
|
259
|
+
fi
|
|
260
|
+
if echo "$clean_out" | grep -qF "OK — no shape issues found"; then
|
|
261
|
+
_pass "CLEAN-BUNDLE: output reports no issues"
|
|
262
|
+
else
|
|
263
|
+
_fail "CLEAN-BUNDLE: expected the no-issues OK line — output: $clean_out"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# ==== AC5: preflight never spawns a manifest command =================================
|
|
267
|
+
echo "=== AC5: reconcile-preflight never spawns a fresh manifest command ==="
|
|
268
|
+
|
|
269
|
+
AC5_SESSION="$TMP/ac5-bundle"
|
|
270
|
+
mkdir -p "$AC5_SESSION"
|
|
271
|
+
cp "$MIXED_BUNDLE" "$AC5_SESSION/trust.bundle"
|
|
272
|
+
SENTINEL="$TMP/ac5-sentinel-should-not-exist"
|
|
273
|
+
rm -f "$SENTINEL"
|
|
274
|
+
|
|
275
|
+
# --manifest override: the mixed bundle's real command ("npm run check:content-boundary --")
|
|
276
|
+
# PLUS a second entry whose command WRITES a sentinel file if actually executed. Since
|
|
277
|
+
# reconcile-preflight resolves the manifest (a pure, local, no-command-execution lookup) but
|
|
278
|
+
# never calls runCommand/spawns a manifest entry's command to prove reconciliation, the
|
|
279
|
+
# sentinel must never appear.
|
|
280
|
+
AC5_MANIFEST='[{"id":"content-boundary","command":"npm run check:content-boundary --"},{"id":"sentinel","command":"touch '"$SENTINEL"'"}]'
|
|
281
|
+
|
|
282
|
+
ac5_out="$(flow_agents_node "$WRITER" reconcile-preflight "$AC5_SESSION" --repo-root "$ROOT" --manifest "$AC5_MANIFEST" 2>&1)"
|
|
283
|
+
ac5_code=$?
|
|
284
|
+
if [[ $ac5_code -eq 0 ]]; then
|
|
285
|
+
_pass "AC5: reconcile-preflight exits 0 on the clean bundle with the sentinel manifest override"
|
|
286
|
+
else
|
|
287
|
+
_fail "AC5: expected exit 0, got $ac5_code — output: $ac5_out"
|
|
288
|
+
fi
|
|
289
|
+
if [[ ! -f "$SENTINEL" ]]; then
|
|
290
|
+
_pass "AC5: sentinel file absent after the preflight run — no manifest command was spawned"
|
|
291
|
+
else
|
|
292
|
+
_fail "AC5: sentinel file EXISTS — reconcile-preflight spawned a manifest command (AC5 violated)"
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# ---- Summary ----
|
|
296
|
+
echo ""
|
|
297
|
+
echo "----------------------------------------------"
|
|
298
|
+
if [[ $errors -eq 0 ]]; then
|
|
299
|
+
echo "test_reconcile_preflight: all checks passed."
|
|
300
|
+
exit 0
|
|
301
|
+
else
|
|
302
|
+
echo "test_reconcile_preflight: $errors check(s) failed."
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|