Skip to content

fix(ci): grandfather the commit-attribution gate by author date (BLO-23894) - #1265

Merged
kkroo merged 5 commits into
masterfrom
blo-23894
Aug 12, 2026
Merged

fix(ci): grandfather the commit-attribution gate by author date (BLO-23894)#1265
kkroo merged 5 commits into
masterfrom
blo-23894

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • The policy CI job on every PR includes a commit-attribution gate (scripts/check-commit-author-attribution.mjs, BLO-21416) that fails a PR if base..head contains a non-merge commit authored by the shared allyblockcast[bot] App identity — REST/MCP write paths stamp that identity regardless of which agent actually wrote the commit.
  • That gate landed with no cutoff date, so it applies retroactively to commits made before the rule existed — on branches already open, reviewed, and in some cases cleared to merge.
  • For those pre-existing commits the gate is unsatisfiable as written: the App stamp already erased the acting agent's identity, so there is no correct author to rewrite to, and guessing one would introduce the exact false-attribution harm the gate exists to prevent. policy gates the whole verify fan-out and the sole ALLGREEN merge queue, so this silently blocked ≥13 open PRs, including one (fix(github-webhook): route PR-review author wakes to the owning issue, not an arbitrary Related: backlink (BLO-20886) #962) that is approved, 21/21 other checks green, with a CEO merge ruling.
  • This PR grandfathers the gate by author date, scoping enforcement to commits authored on/after the moment the gate itself became knowable.
  • The benefit is the forward gate keeps its full force (a post-cutoff App-stamped commit still fails) while pre-existing, unfixable commits stop blocking PRs that predate the rule.

Linked Issues or Issue Description

Fixes: #BLO-23894 (Paperclip issue; no GitHub issue number — tracked at https://paperclip.blockcast.net/BLO/issues/BLO-23894)
Refs: BLO-21416 (the gate this is a follow-on to)

What Changed

  • scripts/check-commit-author-attribution.mjs:
    • Added ATTRIBUTION_GATE_CUTOFF / ATTRIBUTION_GATE_CUTOFF_MS (2026-08-09T01:38:20Z, the committer timestamp of e7162b906/3fa6e41d8, the commits that put this gate on master).
    • findAttributionOffenses(commits, { cutoffMs }) now accepts an optional cutoff and excludes App-attributed commits authored strictly before it. A missing/unparseable authorDate fails closed (still an offense). Omitting cutoffMs preserves the old, uncut behavior.
    • findLocalRangeOffenses — the mode that actually blocks a PR — applies the cutoff by default, and now captures authorDate from git log (%aI) to make the comparison possible.
    • auditRepoCommitAttribution (the --audit-merged mode) now also carries authorDate on normalized commits, but does not pass a cutoff into findAttributionOffenses — it stays advisory and keeps reporting pre-cutoff violations as historical record, on purpose.
    • Docblock documents the cutoff and rationale.
  • AGENTS.md §9: added a subsection documenting the cutoff, why it exists, and what to do (and not do) if policy still fails on a commit that predates it.
  • scripts/check-commit-author-attribution.test.mjs: added cutoff-specific unit tests for findAttributionOffenses (pre/post cutoff, no-cutoff-by-default, fail-closed on missing date) and end-to-end tests for findLocalRangeOffenses using a real git range with backdated GIT_AUTHOR_DATE.

Verification

Risks

  • Low risk. The change only narrows what the local-range gate flags (adds a date-based carve-out); it cannot introduce a new false negative for any commit authored on/after the cutoff, and the cutoff is a fixed past timestamp so no forward drift.
  • The --audit-merged advisory mode is deliberately left uncut (see design note in the diff) — if a future reader expects the audit to also grandfather, that's an intentional asymmetry, not an oversight: the audit never blocks anything, so keeping it historical is strictly more informative.
  • authorDate fail-closed behavior means a corrupted/missing git author-date field is treated as a violation rather than silently passed — matches this script's existing fail-closed conventions (e.g. COMMITS_API_MAX, windowTruncated).

Model Used

Claude Sonnet 5 (claude-sonnet-5[1m]), 1M context window, via Claude Code — extended reasoning, tool use (bash, git, gh CLI, file edit/read tools).

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots (N/A — no UI change)
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green (pending CI run on this PR)
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups (pending)
  • I will address all Greptile and reviewer comments before requesting merge

…23894)

The BLO-21416 attribution gate (scripts/check-commit-author-attribution.mjs)
had no cutoff, so it retroactively failed policy on commits authored before
the gate itself landed (2026-08-09T01:38:20Z, e7162b9/3fa6e41d8) — a rule
those commits could not have known about. Worse, the App stamp the gate
flags has already destroyed the acting agent's identity, so there was no
correct author to rewrite those commits to; guessing one would introduce
the exact false-attribution harm the gate exists to prevent.

findAttributionOffenses now takes an optional cutoffMs and excludes commits
authored strictly before it, failing closed on a missing/unparseable date.
findLocalRangeOffenses (the mode that actually blocks a PR) applies the
BLO-23894 cutoff by default; --audit-merged deliberately omits it so the
advisory audit keeps reporting pre-cutoff violations as historical record.

Verified against PR #962 (the worked example in BLO-23894): the unmodified
gate fails on its two pre-cutoff commits, the fixed gate passes.

Re-ran the blast-radius scan from BLO-23894 across all 13 named PRs
(#1186, #1165, #1161, #1155, #1148, #1138, #1133, #1130, #1126, #1123,
#1114, #1091, #962): 11 now pass cleanly. #1186 and #1123 still fail —
correctly — because each carries at least one commit authored *after*
the cutoff (#1186: 2026-08-09T04:03Z; #1123: 2026-08-10T13:35Z), i.e. a
genuine new violation of the still-forward-enforced gate, not a grandfathering
gap.
@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-21416
🔗 Paperclip issue: BLO-23894

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-21416
🔗 Paperclip issue: BLO-23894

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 5fa36ff

Critical Issues (0)

Important Issues (1)

  • [gstack/review, native-codex] scripts/check-commit-author-attribution.mjs:125 — The grandfather decision trusts the commit author timestamp, which any contributor can forge after the cutoff with GIT_AUTHOR_DATE (or an equivalent API author date). An App-attributed commit created today can therefore be backdated to before 2026-08-09 and bypass the CI policy entirely.
    • Base the cutoff on a non-contributor-controlled arrival signal, such as the commits committer date, or derive grandfathered commits from an immutable allowlist of existing SHAs.

Suggestions (0)

Strengths

  • The local gate fails closed when author-date data is absent or unparsable.
  • The tests cover both intended temporal paths and preserve the historical audit behavior.

Recommended Action

  1. Fix the Important issue before merge.

…mmitter date

Ally's review on #1265 correctly points out that author date is
caller-forgeable (git commit --date, GIT_AUTHOR_DATE, or the low-level Git
Data API's author.date param) and suggests keying the grandfather cutoff on
committer date instead.

Verified empirically that this trade is a net regression: `git rebase`
resets committer date to "now" while leaving author date untouched (`git
merge` / GitHub's default "Update branch" touches neither). Any of the
already-open, already-reviewed PRs BLO-23894 exists to unblock would get
re-flagged the moment it's rebased onto a moving base — reproducing the
incident this gate change fixes — to close a forgery path that doesn't
exist on the Contents/Merge API paths this gate actually polices (both set
author and committer date server-side to request time; only the low-level
Git Data API, which our agents' write paths don't use, accepts a caller
date at all).

Documents the trade-off on findAttributionOffenses and adds a regression
test that pins the rebase-survives-cutoff behavior — it fails if the
comparison is ever switched to committer date, confirmed by temporarily
making that swap and re-running.

No behavior change.
@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Re: scripts/check-commit-author-attribution.mjs:125 — the author-date-is-forgeable concern is correct in the abstract, but I tested the suggested remedy (key the cutoff on committer date instead) before applying it, and it's a net regression, not a fix. Pushed 8420f499 with the analysis and a regression test; summary here:

Committer date does not survive an ordinary git rebase. Verified empirically:

$ git log -1 --format='author=%aI committer=%cI'   # before rebase
author=2026-08-05T16:46:12Z committer=2026-08-06T01:15:46Z
$ git rebase main -q && git log -1 --format='author=%aI committer=%cI'
author=2026-08-05T16:46:12Z committer=2026-08-10T14:14:15Z

git merge / GitHub's default "Update branch" button touches neither date — only an explicit rebase resets committer date to "now". If the cutoff keyed on committer date, any of the 13 already-open, already-reviewed PRs BLO-23894 exists to unblock would get re-flagged the instant someone rebases onto a moving base — reproducing the exact incident this PR fixes, for routine branch upkeep rather than a new violation.

The forgery path doesn't exist on the write paths this gate actually polices. BLO-21416 scopes this gate to the Contents API and Merge API (create_or_update_file/push_files) — neither exposes a caller-settable author.date/committer.date; GitHub stamps both server-side to request time. The only place a date is forgeable via API is the low-level Git Data API (POST /git/commits), which our agents' write paths don't use. Forging it locally via git commit --date/GIT_AUTHOR_DATE requires an agent to also deliberately set git config user.email to the App's noreply address — a two-step, benefit-less action (impersonating the App gains nothing the sanctioned write path doesn't already offer for free), not something that happens incidentally.

Given the demonstrated regression outweighs a forgery path that's theoretical for our actual write paths, I kept author date and instead: (1) documented this trade-off directly on findAttributionOffenses, and (2) added findLocalRangeOffenses keeps grandfathering a pre-cutoff commit after an ordinary rebase — a test that fails if the comparison is ever switched to committer date (confirmed by temporarily making that swap and re-running before reverting). No behavior change, so I'm not asking for a re-review — flagging for visibility since this addresses your Important finding without adopting the suggested diff verbatim.

If there's a write path in scope I'm missing where committer date is also forgeable through legitimate agent tooling, happy to revisit — that would change the trade-off.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8420f49

Prior Findings Dispositioned (1)

  • prior:5fa36ff important 1 — still-present — scripts/check-commit-author-attribution.mjs:116 — The local PR gate continues to decide grandfathering from commit.authorDate; a contributor can set that field to a pre-cutoff value while creating a new App-attributed commit after the cutoff.

Critical Issues (0)

Important Issues (1)

  • [gstack/review, native-codex] scripts/check-commit-author-attribution.mjs:116prior:5fa36ff important 1 The author-date cutoff can be bypassed by backdating a new App-attributed commit with GIT_AUTHOR_DATE (or the Git Data API). The current local-range policy evaluates all non-merge commits, not only trusted Contents/Merge API writes, so this turns a forward-enforced gate into an opt-out for direct pushes.
    • Use an immutable arrival signal or a fixed allowlist of the already-existing grandfathered commit SHAs; retain a path that does not reclassify rebased historical commits without trusting an attacker-controlled timestamp.

Suggestions (0)

Strengths

  • The implementation fails closed when the supplied author date is missing or unparsable.
  • The tests distinguish the blocking local-range behavior from the advisory historical audit.

Recommended Action

  1. Address the Important issue before merge.

…-23894)

CTO measured that 11 of 71 sampled checkouts under /paperclip/work had
local git config already stamped with the shared allyblockcast[bot] App
identity (and 18 more had no local identity at all), so a git-push commit
can still trip this gate — the failure message and AGENTS.md's forward
guidance both asserted the opposite. Widening the match to the bare
allyblockcast[bot]@users.noreply.github.com form was also proposed, but
that email is graphify-reindex's own legitimate git-push identity
(verified against real PRs #789/#944, pinned by an existing test); doing
so would flag its commits, so left unmatched and documented instead.
@allyblockcast

allyblockcast Bot commented Aug 10, 2026

Copy link
Copy Markdown
Author

Addressed 2 of the 3 items from CTO's BLO-23894 comment in 6f5a566a1:

  1. Fixed the false "git push is unaffected" premise. Confirmed while working on this: the shared workspace checkout I started from had local user.email/user.name set to CTO <cto@blockcast.net> (not mine) — a live demonstration of the exact gap CTO measured (11/71 checkouts App-attributed, 18/71 unset). Corrected the script docblock, the CLI failure message, the pr.yml step comment, and AGENTS.md §9 to say a git push commit is only safe if the checkout's local git config is actually per-agent, and to tell the reader to check git config user.email instead of asserting REST/MCP by default.
  2. Declined the "widen the match to allyblockcast[bot]" suggestion. The bare allyblockcast[bot]@users.noreply.github.com form is the graphify-reindex bot's own legitimate git push identity — verified against real PRs chore(graphify): refresh knowledge graphs #789/chore(graphify): refresh knowledge graphs #944 when the gate was first built, and pinned by the findAttributionOffenses ignores the graphify-reindex bot's git-push identity test in this same file. Widening the match would flag graphify-reindex's commits. If a checkout's local config shows the bare form, that's still a misconfigured checkout (covered by fix test(plugin-linear): requestId fixtures + getLinkByLinear mock-leak fix; scripts: ensure-build-deps freshness check #1) — the email alone can't distinguish "misconfigured agent checkout" from "graphify-reindex doing its job," so the gate shouldn't try to.
  3. Provisioning root cause (why checkouts end up with the wrong/no local identity) — not fixed here. I could not find the code that sets a checkout's local git identity in either paperclip or onprem-k8s (both searched); it likely lives in the adapter/pod-provisioning layer, which isn't in either checkout I have access to. Documented the gap and the manual fix in AGENTS.md so it's not silently wrong in the meantime; leaving the actual provisioning fix as a follow-up (see BLO-23894 comment).

25/25 existing + prior tests still pass unmodified (docs/message-only change, no behavior change to findAttributionOffenses).

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 6f5a566

Prior Findings Dispositioned (1)

  • prior:5fa36ff important 1 — still-present — scripts/check-commit-author-attribution.mjs:183 — The local blocking mode still exempts App-attributed commits solely by authorDate; this field is explicitly caller-controlled by GIT_AUTHOR_DATE or git commit --date.

Critical Issues (0)

Important Issues (1)

  • [gstack/review, native-codex] scripts/check-commit-author-attribution.mjs:183 — A contributor can backdate an App-attributed commit before the cutoff and bypass the blocking gate. This is practical in the documented local-config failure mode: configure the App noreply email, set GIT_AUTHOR_DATE before the cutoff, then create a post-cutoff commit. The added test at scripts/check-commit-author-attribution.test.mjs:352 codifies that bypass.
    • Grandfather known historical commit SHAs, or use a server-controlled immutable cutoff signal while preserving an explicit migration path for legitimate rebased commits.

Suggestions (0)

Strengths

  • The parsing change fails closed for missing or malformed author dates, and the test coverage clearly captures the intended grandfather behavior.

Recommended Action

  1. Address the Important issue before merge.

@allyblockcast

allyblockcast Bot commented Aug 11, 2026

Copy link
Copy Markdown
Author

Re: the recurring "Important" finding on the author-date cutoff (scripts/check-commit-author-attribution.mjs, now flagged 3 rounds running as prior:5fa36ff) — declining to fix further, with reasoning below. Not pushing a commit for this, per the review-response policy of not committing just to acknowledge.

The suggested test citation is off. scripts/check-commit-author-attribution.test.mjs:352 at the reviewed head (6f5a566a) is auditRepoCommitAttribution reports a commit list that hit the 250-entry API cap — unrelated to the cutoff entirely. The nearest actual candidate, findLocalRangeOffenses keeps grandfathering a pre-cutoff commit after an ordinary rebase (PR #1265 review) (line 231), doesn't codify a bypass — it pins the opposite: that a genuinely pre-cutoff commit survives a legitimate rebase, which is the exact incident BLO-23894 exists to stop from recurring.

The suggested fix (committer date) closes nothing. Verified directly:

$ GIT_AUTHOR_DATE="2020-01-01T00:00:00Z" GIT_COMMITTER_DATE="2020-01-01T00:00:00Z" git commit -q -m test
$ git log -1 --format='%aI %cI'
2020-01-01T00:00:00Z 2020-01-01T00:00:00Z

GIT_COMMITTER_DATE is exactly as caller-controlled as GIT_AUTHOR_DATE — same mechanism, same trust boundary. Switching the cutoff to committer date wouldn't remove the backdating path you're describing, it would just rename which env var backdates it, while reintroducing the false positive this PR fixes: an ordinary git rebase resets committer date to "now" on an unrelated, already-compliant pre-cutoff commit (line 231's test pins exactly this). That's already documented in the findAttributionOffenses docblock (scripts/check-commit-author-attribution.mjs:~140).

The threat model this gate covers doesn't include deliberate identity forgery. The gate matches exactly one literal string, APP_NOREPLY_EMAIL. Its two documented failure modes (BLO-21416, BLO-23894) are both accidental: the REST/MCP write path stamping it server-side with zero caller control, and a checkout's local git config defaulting to it. Neither involves an adversary choosing to impersonate the shared bot. If someone with commit access actually wanted to land code without attribution, backdating GIT_AUTHOR_DATE while also using the App's exact email is strictly harder than just committing under any other unwatched identity — which this gate has never caught, cutoff or not, and isn't designed to. The grandfather clause doesn't open a new door; the door (arbitrary author identity on a local commit) was always open, and closing it is a different, much larger problem — signed-commit / branch-protection enforcement — not a scoping fix to this gate.

Remaining alternative — a fixed SHA allowlist is the one option that's actually non-forgeable (SHAs are content-addressed), but it's also the one we already weighed and passed on for a concrete operational reason: allowlisted SHAs don't survive a legitimate rebase of a pre-cutoff PR (new parent → new SHA), so it would need continuous manual re-curation for every rebase of every one of the ≥13 affected PRs, indefinitely, with no automatic expiry. Author-date grandfathering degrades gracefully instead: it's self-expiring (every commit is eventually post-cutoff) and requires zero maintenance.

If there's appetite for closing deliberate-forgery risk on this gate specifically, that's worth its own issue scoped to signed-commit enforcement rather than folding into BLO-23894's grandfather fix — happy to file it if wanted, but treating it as this PR's blocker conflates two different problems.

PlatformSREEngineer and others added 2 commits August 11, 2026 15:31
… a date cutoff (BLO-23894)

authorDate is caller-controlled (GIT_AUTHOR_DATE / git commit --date) on the
git push write path this gate also polices, so a pure date cutoff can be
defeated by backdating a brand-new violation straight past it. Replace it
with GRANDFATHERED_OFFENSE_SHAS, an explicit allowlist of the specific
pre-cutoff commit shas built by scanning every commit on all 168 currently-open
paperclip PRs for the App-identity/non-merge/pre-cutoff predicate. Unenumerated
history now fails closed instead of being silently exempted by date, and two
commits that scan turned up with post-cutoff authorDate (#1125, #1220) are
correctly left off the allowlist as live violations.

Trade-off documented in both the module docblock and AGENTS.md §9: an
ordinary GitHub "Update branch" merge leaves a pinned commit's sha untouched,
but an explicit git rebase rewrites it, dropping it off the allowlist — a
fail-closed, forgery-free failure mode fixed by adding the new sha.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 7ae3c19

Prior Findings Dispositioned (1)

  • prior:5fa36ff important 1 — fixed — scripts/check-commit-author-attribution.mjs:249 — The blocking local-range path now passes GRANDFATHERED_OFFENSE_SHAS to findAttributionOffenses, which only exempts an exact SHA; a backdated authorDate cannot bypass the gate.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The immutable, finite allowlist preserves the historical audit while making PR enforcement fail closed for unlisted App-attributed commits.
  • Regression coverage verifies both the allowlisted historical path and rejection of a backdated, unlisted commit.

Recommended Action

  1. No blocking changes requested.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 11, 2026
@kkroo
kkroo removed this pull request from the merge queue due to a manual request Aug 11, 2026
@kkroo
kkroo added this pull request to the merge queue Aug 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@kkroo
kkroo added this pull request to the merge queue Aug 12, 2026
Merged via the queue into master with commit e8e8547 Aug 12, 2026
18 checks passed
@kkroo
kkroo deleted the blo-23894 branch August 29, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant