Skip to content

fix(source-control): read babysit trust files at the branch tip SHA - #10662

Merged
kyle-sexton merged 3 commits into
mainfrom
fix/babysit-trust-reads-at-sha
Oct 28, 2026
Merged

kyle-sexton merged 3 commits into
mainfrom
fix/babysit-trust-reads-at-sha

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Closes #10648

Summary

Problem: the babysit merge gate read the trusted-actor list and admission rules with ?ref=<branch name>, which a tag named like the branch could answer. This PR reads both at the branch's tip SHA, resolved by full ref, and fails closed when the tip does not resolve.

Fix

  • babysit_verify_evidence.py: a new Reader._tip resolves a branch through merge_policy.base_tip (git/ref/heads/<branch>, 40-hex object.sha, else PolicyReadError) and maps a failure to _Unreadable, the existing fail-closed path (verify:unreadable). _read_trusted_list reads at the reader branch's tip; _task_list reads at the default branch's tip. The run-cache keys stay repo@branch; the tip is resolved only on a cache miss.
  • babysit_merge.py _lifted_by_person: the same class, found by the sweep. trusted_actors(repo, default) passed the branch name into _file_at's ?ref=. It now resolves base_tip first; a failure is no lift, so the label stays on.
  • babysit_verify_evidence.py answer key: the GraphQL object(expression:) probes for both files name refs/heads/<branch>:<path>, not <branch>:<path>. A live read-only query against this repository returned the same blob oid for refs/heads/main:<path> and main:<path>, and null for a missing branch.

Verification

  • New tests fail on the unfixed source (6 tests, 18 failures with subtests) and pass after the fix: TheReadsNameACommit (request paths name the tip SHA, never the branch; a 404, short, uppercase, name-valued, or missing sha fails closed with no contents read at the branch name), TheRulesComeFromTheDefaultBranch (rules at trunk's tip, trusted list at feature-x's), test_the_trust_files_are_probed_by_full_branch_ref, and test_an_unresolved_default_branch_tip_is_no_lift.
  • Full babysit-prs/scripts/tests suite: 3337 passed, 3600 subtests passed. scripts/run-ruff.sh check on the changed files: all checks passed.
  • Left out: base_tip itself is unchanged, because test_merge_holds_sync.py checks it against merge-holds.mjs baseTip. It checks the SHA shape, not the object type, so the non-commit case is covered by the SHA check.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 3 commits October 28, 2026 09:37
The verdict gate read the trusted-actor list and admission rules with
?ref=<branch name>, and the escalation lift read the trusted list the
same way, so a tag named like the branch could answer instead. Resolve
each branch to its tip with merge_policy.base_tip (git/ref/heads, 40-hex
or fail closed) and read at that SHA. The answer-key probe names the
files by refs/heads/<branch>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 28, 2026 13:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T13:51:38.183613Z f973aac Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed f973aac5c3681ea6f52256061d2c324596c50033: 0 finding(s), 0 as inline comments.

No security issues found in this change. The PR narrows trust reads rather than widening them.

critical-findings: 0

What I read. The whole diff, plus base_tip, _file_at and trusted_actors in babysit_merge_policy.py and _lifted_by_person in babysit_merge.py. Changed trusted paths: babysit_verify_evidence.py and babysit_merge.py. I judged them from the code, not the PR text.

Trust controls. The change narrows who can supply the trusted-actor list and admission rules. It widens nothing.

  • Reader._read_trusted_list and _task_list (babysit_verify_evidence.py) now read the contents API at a 40-hex SHA from merge_policy.base_tip, which uses git/ref/heads/<branch> (babysit_merge_policy.py:2604-2612). A tag named like the branch can no longer answer the ?ref= read.
  • _tip maps PolicyReadError to _Unreadable, the existing fail-closed verify:unreadable path. A 404, a malformed SHA or a missing object blocks the read.
  • _lifted_by_person (babysit_merge.py:3862-3873) resolves base_tip inside the existing try. Any exception returns (False, ...), so the escalation label stays on. Only the lift path changed. An unresolved default branch is still no lift.
  • The GraphQL answer key now probes refs/heads/<branch>:<path>, matching the full-ref read. A mismatch would only change the verdict key and make it differ, not widen what passes.

Residual points, none a finding.

  • The run-cache keys stay repo@branch. The tip is resolved only on a cache miss, and the cached value comes from a SHA-pinned read. The PR calls out the _default_branch_head gap as babysit _default_branch_head resolves the default branch through commits/<name>, not its full ref #10660, outside this diff.
  • The branch name goes unescaped into the git/ref/heads/ path. That is the existing base_tip behavior and the branch comes from PR metadata. This diff adds no new reach.
  • Reader._tip calls merge_policy.base_tip under that module's token, not the reader's TOKEN_ENV. A token difference can only make the read fail, which fails closed.

No manifest files (plugin.json or marketplace.json) changed, so there are no manifest <field> lines. The .changes entry is a changelog file. No instruction-surface deletions. The PR context contains no embedded directives. No cost or billing surface.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed f973aac5c3681ea6f52256061d2c324596c50033: 0 finding(s), 0 as inline comments.

No blocking or advisory findings on this PR.

What I checked, by reading the diff and the changed code:

  • Closing issue babysit gate reads trusted actors and admission rules by branch name, not a resolved commit #10648, "Both reads name a 40-hex SHA, never a branch name; a test asserts the request paths": met. Reader._read_trusted_list and Reader._task_list in babysit_verify_evidence.py now call merge_policy.base_tip and read at ?ref={tip}. base_tip (babysit_merge_policy.py:2604) requires a 40-hex lowercase SHA. TheReadsNameACommit.test_each_read_names_a_sha_never_a_branch asserts the request paths.
  • Closing issue babysit gate reads trusted actors and admission rules by branch name, not a resolved commit #10648, "An unreadable or non-commit ref fails closed, with a test": met. Reader._tip maps PolicyReadError to _Unreadable, which is the existing verify:unreadable path. test_an_unreadable_tip_fails_closed covers a 404, a short SHA, an uppercase SHA, a branch name as the SHA, a missing sha and a missing object.
  • _lifted_by_person in babysit_merge.py: base_tip is called inside the existing try. A failure comes back as (False, "PolicyReadError: ..."), so the label stays on. test_an_unresolved_default_branch_tip_is_no_lift covers it.
  • Run-cache keys: they stay repo@branch, and the tip is resolved only on a cache miss. Cached answers therefore keep working, and a failed tip resolution never writes to the cache.
  • _answer_key: it now probes refs/heads/<branch>:<path>, matching the full-ref read. A test asserts the probe arguments.
  • Token used for the tip read: base_tip goes through merge_policy._get with the ambient GH_TOKEN, the same token postmerge._read uses for the contents reads. The tip and the file are read under the same identity.
  • Changelog fragment: bump: patch fits a security fix with no interface change.

The PR body says it ran the full suite and ruff. I did not run either, so I treat that as author-claimed and unverified.

No userConfig or manifest files changed, so no manifest lines apply. Closing issue #10648 was read and is not unread. The PR context file holds no embedded instructions. No spend, so no hold.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit fd6b81e Oct 28, 2026
77 checks passed
@kyle-sexton
kyle-sexton deleted the fix/babysit-trust-reads-at-sha branch October 28, 2026 14:02
@github-actions

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 0, survived 0, timeout 0, no coverage 0, invalid 0, not run 8.
Covered-code score n/a; mutation score n/a.

File Killed Survived Timeout No coverage Invalid Not run
plugins/source-control/skills/babysit-prs/scripts/babysit_merge.py 0 0 0 0 0 2
plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py 0 0 0 0 0 6

The run's summary has the full report.

kyle-sexton added a commit that referenced this pull request Oct 28, 2026
…ad through base_tip

#10662 patched base_tip in the unattended harness with its own tips, and
this branch moved the default branch head read onto base_tip, so the head
came from tips["main"] while main_head still answered only the dead
git/ref/heads fake: 13 tests failed in the merge group. main_head is now
tips["main"], one value as in production, and the unresolved-tip lift test
fails only the lift's read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

babysit gate reads trusted actors and admission rules by branch name, not a resolved commit

1 participant