Skip to content

fix(gemini-ledger): hand the tier report only records traceable to trusted runs - #2305

Merged
don-petry merged 5 commits into
mainfrom
claude/issue-2147-trusted-ledger-records
Oct 14, 2026
Merged

don-petry merged 5 commits into
mainfrom
claude/issue-2147-trusted-ledger-records

Conversation

@don-petry

@don-petry don-petry commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The Gemini tier snapshot and its history are built from token-usage-* artifacts collected across the organisation. Those artifacts are untrusted input: a pull request run from a fork of a public repository can upload one under any name, with any content. That is harmless while the snapshot is informational, and stops being harmless the moment anything gates dispatch on it. "Gemini trickle under a budget pause" (#2147) makes this its first acceptance criterion: records feeding a dispatch decision must be traceable to trusted runs.

This is that criterion only. The trickle itself is not in this pull request and is not being built yet; see "What this does not do".

Design

What a record can prove. A record carries only what the job wrote, including its own run_id; all of it, and the artifact's name, can be forged. What cannot be forged is what the Actions API says about the run that uploaded the artifact.

The trust rule. An artifact is trusted when both hold:

  1. The artifact listing's workflow_run.head_repository_id equals repository_id (not a fork).
  2. Either its head branch is the repository's default branch, or a look-up of the run confirms the same run id and head repository and that neither the actor nor the triggering actor is Dependabot.

That is the set of runs GitHub gives the repository's secrets to. Whoever controls such a run's code can already call Gemini with the real keys, so a forged ledger gains them nothing; fork and Dependabot runs get no secrets and cannot have made a real call. Anything that cannot be established (a failed or mismatching look-up, no workflow_run, no actor) is untrusted.

"Default branch only" was rejected: on four days of real data it would drop about a fifth of genuine usage (269 of 1,353 artifacts), and missing usage makes Gemini look more available than it is.

Where it sits. scripts/token_report.sh classifies each artifact at listing time, looks up non-default-branch runs, and stamps every record with provenance {run_id, trusted, basis}, replacing anything the artifact wrote under that key. Only trusted records are handed to the tier report. Ignored and unreadable counts go to the log, a warning and the job summary.

"Cannot tell" is not "available". The snapshot gains a collection block (complete or not, counts, reason). A new reader for future dispatch decisions, gq_tier_decision, answers unknown when the snapshot is unreadable, stale (default one hour), from the future, for a quota day that has ended, built from an incomplete collection, not marked trusted-runs-only, or holding records for a model or key with no caps row. Nothing calls it yet. The existing informational gq_tier_can_run is unchanged.

The history cache key gains v2, so the store filled before the filter existed is not restored. The per-day history table loses up to seven earlier days, once.

What this does not do

Risk

  • More API calls in the daily fleet monitor run: one run look-up per artifact that is not on the default branch, about a fifth of them (roughly 270 a day on the sample). The monitor uses the shared maintainer login, whose hourly quota was exhausted twice today by other reads; if that proves tight, the look-ups can be cached per run id.
  • The listing and look-up field names were checked against the live API with a maintainer login, not with the monitor's own token.
  • The JSON Schema change for the collection block was hand-checked; the schema test needs a Python package that is not installed locally, so CI is the first real validation.
  • The collector's cost report is unfiltered on purpose: existing tests require artifacts without a workflow_run to still be collected for it.

Test plan

  • tests/token_report_trust.bats (new, 27 tests), wired into lint.yml: the trust rule (fork, same-repository branch, default branch, Dependabot, an artifact declaring itself trusted, an unreadable or mismatching look-up), the hand-off, the snapshot's collection block, the decision reader's fail-closed cases, and the wiring.
  • Existing suites unchanged and identical to baseline: token_report 44, test_gemini_quota 44, test_gemini_tier_view 48 (plus one that needs the missing Python package locally).
  • Mutation-checked: 24 mutations, each turning a named test red (fork accepted, self-declared trust believed, unreadable look-up accepted, Dependabot accepted, hand-off unfiltered, stale snapshot accepted, incomplete collection accepted, unlisted records ignored, cache not renamed, and others).
  • Re-run by me on this commit after rebasing: the new suite 27 of 27, token_report 44, test_gemini_quota 44, the fixed-path check 18; shellcheck --severity=warning -x and actionlint clean.
  • Not run: the fleet monitor itself.

Rollback

Revert the squash commit. The tier report goes back to reading every collected record, and the old history cache is used again.

Monitoring

Refs #2147

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

…usted runs

The Gemini tier snapshot and history are built from token-usage artifacts
collected across the organisation. An artifact is untrusted input: a pull
request from a fork can upload one of any name and content, and a record
says nothing about its origin but a run id it wrote itself.

- token_report.sh decides trust from the Actions API, never from the
  artifact: the uploading run's head repository is the repository itself,
  and either the run was on the default branch (from the artifact listing,
  no extra call) or the looked-up run confirms the head repository and was
  not started by Dependabot. A fork run, a Dependabot run, a missing
  workflow_run and a failed or mismatching look-up are not trusted.
- Every collected record is stamped provenance {run_id, trusted, basis},
  replacing anything the artifact wrote. Only trusted records reach
  GEMINI_RECORDS_OUT; the ignored ones are counted in the log, a warning
  and the job summary. The cost report is not filtered.
- The collector reports what it could not read and how many handed-over
  records are for a key index and model the caps file does not list (test
  fixtures written to the real ledger by engine tests run inside an agent
  session). gemini_tier_report.sh copies that into the snapshot as
  collection.
- gq_tier_decision is the reader a dispatch decision must use: unknown
  unless the snapshot is fresh, for the current quota day, trusted-only
  and from a complete collection with no unlisted records. Nothing calls
  it yet.
- The history cache is renamed so no record collected before the filter
  is restored.

Refs #2147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry don-petry added the dev-lead:hands-off Exclude from dev-lead agent automation label Oct 11, 2026
@don-petry
don-petry marked this pull request as ready for review October 11, 2026 18:36
@don-petry
don-petry requested a review from a team as a code owner October 11, 2026 18:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 6dfe4c60-ac00-4ae2-af6d-6408b609eca7

📥 Commits

Reviewing files that changed from the base of the PR and between 04dc6f3 and 0d99896.


📒 Files selected for processing (8)
  • .github/workflows/actions-fleet-monitor.yml
  • .github/workflows/lint.yml
  • AGENTS.md
  • scripts/gemini_tier_report.sh
  • scripts/lib/gemini-quota.sh
  • scripts/lib/gemini-tier-snapshot.schema.json
  • scripts/token_report.sh
  • tests/token_report_trust.bats

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@don-petry

Copy link
Copy Markdown
Collaborator Author

dev-lead is withholding action on this item.

It is labeled dev-lead:hands-off (deliberately kept off-limits to dev-lead (e.g. changes to the dev-lead automation itself)), so dev-lead will not pick it up while that label is present. This notice is posted once so the withhold is visible rather than looking like a stalled run.

To re-enable automated pickup: remove the dev-lead:hands-off label.

@don-petry

Copy link
Copy Markdown
Collaborator Author

QA Lead — test-risk advisory

Risk tier: MEDIUM. Nothing calls gq_tier_decision yet, so dispatch is not affected today. But this PR exists to make the snapshot's "complete, trusted-only" claim safe to gate on, and one path makes that claim untrue.

What I'd shore up (highest leverage first):

  • A failed run look-up is counted as "untrusted", not as "failed". When _artifact_run_trusted hits a timeout, a 403 or the rate limit, the artifact is stamped run-not-verified and counted as ignored. No COLLECT_FAILED_DIR marker is written, so the snapshot still says complete: true. Real usage is silently dropped, and dropped usage is exactly what makes Gemini look more available than it is. The Risk section expects this to happen: the shared login's quota ran out twice today. Record a transport failure as a failed artifact, and add a bats case where the stub fails the look-up (as opposed to returning a mismatch) and the collection must read incomplete.
  • Nothing tests the schema change, in CI or locally. The only schema test, test_gemini_tier_view.bats:554, validates raw gq_tier_snapshot output, which has no collection key. Add a contract test that validates the snapshot gemini_tier_report.sh writes, once with and once without a collection report.
  • Look-up volume: add one test that runs many branch artifacts from the same run and asserts how many look-ups are made. That makes the per-run-id cache measurable before quota trouble starts.
  • The rest is solid: a stubbed gh, a fixed clock, fail-closed cases, and a mutation check of 24 mutations. Nothing in it looks flaky to me.

Escalate? yes. Under the rate-limit condition this PR itself names, the "complete" flag fails open, so it can't be trusted to gate dispatch later.

Advisory only — I comment, I do not change code. Ground: BMAD Test Architecture
(bmad-tea v1.19.0). Opt out on this item with the qa-lead:hands-off label.

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-11T19:39:15Z.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to verify the provenance of token-usage records, ensuring only records from trusted runs are used to build the Gemini tier snapshot and make dispatch decisions. It updates token_report.sh to determine trust via the GitHub Actions API, filters out untrusted records, and writes a collection report. gemini_tier_report.sh is updated to include collection metadata in the snapshot, and gemini-quota.sh introduces gq_tier_decision to validate snapshots before allowing dispatch. Comprehensive tests are added in token_report_trust.bats. The review feedback recommends three improvements: leveraging the existing EXIT trap by creating the temporary trusted_dir inside jsonl_dir to prevent orphaned directories, using jq's optional chaining operator (?) in gq_tier_decision to safely handle potentially missing nested properties, and piping the output of gq_tier_snapshot directly into jq to avoid creating a temporary file.

Comment thread scripts/token_report.sh Outdated
Comment thread scripts/lib/gemini-quota.sh
Comment thread scripts/gemini_tier_report.sh Outdated
Comment thread scripts/token_report.sh
@codeant-ai

codeant-ai Bot commented Oct 11, 2026

Copy link
Copy Markdown

CodeAnt Nitpicks

1 code suggestion

1. When a genuine same-repository branch run lookup fails, its records are dropped but no failure is counted. Dispatch may accept a complete snapshot missing that usage.

Incomplete implementation · scripts/token_report.sh:725-729

@codeant-ai

codeant-ai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

CodeAnt PR Risk: Low Risk

  • The PR appears safe to merge.
  • Only records traceable to trusted runs reach the tier report, and incomplete collections make dispatch decisions unknown.
  • Malformed record files are counted as failed reads, and the tests cover trust filtering and fail-closed decisions.

Assessed commit: 0d998963c200

don-petry and others added 2 commits October 11, 2026 16:19
… file as failed reads

Review of the trusted-records change found reads that fail without making
the collection incomplete, so real usage could be dropped while the
snapshot still said complete.

- A run look-up that FAILS (non-zero exit, timeout, rate limit, an empty or
  unparseable body, a body that is not a run object) is no longer the same
  as one that answers no. It is stamped run-lookup-failed and counted
  (run_lookups_failed); a look-up that answers and does not qualify stays
  run-not-verified and is only ignored.
- A record file that is not valid JSONL is counted (record_files_malformed)
  instead of being dropped silently.
- Either count above zero makes collection.complete false, so
  gq_tier_decision answers unknown. Both are in the snapshot schema.
- A run is looked up once per collection, however many artifacts it
  uploaded (a re-run attempt uploads another under the same run id).
- The trusted copy lives under the collection directory the EXIT trap
  already removes. The tier report builds the snapshot beside its target,
  moves it into place whole and removes its working files on every exit.
- Tests: the snapshot the tier report writes is validated against the
  schema with and without a collection report (fails in CI when the
  validator is missing, skips with a notice elsewhere); a snapshot whose
  window, collection or tiers has the wrong type is unknown.

Refs #2147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry

Copy link
Copy Markdown
Collaborator Author

Review round 1, head 399b38dd.

  • A failed run look-up is now a failed read, not an "untrusted" artifact (QA advisory, escalated; correct). The look-up has three answers: the run qualifies; the API answered and it does not; the API could not be asked or did not answer usably (non-zero exit, timeout, empty or unparseable body, an error body). The third is stamped run-lookup-failed, counted as run_lookups_failed, and makes the collection incomplete, so the decision reader answers unknown. A deleted run (404) cannot be told from a refusal at the CLI's exit code and also reads as a failed look-up, which is the fail-closed direction. I re-ran one mutation myself: treating a failed look-up as "answered no" turns four tests red.
  • A malformed record file is a failed read too (CodeAnt).
  • The schema change is now tested (QA): three tests validate the snapshot gemini_tier_report.sh actually writes, with and without a collection report, and reject an unknown or missing field. They fail in CI if the validator is missing. The validator is not installed where this was written, so CI on this head is the first real validation of the schema.
  • Look-ups are shared per run (QA): both uploaders name the artifact after the run id, and a re-run attempt can add a second artifact under it, so the collector looks each run up once and a test asserts two look-ups for four artifacts of two runs.
  • The working-file and temporary-directory points are answered on their threads.

Suites: token_report_trust 36 (3 of them the schema tests, skipped locally), token_report 44, canary_report 46, the fixed-path check 18; the negation lint and shellcheck clean. main is merged in.

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-11T22:27:32Z.

don-petry and others added 2 commits October 11, 2026 20:41
…s a failed read

An artifact with no usable workflow_run was treated like a fork's: untrusted and
nothing missing. A fork is known to be a fork; this one may be real usage, so
it now makes the collection incomplete. A repository whose listing file could
not be created is counted as a failed listing too. Both from CodeAnt on the
pull request.

Refs #2147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry

Copy link
Copy Markdown
Collaborator Author

Review round 2, head 0d998963. Both points from CodeAnt's read of the previous head are right and fixed:

  • An artifact with missing or incomplete workflow_run metadata was treated like a fork's: untrusted, nothing missing. A fork is known to be a fork and its run got no secrets; an artifact whose run the listing does not name may be real usage. It is now stamped run-unknown, warned about, and counted with the failed look-ups, so the collection is incomplete and the decision reader answers unknown. New test: "an artifact the listing names no run for is a failed read; a fork's is not". Classifying it as a fork again turns it red.
  • A repository whose listing file could not be created is now counted as a failed listing, not only warned about.

Suites after merging main: token_report_trust and token_report pass, canary_report 46; shellcheck, the negation lint and the fixed-path check clean. The schema tests passed in CI on the previous head (unit and bats were green there), which was the first real validation of the schema change.

@sonarqubecloud

Copy link
Copy Markdown

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-12T02:46:20Z.

@don-petry
don-petry merged commit 60b54c5 into main Oct 14, 2026
71 checks passed
@don-petry
don-petry deleted the claude/issue-2147-trusted-ledger-records branch October 14, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-lead:hands-off Exclude from dev-lead agent automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants