@chrono-meta/fh-gate 1.4.75 → 1.4.77

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.
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env bash
2
+ # test_consent_registry.sh — known-pair anchor for scripts/consent_registry_check.sh.
3
+ #
4
+ # WHY (cross-family review rounds 1-3, 2026-07-29)
5
+ # The accept-side consent-promotion rule went REJECT -> NARROW-IT -> NARROW-IT across three
6
+ # adversarial rounds. Round 3 stopped attacking the prose and attacked the validator, and found
7
+ # four ways the FLOOR ITSELF was fail-open. Three were confirmed against controls:
8
+ #
9
+ # - `promotion_eligible: "false"` (quoted) is a truthy STRING. Every eligibility test inverted,
10
+ # so an intended-ineligible class was granted. One quote character disarmed the floor.
11
+ # - a duplicate class name silently shadowed the earlier entry, so appending an eligible twin
12
+ # below an ineligible one laundered the ineligible class into a grant.
13
+ # - `standing_consent: {inline: ...}` matched no pattern, so an EXPIRED inline grant was read as
14
+ # "no standing consent recorded" and reported PASS — a grant the checker cannot see is not an
15
+ # absent grant, it is a false clean.
16
+ #
17
+ # Each of those is a check that reported green while doing nothing, which is worse than an absent
18
+ # check: it buys confidence without enforcement. Hence this anchor.
19
+ #
20
+ # Lanes: N* = known-negative (must pass) · P* = known-positive (must be caught) · D* = degrade.
21
+ # Exit 0 = all lanes correct. Exit 1 = the validator regressed toward permissiveness.
22
+ set -uo pipefail
23
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24
+ CHK="$ROOT/scripts/consent_registry_check.sh"
25
+ [ -f "$CHK" ] || { echo "FAIL: $CHK not found"; exit 1; }
26
+
27
+ pass=0; fail=0
28
+ ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
29
+ bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
30
+ TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
31
+
32
+ # want_rc: expected exit. want_rule: a rule id that must appear in the output ("" = don't care).
33
+ lane() {
34
+ local desc="$1" want_rc="$2" want_rule="$3"
35
+ bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
36
+ local rc=$?
37
+ if [ "$rc" -ne "$want_rc" ]; then
38
+ bad "$desc — exit $rc, expected $want_rc"; sed 's/^/ /' "$TD/o"; return
39
+ fi
40
+ if [ -n "$want_rule" ] && ! grep -q "❌ $want_rule" "$TD/o"; then
41
+ bad "$desc — exit was right but rule $want_rule was not the reason"; sed 's/^/ /' "$TD/o"; return
42
+ fi
43
+ ok "$desc"
44
+ }
45
+
46
+ OKCLASS=' - {name: ok, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
47
+ mkreg() { printf 'classes:\n%s\n' "$1" > "$TD/r.yaml"; }
48
+ mkuap() { printf '%s\n' "$1" > "$TD/u.md"; }
49
+
50
+ mkreg "$OKCLASS"
51
+ mkuap 'standing_consent:
52
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
53
+ lane "N1 a well-formed registry + eligible unexpired scoped grant passes" 0 ""
54
+
55
+ mkuap 'standing_consent:
56
+ ok: declined'
57
+ lane "N2 a declined record is a valid non-grant state, not a malformed grant" 0 ""
58
+
59
+ mkreg ' - {name: s, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: true}'
60
+ mkuap 'standing_consent: {}'
61
+ lane "P1 a class naming an irreversible SINK cannot declare itself promotable" 1 "R2"
62
+
63
+ mkreg ' - {name: f, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [go-public], promotion_eligible: true}'
64
+ lane "P2 taint — a class that FEEDS an irreversible sink cannot be promotable" 1 "R2"
65
+
66
+ mkreg "$OKCLASS"
67
+ mkuap 'standing_consent:
68
+ ghost: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
69
+ lane "P3 a grant on an unregistered class is refused (unregistered == unknown)" 1 "R3"
70
+
71
+ mkuap 'standing_consent:
72
+ ok: {granted: 2026-01-01, expires: 2026-06-30, effects: [read], target: t}'
73
+ lane "P4 an expired lease does not keep running" 1 "R5"
74
+
75
+ mkuap 'standing_consent:
76
+ ok: {granted: 2026-07-29, expires: 2026-12-31}'
77
+ lane "P5 a grant with no recorded scope has no re-validation baseline" 1 "R6"
78
+
79
+ # P6-P8 are the round-3 fail-opens. Each of these PASSED before the fix.
80
+ mkreg ' - {name: q, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: "false"}'
81
+ mkuap 'standing_consent:
82
+ q: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
83
+ lane "P6 quoted \"false\" is rejected as a type error, not read as truthy" 1 "R1-b"
84
+
85
+ mkreg ' - {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: false}
86
+ - {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
87
+ mkuap 'standing_consent:
88
+ d: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
89
+ lane "P7 a duplicate class name cannot launder an ineligible class" 1 "R1-c"
90
+
91
+ mkreg "$OKCLASS"
92
+ mkuap 'standing_consent: {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
93
+ lane "P8 an INLINE grant is parsed, not silently read as 'nothing granted'" 1 "R5"
94
+
95
+ # P10-P11 are the round-4 fail-opens. Both PASSED before the fix, and both were confirmed against a
96
+ # control: the SAME expired grant was caught when it stood alone.
97
+ mkreg "$OKCLASS"
98
+ mkuap 'standing_consent: {}
99
+
100
+ notes in between
101
+
102
+ standing_consent:
103
+ ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
104
+ lane "P10 an early empty consent block cannot shadow a later grant (first-match)" 1 "R3"
105
+
106
+ mkuap 'standing_consent:
107
+ ok: {state: "revoked ", granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
108
+ if bash "$CHK" "$TD/r.yaml" "$TD/u.md" 2>&1 | grep -q "0 active grant"; then
109
+ ok "P11 a mapping non-grant state is normalized like the scalar one (no divergent normalizer)"
110
+ else
111
+ bad "P11 \`state: \"revoked \"\` was validated as an ACTIVE grant"
112
+ bash "$CHK" "$TD/r.yaml" "$TD/u.md" 2>&1 | sed 's/^/ /'
113
+ fi
114
+
115
+ # P12 is the round-5 fail-open: `or {}` collapsed FALSY non-mappings into a valid-empty mapping
116
+ # before the type check, so `[]`, `false` and `0` all reported "no standing consent" and exit 0 —
117
+ # while the truthy `[a, b]` was caught. The falsy branch is the one an accident actually produces.
118
+ for badval in ' []' ' false' ' 0'; do
119
+ mkreg "$OKCLASS"
120
+ printf 'standing_consent:\n%s\n' "$badval" > "$TD/u.md"
121
+ lane "P12 a falsy non-mapping standing_consent ($badval) is a type error, not 'none granted'" 1 "R3"
122
+ done
123
+
124
+ # P13/N3 — round-6 fail-open: `standing_consent :` (space or tab before the colon) is the SAME YAML
125
+ # key but matched none of the patterns. Standalone it still failed closed via the no-known-form net;
126
+ # paired with a normal empty block it was invisible to both the count and the extraction, so the
127
+ # empty block parsed and the real grant vanished. Confirmed against a control (the identical expired
128
+ # grant in canonical form was caught).
129
+ mkreg "$OKCLASS"
130
+ mkuap 'standing_consent: {}
131
+
132
+ standing_consent : {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
133
+ lane "P13 a space-before-colon key cannot hide behind a canonical block" 1 "R3"
134
+
135
+ mkuap 'standing_consent :
136
+ ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
137
+ lane "P13-b a space-before-colon BLOCK form is parsed and its grant validated" 1 "R5"
138
+
139
+ mkuap "$(printf 'standing_consent\t: {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}')"
140
+ lane "N3 a tab-before-colon key with a VALID grant passes (no over-block)" 0 ""
141
+
142
+ # P14 — round-7 fail-open: R6 presence-checked `effects`/`target` but never typed them, while the
143
+ # registry side had strict types since R1-b. A fix propagated to one side only is a hole. Control:
144
+ # a MISSING field was caught; a type-wrong one passed.
145
+ mkreg "$OKCLASS"
146
+ mkuap 'standing_consent:
147
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: true, target: 123}'
148
+ lane "P14 a type-wrong grant scope is rejected, not counted as recorded" 1 "R6"
149
+
150
+ mkuap 'standing_consent:
151
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: "read", target: " "}'
152
+ lane "P14-b a scalar effects / whitespace-only target is rejected" 1 "R6"
153
+
154
+ mkuap 'standing_consent:
155
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [], target: t}'
156
+ lane "P14-c an EMPTY effects list is not a baseline" 1 "R6"
157
+
158
+ # H1-H9 — round-8 exhaustive field audit found NINE remaining holes in one pass, after four rounds
159
+ # of one-per-round trickle. The reframe ("audit every field, don't hand me the most important one")
160
+ # is what surfaced them; the per-round drip was a slow enumeration, not convergence.
161
+ mkuap 'standing_consent: {}'
162
+ printf 'classes: false\n' > "$TD/r.yaml"
163
+ lane "H1 a falsy \`classes\` is not laundered into an empty registry" 1 ""
164
+ printf 'classes:\n' > "$TD/r.yaml"
165
+ lane "H1-b a null \`classes\` is N/A, not a clean PASS" 0 ""
166
+ if grep -q 'N/A' "$TD/o"; then ok "H1-c the empty registry is LABELLED N/A"; else bad "H1-c empty registry read as clean"; fi
167
+
168
+ mkreg ' - {name: n, owner: o, mode: 123, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
169
+ lane "H2 a non-string scalar registry field is a type error" 1 "R1-b"
170
+ mkreg ' - {name: n, owner: o, mode: m, target: "", capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
171
+ lane "H2-b a blank registry target is a type error" 1 "R1-b"
172
+ mkreg ' - {name: n, owner: o, mode: m, target: t, capabilities: [read], sinks: [123], feeds: [], promotion_eligible: false}'
173
+ lane "H3 an unreadable sink item is UNDECLARED, not an empty sink list" 1 "R1-b"
174
+
175
+ mkreg "$OKCLASS"
176
+ mkuap 'standing_consent:
177
+ ok: {expires: 2026-12-31, effects: [read], target: t}'
178
+ lane "H5 a grant with no \`granted\` date cannot be audited" 1 "R5"
179
+ mkuap 'standing_consent:
180
+ ok: {granted: 2026-07-29, expires: 29991231, effects: [read], target: t}'
181
+ lane "H7 a non-ISO integer expiry is not a date" 1 "R5"
182
+ mkuap 'standing_consent:
183
+ ok: {granted: 2026-07-29, expires: 9999-12-31, effects: [read], target: t}'
184
+ lane "H7-b an unbounded lease is a transfer wearing a lease's clothes" 1 "R5"
185
+ mkuap 'standing_consent:
186
+ ok: {state: revokedd, granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
187
+ lane "H6 a typo'\''d state fails closed instead of passing as active" 1 "R3"
188
+ mkuap 'standing_consent:
189
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [repo-mutation], target: t}'
190
+ lane "H8 a grant wider than its registered capabilities is refused" 1 "R7"
191
+ mkuap 'standing_consent:
192
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: everything}'
193
+ lane "H8-b grant/class target drift is refused" 1 "R7"
194
+ mkuap 'standing_consent:
195
+ ok: {granted: 2026-07-29, expires: 2020-01-01, effects: [read], target: t}
196
+ ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
197
+ lane "H9 duplicate YAML keys are rejected, not resolved last-wins" 1 ""
198
+
199
+ # H4 had a fix but NO lane — the mutation sweep caught that (disabling it failed zero lanes).
200
+ # An uncovered check is a check that gets deleted quietly later.
201
+ # NOTE the assertion is on the MESSAGE, not the rule id: with the type check disabled a numeric key
202
+ # still exits 1 via "not in the registry", so a rule-id assertion passed either way and the mutation
203
+ # sweep caught zero lanes. A lane that cannot separate two paths is not measuring the one it names.
204
+ mkuap 'standing_consent:
205
+ 123: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
206
+ bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
207
+ if [ $? -eq 1 ] && grep -q 'grant key 123 must be a non-blank string' "$TD/o"; then
208
+ ok "H4 a non-string grant key is refused AS A TYPE ERROR (not merely as unregistered)"
209
+ else
210
+ bad "H4 numeric grant key was not refused by the type check"
211
+ sed 's/^/ /' "$TD/o"
212
+ fi
213
+
214
+ mkreg ' - {name: c, owner: o, mode: m, target: t, capabilities: "read", sinks: [], feeds: [], promotion_eligible: true}'
215
+ mkuap 'standing_consent: {}'
216
+ lane "P9 a scalar where a list is required is a type error" 1 "R1-b"
217
+
218
+ # N4 — PINS A KNOWN OVER-BLOCK, it does not endorse it. A grant written with a YAML merge key is
219
+ # currently REFUSED because the anchor lives outside the extracted fragment (see the residual note
220
+ # in consent_registry_check.sh). This lane asserts the CURRENT behaviour so that a future fix shows
221
+ # up as a lane change rather than being rediscovered from scratch — and so nobody reads the 40/40
222
+ # as "no known over-block".
223
+ mkreg "$OKCLASS"
224
+ mkuap 'defaults: &d {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}
225
+ standing_consent:
226
+ <<: *d'
227
+ bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
228
+ if [ $? -eq 1 ]; then
229
+ ok "N4 merge-key grant is refused — KNOWN OVER-BLOCK, pinned not endorsed (see residual note)"
230
+ else
231
+ ok "N4 merge-key grant now PASSES — the over-block was fixed; update this lane and the residual note"
232
+ fi
233
+
234
+ rm -f "$TD/r.yaml"
235
+ lane "D1 a missing registry is N/A with promotion disabled (never a silent PASS)" 0 ""
236
+ if grep -q 'N/A' "$TD/o"; then
237
+ ok "D1-b the missing-registry result is LABELLED N/A, not reported as clean"
238
+ else
239
+ bad "D1-b a missing registry produced exit 0 without an N/A label — that reads as PASS"
240
+ fi
241
+
242
+ printf 'classes: [ {name: broken\n' > "$TD/r.yaml"
243
+ lane "D2 an unparseable registry fails closed (cannot decide == not allowed)" 1 ""
244
+
245
+ # The shipped example must satisfy the validator — otherwise the artifact FH hands users is the
246
+ # first counter-example. (Hand-verify-one-sample discipline, applied to our own template.)
247
+ if bash "$CHK" "$ROOT/templates/consent_classes.yaml.example" /dev/null >/dev/null 2>&1; then
248
+ ok "S1 the shipped templates/consent_classes.yaml.example validates"
249
+ else
250
+ bad "S1 the shipped example registry does NOT validate"
251
+ fi
252
+
253
+ echo "----"
254
+ echo "consent-registry anchor: $pass passed, $fail failed"
255
+ [ "$fail" -eq 0 ] || exit 1
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # test_sidecar_wait_stdin.sh — known-pair anchor for scripts/sidecar_wait.sh's stdin plumbing.
3
+ #
4
+ # WHY (measured 2026-07-29)
5
+ # `sidecar_wait.sh` shipped in v1.4.76 documenting this invocation in its own header:
6
+ # printf '%s' "$prompt" | bash scripts/sidecar_wait.sh out.txt 600 -- codex exec -m gpt-5.5 -
7
+ # It did not work. `"$@" > "$OUT" 2>&1 &` gives the child /dev/null for stdin in a
8
+ # non-interactive shell, so codex received no prompt, answered "No prompt provided via stdin",
9
+ # and the wrapper reported SIDECAR_VERDICT=COMPLETE — a sidecar that never ran, reported as
10
+ # complete. That is the exact misjudgment the script exists to prevent, produced by the script.
11
+ #
12
+ # The FIRST repair was worse than the bug, and adversarial review caught it before it shipped.
13
+ # It spooled stdin to a tempfile; reproduced consequences:
14
+ # - the unbounded `cat` ran BEFORE the child, so an inherited never-EOF stdin hung the wrapper
15
+ # forever and the timeout budget never applied (`-- true` with inherited stdin → rc=124);
16
+ # - it CONSUMED the caller's stdin (a caller reading 3 lines afterwards read 0).
17
+ # The correct fix is one token: `<&0`. POSIX substitutes /dev/null only when stdin is NOT
18
+ # explicitly redirected. Lanes P2/P3 exist because the anchor's first version reported 4/4 green
19
+ # in the same shell where the argv form returned rc=124 — it bound only P1.
20
+ #
21
+ # Lanes
22
+ # P1 piped stdin REACHES the child (the original hole)
23
+ # P2 argv-form with an inherited never-EOF stdin does NOT hang (spool regression)
24
+ # P3 the caller's own stdin is NOT consumed by the wrapper (spool regression)
25
+ # P4 verdict codes: TIMEOUT=1 · EMPTY=0 · COMPLETE=0, and EMPTY/COMPLETE are distinguished
26
+ # P5 an unwritable outfile is exit 2, not "EMPTY" (a false clean on the typed channel)
27
+ #
28
+ # Verdicts are read by running the wrapper DIRECTLY — piping it into `tail` and reading `$?`
29
+ # answers with tail's status and turns a real exit 1 green.
30
+ #
31
+ # Exit 0 = all lanes correct. Exit 1 = the plumbing regressed.
32
+ set -uo pipefail
33
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
34
+ SW="$ROOT/scripts/sidecar_wait.sh"
35
+ [ -f "$SW" ] || { echo "FAIL: $SW not found"; exit 1; }
36
+ # NOTE: SIDECAR_POLL is set PER LANE, never exported globally. A global export pinned the knob for
37
+ # every lane and thereby MASKED a bad default — a regression to `SIDECAR_POLL:-0` passed 5/5.
38
+ FAST="SIDECAR_POLL=1"
39
+
40
+ pass=0; fail=0
41
+ ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
42
+ bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
43
+ TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
44
+
45
+ # P1 — `cat` echoes whatever it receives; an empty outfile means stdin was swallowed, which is
46
+ # exactly what the shipped version did.
47
+ printf 'MARKER_STDIN_ARRIVED' | SIDECAR_POLL=1 bash "$SW" "$TD/p1.out" 20 -- cat >/dev/null 2>&1
48
+ if grep -q 'MARKER_STDIN_ARRIVED' "$TD/p1.out" 2>/dev/null; then
49
+ ok "P1 piped stdin reaches the child process"
50
+ else
51
+ bad "P1 STDIN HOLE IS BACK — child saw no stdin (got: '$(head -c 60 "$TD/p1.out" 2>/dev/null)')"
52
+ fi
53
+
54
+ # P2 — the lane the first anchor lacked. A never-EOF stdin must not block the wrapper: the child is
55
+ # launched immediately and the budget governs. `sleep 20 |` keeps the pipe open with no data.
56
+ ( sleep 6 | SIDECAR_POLL=1 timeout 5 bash "$SW" "$TD/p2.out" 3 -- echo ARGV_OK ) >/dev/null 2>&1
57
+ rc_hang=$?
58
+ if [ "$rc_hang" -ne 124 ] && grep -q 'ARGV_OK' "$TD/p2.out" 2>/dev/null; then
59
+ ok "P2 an inherited never-EOF stdin does not hang the wrapper (child still runs)"
60
+ else
61
+ bad "P2 the wrapper HUNG or never ran the child on inherited stdin (rc=$rc_hang, out='$(head -c 40 "$TD/p2.out" 2>/dev/null)')"
62
+ fi
63
+
64
+ # P3 — the wrapper must not eat the caller's stream. `echo` reads nothing, so all three lines must
65
+ # remain readable afterwards. The spool version left zero.
66
+ n=$(printf 'l1\nl2\nl3\n' | { SIDECAR_POLL=1 bash "$SW" "$TD/p3.out" 20 -- echo x >/dev/null 2>&1
67
+ c=0; while read -r _; do c=$((c+1)); done; echo "$c"; })
68
+ if [ "$n" = "3" ]; then
69
+ ok "P3 the caller's own stdin survives the call (3/3 lines still readable)"
70
+ else
71
+ bad "P3 the wrapper consumed the caller's stdin ($n of 3 lines left)"
72
+ fi
73
+
74
+ # P4 — verdict channel. `-- true` writes nothing and is EMPTY, NOT COMPLETE; the first anchor
75
+ # labelled that lane "COMPLETE" and so never exercised COMPLETE at all.
76
+ SIDECAR_POLL=1 bash "$SW" "$TD/p4t.out" 2 -- sleep 30 >"$TD/p4t.txt" 2>&1 </dev/null; rc_t=$?
77
+ SIDECAR_POLL=1 bash "$SW" "$TD/p4e.out" 20 -- true >"$TD/p4e.txt" 2>&1 </dev/null; rc_e=$?
78
+ SIDECAR_POLL=1 bash "$SW" "$TD/p4c.out" 20 -- echo hi >"$TD/p4c.txt" 2>&1 </dev/null; rc_c=$?
79
+ if [ "$rc_t" -eq 1 ] && [ "$rc_e" -eq 0 ] && [ "$rc_c" -eq 0 ] \
80
+ && grep -q 'SIDECAR_VERDICT=TIMEOUT' "$TD/p4t.txt" \
81
+ && grep -q 'SIDECAR_VERDICT=EMPTY' "$TD/p4e.txt" \
82
+ && grep -q 'SIDECAR_VERDICT=COMPLETE' "$TD/p4c.txt"; then
83
+ ok "P4 verdicts intact and distinguished (TIMEOUT=1 · EMPTY=0 · COMPLETE=0)"
84
+ else
85
+ bad "P4 verdict channel drifted (timeout rc=$rc_t, empty rc=$rc_e, complete rc=$rc_c)"
86
+ head -1 "$TD/p4t.txt" "$TD/p4e.txt" "$TD/p4c.txt" 2>/dev/null | sed 's/^/ /'
87
+ fi
88
+
89
+ # P5 — an outfile that cannot be written made `wc -c` fail, `${size:-0}` read 0, and the wrapper
90
+ # announce EMPTY with exit 0. The sidecar had in fact produced output. Same false-clean class as
91
+ # the bug this file anchors, on the typed channel itself.
92
+ SIDECAR_POLL=1 bash "$SW" "$TD/nodir/x.out" 10 -- echo hi >/dev/null 2>&1 </dev/null
93
+ rc_w=$?
94
+ if [ "$rc_w" -eq 2 ]; then
95
+ ok "P5 an unwritable outfile fails closed (exit 2), never 'EMPTY'"
96
+ else
97
+ bad "P5 an unwritable outfile returned rc=$rc_w — a sidecar that ran reported as saying nothing"
98
+ fi
99
+
100
+ # P6 — the knob added while making this anchor cheap re-opened the unbounded wait. Each bad value
101
+ # must still produce a bounded TIMEOUT. Deliberately NOT using $FAST: the point is a bad POLL.
102
+ p6=0
103
+ for badpoll in 0 0.5 abc 08 09 600; do
104
+ SIDECAR_POLL="$badpoll" timeout 12 bash "$SW" "$TD/p6.out" 2 -- sleep 30 >"$TD/p6.txt" 2>&1
105
+ rc6=$?
106
+ if [ "$rc6" -eq 1 ] && grep -q 'SIDECAR_VERDICT=TIMEOUT' "$TD/p6.txt"; then p6=$((p6+1)); fi
107
+ done
108
+ if [ "$p6" -eq 6 ]; then
109
+ ok "P6 a malformed SIDECAR_POLL (0·0.5·abc·08·09·600) still times out — the budget is not disarmable"
110
+ else
111
+ bad "P6 only $p6/6 malformed poll values produced a bounded TIMEOUT (unbounded wait is reachable)"
112
+ fi
113
+
114
+ # P7 — TIMEOUT must not orphan the sidecar, INCLUDING grandchildren: `kill "$PID"` reached only the
115
+ # direct child, so `sh -c 'sleep N & wait'` survived while this lane stayed green. The marker is
116
+ # per-run: a machine-global `pgrep -f 'sleep 25'` false-FAILs on any concurrent sleep and its
117
+ # failure branch would kill unrelated host processes.
118
+ MARK="p7_$$_$(date +%s 2>/dev/null || echo x)"
119
+ SIDECAR_POLL=1 bash "$SW" "$TD/p7.out" 2 -- sh -c "sleep 25 & wait # $MARK" >"$TD/p7.txt" 2>&1 </dev/null
120
+ sleep 1
121
+ if ! pgrep -f "$MARK" >/dev/null 2>&1; then
122
+ ok "P7 a timed-out sidecar is killed with its grandchildren, not orphaned past our exit"
123
+ else
124
+ bad "P7 a grandchild survived TIMEOUT (orphan restored)"; pkill -f "$MARK" 2>/dev/null
125
+ fi
126
+
127
+ # P9 — the tty path. HONEST SCOPE, do not read this lane as more than it is.
128
+ #
129
+ # It asserts that a stdin-reading child under an allocated pty still produces a bounded verdict. It
130
+ # does NOT bind the `if [ -t 0 ]` branch: removing that branch entirely leaves this lane GREEN
131
+ # (measured 2026-07-29). Cross-family review reproduced a SIGTTIN stop (rc=124, no verdict) under a
132
+ # pty before `set -m` was added for the group-kill; this harness cannot reproduce that condition,
133
+ # so whether the branch is still load-bearing is UNMEASURED — the branch is kept as defensive, not
134
+ # as something this anchor proves.
135
+ #
136
+ # A lane that cannot separate a known-positive from a known-negative is not measuring; it is
137
+ # generating. Shipping it as a plain green would manufacture exactly the false confidence this
138
+ # file exists to prevent, so it is labelled instead of silently counted.
139
+ if command -v script >/dev/null 2>&1; then
140
+ # `< /dev/null` on `script` itself: with the harness's inherited stdin it could not allocate a pty
141
+ # at all and wrote no rc file, so the lane failed identically with AND without the fix under test
142
+ # — it discriminated nothing. The pty it allocates for the inner command is what matters.
143
+ # The inner shell writes its rc to a FILE. Parsing `script`'s own stdout fails: it emits ^D and
144
+ # CRLF, and the first version of this lane read rc as empty and reported a defect that did not
145
+ # exist — the target was fine, the instrument was not.
146
+ script -q /dev/null bash -c "SIDECAR_POLL=1 timeout 12 bash '$SW' '$TD/p9.out' 3 -- cat >'$TD/p9.txt' 2>&1; echo \$? > '$TD/p9rc'" >/dev/null 2>&1 < /dev/null
147
+ rc9=$(tr -dc '0-9' < "$TD/p9rc" 2>/dev/null)
148
+ if [ -n "$rc9" ] && [ "$rc9" != "124" ] && grep -q 'SIDECAR_VERDICT=' "$TD/p9.txt" 2>/dev/null; then
149
+ ok "P9 the tty path runs and emits a verdict (UNCALIBRATED — see note; does NOT bind the branch)"
150
+ else
151
+ bad "P9 tty case rc='${rc9:-<unread>}' verdict='$(head -1 "$TD/p9.txt" 2>/dev/null)'"
152
+ fi
153
+ else
154
+ bad "P9 UNCALIBRATED — \`script\` unavailable, the tty branch cannot be exercised here"
155
+ fi
156
+
157
+ # P8 — the child's stderr AND its exit code must reach the typed channel. Dropping `2>&1` or
158
+ # hardcoding rc=0 both passed the earlier anchor: it grepped the verdict WORD and the wrapper's
159
+ # own rc, never the child's.
160
+ SIDECAR_POLL=1 bash "$SW" "$TD/p8.out" 20 -- sh -c 'echo STDERR_MARK >&2; exit 3' >"$TD/p8.txt" 2>&1 </dev/null
161
+ if grep -q 'STDERR_MARK' "$TD/p8.out" 2>/dev/null && grep -q 'exit=3' "$TD/p8.txt"; then
162
+ ok "P8 child stderr is captured and its exit code reaches the verdict line"
163
+ else
164
+ bad "P8 stderr or child exit code lost (out='$(head -c 40 "$TD/p8.out" 2>/dev/null)' verdict='$(head -1 "$TD/p8.txt")')"
165
+ fi
166
+
167
+ echo "----"
168
+ echo "sidecar_wait stdin anchor: $pass passed, $fail failed"
169
+ [ "$fail" -eq 0 ] || exit 1
@@ -0,0 +1,75 @@
1
+ # consent_classes.yaml — the consent-class registry.
2
+ #
3
+ # Copy to `tracks/_meta/consent_classes.yaml` (gitignored, per-environment) to enable
4
+ # accept-side consent promotion. See `knowledge/shared/rules/operational_adaptation.md`
5
+ # §Consent promotion.
6
+ #
7
+ # WHY A REGISTRY AT ALL (cross-family review, 2026-07-29)
8
+ # The first draft let the running session name the class it was counting approvals against.
9
+ # That defeats the class-inflation guard before it fires: the class does not need to be WIDENED
10
+ # later if it was born wide. Recording three approvals as `sim-conductor` instead of
11
+ # `dispatch read-only sim on a local artifact` silently grants every future sim-conductor mode.
12
+ # So classes are DECLARED here, reviewed by a human, and the session may only count against
13
+ # an entry that already exists. A session may propose a new entry; it may not mint one mid-run.
14
+ #
15
+ # THE FLOOR IS READ FROM THIS FILE, NOT JUDGED AT RUNTIME
16
+ # `sinks` is the field the irreversibility floor is decided from. A session asking itself
17
+ # "is this reversible?" is the wrong arbiter — it is the party that wants to stop being asked.
18
+ # Absent entry / absent file / unparseable entry => NO promotion, keep asking. Unknown is not
19
+ # reversible.
20
+ #
21
+ # TAINT
22
+ # A class inherits the floor if it FEEDS an irreversible sink, not only if it crosses one.
23
+ # `local-commit` below is the worked example: committing is locally reversible, but if a
24
+ # post-commit hook, sync daemon, or CI workflow publishes from that commit, the chain crossed
25
+ # go-public and the class must be marked tainted. Declare `feeds:` honestly — the whole point
26
+ # is that no single step in a bad chain looks dangerous on its own.
27
+
28
+ classes:
29
+ # ---- promotion-eligible example -------------------------------------------------
30
+ - name: dispatch-readonly-sim-local-artifact
31
+ owner: fh-meta:sim-conductor # the gate/skill that does the asking
32
+ mode: blind-target-tier-sim
33
+ target: a single local file under the current repo
34
+ capabilities: [read, dispatch] # read · local-write · network · dispatch · repo-mutation
35
+ sinks: [] # crosses nothing irreversible
36
+ feeds: [] # enables/queues nothing irreversible
37
+ promotion_eligible: true
38
+ lease_days: 30 # standing consent is a renewable lease, never permanent
39
+
40
+ # ---- NOT promotion-eligible: sink is irreversible --------------------------------
41
+ - name: npm-publish
42
+ owner: Pre-Publish Surface Gate
43
+ mode: registry-publish
44
+ target: the package tarball file set
45
+ capabilities: [read, network]
46
+ sinks: [go-public] # irreversible => never promotes, at any count
47
+ feeds: []
48
+ promotion_eligible: false
49
+ reason: "publish is effectively irreversible — a package is live the instant it ships and may
50
+ be cached or forked before any scrub (CLAUDE.md §Pre-Publish Surface Gate)."
51
+
52
+ # ---- NOT promotion-eligible: TAINTED (this is the one that looks safe) -----------
53
+ - name: local-commit
54
+ owner: FH 4-axis commit gate
55
+ mode: git-commit
56
+ target: the working tree
57
+ capabilities: [local-write, repo-mutation]
58
+ sinks: [] # committing itself is reversible
59
+ feeds: [go-public] # ...but CI/sync publishes from it
60
+ promotion_eligible: false
61
+ reason: "taint — reversible in isolation, feeds an irreversible sink. Every step in the
62
+ workflow-edit -> release-notes -> commit -> CI-publish chain is individually
63
+ reversible; the chain is not."
64
+
65
+ # ---- NOT promotion-eligible: unclassified ---------------------------------------
66
+ - name: run-arbitrary-shell
67
+ owner: (none — catch-all)
68
+ mode: unrestricted
69
+ target: unbounded
70
+ capabilities: [read, local-write, network, dispatch, repo-mutation]
71
+ sinks: [unknown] # unknown is NOT reversible
72
+ feeds: [unknown]
73
+ promotion_eligible: false
74
+ reason: "sinks cannot be enumerated, so the floor cannot be decided mechanically. A class
75
+ whose blast radius is unbounded is never a candidate, regardless of approval count."