@chrono-meta/fh-gate 1.4.83 → 1.4.85
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/rules/fh_4axis_gate.md +45 -1
- package/.claude-plugin/marketplace.json +2 -2
- package/CATALOG.md +23 -1
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +21 -7
- package/knowledge/shared/harness-core/field_harness_diagnostic.md +1 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +10 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +167 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +206 -0
- package/knowledge/shared/rules/operational_adaptation.md +35 -9
- package/package.json +3 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/convergence-loop/SKILL.md +25 -10
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/agents/challenger.md +1 -1
- package/plugins/fh-meta/skills/phantom-quench/SKILL.md +1 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +78 -10
- package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +3 -3
- package/scripts/consent_registry_check.sh +187 -15
- package/scripts/destructive_pre_gate.sh +32 -5
- package/scripts/package_coverage_check.sh +10 -0
- package/scripts/pipe_verdict_guard.sh +43 -3
- package/scripts/selfcheck.sh +117 -2
- package/scripts/session_close_check.sh +103 -1
- package/scripts/stale_clone_guard.sh +49 -6
- package/scripts/substrate_jump_detector.sh +21 -0
- package/scripts/test_consent_registry.sh +341 -42
- package/scripts/test_destructive_pre_gate_lanes.sh +42 -0
- package/scripts/test_dispatch_log_lanes.sh +99 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +71 -0
- package/scripts/test_selfcheck_state_lanes.sh +102 -0
- package/scripts/test_session_close_lanes.sh +187 -15
- package/scripts/test_stale_clone_guard_lanes.sh +54 -0
- package/templates/.git-hooks/pre-push +10 -1
- package/templates/consent_classes.yaml.example +18 -0
|
@@ -55,12 +55,17 @@ mkuap_raw() { printf '%s\n' "$1" > "$TD/u.md"; }
|
|
|
55
55
|
|
|
56
56
|
mkreg "$OKCLASS"
|
|
57
57
|
mkuap 'standing_consent:
|
|
58
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
58
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
59
59
|
lane "N1 a well-formed registry + eligible unexpired scoped grant passes" 0 ""
|
|
60
60
|
|
|
61
61
|
mkuap 'standing_consent:
|
|
62
62
|
ok: declined'
|
|
63
|
-
|
|
63
|
+
# EXPECTATION CHANGED 0 -> 3 (F4-b, 2026-07-31). A `declined` record is still a valid non-grant
|
|
64
|
+
# state and still must not be a VIOLATION — that half of the lane is unchanged and is what the
|
|
65
|
+
# 3-vs-1 distinction now asserts. What changed is that "the user said no" may not be reported
|
|
66
|
+
# through the same channel as "the user said yes": exit 0 is the only code a caller may skip a
|
|
67
|
+
# prompt on, and a file recording nothing but refusals has granted nothing.
|
|
68
|
+
lane "N2 a declined record is a non-grant state (3), never a malformed grant (1)" 3 ""
|
|
64
69
|
|
|
65
70
|
mkreg ' - {name: s, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: true}'
|
|
66
71
|
mkuap 'standing_consent: {}'
|
|
@@ -71,31 +76,31 @@ lane "P2 taint — a class that FEEDS an irreversible sink cannot be promotable"
|
|
|
71
76
|
|
|
72
77
|
mkreg "$OKCLASS"
|
|
73
78
|
mkuap 'standing_consent:
|
|
74
|
-
ghost: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
79
|
+
ghost: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
75
80
|
lane "P3 a grant on an unregistered class is refused (unregistered == unknown)" 1 "R3"
|
|
76
81
|
|
|
77
82
|
mkuap 'standing_consent:
|
|
78
|
-
ok: {granted: 2026-01-01, expires: 2026-06-30, effects: [read], target: t}'
|
|
83
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2026-06-30, effects: [read], target: t}'
|
|
79
84
|
lane "P4 an expired lease does not keep running" 1 "R5"
|
|
80
85
|
|
|
81
86
|
mkuap 'standing_consent:
|
|
82
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31}'
|
|
87
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31}'
|
|
83
88
|
lane "P5 a grant with no recorded scope has no re-validation baseline" 1 "R6"
|
|
84
89
|
|
|
85
90
|
# P6-P8 are the round-3 fail-opens. Each of these PASSED before the fix.
|
|
86
91
|
mkreg ' - {name: q, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: "false"}'
|
|
87
92
|
mkuap 'standing_consent:
|
|
88
|
-
q: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
93
|
+
q: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
89
94
|
lane "P6 quoted \"false\" is rejected as a type error, not read as truthy" 1 "R1-b"
|
|
90
95
|
|
|
91
96
|
mkreg ' - {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [go-public], feeds: [], promotion_eligible: false}
|
|
92
97
|
- {name: d, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
93
98
|
mkuap 'standing_consent:
|
|
94
|
-
d: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
99
|
+
d: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
95
100
|
lane "P7 a duplicate class name cannot launder an ineligible class" 1 "R1-c"
|
|
96
101
|
|
|
97
102
|
mkreg "$OKCLASS"
|
|
98
|
-
mkuap 'standing_consent: {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
|
|
103
|
+
mkuap 'standing_consent: {ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
|
|
99
104
|
lane "P8 an INLINE grant is parsed, not silently read as 'nothing granted'" 1 "R5"
|
|
100
105
|
|
|
101
106
|
# P10-P11 are the round-4 fail-opens. Both PASSED before the fix, and both were confirmed against a
|
|
@@ -106,16 +111,21 @@ mkuap 'standing_consent: {}
|
|
|
106
111
|
notes in between
|
|
107
112
|
|
|
108
113
|
standing_consent:
|
|
109
|
-
ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
114
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
110
115
|
lane "P10 an early empty consent block cannot shadow a later grant (first-match)" 1 "R3"
|
|
111
116
|
|
|
112
117
|
mkuap 'standing_consent:
|
|
113
|
-
ok: {state: "revoked ", granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
114
|
-
|
|
118
|
+
ok: {owner: o, mode: m, sinks: [], state: "revoked ", granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
119
|
+
# ASSERTION REWRITTEN 2026-07-31 (F4-b). This lane read the VERDICT out of prose ("0 active grant")
|
|
120
|
+
# through a pipe, which is both the grep-a-prose-verdict pattern this repo has a memory entry
|
|
121
|
+
# against and — via the pipe — a read of grep's status, not the checker's. It now asserts the typed
|
|
122
|
+
# channel: a normalized `revoked ` leaves zero active grants, which is exit 3, not 0 and not 1.
|
|
123
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1; rc=$?
|
|
124
|
+
if [ "$rc" -eq 3 ] && grep -q "NONE of them an active grant" "$TD/o"; then
|
|
115
125
|
ok "P11 a mapping non-grant state is normalized like the scalar one (no divergent normalizer)"
|
|
116
126
|
else
|
|
117
|
-
bad "P11 \`state: \"revoked \"\` was
|
|
118
|
-
|
|
127
|
+
bad "P11 \`state: \"revoked \"\` was not normalized to a non-grant (exit $rc, expected 3)"
|
|
128
|
+
sed 's/^/ /' "$TD/o"
|
|
119
129
|
fi
|
|
120
130
|
|
|
121
131
|
# P12 is the round-5 fail-open: `or {}` collapsed FALSY non-mappings into a valid-empty mapping
|
|
@@ -140,11 +150,11 @@ done
|
|
|
140
150
|
mkreg "$OKCLASS"
|
|
141
151
|
mkuap 'standing_consent: {}
|
|
142
152
|
|
|
143
|
-
standing_consent : {ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
|
|
153
|
+
standing_consent : {ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}}'
|
|
144
154
|
lane "P13 a space-before-colon key cannot hide behind a canonical block" 1 "R3"
|
|
145
155
|
|
|
146
156
|
mkuap 'standing_consent :
|
|
147
|
-
ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
157
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
148
158
|
lane "P13-b a space-before-colon BLOCK form is parsed and its grant validated" 1 "R5"
|
|
149
159
|
|
|
150
160
|
# N3 EXPECTATION INVERTED 2026-07-31 — and the reason is the whole point of the storage-form change.
|
|
@@ -154,7 +164,7 @@ lane "P13-b a space-before-colon BLOCK form is parsed and its grant validated" 1
|
|
|
154
164
|
# So the belief was false, and a regex that approximates the YAML spec had been quietly ratifying a
|
|
155
165
|
# document the spec rejects. Handing the region to the real loader replaces a guess with an answer —
|
|
156
166
|
# a malformed document is now BROKEN (fail-closed), not silently accepted as equivalent.
|
|
157
|
-
mkuap "$(printf 'standing_consent\t: {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}')"
|
|
167
|
+
mkuap "$(printf 'standing_consent\t: {ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}')"
|
|
158
168
|
lane "N3 a tab-before-colon key is invalid YAML and fails closed (loader, not regex, decides)" 1 "R3"
|
|
159
169
|
|
|
160
170
|
# P14 — round-7 fail-open: R6 presence-checked `effects`/`target` but never typed them, while the
|
|
@@ -162,15 +172,15 @@ lane "N3 a tab-before-colon key is invalid YAML and fails closed (loader, not re
|
|
|
162
172
|
# a MISSING field was caught; a type-wrong one passed.
|
|
163
173
|
mkreg "$OKCLASS"
|
|
164
174
|
mkuap 'standing_consent:
|
|
165
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: true, target: 123}'
|
|
175
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: true, target: 123}'
|
|
166
176
|
lane "P14 a type-wrong grant scope is rejected, not counted as recorded" 1 "R6"
|
|
167
177
|
|
|
168
178
|
mkuap 'standing_consent:
|
|
169
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: "read", target: " "}'
|
|
179
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: "read", target: " "}'
|
|
170
180
|
lane "P14-b a scalar effects / whitespace-only target is rejected" 1 "R6"
|
|
171
181
|
|
|
172
182
|
mkuap 'standing_consent:
|
|
173
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [], target: t}'
|
|
183
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [], target: t}'
|
|
174
184
|
lane "P14-c an EMPTY effects list is not a baseline" 1 "R6"
|
|
175
185
|
|
|
176
186
|
# H1-H9 — round-8 exhaustive field audit found NINE remaining holes in one pass, after four rounds
|
|
@@ -196,26 +206,26 @@ lane "H3 an unreadable sink item is UNDECLARED, not an empty sink list" 1 "R1-b"
|
|
|
196
206
|
|
|
197
207
|
mkreg "$OKCLASS"
|
|
198
208
|
mkuap 'standing_consent:
|
|
199
|
-
ok: {expires: 2026-12-31, effects: [read], target: t}'
|
|
209
|
+
ok: {owner: o, mode: m, sinks: [], expires: 2026-12-31, effects: [read], target: t}'
|
|
200
210
|
lane "H5 a grant with no \`granted\` date cannot be audited" 1 "R5"
|
|
201
211
|
mkuap 'standing_consent:
|
|
202
|
-
ok: {granted: 2026-07-29, expires: 29991231, effects: [read], target: t}'
|
|
212
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 29991231, effects: [read], target: t}'
|
|
203
213
|
lane "H7 a non-ISO integer expiry is not a date" 1 "R5"
|
|
204
214
|
mkuap 'standing_consent:
|
|
205
|
-
ok: {granted: 2026-07-29, expires: 9999-12-31, effects: [read], target: t}'
|
|
215
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 9999-12-31, effects: [read], target: t}'
|
|
206
216
|
lane "H7-b an unbounded lease is a transfer wearing a lease's clothes" 1 "R5"
|
|
207
217
|
mkuap 'standing_consent:
|
|
208
|
-
ok: {state: revokedd, granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
218
|
+
ok: {owner: o, mode: m, sinks: [], state: revokedd, granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
209
219
|
lane "H6 a typo'\''d state fails closed instead of passing as active" 1 "R3"
|
|
210
220
|
mkuap 'standing_consent:
|
|
211
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [repo-mutation], target: t}'
|
|
221
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [repo-mutation], target: t}'
|
|
212
222
|
lane "H8 a grant wider than its registered capabilities is refused" 1 "R7"
|
|
213
223
|
mkuap 'standing_consent:
|
|
214
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: everything}'
|
|
224
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: everything}'
|
|
215
225
|
lane "H8-b grant/class target drift is refused" 1 "R7"
|
|
216
226
|
mkuap 'standing_consent:
|
|
217
|
-
ok: {granted: 2026-07-29, expires: 2020-01-01, effects: [read], target: t}
|
|
218
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
227
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2020-01-01, effects: [read], target: t}
|
|
228
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
219
229
|
lane "H9 duplicate YAML keys are rejected, not resolved last-wins" 1 ""
|
|
220
230
|
|
|
221
231
|
# H4 had a fix but NO lane — the mutation sweep caught that (disabling it failed zero lanes).
|
|
@@ -224,7 +234,7 @@ lane "H9 duplicate YAML keys are rejected, not resolved last-wins" 1 ""
|
|
|
224
234
|
# still exits 1 via "not in the registry", so a rule-id assertion passed either way and the mutation
|
|
225
235
|
# sweep caught zero lanes. A lane that cannot separate two paths is not measuring the one it names.
|
|
226
236
|
mkuap 'standing_consent:
|
|
227
|
-
123: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
237
|
+
123: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
228
238
|
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1
|
|
229
239
|
if [ $? -eq 1 ] && grep -q 'grant key 123 must be a non-blank string' "$TD/o"; then
|
|
230
240
|
ok "H4 a non-string grant key is refused AS A TYPE ERROR (not merely as unregistered)"
|
|
@@ -243,7 +253,7 @@ lane "P9 a scalar where a list is required is a type error" 1 "R1-b"
|
|
|
243
253
|
# up as a lane change rather than being rediscovered from scratch — and so nobody reads the 40/40
|
|
244
254
|
# as "no known over-block".
|
|
245
255
|
mkreg "$OKCLASS"
|
|
246
|
-
mkuap 'defaults: &d {ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}
|
|
256
|
+
mkuap 'defaults: &d {ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}}
|
|
247
257
|
standing_consent:
|
|
248
258
|
<<: *d'
|
|
249
259
|
# N4 REMOVED 2026-07-31 — it called ok() in BOTH branches, so it passed on every possible exit code
|
|
@@ -287,7 +297,7 @@ lane "D1-d a missing UAP is UNMEASURED (3) — no grants is not a verified pass"
|
|
|
287
297
|
# return the same code again, the channel has collapsed back into prose.
|
|
288
298
|
mkreg "$OKCLASS"
|
|
289
299
|
mkuap 'standing_consent:
|
|
290
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
300
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
291
301
|
lane "D1-e 0 still means VERIFIED, and only that" 0 ""
|
|
292
302
|
|
|
293
303
|
printf 'classes: [ {name: broken\n' > "$TD/r.yaml"
|
|
@@ -306,7 +316,7 @@ lane "D2 an unparseable registry fails closed (cannot decide == not allowed)" 1
|
|
|
306
316
|
# through — which is exactly why the doctrine calls local tier evidence-of and never terminal.
|
|
307
317
|
mkreg ' - {name: rewrite, owner: o, mode: m, target: t, capabilities: [history-rewrite], sinks: [], feeds: [], promotion_eligible: true}'
|
|
308
318
|
mkuap 'standing_consent:
|
|
309
|
-
rewrite: {granted: 2026-07-29, expires: 2026-12-31, effects: [history-rewrite], target: t}'
|
|
319
|
+
rewrite: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [history-rewrite], target: t}'
|
|
310
320
|
lane "R9-1 a declared irreversible CAPABILITY is not promotable, empty sinks or not" 1 "R2-b"
|
|
311
321
|
|
|
312
322
|
# The same class WITHOUT the promotion claim is fine: declaring an irreversible capability is not
|
|
@@ -314,7 +324,9 @@ lane "R9-1 a declared irreversible CAPABILITY is not promotable, empty sinks or
|
|
|
314
324
|
# can do. Over-blocking trains the override reflex — the checker already records one such revert.
|
|
315
325
|
mkreg ' - {name: rewrite, owner: o, mode: m, target: t, capabilities: [history-rewrite], sinks: [], feeds: [], promotion_eligible: false}'
|
|
316
326
|
mkuap 'standing_consent: {}'
|
|
317
|
-
|
|
327
|
+
# EXPECTATION CHANGED 0 -> 3 (F4-b): the anti-over-block point of this lane is that the registry
|
|
328
|
+
# must NOT be a violation, i.e. not 1. Its UAP grants nothing, so the join is empty -> UNMEASURED.
|
|
329
|
+
lane "R9-1b the same irreversible capability is not a violation when not claimed promotable" 3 ""
|
|
318
330
|
|
|
319
331
|
# `unknown` is in IRREVERSIBLE for the sink rule ("unlisted sinks are UNKNOWN, and unknown is not
|
|
320
332
|
# reversible"); it must mean the same thing in the capability position.
|
|
@@ -326,7 +338,7 @@ lane "R9-1c an \`unknown\` capability is not promotable either" 1 "R2-b"
|
|
|
326
338
|
# sinks is the N1 shape and must stay passing. If this lane ever flips, the fix went too wide.
|
|
327
339
|
mkreg "$OKCLASS"
|
|
328
340
|
mkuap 'standing_consent:
|
|
329
|
-
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
341
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
330
342
|
lane "R9-1d a reversible capability with empty sinks still passes (anti-over-block)" 0 ""
|
|
331
343
|
|
|
332
344
|
# R9-1e — the SAME field must be normalised the SAME way by every rule that reads it. R7 compares
|
|
@@ -340,17 +352,24 @@ lane "R9-1e whitespace around an irreversible capability does not evade R2-b" 1
|
|
|
340
352
|
|
|
341
353
|
# The shipped example must satisfy the validator — otherwise the artifact FH hands users is the
|
|
342
354
|
# first counter-example. (Hand-verify-one-sample discipline, applied to our own template.)
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
# ASSERTION REWRITTEN 2026-07-31 (F4-b). This lane used the bare-command form — precisely the
|
|
356
|
+
# `if check; then ...` convention finding F4 identified as the fail-open — so it asked "is the exit
|
|
357
|
+
# status truthy?" when the question is "is the REGISTRY broken?". With no grants to join the answer
|
|
358
|
+
# is now 3 (UNMEASURED), which is a correct verdict about the grants and says nothing bad about the
|
|
359
|
+
# registry. 1 is the only code that means the shipped example is invalid.
|
|
360
|
+
bash "$CHK" "$ROOT/templates/consent_classes.yaml.example" /dev/null >"$TD/o" 2>&1; rc=$?
|
|
361
|
+
if [ "$rc" -ne 1 ]; then
|
|
362
|
+
ok "S1 the shipped templates/consent_classes.yaml.example validates (exit $rc, not BROKEN)"
|
|
345
363
|
else
|
|
346
364
|
bad "S1 the shipped example registry does NOT validate"
|
|
365
|
+
sed 's/^/ /' "$TD/o"
|
|
347
366
|
fi
|
|
348
367
|
|
|
349
368
|
# ---- F: STORAGE FORM (frontmatter) -------------------------------------------------
|
|
350
369
|
# These lanes test the machine-region boundary itself, not grant semantics, so they write the UAP
|
|
351
370
|
# byte-exact via mkuap_raw instead of going through the frontmatter-wrapping helper.
|
|
352
371
|
mkreg "$OKCLASS"
|
|
353
|
-
GRANT=' ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
372
|
+
GRANT=' ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
354
373
|
|
|
355
374
|
# F1/F2 are the two false-clean shapes, and they are the reason this section exists. A grant that
|
|
356
375
|
# sits where no parser reads it must never report as "nothing granted" — an unread grant is not an
|
|
@@ -364,17 +383,20 @@ lane "F2 frontmatter exists but the grant was written BELOW it — fails closed"
|
|
|
364
383
|
# F3 is the anti-over-block control for F1: absence must stay cheap. A UAP predating this format has
|
|
365
384
|
# no machine region and grants nothing; painting that red would train the override reflex.
|
|
366
385
|
mkuap_raw "$(printf '# UAP\n\nno machine region here, and nothing granted.\n')"
|
|
367
|
-
|
|
386
|
+
# EXPECTATION CHANGED 0 -> 3 (F4-b). "Absence must stay cheap" is preserved exactly: 3 is not red,
|
|
387
|
+
# it is N/A. What it no longer does is answer the conventional `if check; then run_unprompted; fi`
|
|
388
|
+
# with success on a UAP that granted nothing at all.
|
|
389
|
+
lane "F3 no frontmatter and no grant mentioned is nothing granted (3, not a failure)" 3 ""
|
|
368
390
|
|
|
369
391
|
# F4 pins the OVER-BLOCK THIS CHANGE RETIRES. Under the slicer a merge-key grant was refused because
|
|
370
392
|
# the anchor lived outside the extracted fragment; the fragment is now the whole document, so
|
|
371
393
|
# ordinary DRY YAML resolves. Over-blocking was a defect of the same weight as a fail-open.
|
|
372
|
-
mkuap_raw "$(printf -- '---\ndefaults: &d {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}\nstanding_consent:\n ok:\n <<: *d\n---\n')"
|
|
394
|
+
mkuap_raw "$(printf -- '---\ndefaults: &d {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}\nstanding_consent:\n ok:\n <<: *d\n---\n')"
|
|
373
395
|
lane "F4 a merge-key/anchor grant now resolves (the measured over-block is retired)" 0 ""
|
|
374
396
|
|
|
375
397
|
# F5 — duplicate keys stay rejected on this side too. safe_load is last-wins, so an expired grant
|
|
376
398
|
# followed by a future one would silently keep the future one.
|
|
377
|
-
mkuap_raw "$(printf -- '---\nstanding_consent:\n ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}\n ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}\n---\n')"
|
|
399
|
+
mkuap_raw "$(printf -- '---\nstanding_consent:\n ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}\n ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}\n---\n')"
|
|
378
400
|
lane "F5 duplicate grant keys are refused, not last-wins" 1 "R3"
|
|
379
401
|
|
|
380
402
|
# F6 — a leading BOM must not make the machine region look absent. Without the tolerance this still
|
|
@@ -398,18 +420,22 @@ lane "F8 an unterminated --- block is not a machine region and fails closed" 1 "
|
|
|
398
420
|
# went green without the empty-region path ever running. Lane F9-b below is its control: same empty
|
|
399
421
|
# region, but WITH a prose mention, which can only stay green if the region was really recognized.
|
|
400
422
|
mkuap_raw "$(printf -- '---\n---\n\n# UAP\n')"
|
|
401
|
-
|
|
423
|
+
# EXPECTATION CHANGED 0 -> 3 (F4-b): still not an error (not 1), but an empty region grants nothing.
|
|
424
|
+
lane "F9 an empty frontmatter block grants nothing (3) and is not an error (1)" 3 ""
|
|
402
425
|
|
|
403
426
|
# F9-b — the discriminating control for F9. If the empty region is NOT recognized, this file reads as
|
|
404
427
|
# "no frontmatter + standing_consent mentioned" and fails closed via F1. Green here means the empty
|
|
405
428
|
# frontmatter was genuinely parsed. (Without this control F9 cannot tell the two paths apart.)
|
|
406
429
|
mkuap_raw "$(printf -- '---\n---\n\n# UAP\n\nthe key name standing_consent appears in prose but not as a key.\n')"
|
|
407
|
-
|
|
430
|
+
# EXPECTATION CHANGED 0 -> 3 (F4-b). The discriminating power of this control is UNCHANGED: if the
|
|
431
|
+
# empty region were not recognized, this file would fail CLOSED via the F1 net at exit 1, which is
|
|
432
|
+
# still distinct from the 3 asserted here.
|
|
433
|
+
lane "F9-b empty region + a prose MENTION is still nothing granted (region truly parsed)" 3 ""
|
|
408
434
|
|
|
409
435
|
# F10-F13 — YAML stream forms the canonical loader accepts and the first fence regex rejected. Each
|
|
410
436
|
# failed CLOSED (no leak) but as an OVER-BLOCK whose message blamed an absent frontmatter that was
|
|
411
437
|
# present. A gate that misdirects the fix gets overridden, so these are lanes, not footnotes.
|
|
412
|
-
EXPIRED=' ok: {granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
438
|
+
EXPIRED=' ok: {owner: o, mode: m, sinks: [], granted: 2026-01-01, expires: 2020-01-01, effects: [read], target: t}'
|
|
413
439
|
mkreg "$OKCLASS"
|
|
414
440
|
mkuap_raw "$(printf -- '---\nstanding_consent:\n%s\n...\n' "$EXPIRED")"
|
|
415
441
|
lane "F10 a '...' document terminator closes the machine region (grant is READ, then R5-caught)" 1 "R5"
|
|
@@ -429,6 +455,279 @@ lane "F13 a %YAML directive before the fence does not hide the region" 1 "R5"
|
|
|
429
455
|
mkuap_raw "$(printf -- '---\nsidecar_consent: granted\n---\n\n# UAP\n\n"standing_consent":\n%s\n' "$EXPIRED")"
|
|
430
456
|
lane "F14 a QUOTED grant key in the prose region is caught too (nets agree)" 1 "R3"
|
|
431
457
|
|
|
458
|
+
# ---- R9-F3: THE FINGERPRINT WAS A PARTIAL HASH ------------------------------------
|
|
459
|
+
# Cross-family round 9 (codex @ gpt-5.6-sol). The rule says consent binds to the action's SHAPE and
|
|
460
|
+
# enumerates that shape — "the owning gate/skill, and the set of effect classes ... plus the
|
|
461
|
+
# `target` scope and the `sinks` fingerprint", offered to the user as `<mode · target ·
|
|
462
|
+
# capabilities · sinks>`. The baseline the checker recorded was `effects` + `target` ONLY.
|
|
463
|
+
# So the fields that say WHO acts (`owner`) and WHAT IT DOES when it acts (`mode`) were outside the
|
|
464
|
+
# floor: keep the class name, target, capabilities and empty sinks, swap the owner to a different
|
|
465
|
+
# skill, and R6 found its two fields present, R7 found the effects still a subset, and the gate
|
|
466
|
+
# returned 0 with the blast radius re-pointed under a live grant.
|
|
467
|
+
# The rule's own sentence is the indictment: "the name is exactly what does not change when the
|
|
468
|
+
# danger does" — and neither, it turned out, did anything the floor was reading.
|
|
469
|
+
# Each P-lane below was confirmed to return 0 before the fix and 1 after; each is paired with an
|
|
470
|
+
# anti-over-block N-lane so the check is calibrated on a known-negative, not only a known-positive.
|
|
471
|
+
FPGRANT=' ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
472
|
+
|
|
473
|
+
# P-F3a / P-F3b — the drift the review reproduced: same name, same target, same capabilities, same
|
|
474
|
+
# empty sinks; only the acting skill (or what it does) changed.
|
|
475
|
+
mkreg ' - {name: ok, owner: DANGEROUS-OTHER-SKILL, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
476
|
+
mkuap "standing_consent:
|
|
477
|
+
$FPGRANT"
|
|
478
|
+
lane "P-F3a a class re-pointed to a different OWNER under a live grant is refused" 1 "R7"
|
|
479
|
+
|
|
480
|
+
mkreg ' - {name: ok, owner: o, mode: unrestricted, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: true}'
|
|
481
|
+
mkuap "standing_consent:
|
|
482
|
+
$FPGRANT"
|
|
483
|
+
lane "P-F3b a class whose MODE changed under a live grant is refused" 1 "R7"
|
|
484
|
+
|
|
485
|
+
# N-F3c — the control for both: an intact fingerprint must still pass. Without it P-F3a/b prove
|
|
486
|
+
# only that something fails, not that the owner/mode comparison is what fired.
|
|
487
|
+
mkreg "$OKCLASS"
|
|
488
|
+
mkuap "standing_consent:
|
|
489
|
+
$FPGRANT"
|
|
490
|
+
lane "N-F3c an intact owner/mode/target/sinks fingerprint still passes (anti-over-block)" 0 ""
|
|
491
|
+
|
|
492
|
+
# P-F3d — presence. A grant written without the fingerprint cannot detect drift in it later, which
|
|
493
|
+
# is the same argument R6 already made for effects/target and did not apply to the other three.
|
|
494
|
+
mkuap 'standing_consent:
|
|
495
|
+
ok: {granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
496
|
+
lane "P-F3d a grant recording no owner/mode/sinks has no fingerprint to compare" 1 "R6"
|
|
497
|
+
|
|
498
|
+
# N-F3e — the FALSY-LAUNDERING control, the defect class this file has now been bitten by three
|
|
499
|
+
# times. `sinks: []` is a REAL fingerprint ("this class crossed nothing at grant time") and must not
|
|
500
|
+
# collapse into "not recorded". If this lane ever goes red with an R6 sinks message, a sentinel
|
|
501
|
+
# check was replaced by a truthiness test.
|
|
502
|
+
mkuap "standing_consent:
|
|
503
|
+
$FPGRANT"
|
|
504
|
+
lane "N-F3e an EMPTY sinks list is a recorded fingerprint, not a missing one" 0 ""
|
|
505
|
+
|
|
506
|
+
# P-F3f/g — types. The registry side got strict types at R1-b and the grant side got them for
|
|
507
|
+
# effects/target at R6; the three new fields inherit the same rule rather than a laxer one.
|
|
508
|
+
mkuap 'standing_consent:
|
|
509
|
+
ok: {owner: o, mode: m, sinks: "go-public", granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
510
|
+
lane "P-F3f a scalar sinks fingerprint is unreadable, and unreadable is undeclared" 1 "R6"
|
|
511
|
+
|
|
512
|
+
mkuap 'standing_consent:
|
|
513
|
+
ok: {owner: " ", mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
514
|
+
lane "P-F3g a blank owner is not a recorded owner" 1 "R6"
|
|
515
|
+
|
|
516
|
+
# N-F3h — ONE normalizer, both sides. The registry side reads `owner` through _norm; if the grant
|
|
517
|
+
# side ever compares raw strings, ` o ` and `o` become two values and the gate fires on a file that
|
|
518
|
+
# did not drift. Divergent normalizers is exactly how R2-b was first written wrong.
|
|
519
|
+
mkuap 'standing_consent:
|
|
520
|
+
ok: {owner: " o ", mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}'
|
|
521
|
+
lane "N-F3h whitespace around an owner is not drift (single normalizer, both sides)" 0 ""
|
|
522
|
+
|
|
523
|
+
# ---- R9-F4-b: THE SAME FAIL-OPEN, ONE BRANCH OVER ---------------------------------
|
|
524
|
+
# F4 gave "nothing to join" its own exit code for a MISSING registry, ZERO classes and a MISSING
|
|
525
|
+
# UAP — and left a PRESENT UAP holding ZERO grants returning 0. Identical state, two codes. Lane
|
|
526
|
+
# D1-d already asserted in its own name that "no grants is not a verified pass", and the EXIT
|
|
527
|
+
# CONTRACT already defined 3 as "there was nothing to join"; only the code disagreed. A caller
|
|
528
|
+
# writing `if scripts/consent_registry_check.sh; then run_unprompted; fi` ran unprompted against a
|
|
529
|
+
# profile that had granted it nothing.
|
|
530
|
+
mkreg "$OKCLASS"
|
|
531
|
+
mkuap 'standing_consent: {}'
|
|
532
|
+
lane "D3 a valid registry with an EXPLICITLY empty grant set is UNMEASURED (3)" 3 ""
|
|
533
|
+
|
|
534
|
+
# D3-b — the discriminating control. If D3 and this lane ever return the same code again, the typed
|
|
535
|
+
# channel has collapsed back into prose and 0 no longer means "a real grant was joined".
|
|
536
|
+
mkuap "standing_consent:
|
|
537
|
+
$FPGRANT"
|
|
538
|
+
lane "D3-b 0 is still reachable ONLY by joining a real grant" 0 ""
|
|
539
|
+
|
|
540
|
+
# D3-c — precedence. BROKEN outranks UNMEASURED: a registry violation is a decided negative, while
|
|
541
|
+
# "nothing granted" is merely nothing to join. Without this lane the F4-b change could silently
|
|
542
|
+
# convert a real violation into a soft N/A whenever the UAP happened to be empty.
|
|
543
|
+
mkreg ' - {name: ok, owner: o, mode: m, target: t, capabilities: [read], sinks: [], feeds: [], promotion_eligible: "false"}'
|
|
544
|
+
mkuap 'standing_consent: {}'
|
|
545
|
+
lane "D3-c a registry violation still outranks 'nothing granted' (1 beats 3)" 1 "R1-b"
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# ── D3-d: the prose summary must agree with the typed exit ────────────────────
|
|
549
|
+
# A tail-reading operator saw "consent-registry: PASS" on a run that exited 3 and whose own line
|
|
550
|
+
# said "(not a PASS)". Machines read the code; humans read the last line. They must not disagree.
|
|
551
|
+
cat > "$TD/r.yaml" <<'YAML'
|
|
552
|
+
classes:
|
|
553
|
+
- name: safe-thing
|
|
554
|
+
owner: some-skill
|
|
555
|
+
mode: readonly
|
|
556
|
+
target: "tracks/_meta/**"
|
|
557
|
+
capabilities: [read]
|
|
558
|
+
effects: [read]
|
|
559
|
+
sinks: []
|
|
560
|
+
feeds: []
|
|
561
|
+
promotion_eligible: true
|
|
562
|
+
YAML
|
|
563
|
+
printf -- '---\nstanding_consent: {}\n---\n' > "$TD/u.md"
|
|
564
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1; _rc=$?
|
|
565
|
+
if [ "$_rc" -ne 0 ] && tail -1 "$TD/o" | grep -q '^consent-registry: PASS$'; then
|
|
566
|
+
bad "D3-d prose summary says PASS while the typed exit is $_rc"
|
|
567
|
+
else
|
|
568
|
+
ok "D3-d prose summary agrees with the typed exit (rc=$_rc)"
|
|
569
|
+
fi
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# ── P-VOCAB: the irreversible vocabulary must not be evadable by case or invisible characters ──
|
|
573
|
+
# Carried for two sessions as a named residual ("_norm does not case-fold"). A cross-family round
|
|
574
|
+
# MEASURED it as a live fail-open: a class whose capability is a history rewrite was PASSing and
|
|
575
|
+
# therefore promotable. Each variant below returned rc=0 before the fix.
|
|
576
|
+
for _v in 'History-Rewrite' 'HISTORY-REWRITE' 'local-wrtie'; do
|
|
577
|
+
cat > "$TD/r.yaml" <<YAML
|
|
578
|
+
classes:
|
|
579
|
+
- name: rewrite
|
|
580
|
+
owner: o
|
|
581
|
+
mode: m
|
|
582
|
+
target: t
|
|
583
|
+
capabilities: ["$_v"]
|
|
584
|
+
effects: [read]
|
|
585
|
+
sinks: []
|
|
586
|
+
feeds: []
|
|
587
|
+
promotion_eligible: true
|
|
588
|
+
YAML
|
|
589
|
+
cat > "$TD/u.md" <<'YAML'
|
|
590
|
+
---
|
|
591
|
+
standing_consent:
|
|
592
|
+
rewrite: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}
|
|
593
|
+
---
|
|
594
|
+
YAML
|
|
595
|
+
bash "$CHK" "$TD/r.yaml" "$TD/u.md" >"$TD/o" 2>&1; _rc=$?
|
|
596
|
+
# Assert the RULE, not merely a non-zero exit. These fixtures also trip R7 (their grant effect is
|
|
597
|
+
# not a subset of the malformed capability list), so "exit != 0" would have passed even with the
|
|
598
|
+
# vocabulary check removed — right answer, wrong reason, which is the shape this suite's own
|
|
599
|
+
# lane() helper exists to reject. (Partial vacuity found by cross-family round 2.)
|
|
600
|
+
if [ "$_rc" -eq 0 ]; then bad "P-VOCAB '$_v' still PASSes (irreversible/unknown floor evaded)"
|
|
601
|
+
elif grep -qE '❌ (R2-b|R2-c)' "$TD/o"; then ok "P-VOCAB '$_v' refused BY THE FLOOR (R2-b/R2-c)"
|
|
602
|
+
else bad "P-VOCAB '$_v' refused, but not by R2-b/R2-c — the vocabulary check was not the reason"; fi
|
|
603
|
+
done
|
|
604
|
+
|
|
605
|
+
# N-VOCAB: the anti-over-block control. A class using ONLY declared capabilities must still pass —
|
|
606
|
+
# a vocabulary check that refuses legitimate input teaches bypass and is a defect of equal rank.
|
|
607
|
+
cat > "$TD/r.yaml" <<'YAML'
|
|
608
|
+
classes:
|
|
609
|
+
- name: ok-class
|
|
610
|
+
owner: o
|
|
611
|
+
mode: m
|
|
612
|
+
target: t
|
|
613
|
+
capabilities: [read, local-write, network, dispatch, repo-mutation]
|
|
614
|
+
effects: [read]
|
|
615
|
+
sinks: []
|
|
616
|
+
feeds: []
|
|
617
|
+
promotion_eligible: true
|
|
618
|
+
YAML
|
|
619
|
+
cat > "$TD/u.md" <<'YAML'
|
|
620
|
+
---
|
|
621
|
+
standing_consent:
|
|
622
|
+
ok-class: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}
|
|
623
|
+
---
|
|
624
|
+
YAML
|
|
625
|
+
lane "N-VOCAB every declared capability is still accepted (no over-block)" 0 ""
|
|
626
|
+
|
|
627
|
+
# ── D4: BROKEN outranks UNMEASURED even at the zero-class early return ──
|
|
628
|
+
printf 'classes: []\n' > "$TD/r.yaml"
|
|
629
|
+
cat > "$TD/u.md" <<'YAML'
|
|
630
|
+
---
|
|
631
|
+
standing_consent:
|
|
632
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}
|
|
633
|
+
---
|
|
634
|
+
YAML
|
|
635
|
+
lane "D4 live grant + zero registered classes = BROKEN (1), not 'nothing to join' (3)" 1 ""
|
|
636
|
+
printf 'classes: []\n' > "$TD/r.yaml"
|
|
637
|
+
printf -- '---\nstanding_consent: {}\n---\n' > "$TD/u.md"
|
|
638
|
+
lane "D4-b no grant + zero classes is still UNMEASURED (3) — the discriminating control" 3 ""
|
|
639
|
+
|
|
640
|
+
# ── K1: KNOWN OVER-BLOCK, pinned as known, NOT as correct ─────────────────────
|
|
641
|
+
# A canonical YAML merge-key override (`<<: *anchor` then a field override) is reported as a
|
|
642
|
+
# duplicate key and refused. This is a REAL over-block. It is pinned rather than fixed because the
|
|
643
|
+
# one attempt to rewrite the parser regressed 16 of 41 lanes and was reverted in the same session;
|
|
644
|
+
# the fix belongs in its own change with its own lanes. If this lane ever flips to rc=0, the
|
|
645
|
+
# over-block was fixed — update the label, do not silence it.
|
|
646
|
+
cat > "$TD/r.yaml" <<'YAML'
|
|
647
|
+
classes:
|
|
648
|
+
- name: ok
|
|
649
|
+
owner: o
|
|
650
|
+
mode: m
|
|
651
|
+
target: t
|
|
652
|
+
capabilities: [read]
|
|
653
|
+
effects: [read]
|
|
654
|
+
sinks: []
|
|
655
|
+
feeds: []
|
|
656
|
+
promotion_eligible: true
|
|
657
|
+
YAML
|
|
658
|
+
cat > "$TD/u.md" <<'YAML'
|
|
659
|
+
---
|
|
660
|
+
defaults: &d {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: WRONG}
|
|
661
|
+
standing_consent:
|
|
662
|
+
ok:
|
|
663
|
+
<<: *d
|
|
664
|
+
target: t
|
|
665
|
+
---
|
|
666
|
+
YAML
|
|
667
|
+
# PROMOTED from "known over-block, pinned not endorsed" to a real assertion (2026-08-02). The
|
|
668
|
+
# over-block was closed by moving duplicate detection BEFORE merge resolution — a reorder, not the
|
|
669
|
+
# parser rewrite that regressed 16 of 41 lanes. Both directions are asserted: a canonical override is
|
|
670
|
+
# accepted here, and K1-b below keeps the literal-duplicate detection it was protecting.
|
|
671
|
+
lane "K1 a canonical YAML merge-key override is ACCEPTED (over-block closed)" 0 ""
|
|
672
|
+
|
|
673
|
+
cat > "$TD/u.md" <<'YAML'
|
|
674
|
+
---
|
|
675
|
+
standing_consent:
|
|
676
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}
|
|
677
|
+
ok: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}
|
|
678
|
+
---
|
|
679
|
+
YAML
|
|
680
|
+
lane "K1-b a LITERAL duplicate key is still fail-closed (the paired control)" 1 ""
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ── N-SPELL / P-SPELL: effects⊆capabilities is a VOCABULARY question ────────────
|
|
685
|
+
# Splitting the normalizer by purpose fixed a fail-open and opened an over-block: this comparison was
|
|
686
|
+
# left on the identity normalizer, so `capabilities: [READ]` with a grant `effects: [read]` — the same
|
|
687
|
+
# effect class, spelled differently — was refused. Over-blocking is not a safe default; a gate that
|
|
688
|
+
# refuses correct input is one the operator learns to route around. Both directions are pinned so a
|
|
689
|
+
# future normalizer change cannot fix one by breaking the other.
|
|
690
|
+
cat > "$TD/r.yaml" <<'YAML'
|
|
691
|
+
classes:
|
|
692
|
+
- name: c
|
|
693
|
+
owner: o
|
|
694
|
+
mode: m
|
|
695
|
+
target: t
|
|
696
|
+
capabilities: [READ]
|
|
697
|
+
effects: [READ]
|
|
698
|
+
sinks: []
|
|
699
|
+
feeds: []
|
|
700
|
+
promotion_eligible: true
|
|
701
|
+
YAML
|
|
702
|
+
cat > "$TD/u.md" <<'YAML'
|
|
703
|
+
---
|
|
704
|
+
standing_consent:
|
|
705
|
+
c: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [read], target: t}
|
|
706
|
+
---
|
|
707
|
+
YAML
|
|
708
|
+
lane "N-SPELL a spelling variant of the SAME effect class is accepted (no over-block)" 0 ""
|
|
709
|
+
|
|
710
|
+
cat > "$TD/r.yaml" <<'YAML'
|
|
711
|
+
classes:
|
|
712
|
+
- name: c
|
|
713
|
+
owner: o
|
|
714
|
+
mode: m
|
|
715
|
+
target: t
|
|
716
|
+
capabilities: [read]
|
|
717
|
+
effects: [read]
|
|
718
|
+
sinks: []
|
|
719
|
+
feeds: []
|
|
720
|
+
promotion_eligible: true
|
|
721
|
+
YAML
|
|
722
|
+
cat > "$TD/u.md" <<'YAML'
|
|
723
|
+
---
|
|
724
|
+
standing_consent:
|
|
725
|
+
c: {owner: o, mode: m, sinks: [], granted: 2026-07-29, expires: 2026-12-31, effects: [delete], target: t}
|
|
726
|
+
---
|
|
727
|
+
YAML
|
|
728
|
+
lane "P-SPELL a grant WIDER than its class is still refused (the paired control)" 1 "R7"
|
|
729
|
+
|
|
730
|
+
|
|
432
731
|
echo "----"
|
|
433
732
|
echo "consent-registry anchor: $pass passed, $fail failed"
|
|
434
733
|
[ "$fail" -eq 0 ] || exit 1
|
|
@@ -60,6 +60,26 @@ expect "stash pop is CLEAN" CLEAN 'git stash pop'
|
|
|
60
60
|
expect "worktree remove --force" HIT 'git worktree remove --force ../wt'
|
|
61
61
|
expect "worktree remove plain is CLEAN" CLEAN 'git worktree remove ../wt'
|
|
62
62
|
|
|
63
|
+
echo "-- global-option allowlist completion (leg-C MED round, 2026-08-01) --"
|
|
64
|
+
# Any global option the normalizer does not consume lets `git <opt> reset --hard` sail past every
|
|
65
|
+
# git row. One HIT lane per newly consumed class + adjacent CLEANs (known-pair rule).
|
|
66
|
+
expect "literal-pathspecs then reset --hard" HIT 'git --literal-pathspecs reset --hard'
|
|
67
|
+
expect "namespace= then reset --hard" HIT 'git --namespace=ns reset --hard'
|
|
68
|
+
expect "config-env= then clean -fd" HIT 'git --config-env=AUTHOR=E clean -fd'
|
|
69
|
+
expect "exec-path= then reset --hard" HIT 'git --exec-path=/opt/git reset --hard'
|
|
70
|
+
expect "no-optional-locks then push --force" HIT 'git --no-optional-locks push --force origin main'
|
|
71
|
+
expect "stacked globals then reset --hard" HIT 'git --bare --no-advice -c a=b reset --hard'
|
|
72
|
+
expect "literal-pathspecs status is CLEAN" CLEAN 'git --literal-pathspecs status'
|
|
73
|
+
expect "namespace= log is CLEAN" CLEAN 'git --namespace=ns log --oneline'
|
|
74
|
+
|
|
75
|
+
# Healing-path citation (leg-C MED): the advisory must point at the runnable path, not a bare name.
|
|
76
|
+
h_out=$(printf '%s' 'git reset --hard' | bash "$G" --stdin-raw 2>&1)
|
|
77
|
+
if printf '%s' "$h_out" | grep -q 'templates/predelete_check.sh'; then
|
|
78
|
+
printf ' ✅ %-52s OK\n' "advisory cites templates/predelete_check.sh path"; pass=$((pass+1))
|
|
79
|
+
else
|
|
80
|
+
printf ' ❌ %-52s out=%s\n' "advisory cites templates/predelete_check.sh path" "$h_out"; fail=$((fail+1))
|
|
81
|
+
fi
|
|
82
|
+
|
|
63
83
|
echo "-- rm scope (precision over recall: only root/home/cwd/glob) --"
|
|
64
84
|
expect "rm -rf /" HIT 'rm -rf /'
|
|
65
85
|
expect "rm -rf ~" HIT 'rm -rf ~'
|
|
@@ -218,6 +238,28 @@ else
|
|
|
218
238
|
printf ' ❌ %-52s rc=%s out=%s\n' "C5 non-Bash payload: silent" "$c_rc" "$c_out"; fail=$((fail+1))
|
|
219
239
|
fi
|
|
220
240
|
|
|
241
|
+
echo "-- GPT leg-C round (2026-08-01): option-before-pathspec + staged-only + unicode-input --"
|
|
242
|
+
expect "checkout -f . (leg-C bypass)" HIT 'git checkout -f .'
|
|
243
|
+
expect "restore --source=HEAD . (leg-C bypass)" HIT 'git restore --source=HEAD .'
|
|
244
|
+
expect "restore -s HEAD ." HIT 'git restore -s HEAD .'
|
|
245
|
+
expect "restore --staged . is CLEAN (unstage only)" CLEAN 'git restore --staged .'
|
|
246
|
+
expect "restore --staged --worktree ." HIT 'git restore --staged --worktree .'
|
|
247
|
+
expect "checkout .gitignore still CLEAN" CLEAN 'git checkout .gitignore'
|
|
248
|
+
|
|
249
|
+
# C6 — unicode command + ascii-codec env: the INPUT extractor must survive (leg-C HIGH #8 —
|
|
250
|
+
# output encoder was pinned, input was not; a non-ASCII char anywhere in the payload emptied CMD
|
|
251
|
+
# and the guard failed CLEAN-open).
|
|
252
|
+
c_out=$(python3 -c 'import json;print(json.dumps({"tool_name":"Bash","tool_input":{"command":"git reset --hard # 한글 주석"}}))' | PYTHONIOENCODING=ascii bash "$G" 2>/dev/null); c_rc=$?
|
|
253
|
+
if [ "$c_rc" -eq 0 ] && printf '%s' "$c_out" | python3 -c '
|
|
254
|
+
import json, sys
|
|
255
|
+
d = json.load(sys.stdin)
|
|
256
|
+
assert "DESTRUCTIVE-OP" in d["hookSpecificOutput"]["additionalContext"]
|
|
257
|
+
' 2>/dev/null; then
|
|
258
|
+
printf ' ✅ %-52s %s\n' "C6 unicode cmd + ascii env: still HIT" OK; pass=$((pass+1))
|
|
259
|
+
else
|
|
260
|
+
printf ' ❌ %-52s rc=%s out=%s\n' "C6 unicode cmd + ascii env: still HIT" "$c_rc" "$c_out"; fail=$((fail+1))
|
|
261
|
+
fi
|
|
262
|
+
|
|
221
263
|
echo
|
|
222
264
|
if [ "$fail" -eq 0 ]; then
|
|
223
265
|
echo "[destructive-pre-gate] ✅ all $pass known pairs hold"; exit 0
|