@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +18 -0
- package/CLAUDE.md +7 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +6 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +21 -2
- package/knowledge/shared/rules/operational_adaptation.md +130 -1
- package/package.json +5 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +38 -3
- package/plugins/fh-meta/skills/sim-conductor/SKILL_detail.md +6 -0
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +6 -0
- package/scripts/consent_registry_check.sh +390 -0
- package/scripts/selfcheck.sh +30 -0
- package/scripts/sidecar_wait.sh +55 -5
- package/scripts/test_consent_registry.sh +255 -0
- package/scripts/test_sidecar_wait_stdin.sh +169 -0
- package/templates/consent_classes.yaml.example +75 -0
|
@@ -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."
|