Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,16 @@ repository still gets a decision, never by following the link; no release carrie
- :lock: fix(release): widen the cosign identity pin to the real signer casing (SEC-03)
- :lock: fix(provider): bound exec stdout, set WaitDelay, capture stderr
- :lock: fix(release): pin cosign signer identity on the maintainer verify path (AUD2-F01)
- :lock: fix(hack): refuse to run gate scripts under a too-old bash (BASH32-F01)

### Testing
- :white_check_mark: test(cmd): assert the REL-03 error wrap as one contiguous substring
- :test: test(compare): kill the surviving EffectChallenge intervention mutant (TEST-02)
- :white_check_mark: test(audit): pin the AUD2 remediations behind a PR-visible exit gate
- :white_check_mark: test(audit): assert verify.yaml still triggers on pull_request
- :white_check_mark: test(hack): gate that hack/** bash 4+ features declare a floor (BASH32-F01)
- :white_check_mark: test(hack): the bash-3.2-clean assertion could not fail (BASH32-F01)
- :white_check_mark: test(hack): close two gate fail-opens found by review (BASH32-F01)
## [0.3.0] - 2026-08-18

### Chores
Expand Down
13 changes: 13 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ tasks:
# .github/workflows/**, so without this line an unpinned workflow is caught
# only by CI — and only for as long as the CI step itself survives.
- task: lint-workflow-pins-test
# BASH32-F01 / D-154: every hack/** script that uses a bash 4+ feature must
# declare a version floor. Without it, `hack/docs/truthlag_pins_test.sh` under
# stock macOS /bin/bash 3.2 died at its `declare -A` and EXITED 0 — the
# docs-gates stage above read that as green while almost nothing had been
# checked. Nothing in CI can see this (ubuntu is bash 5), which is why it
# survived; the gate is cheap and offline, and its behavioural control runs
# only where a bash 3.2 exists and says so loudly when it skips.
- task: lint-bash-version-guard-test
# EX-S08 (REQ-EX-S08-03): the dogfood-examples gate's own guard. Without
# this, deleting `- task: dogfood-examples` from check: above passes
# every wired gate — the same manual-gate defect D-124 closed elsewhere.
Expand Down Expand Up @@ -165,6 +173,11 @@ tasks:
cmds:
- bash hack/lint/workflow_pins_test.sh

lint-bash-version-guard-test:
desc: "BASH32-F01 gate: every hack/** script using a bash 4+ feature declares a sufficient bash version floor (D-154)"
cmds:
- bash hack/lint/bash_version_guard_test.sh

lint-depguard-test:
desc: "AUD-S07 gate: adversarial proof the D-123 depguard boundary rules fire (REQ-AUD-S07-01)"
cmds:
Expand Down
1 change: 1 addition & 0 deletions docs/decisions/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,5 @@ project/process decisions.
| D-151 | 2026-08-16 | **OpenSSF Best Practices project creation is RESOLVED: (a) — the operator will create the `bestpractices.dev` project for `PlatformRelay/assent`.** Unblocks SEC-SC-S02 (evidence page + README badge, once *passing*); SEC-SC-S01 (fuzzing) is not blocked by this and can proceed independently. Operator action only — requires a personal account at bestpractices.dev, not automatable in-tree. Current CII score is 0, which drags the OpenSSF Scorecard (~7); the repo already satisfies most criteria (pinned CodeQL, Dependabot, secret scanning, signed releases, SLSA provenance). Revert: leave CII at 0 indefinitely — SEC-SC-S01 is unaffected either way. |
| D-152 | 2026-08-18 | **The 2026-08-18 audit's "Next (risk reduction)" wave is decomposed as its own epic P5-AUD2 (`openspec/specs/p5-aud2-audit-remediation/spec.md`), five stories, spec-first.** Context: `agent-context/PROJECT-AUDIT-2026-08-18.md` closed both P1 conditions the same day and v0.3.0 shipped, leaving a named but untracked wave — exec-transport trio (REL-01/02/07), REL-03 `ErrNotFound` discrimination, SEC-03 cosign identity pin, TEST-02 mutant. Options considered: **(a)** hand-fix them as loose commits (rejected — AGENTS.md rule 4 is spec-first, and REL-01 is now byte-identical across *three* audits precisely because nothing ever tracked it); **(b)** append them to the existing P5-AUD epic (rejected — that epic's exit gate is CLOSED and reopening it would make "AUD complete" a moving claim); **(c)** a new AUD2 epic keyed to this audit **(chosen)**. Three consequences recorded here so they are not re-litigated: **(1)** AUD2 has **no release-condition story** — the audit had exactly two P1s and both are already closed; **(2)** **WG-S01 is deliberately excluded** — it carries the LGTM governance marker, and GOVERNANCE says such stories are surfaced to the maintainer, which an autonomous loop's decide-and-log does not override; **(3)** AUD2's exit gate is a **`task check` stage**, not a `release-exitgate` step, because that job is `pull_request`-skipped (RELSE-08) and wiring the gate there is how AUD-S18's own stale `CHECK_STAGES` pin survived four merges. A 2026-08-10-keyed AUD2 draft exists only in the local stash `leave-aud2-not-this-epic`, was never committed, and is superseded; its still-open items (F3/F5/F7) stay Later-wave. Revert: delete the spec + backlog section; the four findings return to the audit report untracked. |
| D-153 | 2026-08-19 | **The cosign signer-identity pin published in `SECURITY.md` was WRONG, not merely missing from `hack/install.sh`: the identity regexp is widened to `^https://github\.com/PlatformRelay/[Aa]ssent/` in both files (AUD2-S03 / SEC-03).** Substance first: `SECURITY.md`'s "Verify a tagged release" instructions pinned `--certificate-identity-regexp '^https://github.com/PlatformRelay/assent/'`, and an adopter who followed them on **v0.2.0 or v0.3.0 got a verification FAILURE on a genuine, correctly signed artifact** — the published recipe has been broken for every release since v0.2.0, and the natural reading of that failure is "this release was tampered with". Cause: the repository was renamed `PlatformRelay/assent` → `PlatformRelay/Assent` between v0.1.0 and v0.2.0; the keyless signing certificate's Subject Alternative Name carries GitHub's canonical casing; cosign compiles `--certificate-identity-regexp` as a Go RE2 pattern and matches it **case-sensitively**. Decoded from the published bundles: v0.3.0 and v0.2.0 sign as `https://github.com/PlatformRelay/Assent/.github/workflows/release.yaml@refs/tags/vX.Y.Z`, v0.1.0 as `https://github.com/PlatformRelay/assent/.github/workflows/release.yaml@refs/heads/main`. All three verify under the new value; independently re-confirmed with real cosign against the real v0.3.0 artifact (`Verified OK` under the new pin, exit 1 "expected SAN value to match regex" under the old). Options considered: **(a)** ship the pin byte-identical to the published-but-broken value and file the breakage as a follow-up (rejected — it would land a green gate asserting a guarantee that fails closed on this project's own releases, D-124's defect one level up); **(b)** case-insensitive `(?i)` (rejected — wider than the defect and it silently accepts casings GitHub never issues); **(c)** an explicit `[Aa]` class in both files, dots escaped (chosen). Not widened otherwise: the `^` anchor and the owner/repo scope stand, so another owner, an `assent-mirror` typosquat, another forge and an unescaped-dot host all still fail. Enforcement, per D-128: **one** published truth — `hack/release/install_cosign_pin_test.sh` extracts the pair from `hack/install.sh` and `SECURITY.md`, requires exactly one distinct value per file, reddens on drift, and (this is the assertion that would have caught the defect) matches the pin against the three **real** SANs above, committed as offline fixtures, plus six negatives. Extends D-110 (bundles beside archives) and D-109; the maintainer-path twin at `hack/release/verify-artifacts.sh:124` is still unpinned and is tracked in the backlog, not fixed here. Revert: restore `'^https://github.com/PlatformRelay/assent/'` in both files — which re-breaks verification of v0.2.0 and every later release, so revert only together with a repo rename back to lowercase. |
| D-154 | 2026-08-23 | **Every `hack/**` script that uses a bash 4+ feature declares a PER-SCRIPT version floor via the shared `hack/lib/require-bash.sh`, because one of them was a silent local FAIL-OPEN under stock macOS bash 3.2 (BASH32-F01).** Substance first: `/bin/bash hack/docs/truthlag_pins_test.sh` on macOS printed 20 `PASS` lines, died at its `declare -A ex_s09_prefix_hint=(…)`, never printed its final `OK: all truth-lag pins green` banner — and **exited 0**. Its caller `task docs-gates`, and therefore `task check`, read that as a green gate, and AGENTS.md rule 4 makes a green local `task check` a per-commit precondition; CI is ubuntu/bash 5, so nothing merges through this path, which is precisely why it survived. Mechanism, measured on bash 3.2.57: 3.2 has no associative arrays, so under `set -u` it re-reads `declare -A x=([k]=v)` as an INDEXED array assignment and evaluates the subscript `k` **arithmetically**, hitting "unbound variable"; the shell terminates the script and the exit status is 0. Two theories were disproved en route and are recorded so they are not re-explored: the `trap … EXIT` status-swallow (3.2 propagates 1 through an EXIT trap correctly) and `set -e` (the script runs `set -uo pipefail`). The other two affected scripts fail CLOSED, but only incidentally: `hack/release/verify-artifacts.sh:202`'s literal is EMPTY, so there is no subscript to evaluate and it degrades to `declare: -A: invalid option`, which its `set -e` turns into exit 2 (measured with a `--dist` that reaches line 202 — an unreachable dist exits 1 much earlier); `hack/validate-schemas-stock.sh`'s `mapfile` degrades to "command not found" and it exits 1. Populating an empty literal or dropping `-e` silently converts a closed failure into the open one, so "how this construct happens to degrade" is not a guarantee worth depending on. Options considered: **(a)** one blanket repo-wide floor (rejected — the binding minimum differs per feature: `declare -A` 4.0, `local -n` 4.3, `mapfile -d ''` 4.4; a blanket floor both refuses shells that could run the 4.0 gates and hides which feature actually binds); **(b)** rewrite the three scripts to be 3.2-compatible (rejected — associative arrays and `mapfile` are the right tools here, a rewrite is a large risky change to scripts whose correctness IS the product, and it does nothing about the next gate author); **(c)** copy the existing inline `BASH_VERSINFO` guard from `hack/audit/aud2_exitgate_test.sh:71` into each script (rejected as the *only* measure — it duplicates the explanation three more times and still leaves the next author to rediscover the hazard); **(d) chosen** — a shared `require_bash <major>[.<minor>] <feature>` helper, called with each script's own correct floor (4.0 / 4.0 / 4.4), plus a meta-gate. Enforcement, per D-128, is one place: `hack/lint/bash_version_guard_test.sh`, wired as `task check` stage **20** and pinned in `hack/audit/exitgate_test.sh`'s `CHECK_STAGES` in the same commit. It scans every `hack/**/*.sh` for command-position bash 4+ constructs, requires a guard on each, requires the declared floor to be at least the feature's own minimum, and — the assertion that makes the rest non-vacuous — requires the scan to still find all four known feature-using files, so a typo in a detection pattern reds instead of quietly matching nothing. Its own negative control mutates real copies of the three scripts with the guard lines stripped and asserts they are flagged. `hack/audit/aud2_exitgate_test.sh` keeps its inline guard and is accepted by the gate as guarded; adopting the helper there is a follow-up, not this lane. Stated limits: the gate sees only command-position constructs in `hack/**/*.sh` (not `eval`, dynamically built command names, case-modification expansions, or scripts outside `hack/`), and the exit-0 control needs a real bash 3.2 — present on macOS, absent on CI ubuntu, where it SKIPs **loudly** rather than passing silently. Accepted behaviour change: under 3.2, `hack/release/verify-artifacts.sh --help` now refuses instead of printing usage. Revert: delete the guard calls — `task check` then silently stops certifying the docs truth-lag pins for every contributor whose `bash` is 3.2. |
| D-158 | 2026-08-23 | **CI-TOOLCHAIN — `verify` was red on every PR from a golangci-lint/Go skew; the remedy is to bump `GOLANGCI_LINT_VERSION` v2.12.2 → v2.13.1 and KEEP `go-version: stable`.** Symptom: PR #85, a six-file Markdown diff with **zero Go bytes**, failed `verify` with `/opt/hostedtoolcache/go/1.27.0/x64/src/crypto/internal/randutil/randutil.go:11:2: could not import math/rand/v2 (… method must have no type parameters) (typecheck)` — the failing path is inside the **Go toolchain's own source tree**, so no author change can fix it and re-running cannot clear it. `Analyze (go)`, `Analyze (actions)` and `CodeQL` all passed on the same run: the tree compiles clean under Go 1.27, only the linter chokes. **Mechanism**: golangci-lint typechecks the standard library with the `go/types` it was **compiled with**, not the `go` on `PATH`. GitHub's `stable` rolled 1.26 → 1.27.0 between this repo's last green `main` run (2026-08-19) and 2026-08-23; the v2.12.2 release binary is built with go1.26.x and cannot read the 1.27 stdlib. **Reproduced locally, not reasoned** (Go 1.27.0 SDK on `PATH`, `GOTOOLCHAIN=local`, whole tree): v2.12.2 → **panic** in `pkg/goanalysis/runner_loadingpackage.go:335` inside `go/types.(*Checker).Files`, exit 2; official v2.13.1 asset (`built with go1.27.0`) → **0 issues**, exit 0; `golangci-lint config verify` → exit 0, so the v2 config schema in `.golangci.yml` needs no migration; v2.13.1 also → **0 issues** against local Go 1.26.6, so the bump is safe for contributors who have not moved; and `go install …@v2.13.1` under Go 1.27.0 → 0 issues, which is the **`release-exitgate`** path (that job builds the binary rather than using the action, and `if: github.event_name != 'pull_request'` / RELSE-08 means no PR ever exercises it). **Diagnosis heuristic worth keeping**: golangci-lint's `go.mod` carries the comment *"the minimum Go version must always be latest-1"*, so its `go` directive is a machine-readable "supports Go 1.N+1" signal — v2.12.2 says `go 1.25.0` (supports ≤1.26), v2.13.1 says `go 1.26.0` (supports ≤1.27), and upstream commit `42a0530` in v2.13.0 is literally "go1.27 support". **Options considered.** (A) *bump the linter* — CHOSEN: treats the root cause, is a **one-line** diff because the version is single-sourced at workflow scope (D-128), fixes BOTH exposed sites (the `golangci-lint-action` in `verify` and the `go install` in `release-exitgate`) at once, and the risk it carried — new findings from a newer linter blocking every merge, since `golangci-lint` runs inside `task lint` inside `task check` — was **measured at zero** on the whole tree under both Go versions. (B) *pin `go-version: 1.26.x`* — REJECTED as the primary: it needs two pins (`verify.yaml:56` and `:171`), leaves the class of bug live, undercuts the stated reason `stable` is there (`# govulncheck needs it`, with `setup-go` exporting `GOTOOLCHAIN=local` so a pinned toolchain can fail to `go run` a tool whose module requires a newer Go — unverified, and moot under (A)), and trades a loud recurrence for **silent indefinite drift onto an unsupported Go** in a repo whose posture is govulncheck + cosign + SLSA + Scorecard. (C) *both* — REJECTED as gold-plating: (A) alone is measured green, and a second simultaneous remedy would make a still-red PR ambiguous. **Per-file sweep of `go-version: stable`, stated rather than silently skipped:** `verify.yaml:56` and `:171` were the only exposed sites and are fixed by the env bump without touching either line; `schemas.yml:57` (runs `go test` + ajv, no linter), `vulncheck.yaml:30` (govulncheck only — `stable` is actively *desirable* there) and `codeql.yaml:45` (CodeQL, which passed on 1.27.0) carry no golangci-lint and are NOT exposed; `release.yaml` uses `go-version-file: go.mod` and is unaffected. `hack/lint/workflow_pins_test.sh` asserts `TASK_VERSION`, action SHA pins and step wiring but **not** `go-version` or `GOLANGCI_LINT_VERSION`; it passes on this change. **Revert is (B), not bare v2.12.2** — reverting the env line alone restores a known-red state: revert = `GOLANGCI_LINT_VERSION` back to `v2.12.2` **and** pin `verify.yaml:56` + `:171` to `go-version: 1.26.x`. **Residual risk, stated plainly:** when `stable` rolls to Go 1.28, v2.13.1 breaks identically. The repo finds out because `verify` reds on **every** PR including zero-Go diffs — loud and immediate, with the signature recorded in `verify.yaml`'s env comment and the fix being one env line. Nothing detects it *earlier*: dependabot's `github-actions` ecosystem updates `uses:` refs, not `env:` literals (backlog `CI-TOOLCHAIN-F01`). Accepted on purpose over (B)'s failure mode. Note also the pre-existing local/CI linter skew — `Taskfile.yml`'s `lint` task is bare `golangci-lint run ./...` against whatever binary a contributor has installed, so this pin has never governed local `task check`; deliberately not changed here (that file was owned by a concurrent lane). |
6 changes: 6 additions & 0 deletions hack/audit/exitgate_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ CHECK_STAGES=(
docs-gates
lint-depguard-test
lint-workflow-pins-test
# BASH32-F01 (D-154): the bash-version-floor gate. Added in the same commit as
# its `check:` line — hack/docs/truthlag_pins_test.sh exited 0 under stock macOS
# bash 3.2 after dying at its `declare -A`, so `task check` was locally green
# while certifying almost nothing, and no CI lane can see that (ubuntu is bash 5).
lint-bash-version-guard-test
dogfood-wiring-test
ci-audit-test
# AUD2-S05 (REQ-AUD2-S05-03): the P5-AUD2 exit gate — the four 2026-08-18
Expand Down Expand Up @@ -181,6 +186,7 @@ STAGE_BODY_PINS=(
'coverage|min="{{.COVERAGE_MIN}}"|an unrendered or dropped threshold makes the awk compare against 0 and admit anything (D-128)'
'lint-workflow-pins-test|bash hack/lint/workflow_pins_test.sh|a wired stage with a gutted body is the same defect one level down'
'ci-audit-test|bash hack/release/ci_audit_test.sh|a wired stage with a gutted body is the same defect one level down'
'lint-bash-version-guard-test|bash hack/lint/bash_version_guard_test.sh|a wired stage with a gutted body is the same defect one level down (BASH32-F01)'
)

# (6) Immutable base ref for the schema freeze. Overridable only to move it
Expand Down
21 changes: 21 additions & 0 deletions hack/docs/truthlag_pins_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@
# paper-gate). Deleting that line from Taskfile.yml reddens the pin below.
set -uo pipefail

# BASH >= 4.0 REQUIRED, ASSERTED FIRST — before anything else can partially run (D-154).
# The REQ-EX-S09-02 pins below build an associative array. Stock macOS /bin/bash is 3.2, which
# has none: under `set -u` it re-reads the literal as an INDEXED array assignment, evaluates the
# `[topics/]` subscript arithmetically, dies with "unbound variable" after 20 PASS lines, never
# prints the final `OK:` banner — and EXITS 0. `task docs-gates`, and therefore `task check`,
# read that as a green gate. AGENTS.md rule 4 makes a green local `task check` a per-commit
# precondition, so this was a live LOCAL trust hole, not a theoretical one; CI is ubuntu/bash 5,
# which is exactly why it survived. Measured; pinned by hack/lint/bash_version_guard_test.sh.
# The lib is resolved script-relative first, then from the enclosing git checkout, because other
# gates execute mutated COPIES of these scripts out of a mktemp dir (see the failure that shape
# caused in hack/release/install_cosign_pin_test.sh §5g). Unresolvable => refuse, never proceed.
_assent_lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd || true)"
[ -n "$_assent_lib" ] || _assent_lib="$(cd "$(git rev-parse --show-toplevel 2>/dev/null)/hack/lib" 2>/dev/null && pwd || true)"
[ -r "${_assent_lib}/require-bash.sh" ] || {
echo "FAIL: $0 cannot locate hack/lib/require-bash.sh — refusing to run without its bash version floor (D-154)." >&2
exit 1
}
# shellcheck source=hack/lib/require-bash.sh
. "${_assent_lib}/require-bash.sh" || exit 1
require_bash 4.0 "declare -A (associative arrays)" || exit 1

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"

Expand Down
Loading