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.
- package/bin/engine/admission.mjs +23 -4
- package/bin/engine/admission.test.mjs +37 -0
- package/commands/orchestrate/phase-1-resolve.md +40 -24
- package/commands/orchestrate/phase-3-dependency.md +56 -11
- package/commands/orchestrate/phase-4-execution.md +359 -111
- package/commands/orchestrate/phase-5-cleanup.md +17 -0
- package/commands/orchestrate/phase-6-report.md +1 -1
- package/commands/quality-gate.md +80 -0
- package/commands/recover-orphans.md +3 -3
- package/commands/review-pr.md +98 -16
- package/commands/work-on/build/context.md +25 -4
- package/commands/work-on/build/validate.md +16 -9
- package/commands/work-on/build.md +26 -9
- package/commands/work-on/close.md +104 -40
- package/commands/work-on/decompose.md +13 -7
- package/commands/work-on/investigate.md +13 -7
- package/commands/work-on/remediate.md +15 -4
- package/commands/work-on/review.md +48 -19
- package/commands/work-on.md +62 -9
- package/docs/CODEX.md +12 -0
- package/docs/CONFIG.md +49 -1
- package/docs/RELEASE-NOTES.md +61 -0
- package/package.json +1 -1
- package/scripts/amplification-breaker.sh +89 -0
- package/scripts/amplification-breaker.test.sh +39 -0
- package/scripts/check-admission-parity.mjs +74 -1
- package/scripts/check-spec-bash.sh +120 -0
- package/scripts/check-spec-bash.test.sh +90 -0
- package/scripts/check-spec-markers.sh +67 -3
- package/scripts/classify-finding.sh +123 -0
- package/scripts/classify-finding.test.sh +77 -0
- package/scripts/close-dossier-no-main-commit.test.sh +139 -0
- package/scripts/forge-root.test.sh +84 -27
- package/scripts/lint-build-validate-files.sh +29 -0
- package/scripts/lint-build-validate-files.test.sh +25 -0
- package/scripts/lint-dispatch-prompt.test.sh +2 -1
- package/scripts/recovery-claim-live.sh +73 -0
- package/scripts/recovery-claim-live.test.sh +66 -0
- package/scripts/verify-phase-trail.sh +183 -4
- 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
|
|
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
|
-
|
|
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 ]
|