forgedock 1.8.4 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/engine/admission.mjs +23 -4
  2. package/bin/engine/admission.test.mjs +37 -0
  3. package/commands/orchestrate/phase-1-resolve.md +40 -24
  4. package/commands/orchestrate/phase-3-dependency.md +56 -11
  5. package/commands/orchestrate/phase-4-execution.md +359 -111
  6. package/commands/orchestrate/phase-5-cleanup.md +17 -0
  7. package/commands/orchestrate/phase-6-report.md +1 -1
  8. package/commands/quality-gate.md +80 -0
  9. package/commands/recover-orphans.md +3 -3
  10. package/commands/review-pr.md +98 -16
  11. package/commands/work-on/build/context.md +25 -4
  12. package/commands/work-on/build/validate.md +16 -9
  13. package/commands/work-on/build.md +26 -9
  14. package/commands/work-on/close.md +104 -40
  15. package/commands/work-on/decompose.md +13 -7
  16. package/commands/work-on/investigate.md +13 -7
  17. package/commands/work-on/remediate.md +15 -4
  18. package/commands/work-on/review.md +48 -19
  19. package/commands/work-on.md +62 -9
  20. package/docs/CODEX.md +12 -0
  21. package/docs/CONFIG.md +49 -1
  22. package/docs/RELEASE-NOTES.md +61 -0
  23. package/package.json +1 -1
  24. package/scripts/amplification-breaker.sh +89 -0
  25. package/scripts/amplification-breaker.test.sh +39 -0
  26. package/scripts/check-admission-parity.mjs +74 -1
  27. package/scripts/check-spec-bash.sh +120 -0
  28. package/scripts/check-spec-bash.test.sh +90 -0
  29. package/scripts/check-spec-markers.sh +67 -3
  30. package/scripts/classify-finding.sh +123 -0
  31. package/scripts/classify-finding.test.sh +77 -0
  32. package/scripts/close-dossier-no-main-commit.test.sh +139 -0
  33. package/scripts/forge-root.test.sh +84 -27
  34. package/scripts/lint-build-validate-files.sh +29 -0
  35. package/scripts/lint-build-validate-files.test.sh +25 -0
  36. package/scripts/lint-dispatch-prompt.test.sh +2 -1
  37. package/scripts/recovery-claim-live.sh +73 -0
  38. package/scripts/recovery-claim-live.test.sh +66 -0
  39. package/scripts/verify-phase-trail.sh +183 -4
  40. package/scripts/verify-phase-trail.test.sh +211 -1
@@ -9,7 +9,7 @@
9
9
  # verdict alone after phases were silently skipped.
10
10
  #
11
11
  # Usage:
12
- # verify-phase-trail.sh <issue> -R <owner/repo> [--docs-only] [--code-diff] [--head-tree <sha>]
12
+ # verify-phase-trail.sh <issue> -R <owner/repo> [--docs-only] [--code-diff] [--head-tree <sha>] [--head-sha <sha>]
13
13
  #
14
14
  # --docs-only The diff is documentation-only; the FORGE:QUALITY_GATE marker
15
15
  # is not required.
@@ -22,6 +22,9 @@
22
22
  # An empty or non-hex value is a usage error (exit 2, fails closed).
23
23
  # Scope: this binds the PASS to the built tree; it is not tamper-proof, because the
24
24
  # pipeline agent posts under the same trusted identity and could compute the tree itself.
25
+ # --head-sha Commit id (40 or 64 hex) of the head being gated. Enables the human break-glass
26
+ # override (below); without it an override comment is never evaluated. An empty or
27
+ # non-hex value is a usage error (exit 2, fails closed).
25
28
  #
26
29
  # Requirements by COMPLEXITY_BAND (read from the FORGE:FAST_PATH comment).
27
30
  # The authoritative table is the `case "$EFFECTIVE"` block below; this summary must be
@@ -38,9 +41,14 @@
38
41
  # PHASE_TRAIL: PASS|FAIL|ERROR
39
42
  # BAND: <band>
40
43
  # MISSING: <marker> -> <phase to re-run> (one line per missing artifact)
44
+ # PHASE_TRAIL: OVERRIDDEN (exit 0 via an accepted break-glass override)
45
+ # WAIVED: <marker list> (the MISSING set the override waived)
46
+ # OVERRIDE: approver=<login> head=<sha> missing=<list> reason=<sanitised>
47
+ # (the ONLY source callers use to record the override)
41
48
  #
42
- # Exit codes: 0 pass, 1 one or more artifacts missing, 2 could not
49
+ # Exit codes: 0 pass (or accepted override), 1 one or more artifacts missing, 2 could not
43
50
  # read the issue (fails closed — an unreadable trail is never treated as a pass).
51
+ # Exit 2 is NEVER overridable: override evaluation only runs on the exit-1 (MISSING) path.
44
52
  # Callers route on the exit code: 1 -> re-run the MISSING phases; 2 (or 127 when the
45
53
  # verifier itself cannot be resolved) -> infrastructure BLOCKED, never "missing phases".
46
54
  #
@@ -62,11 +70,36 @@
62
70
  # Identities outside that set -- e.g. a human with author_association CONTRIBUTOR/NONE/FIRST_TIME_CONTRIBUTOR
63
71
  # (an external contributor running the pipeline under their own login) -- are NOT trusted, so their
64
72
  # markers are ignored and the gate reports them MISSING. To accept such an identity add its login to
65
- # FORGE_TRAIL_TRUSTED_LOGINS, or widen FORGE_TRAIL_TRUSTED_ASSOCIATIONS (e.g. add CONTRIBUTOR). On FAIL the
73
+ # FORGE_TRAIL_TRUSTED_LOGINS (do NOT widen FORGE_TRAIL_TRUSTED_ASSOCIATIONS to CONTRIBUTOR: that lets any
74
+ # contributor forge FORGE:* trail markers). On FAIL the
66
75
  # script prints a NOTE when untrusted-author FORGE markers were seen, so this is diagnosable (#3123).
67
76
  # Limits: "Bot" trusts any GitHub App/bot that can comment on the repo (set
68
77
  # FORGE_TRAIL_TRUSTED_ASSOCIATIONS and FORGE_TRAIL_TRUSTED_LOGINS to tighten);
69
78
  # COLLABORATOR includes read-level collaborators; login matching is case-sensitive.
79
+ # The override prefilter uses FORGE_TRAIL_OVERRIDE_ASSOCIATIONS (default: the trail list). Widen THAT variable,
80
+ # not the trail list, for concealed-membership approvers; it never affects which FORGE markers count toward the
81
+ # gate. Blast radius of widening FORGE_TRAIL_TRUSTED_ASSOCIATIONS: every listed association can forge trail markers.
82
+ #
83
+ # Break-glass override (#3152): a misfiring gate can be cleared by a HUMAN, never by the pipeline.
84
+ # A comment whose body starts with `<!-- FORGE:PHASE_TRAIL_OVERRIDE -->` and carries the lines
85
+ # **Head**: <full head sha>
86
+ # **Missing**: <marker names, comma separated>
87
+ # **Reason**: <one line>
88
+ # is honoured only when ALL of these hold (any error, null field or API failure rejects it and the
89
+ # gate stays at exit 1; never a pass):
90
+ # - user.type == "User" (allowlist: Bots and null users are rejected) with a plain login;
91
+ # - updated_at == created_at (an edited comment is rejected: tamper evidence);
92
+ # - the author has repo permission admin or write (collaborators/<login>/permission), not mere
93
+ # org membership or read access;
94
+ # - the author is NOT a pipeline identity: FORGE_TRAIL_PIPELINE_LOGINS (comma-separated, case-insensitive)
95
+ # plus the author of every trusted FORGE marker comment (BUILDER, INVESTIGATOR, CONTRACT, ...), so a
96
+ # pipeline running under a human token cannot approve its own gate. A solo operator therefore needs a
97
+ # second human with write access to post the override;
98
+ # - **Head** equals --head-sha and **Missing** equals the current MISSING marker-name set exactly
99
+ # (a new commit or a different failure invalidates it);
100
+ # - created_at is later than the latest trusted FORGE:BUILDER:COMPLETE update time.
101
+ # The reason is sanitised (control chars stripped, comment markers neutralised, whitespace collapsed,
102
+ # capped at 200 chars); an empty reason rejects the override.
70
103
  # END-HELP (`-h` prints the header up to this line; keep it last)
71
104
 
72
105
  set -uo pipefail
@@ -77,6 +110,8 @@ DOCS_ONLY=0
77
110
  CODE_DIFF=0
78
111
  HEAD_TREE=""
79
112
  HEAD_TREE_SET=0
113
+ HEAD_SHA=""
114
+ HEAD_SHA_SET=0
80
115
 
81
116
  while [ $# -gt 0 ]; do
82
117
  case "$1" in
@@ -88,6 +123,9 @@ while [ $# -gt 0 ]; do
88
123
  --head-tree)
89
124
  if [ $# -lt 2 ]; then echo "PHASE_TRAIL: ERROR"; echo "usage error: $1 needs a value" >&2; exit 2; fi
90
125
  HEAD_TREE="$2"; HEAD_TREE_SET=1; shift 2 ;;
126
+ --head-sha)
127
+ if [ $# -lt 2 ]; then echo "PHASE_TRAIL: ERROR"; echo "usage error: $1 needs a value" >&2; exit 2; fi
128
+ HEAD_SHA="$2"; HEAD_SHA_SET=1; shift 2 ;;
91
129
  -h|--help) awk 'NR >= 5 { if (/^# END-HELP/) exit; print }' "$0"; exit 0 ;;
92
130
  *)
93
131
  if [ -z "$ISSUE" ] && [[ "$1" =~ ^[0-9]+$ ]]; then ISSUE="$1"; shift
@@ -98,7 +136,7 @@ done
98
136
 
99
137
  if [ -z "$ISSUE" ] || [ -z "$REPO" ]; then
100
138
  echo "PHASE_TRAIL: ERROR"
101
- echo "usage: verify-phase-trail.sh <issue> -R <owner/repo> [--docs-only] [--code-diff] [--head-tree <sha>]" >&2
139
+ echo "usage: verify-phase-trail.sh <issue> -R <owner/repo> [--docs-only] [--code-diff] [--head-tree <sha>] [--head-sha <sha>]" >&2
102
140
  exit 2
103
141
  fi
104
142
  if [ "$HEAD_TREE_SET" = "1" ]; then
@@ -111,6 +149,16 @@ if [ "$HEAD_TREE_SET" = "1" ]; then
111
149
  fi
112
150
  fi
113
151
 
152
+ if [ "$HEAD_SHA_SET" = "1" ]; then
153
+ # Fail closed: a caller that could not resolve the head passes an empty value and must not silently skip the binding.
154
+ HEAD_SHA=$(printf '%s' "$HEAD_SHA" | tr 'A-F' 'a-f')
155
+ if ! [[ "$HEAD_SHA" =~ ^([0-9a-f]{40}|[0-9a-f]{64})$ ]]; then
156
+ echo "PHASE_TRAIL: ERROR"
157
+ echo "usage error: --head-sha needs a 40 or 64 hex commit id" >&2
158
+ exit 2
159
+ fi
160
+ fi
161
+
114
162
  RAW=$(gh api "repos/${REPO}/issues/${ISSUE}/comments" --paginate 2>/dev/null) || {
115
163
  echo "PHASE_TRAIL: ERROR"
116
164
  echo "could not read comments for ${REPO}#${ISSUE}" >&2
@@ -130,6 +178,10 @@ RAW=$(printf '%s' "$RAW" | jq -s 'if all(.[]; type == "array" and all(.[]; type
130
178
  # One line per comment, newlines folded to \x1f so a marker and its sentinel can
131
179
  # be matched within the SAME comment.
132
180
  TRUSTED_ASSOC="${FORGE_TRAIL_TRUSTED_ASSOCIATIONS-OWNER,MEMBER,COLLABORATOR}"
181
+ # Separate trust decision (forge#3350): the break-glass override prefilter has its own list so it can be widened
182
+ # (e.g. CONTRIBUTOR for concealed-membership approvers) without loosening the trail-marker gate. Defaults to the
183
+ # trail list; ${VAR-default} (no colon) so an explicitly empty value yields no override candidates (fail closed).
184
+ OVERRIDE_ASSOC="${FORGE_TRAIL_OVERRIDE_ASSOCIATIONS-$TRUSTED_ASSOC}"
133
185
  TRUSTED_LOGINS="${FORGE_TRAIL_TRUSTED_LOGINS-}"
134
186
  COMMENTS=$(printf '%s' "$RAW" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" '
135
187
  ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
@@ -263,6 +315,133 @@ if [ "${#MISSING[@]}" -eq 0 ]; then
263
315
  exit 0
264
316
  fi
265
317
 
318
+ # --- Break-glass override (#3152) ---------------------------------------------------------------
319
+ # Reached ONLY with a non-empty MISSING set (exit-1 path); every `exit 2` above precedes it.
320
+ # Prints the OVERRIDE/WAIVED lines and returns 0 only when a comment passes every rule; any failure
321
+ # (jq error, permission API error, empty field) returns 1 and the caller falls through to FAIL.
322
+ # Name set normaliser shared by BOTH sides of the comparison: drops ` -> action` and `(qualifier)`,
323
+ # trims, de-duplicates, sorts, joins with commas. bash 3.2/BSD portable.
324
+ norm_set() {
325
+ sed -e 's/ -> .*$//' -e 's/([^)]*)//g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \
326
+ | grep -v '^$' | LC_ALL=C sort -u | paste -sd, -
327
+ }
328
+
329
+ evaluate_override() {
330
+ [ "$HEAD_SHA_SET" = "1" ] || return 1
331
+ local cur_set cands pipeline_logins login created head missing reason perm n=0 total=0 seen_logins="" llogin
332
+ cur_set=$(printf '%s\n' "${MISSING[@]}" | norm_set) || return 1
333
+ [ -n "$cur_set" ] || return 1
334
+ # BUILDER exists but its completion time is unknown -> cannot prove the override is newer: reject.
335
+ if has '<!-- FORGE:BUILDER -->' && [ -z "$BUILD_AT" ]; then return 1; fi
336
+ # Pipeline identity set (configured + derived): authors of trusted FORGE marker comments other than overrides.
337
+ # The token this verifier runs under is the pipeline's identity too (forge#3269): a pipeline using a human
338
+ # token with no markers yet must not approve its own override. Lookup failure adds nothing (derived set still applies).
339
+ local self_login extra_logins marker_floor
340
+ self_login=$(gh api user --jq '.login' 2>/dev/null || true)
341
+ case "$self_login" in *[!A-Za-z0-9_-]*) self_login="" ;; esac
342
+ extra_logins="${FORGE_TRAIL_PIPELINE_LOGINS-}${self_login:+,$self_login}"
343
+ pipeline_logins=$(printf '%s' "$RAW" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg extra "$extra_logins" '
344
+ ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
345
+ | ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
346
+ | ($extra | split(",") | map(gsub("^\\s+|\\s+$"; "") | ascii_downcase) | map(select(length > 0))) as $E
347
+ | ( [ .[]
348
+ | select(
349
+ ((.author_association // "") as $x | $A | index($x) != null)
350
+ or ((.user.type // "") == "Bot")
351
+ or ((.user.login // "") as $x | $L | index($x) != null))
352
+ | select((.body // "") | startswith("<!-- FORGE:") and (startswith("<!-- FORGE:PHASE_TRAIL_OVERRIDE -->") | not))
353
+ | (.user.login // empty) | ascii_downcase ] + $E ) | unique | .[]' 2>/dev/null) || return 1
354
+ # Time floor (forge#3271): the latest trusted BUILDER:COMPLETE. With none, fall back to the newest trusted
355
+ # non-override FORGE marker so an override still has to post-date the pipeline's own activity.
356
+ marker_floor="$BUILD_AT"
357
+ if [ -z "$marker_floor" ]; then
358
+ marker_floor=$(printf '%s' "$RAW" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" '
359
+ ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
360
+ | ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
361
+ | [ .[]
362
+ | select(((.author_association // "") as $x | $A | index($x) != null)
363
+ or ((.user.type // "") == "Bot")
364
+ or ((.user.login // "") as $x | $L | index($x) != null))
365
+ | select((.body // "") | startswith("<!-- FORGE:") and (startswith("<!-- FORGE:PHASE_TRAIL_OVERRIDE -->") | not))
366
+ | .created_at // empty ] | sort | .[-1] // empty' 2>/dev/null) || return 1
367
+ fi
368
+ # Candidates, newest first, as TSV: login created_at head missing reason (reason last, others never empty).
369
+ # The author_association prefilter uses its own list ($OVERRIDE_ASSOC, default = trail list) and is only an
370
+ # optimization (forge#3279): the permission API stays authoritative. A real write/admin approver with concealed
371
+ # org membership can show CONTRIBUTOR/NONE and is filtered out here; widen FORGE_TRAIL_OVERRIDE_ASSOCIATIONS to
372
+ # accept them (never the trail list). An empty list yields no candidates (fail closed). Filtered candidates are counted in a NOTE.
373
+ # One jq pass (forge#3351): the eligibility predicate is defined once (`eligible`); the first output line is the
374
+ # count of eligible comments dropped by the association prefilter, the rest are the candidate TSV rows.
375
+ local assoc_dropped out
376
+ out=$(printf '%s' "$RAW" | jq -r --arg assoc "$OVERRIDE_ASSOC" --arg bt "$marker_floor" '
377
+ ($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
378
+ | def clean: explode | map(select(. >= 32 and . != 127 and (. < 128 or . > 159) and (. < 8203 or . > 8207) and (. < 8232 or . > 8238) and (. < 8288 or . > 8297) and . != 65279)) | implode;
379
+ def eligible:
380
+ select((.body // "") | startswith("<!-- FORGE:PHASE_TRAIL_OVERRIDE -->"))
381
+ | select((.user.type // "") == "User")
382
+ | select(((.user.login // "") | test("^[A-Za-z0-9_-]+$")))
383
+ | select((.created_at // "") != "" and (.updated_at // "") != "" and .created_at == .updated_at)
384
+ | select($bt == "" or .created_at > $bt);
385
+ def assoc_ok: (.author_association // "") as $x | $A | index($x) != null;
386
+ ( [ .[] | eligible | select(assoc_ok | not) ] | length ) as $dropped
387
+ | ( [ .[]
388
+ | eligible
389
+ | select(assoc_ok)
390
+ | (.body | split("\n") | map(sub("\r$"; ""))) as $ln
391
+ | { login: .user.login, at: .created_at,
392
+ head: ([$ln[] | capture("^\\*\\*Head\\*\\*: *(?<v>[0-9A-Fa-f]{40,64}) *$")? | .v] | .[0] // "-"),
393
+ missing: ([$ln[] | capture("^\\*\\*Missing\\*\\*: *(?<v>.+)$")? | .v] | .[0] // "-"),
394
+ reason: ([$ln[] | capture("^\\*\\*Reason\\*\\*: *(?<v>.*)$")? | .v] | .[0] // "")
395
+ | clean | gsub("<!--"; "<!-/-") | gsub("-->"; "-/->") | gsub("`"; "'"'"'") | gsub("@"; "(at)")
396
+ | gsub("\\s+"; " ") | sub("^ "; "") | sub(" $"; "") | .[0:200] } ]
397
+ | sort_by(.at) | reverse ) as $rows
398
+ | "#DROPPED\t\($dropped)",
399
+ ($rows[] | [.login, .at, .head, .missing, (if .reason == "" then "-" else .reason end)] | @tsv)' 2>/dev/null) || return 1
400
+ assoc_dropped=$(printf '%s\n' "$out" | head -n 1 | cut -f2)
401
+ case "$assoc_dropped" in ''|*[!0-9]*) assoc_dropped=0 ;; esac
402
+ if [ "$assoc_dropped" -gt 0 ]; then echo "NOTE: ${assoc_dropped} override candidate(s) ignored: author_association not in override-approver set" >&2; fi
403
+ cands=$(printf '%s\n' "$out" | tail -n +2)
404
+ [ -n "$cands" ] || return 1
405
+ while IFS=$'\t' read -r login created head missing reason; do
406
+ [ -n "$login" ] || continue
407
+ [ "$reason" != "-" ] && [ -n "$reason" ] || continue
408
+ reason=$(printf '%s' "$reason" | tr -d '[:cntrl:]') # belt and braces: cntrl (incl. ESC) stripped again
409
+ [ -n "$reason" ] || continue
410
+ [ "$(printf '%s' "$head" | tr 'A-F' 'a-f')" = "$HEAD_SHA" ] || continue
411
+ [ "$(printf '%s' "$missing" | tr ',' '\n' | norm_set)" = "$cur_set" ] || continue
412
+ if printf '%s\n' "$pipeline_logins" | grep -Fx -- "$(printf '%s' "$login" | tr 'A-Z' 'a-z')" >/dev/null; then continue; fi
413
+ # forge#3279/#3306: dedupe distinct logins (bash 3.2-portable newline list, no associative arrays); a login seen
414
+ # before was already rejected (an accepted one returns), so repeat comments cost no slot and no API call.
415
+ # Cap accounting: a DEFINITIVE negative (read/triage/none) is free so read-level members cannot crowd out an older
416
+ # real approver; only UNREADABLE lookups spend the 5-slot error cap, and 25 distinct lookups is a hard ceiling.
417
+ llogin=$(printf '%s' "$login" | tr 'A-Z' 'a-z')
418
+ if printf '%s\n' "$seen_logins" | grep -Fx -- "$llogin" >/dev/null; then continue; fi
419
+ total=$((total+1)); [ "$total" -le 25 ] || { echo "NOTE: break-glass override: more than 25 distinct candidate logins; extra ones ignored" >&2; break; } # bound API calls
420
+ seen_logins="${seen_logins}${llogin}"$'\n'
421
+ # Login already matched ^[A-Za-z0-9_-]+$ in jq, so it is safe to interpolate into the API path.
422
+ perm=$(gh api "repos/${REPO}/collaborators/${login}/permission" 2>/dev/null | jq -r '.permission // empty' 2>/dev/null) || perm=""
423
+ case "$perm" in
424
+ admin|write) ;;
425
+ "")
426
+ echo "NOTE: break-glass override by ${login} rejected: permission='unreadable' (needs admin/write; the lookup needs a token with push access)" >&2
427
+ n=$((n+1)); [ "$n" -le 5 ] || { echo "NOTE: break-glass override: more than 5 unreadable permission lookups; extra candidates ignored" >&2; break; }
428
+ continue ;;
429
+ *) echo "NOTE: break-glass override by ${login} rejected: permission='${perm}' (needs admin/write)" >&2; continue ;;
430
+ esac
431
+ echo "PHASE_TRAIL: OVERRIDDEN"
432
+ echo "BAND: ${BAND:-UNKNOWN}"
433
+ echo "WAIVED: ${cur_set}"
434
+ echo "OVERRIDE: approver=${login} head=${HEAD_SHA} missing=${cur_set} reason=${reason}"
435
+ return 0
436
+ done <<< "$cands"
437
+ return 1
438
+ }
439
+
440
+ if evaluate_override; then
441
+ [ -n "${BAND_NOTE:-}" ] && echo "NOTE: $BAND_NOTE"
442
+ exit 0
443
+ fi
444
+
266
445
  echo "PHASE_TRAIL: FAIL"
267
446
  echo "BAND: ${BAND:-UNKNOWN}"
268
447
  for m in "${MISSING[@]}"; do echo "MISSING: $m"; done
@@ -16,7 +16,21 @@ trap 'rm -rf "$TMP_BIN" "$TMP_FX"' EXIT
16
16
  cat > "$TMP_BIN/gh" <<'MOCK'
17
17
  #!/usr/bin/env bash
18
18
  if [ "${MOCK_GH_FAIL:-}" = "1" ]; then echo "mock gh: outage" >&2; exit 1; fi
19
- cat "$MOCK_GH_JSON"
19
+ # Route by endpoint (#3152): unknown endpoints fail, so a test can never pass by accident on the wrong fixture.
20
+ for a in "$@"; do
21
+ case "$a" in
22
+ repos/*/issues/*/comments) cat "$MOCK_GH_JSON"; exit 0 ;;
23
+ user) [ -n "${MOCK_SELF_LOGIN:-}" ] && { echo "$MOCK_SELF_LOGIN"; exit 0; }; exit 1 ;;
24
+ repos/*/collaborators/*/permission)
25
+ [ "${MOCK_PERM_FAIL:-}" = "1" ] && { echo "mock gh: permission api error" >&2; exit 1; }
26
+ u="${a#repos/*/collaborators/}"; u="${u%/permission}"
27
+ [ -n "${MOCK_PERM_LOG:-}" ] && echo "$u" >> "$MOCK_PERM_LOG"
28
+ f="${MOCK_PERM_DIR:-/nonexistent}/$u"
29
+ [ -f "$f" ] && { cat "$f"; exit 0; }
30
+ echo "mock gh: 404" >&2; exit 1 ;;
31
+ esac
32
+ done
33
+ echo "mock gh: unexpected endpoint: $*" >&2; exit 1
20
34
  MOCK
21
35
  chmod +x "$TMP_BIN/gh"
22
36
  export PATH="$TMP_BIN:$PATH"
@@ -273,6 +287,9 @@ OUT=$(MOCK_GH_JSON="$UT" bash "$VERIFY" 3061 -R o/r 2>/dev/null); RC=$?
273
287
  [ $RC -eq 1 ] && echo "$OUT" | grep -q 'MISSING: INVESTIGATOR' && echo "$OUT" | grep -q 'NOTE: .*untrusted authors were ignored' && ok "CONTRIBUTOR markers ignored with NOTE" || bad "contributor note (rc=$RC out=$OUT)"
274
288
  OUT=$(FORGE_TRAIL_TRUSTED_LOGINS=ext MOCK_GH_JSON="$UT" bash "$VERIFY" 3061 -R o/r 2>/dev/null); RC=$?
275
289
  [ $RC -eq 0 ] && ok "FORGE_TRAIL_TRUSTED_LOGINS accepts a CONTRIBUTOR login" || bad "trusted login override (rc=$RC out=$OUT)"
290
+ # forge#3350: widening only the override list must not make CONTRIBUTOR trail markers count
291
+ OUT=$(FORGE_TRAIL_OVERRIDE_ASSOCIATIONS="OWNER,MEMBER,COLLABORATOR,CONTRIBUTOR" MOCK_GH_JSON="$UT" bash "$VERIFY" 3061 -R o/r 2>/dev/null); RC=$?
292
+ [ $RC -eq 1 ] && echo "$OUT" | grep -q 'MISSING: INVESTIGATOR' && ok "override list widened only: CONTRIBUTOR trail markers still ignored (#3350)" || bad "override-only widen trail (rc=$RC out=$OUT)"
276
293
  expect_pass "marker-only CONTEXT satisfies STANDARD" "$(mk minctx INV FP_STANDARD CONTRACT CONTEXT ARCH QG_PASS)"
277
294
 
278
295
  # -h is bounded by the END-HELP sentinel, not a hardcoded line range (#3147)
@@ -294,5 +311,198 @@ OUT=$("${TMO[@]}" bash "$VERIFY" 5 -R 2>/dev/null); RC=$?
294
311
  OUT=$(bash "$VERIFY" 2>/dev/null); RC=$?
295
312
  [ $RC -eq 2 ] && ok "no args is a usage error" || bad "usage error (rc=$RC)"
296
313
 
314
+ # --- Break-glass override (#3152) ---
315
+ HS=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; HS2=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
316
+ PERM="$TMP_FX/perm"; mkdir -p "$PERM"
317
+ echo '{"permission":"write"}' > "$PERM/alice"; echo '{"permission":"admin"}' > "$PERM/carol"
318
+ echo '{"permission":"read"}' > "$PERM/reader"; echo '{"permission":"write"}' > "$PERM/pipeuser"
319
+ echo '{"permission":"write"}' > "$PERM/builduser"
320
+ export MOCK_PERM_DIR="$PERM"
321
+ BT=2026-10-08T01:00:00Z; AFTER=2026-10-08T02:00:00Z; BEFORE=2026-10-08T00:30:00Z
322
+ # Base trail: everything but CONTEXT/ARCHITECT/QUALITY_GATE on a STANDARD band, built (BUILDER:COMPLETE) at $BT by `builduser`.
323
+ ov_base() {
324
+ jq -nc --arg bt "$BT" '[
325
+ {body:"<!-- FORGE:INVESTIGATOR -->\n<!-- INVESTIGATION:COMPLETE -->",user:{login:"builduser",type:"User"},author_association:"OWNER"},
326
+ {body:"<!-- FORGE:FAST_PATH -->\n**COMPLEXITY_BAND**: STANDARD",user:{login:"builduser",type:"User"},author_association:"OWNER"},
327
+ {body:"<!-- FORGE:CONTRACT -->\nc",user:{login:"builduser",type:"User"},author_association:"OWNER"},
328
+ {body:"<!-- FORGE:BUILDER -->\nx\n<!-- FORGE:BUILDER:COMPLETE -->",created_at:$bt,updated_at:$bt,user:{login:"builduser",type:"User"},author_association:"OWNER"}]'
329
+ }
330
+ CUR_MISSING='ARCHITECT,CONTEXT,QUALITY_GATE'
331
+ # ovc <login> <type> <created> <updated> <head> <missing> <reason> -> one override comment (JSON)
332
+ ovc() {
333
+ jq -nc --arg l "$1" --arg t "$2" --arg c "$3" --arg u "$4" --arg h "$5" --arg m "$6" --arg r "$7" \
334
+ '{body:("<!-- FORGE:PHASE_TRAIL_OVERRIDE -->\n**Head**: "+$h+"\n**Missing**: "+$m+"\n**Reason**: "+$r),created_at:$c,updated_at:$u,author_association:"OWNER",user:{login:$l,type:$t}}'
335
+ }
336
+ ovf() { # ovf <name> <override comment json...> -> fixture = base trail + comments
337
+ local out="$TMP_FX/$1.json"; shift
338
+ { ov_base | jq -c '.[]'; for c in "$@"; do echo "$c"; done; } | jq -sc '.' > "$out"; echo "$out"
339
+ }
340
+ GOODC() { ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "gate misfire, verified by hand"; }
341
+ orun() { OUT=$(MOCK_GH_JSON="$1" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" "${@:2}" 2>/dev/null); RC=$?; }
342
+ expect_override() { orun "$2" "${@:3}"; [ $RC -eq 0 ] && echo "$OUT" | grep -q 'PHASE_TRAIL: OVERRIDDEN' && echo "$OUT" | grep -q '^OVERRIDE: approver=' && ok "$1" || bad "$1 (rc=$RC out=$OUT)"; }
343
+ expect_blocked() { orun "$2" "${@:3}"; [ $RC -eq 1 ] && echo "$OUT" | grep -q 'PHASE_TRAIL: FAIL' && ! echo "$OUT" | grep -q 'OVERRIDDEN' && ok "$1" || bad "$1 (rc=$RC out=$OUT)"; }
344
+
345
+ expect_blocked "no override comment stays blocked" "$(ovf o0)"
346
+ expect_override "valid human override is accepted" "$(ovf o1 "$(GOODC)")"
347
+ orun "$(ovf o1b "$(GOODC)")"
348
+ echo "$OUT" | grep -q "^OVERRIDE: approver=alice head=$HS missing=$CUR_MISSING reason=gate misfire, verified by hand$" && ok "OVERRIDE line carries approver, head, missing set and reason" || bad "OVERRIDE line ($OUT)"
349
+ echo "$OUT" | grep -q '^MISSING:' && bad "accepted override must not emit MISSING lines" || ok "accepted override emits no MISSING lines"
350
+ expect_override "admin permission is accepted" "$(ovf o2 "$(ovc carol User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "ok")")"
351
+ expect_override "uppercase head and shuffled/spaced missing list still bind to the same set" \
352
+ "$(ovf o3 "$(ovc alice User "$AFTER" "$AFTER" "$(echo $HS | tr a-f A-F)" "QUALITY_GATE , ARCHITECT,CONTEXT -> x" "ok")")"
353
+ expect_override "a later valid override wins over an earlier rejected one" \
354
+ "$(ovf o4 "$(ovc reader User "$BEFORE" "$BEFORE" "$HS" "$CUR_MISSING" "old")" "$(GOODC)")"
355
+
356
+ # One test per rejection rule
357
+ expect_blocked "rejects: Bot author" "$(ovf r1 "$(ovc alice Bot "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r")")"
358
+ expect_blocked "rejects: null user" "$(ovf r2 "$(GOODC | jq -c '.user=null')")"
359
+ expect_blocked "rejects: missing user.type" "$(ovf r3 "$(GOODC | jq -c 'del(.user.type)')")"
360
+ expect_blocked "rejects: edited comment (updated_at != created_at)" "$(ovf r4 "$(ovc alice User "$AFTER" "2026-10-08T03:00:00Z" "$HS" "$CUR_MISSING" "r")")"
361
+ expect_blocked "rejects: missing timestamps" "$(ovf r5 "$(GOODC | jq -c 'del(.updated_at)')")"
362
+ expect_blocked "rejects: read-only collaborator" "$(ovf r6 "$(ovc reader User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r")")"
363
+ expect_blocked "rejects: author with no permission record" "$(ovf r7 "$(ovc stranger User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r")")"
364
+ orun "$(ovf r8 "$(GOODC)")"; OUT=$(MOCK_PERM_FAIL=1 MOCK_GH_JSON="$TMP_FX/r8.json" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>/dev/null); RC=$?
365
+ [ $RC -eq 1 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "rejects: permission API error fails closed (exit 1)" || bad "permission api error (rc=$RC out=$OUT)"
366
+ expect_blocked "rejects: wrong head sha" "$(ovf r9 "$(ovc alice User "$AFTER" "$AFTER" "$HS2" "$CUR_MISSING" "r")")"
367
+ expect_blocked "rejects: truncated head sha" "$(ovf r10 "$(ovc alice User "$AFTER" "$AFTER" "${HS:0:12}" "$CUR_MISSING" "r")")"
368
+ expect_blocked "rejects: override for a different MISSING set" "$(ovf r11 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "ARCHITECT,CONTEXT" "r")")"
369
+ expect_blocked "rejects: override for a superset of MISSING" "$(ovf r12 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING,CONTRACT" "r")")"
370
+ expect_blocked "rejects: posted before BUILDER:COMPLETE" "$(ovf r13 "$(ovc alice User "$BEFORE" "$BEFORE" "$HS" "$CUR_MISSING" "r")")"
371
+ expect_blocked "rejects: posted exactly at BUILDER:COMPLETE time" "$(ovf r14 "$(ovc alice User "$BT" "$BT" "$HS" "$CUR_MISSING" "r")")"
372
+ expect_blocked "rejects: empty reason" "$(ovf r15 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" " ")")"
373
+ expect_blocked "rejects: reason that is only control characters" "$(ovf r16 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" $'\x1b\x07')")"
374
+ expect_blocked "rejects: login with path characters (no API path injection)" "$(ovf r17 "$(ovc 'alice/../x' User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r")")"
375
+ # Pipeline identity: configured login, and derived (author of the pipeline's own FORGE markers)
376
+ FORGE_TRAIL_PIPELINE_LOGINS="Alice" expect_blocked "rejects: configured pipeline login (case-insensitive)" "$(ovf p1 "$(GOODC)")"
377
+ FORGE_TRAIL_PIPELINE_LOGINS="x, alice ,y" expect_blocked "rejects: configured pipeline login in a list" "$(ovf p2 "$(GOODC)")"
378
+ expect_blocked "rejects: derived pipeline identity (author of FORGE:BUILDER/INVESTIGATOR/CONTRACT)" "$(ovf p3 "$(ovc builduser User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "self approve")")"
379
+ expect_blocked "rejects: derived pipeline identity matches case-insensitively" "$(ovf p4 "$(ovc BuildUser User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "self approve")")"
380
+ # A pipeline App identity that posts markers is also excluded even if typed User in the override
381
+ PIPE_C=$(jq -nc '{body:"<!-- FORGE:QUALITY_GATE -->\n**Result**: FAIL",user:{login:"pipeuser",type:"User"},author_association:"OWNER"}')
382
+ expect_blocked "rejects: author of any trusted FORGE marker (QUALITY_GATE) is pipeline" "$(ovf p5 "$PIPE_C" "$(ovc pipeuser User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r")")"
383
+ # Override cannot be evaluated without --head-sha
384
+ OUT=$(MOCK_GH_JSON="$(ovf n1 "$(GOODC)")" bash "$VERIFY" 3061 -R o/r 2>/dev/null); RC=$?
385
+ [ $RC -eq 1 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "rejects: no --head-sha means the override is never evaluated" || bad "no head-sha (rc=$RC out=$OUT)"
386
+ for bs in "" "xyz" "${HS}0"; do
387
+ OUT=$(MOCK_GH_JSON="$(ovf n2 "$(GOODC)")" bash "$VERIFY" 3061 -R o/r --head-sha "$bs" 2>/dev/null); RC=$?
388
+ [ $RC -eq 2 ] && echo "$OUT" | grep -q 'PHASE_TRAIL: ERROR' && ok "invalid --head-sha '$bs' fails closed (exit 2)" || bad "head-sha '$bs' (rc=$RC out=$OUT)"
389
+ done
390
+ OUT=$(MOCK_GH_JSON="$TMP_FX/n2.json" bash "$VERIFY" 3061 -R o/r --head-sha 2>/dev/null); RC=$?
391
+ [ $RC -eq 2 ] && ok "--head-sha without a value exits 2" || bad "head-sha no value (rc=$RC)"
392
+ # Exit 2 is never overridable
393
+ OUT=$(MOCK_GH_FAIL=1 MOCK_GH_JSON="$TMP_FX/o1.json" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>/dev/null); RC=$?
394
+ [ $RC -eq 2 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "exit 2 (unreadable trail) is not overridable" || bad "exit 2 override (rc=$RC out=$OUT)"
395
+ { cat "$TMP_FX/o1.json"; echo '{"message":"Server Error"}'; } > "$TMP_FX/o2bad.json"
396
+ OUT=$(MOCK_GH_JSON="$TMP_FX/o2bad.json" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>/dev/null); RC=$?
397
+ [ $RC -eq 2 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "exit 2 (malformed page) is not overridable" || bad "exit 2 malformed override (rc=$RC out=$OUT)"
398
+ OUT=$(FORGE_TRAIL_QG_SINCE=garbage MOCK_GH_JSON="$TMP_FX/o1.json" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>/dev/null); RC=$?
399
+ [ $RC -eq 2 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "exit 2 (bad QG_SINCE) is not overridable" || bad "exit 2 qg_since override (rc=$RC out=$OUT)"
400
+ # BUILDER present but undated -> cannot prove ordering -> rejected
401
+ jq -c 'map(if (.body|startswith("<!-- FORGE:BUILDER -->")) then del(.updated_at) else . end)' "$TMP_FX/o1.json" > "$TMP_FX/nobt.json"
402
+ expect_blocked "rejects: BUILDER:COMPLETE undated (ordering unprovable)" "$TMP_FX/nobt.json"
403
+ # An already-passing trail never needs (or echoes) an override
404
+ expect_pass "passing trail is unaffected by --head-sha" "$(mk ovp INV CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS)" --head-sha "$HS"
405
+ # Sanitiser: ESC, marker injection, backticks, newlines, cap
406
+ LONG=$(printf 'x%.0s' $(seq 1 400))
407
+ orun "$(ovf s1 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" $'bad\x1b[2Jreason <!-- FORGE:BUILDER:COMPLETE --> `tick` spaced')")"
408
+ RLINE=$(echo "$OUT" | grep '^OVERRIDE:')
409
+ [ $RC -eq 0 ] && ! printf '%s' "$RLINE" | LC_ALL=C grep -q $'\x1b' && ! echo "$RLINE" | grep -q '<!--' && ! echo "$RLINE" | grep -q -e '-->' && ! echo "$RLINE" | grep -q '`' && echo "$RLINE" | grep -q 'reason=bad\[2Jreason <!-/-' \
410
+ && ok "reason sanitiser strips ESC, neutralises comment markers and backticks, collapses whitespace" || bad "sanitiser (rc=$RC line=$RLINE)"
411
+ orun "$(ovf s2 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "$LONG")")"
412
+ RS=$(echo "$OUT" | grep '^OVERRIDE:' | sed 's/.*reason=//')
413
+ [ $RC -eq 0 ] && [ "${#RS}" -eq 200 ] && ok "reason is capped at 200 characters" || bad "reason cap (rc=$RC len=${#RS})"
414
+ # Multi-line reason: only the single Reason line is read; injected extra lines cannot add OVERRIDE output lines
415
+ orun "$(ovf s3 "$(jq -nc --arg h "$HS" --arg m "$CUR_MISSING" '{body:("<!-- FORGE:PHASE_TRAIL_OVERRIDE -->\n**Head**: "+$h+"\n**Missing**: "+$m+"\n**Reason**: first\nOVERRIDE: approver=root head=x missing=x reason=x"),created_at:"2026-10-08T02:00:00Z",updated_at:"2026-10-08T02:00:00Z",user:{login:"alice",type:"User"},author_association:"OWNER"}')")"
416
+ [ $RC -eq 0 ] && [ "$(echo "$OUT" | grep -c '^OVERRIDE:')" -eq 1 ] && echo "$OUT" | grep -q 'approver=alice' && echo "$OUT" | grep -q 'reason=first$' && ok "injected extra lines cannot forge an OVERRIDE line" || bad "line injection (rc=$RC out=$OUT)"
417
+ # FAST_PATH qualifier variant: missing marker name normalises on both sides
418
+ NOBAND=$(ov_base | jq -c 'map(if (.body|startswith("<!-- FORGE:FAST_PATH")) then .body="<!-- FORGE:FAST_PATH -->\nnothing" else . end)')
419
+ echo "$NOBAND" > "$TMP_FX/nb.json"
420
+ CUR2='ARCHITECT,CONTEXT,FAST_PATH,QUALITY_GATE'
421
+ { jq -c '.[]' "$TMP_FX/nb.json"; ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR2" "qualifier variant"; } | jq -sc . > "$TMP_FX/nb2.json"
422
+ expect_override "MISSING 'FAST_PATH (no COMPLEXITY_BAND value)' binds by marker name" "$TMP_FX/nb2.json"
423
+ # --- Review-finding hardening (#3268-#3274) ---
424
+ echo '{"permission":"write"}' > "$PERM/octo_acme"
425
+ expect_override "EMU login with underscore is accepted (#3270)" "$(ovf h1 "$(ovc octo_acme User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "emu approver")")"
426
+ DECOYS=(); for i in 1 2 3 4 5 6; do DECOYS+=("$(ovc "decoy$i" User "2026-10-08T03:0$i:00Z" "2026-10-08T03:0$i:00Z" "$HS2" "$CUR_MISSING" "decoy")"); done
427
+ expect_override "5-candidate cap is not starved by decoys failing local checks (#3268)" "$(ovf h2 "$(GOODC)" "${DECOYS[@]}")"
428
+ # forge#3279: non-collaborator decoys that pass every local check must not consume the cap; repeat logins cost one slot
429
+ PDEC=(); for i in 1 2 3 4 5 6 7; do PDEC+=("$(ovc "pdecoy$i" User "2026-10-08T04:0$i:00Z" "2026-10-08T04:0$i:00Z" "$HS" "$CUR_MISSING" "decoy" | jq -c '.author_association="NONE"')"); done
430
+ expect_override "non-collaborator decoy flood (newer, all-local-checks-passing) does not starve a real override (#3279)" "$(ovf h5 "$(GOODC)" "${PDEC[@]}")"
431
+ echo '{"permission":"read"}' > "$PERM/rdr1"
432
+ RDEC=(); for i in 1 2 3 4 5 6; do RDEC+=("$(ovc rdr1 User "2026-10-08T05:0$i:00Z" "2026-10-08T05:0$i:00Z" "$HS" "$CUR_MISSING" "same login repeated")"); done
433
+ expect_override "one login repeated many times costs a single cap slot (#3279)" "$(ovf h6 "$(GOODC)" "${RDEC[@]}")"
434
+ # forge#3306: read-level members (pass the association prefilter) must not consume the cap ahead of an older write approver
435
+ RLV=(); for i in 1 2 3 4 5 6 7; do echo '{"permission":"read"}' > "$PERM/rlv$i"; RLV+=("$(ovc "rlv$i" User "2026-10-08T06:0$i:00Z" "2026-10-08T06:0$i:00Z" "$HS" "$CUR_MISSING" "decoy")"); done
436
+ expect_override "7 distinct read-level members newer than a write approver do not starve it (#3306)" "$(ovf h7 "$(GOODC)" "${RLV[@]}")"
437
+ orun "$(ovf h8 "${RLV[@]}")"
438
+ [ $RC -eq 1 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "read-level members only: override rejected (#3306)" || bad "read-level only (rc=$RC out=$OUT)"
439
+ ULV=(); for i in 1 2 3 4 5 6 7; do ULV+=("$(ovc "ulv$i" User "2026-10-08T07:0$i:00Z" "2026-10-08T07:0$i:00Z" "$HS" "$CUR_MISSING" "unreadable")"); done
440
+ expect_blocked "unreadable lookups stay bounded by the error cap and fail closed (#3306)" "$(ovf h9 "$(GOODC)" "${ULV[@]}")"
441
+ # forge#3307: association prefilter reuses the configured list; each association value is pinned
442
+ echo '{"permission":"write"}' > "$PERM/memb"; echo '{"permission":"write"}' > "$PERM/collab"; echo '{"permission":"write"}' > "$PERM/concealed"
443
+ assoc() { jq -c --arg a "$1" '.author_association=$a'; }
444
+ expect_override "MEMBER write approver is accepted (#3307)" "$(ovf a1 "$(ovc memb User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r" | assoc MEMBER)")"
445
+ expect_override "COLLABORATOR write approver is accepted (#3307)" "$(ovf a2 "$(ovc collab User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r" | assoc COLLABORATOR)")"
446
+ CDEC=(); for i in 1 2 3 4 5 6 7; do CDEC+=("$(ovc "cdecoy$i" User "2026-10-08T08:0$i:00Z" "2026-10-08T08:0$i:00Z" "$HS" "$CUR_MISSING" "decoy" | assoc CONTRIBUTOR)"); done
447
+ expect_override "CONTRIBUTOR decoy flood does not starve a real override (#3307)" "$(ovf a3 "$(GOODC)" "${CDEC[@]}")"
448
+ CONC=$(ovc concealed User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "concealed membership" | assoc CONTRIBUTOR)
449
+ expect_blocked "CONTRIBUTOR write approver is rejected under the default list (#3307)" "$(ovf a4 "$CONC")"
450
+ FORGE_TRAIL_OVERRIDE_ASSOCIATIONS="OWNER,MEMBER,COLLABORATOR,CONTRIBUTOR" expect_override "widened override list accepts a CONTRIBUTOR write approver (#3307, #3350)" "$(ovf a4 "$CONC")"
451
+ FORGE_TRAIL_TRUSTED_ASSOCIATIONS="OWNER,MEMBER,COLLABORATOR,CONTRIBUTOR" expect_override "trail list widened still widens override by default inheritance (#3350)" "$(ovf a4i "$CONC")"
452
+ FORGE_TRAIL_OVERRIDE_ASSOCIATIONS="" expect_blocked "empty override list yields no candidates with default trail list (#3350)" "$(ovf a4e "$(GOODC)")"
453
+ FORGE_TRAIL_TRUSTED_ASSOCIATIONS="OWNER" expect_blocked "narrowed list rejects a MEMBER approver (#3307)" "$(ovf a5 "$(ovc memb User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "r" | assoc MEMBER)")"
454
+ FORGE_TRAIL_TRUSTED_ASSOCIATIONS="" expect_blocked "empty list yields no override candidates (fail closed) (#3307)" "$(ovf a6 "$(GOODC)")"
455
+ # Case-variant duplicate logins cost one lookup / one slot
456
+ echo '{"permission":"read"}' > "$PERM/cvx"
457
+ CVV=(); i=0; for l in Cvx cvx CVX cvX Cvx CVx; do i=$((i+1)); CVV+=("$(ovc "$l" User "2026-10-08T09:0$i:00Z" "2026-10-08T09:0$i:00Z" "$HS" "$CUR_MISSING" "dup")"); done
458
+ PL="$TMP_FX/permlog-cv"; : > "$PL"
459
+ OUT=$(MOCK_PERM_LOG="$PL" MOCK_GH_JSON="$(ovf a7 "$(GOODC)" "${CVV[@]}")" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>/dev/null); RC=$?
460
+ [ $RC -eq 0 ] && echo "$OUT" | grep -q OVERRIDDEN && [ "$(grep -ic '^cvx$' "$PL")" -eq 1 ] && ok "case-variant duplicate logins cost a single lookup (#3307)" || bad "case-variant dup (rc=$RC lookups=$(grep -ic '^cvx$' "$PL"))"
461
+ # Decoys (NONE/CONTRIBUTOR) never reach the permission API; the NOTE reports the filtered count without logins
462
+ MIXD=("${PDEC[@]}" "${CDEC[@]}")
463
+ PL="$TMP_FX/permlog-decoy"; : > "$PL"
464
+ ERR=$(MOCK_PERM_LOG="$PL" MOCK_GH_JSON="$(ovf a8 "${MIXD[@]}")" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>&1 >/dev/null);
465
+ [ ! -s "$PL" ] && echo "$ERR" | grep -q 'NOTE: 14 override candidate(s) ignored: author_association not in override-approver set' && ! echo "$ERR" | grep -q 'pdecoy\|cdecoy' \
466
+ && ok "decoys trigger no permission lookups; NOTE carries the count and no logins (#3307)" || bad "decoy no-call (log=$(cat "$PL") err=$ERR)"
467
+ # forge#3351: the NOTE count shares one predicate with the candidates: only otherwise-eligible, association-excluded
468
+ # comments are counted (edited, bot, and pre-floor ones are not)
469
+ EXC=$(ovc xedit User "2026-10-08T11:01:00Z" "2026-10-08T11:02:00Z" "$HS" "$CUR_MISSING" "edited" | assoc NONE)
470
+ EXB=$(ovc xbot Bot "2026-10-08T11:03:00Z" "2026-10-08T11:03:00Z" "$HS" "$CUR_MISSING" "bot" | assoc NONE)
471
+ EXP=$(ovc xold User "2000-01-01T00:00:00Z" "2000-01-01T00:00:00Z" "$HS" "$CUR_MISSING" "pre-floor" | assoc NONE)
472
+ EX1=$(ovc xok1 User "2026-10-08T11:04:00Z" "2026-10-08T11:04:00Z" "$HS" "$CUR_MISSING" "dropped" | assoc NONE)
473
+ EX2=$(ovc xok2 User "2026-10-08T11:05:00Z" "2026-10-08T11:05:00Z" "$HS" "$CUR_MISSING" "dropped" | assoc CONTRIBUTOR)
474
+ ERR=$(MOCK_GH_JSON="$(ovf a10 "$(GOODC)" "$EXC" "$EXB" "$EXP" "$EX1" "$EX2")" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>&1 >/dev/null)
475
+ echo "$ERR" | grep -q 'NOTE: 2 override candidate(s) ignored: author_association not in override-approver set' \
476
+ && ok "NOTE count equals only eligible association-excluded candidates (#3351)" || bad "NOTE count shared predicate (err=$ERR)"
477
+ # 6th distinct unreadable login is cut off: the cap stops the scan before an older real approver
478
+ UNR=(); for i in 1 2 3 4 5 6 7 8; do UNR+=("$(ovc "unr$i" User "2026-10-08T10:0$i:00Z" "2026-10-08T10:0$i:00Z" "$HS" "$CUR_MISSING" "unreadable")"); done
479
+ PL="$TMP_FX/permlog-unr"; : > "$PL"
480
+ OUT=$(MOCK_PERM_LOG="$PL" MOCK_GH_JSON="$(ovf a9 "$(GOODC)" "${UNR[@]}")" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>/dev/null); RC=$?
481
+ [ $RC -eq 1 ] && ! echo "$OUT" | grep -q OVERRIDDEN && [ "$(wc -l < "$PL" | tr -d ' ')" -eq 6 ] && ! grep -qx alice "$PL" && ok "unreadable lookups stop at the 6th login; older approver never queried (#3307)" || bad "unreadable cutoff (rc=$RC lookups=$(wc -l < "$PL"))"
482
+ MOCK_SELF_LOGIN=alice orun "$(ovf h3 "$(GOODC)")"
483
+ [ $RC -eq 1 ] && ! echo "$OUT" | grep -q OVERRIDDEN && ok "verifier's own login cannot approve an override (#3269)" || bad "self-login exclusion (rc=$RC out=$OUT)"
484
+ MOCK_SELF_LOGIN=someoneelse orun "$(ovf h3b "$(GOODC)")"
485
+ [ $RC -eq 0 ] && echo "$OUT" | grep -q OVERRIDDEN && ok "an unrelated self-login does not block a valid override (#3269)" || bad "self-login unrelated (rc=$RC out=$OUT)"
486
+ orun "$(ovf h4 "$(ovc alice User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" $'ping @octocat\u2028next\u2029line')")"
487
+ RS=$(echo "$OUT" | grep '^OVERRIDE:' | sed 's/.*reason=//')
488
+ [ $RC -eq 0 ] && ! printf '%s' "$RS" | grep -q '@' && ! printf '%s' "$RS" | grep -q $'\xe2\x80\xa8' && ok "reason sanitiser neutralises @mentions and U+2028/2029 (#3274)" || bad "sanitiser unicode (rc=$RC rs=$RS)"
489
+ # No BUILDER at all: floor falls back to the newest trusted FORGE marker (#3271)
490
+ ov_base | jq -c '[.[] | select((.body|startswith("<!-- FORGE:BUILDER -->"))|not)] | map(if (.body|startswith("<!-- FORGE:CONTRACT -->")) then .created_at="2026-10-08T01:30:00Z" else . end)' > "$TMP_FX/nobuilder.json"
491
+ orun "$TMP_FX/nobuilder.json"; NB_OUT="$OUT"
492
+ NB_MISS=$(echo "$NB_OUT" | sed -n 's/^MISSING: //p' | sed -e 's/ -> .*$//' | paste -sd, -)
493
+ if [ -n "$NB_MISS" ]; then
494
+ { jq -c '.[]' "$TMP_FX/nobuilder.json"; ovc alice User "2026-10-08T01:10:00Z" "2026-10-08T01:10:00Z" "$HS" "$NB_MISS" "older than marker"; } | jq -sc . > "$TMP_FX/nbo1.json"
495
+ expect_blocked "override older than newest trusted marker is rejected when BUILDER is absent (#3271)" "$TMP_FX/nbo1.json"
496
+ { jq -c '.[]' "$TMP_FX/nobuilder.json"; ovc alice User "$AFTER" "$AFTER" "$HS" "$NB_MISS" "newer than marker"; } | jq -sc . > "$TMP_FX/nbo2.json"
497
+ expect_override "override newer than newest trusted marker is accepted when BUILDER is absent (#3271)" "$TMP_FX/nbo2.json"
498
+ else bad "no-BUILDER fixture produced no MISSING set ($NB_OUT)"; fi
499
+ # Rejected permission is explained on stderr (#3272)
500
+ ERRTXT=$(MOCK_GH_JSON="$(ovf h5 "$(ovc reader User "$AFTER" "$AFTER" "$HS" "$CUR_MISSING" "no perm")")" bash "$VERIFY" 3061 -R o/r --head-sha "$HS" 2>&1 >/dev/null)
501
+ echo "$ERRTXT" | grep -q "rejected: permission='read'" && ok "rejected permission prints a diagnostic to stderr (#3272)" || bad "permission diagnostic ($ERRTXT)"
502
+
503
+ # -h documents the override
504
+ HOUT2=$(bash "$VERIFY" -h)
505
+ echo "$HOUT2" | grep -q 'FORGE_TRAIL_PIPELINE_LOGINS' && echo "$HOUT2" | grep -q -- '--head-sha' && ok "-h documents the override and --head-sha" || bad "-h override docs"
506
+
297
507
  echo "passed=$PASS failed=$FAILN"
298
508
  [ "$FAILN" -eq 0 ]