Repository navigation
Conversation
A sandbox holds one grant at a time, and a grant names one operation and a list of repositories, so a pod that both fetches and pushes can't get both, and nothing limits either to branches. GrantV2 holds a list of rules, each a repository, an operation and the branches it may touch. A fetch rule sets any_object instead of branches to read everything, so a rule that leaves its branches out is refused rather than read as a full read. Grant, version 1, still signs and verifies, and its pushes are relayed unread, so signers can switch once verifiers read both. GitServer reads a push's ref updates before the pack and refuses the whole push if one updates an unlisted branch, deletes a branch, updates another ref or carries push options. git reads the answer only after sending the whole push, so the server reads the pack to the end, then answers with a status report that git shows as rejected refs with their reasons. Fetch rules that list branches are refused until the server can keep a fetch to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anches Seer can let the pod push in two ways: add each branch to the grant before the pod pushes it, or let the pod start branches under a prefix and add each one once it exists. The second needs a rule that matches names it doesn't know yet and can't move a branch someone else made. A push rule now sets branch_patterns, full-match regular expressions, in place of branches, and create_only allows only updates whose old id is all zeros, which the provider refuses for a branch that exists. Several rules may name one repository and operation, and an update passes if any allows it, so a create_only pattern sits next to the branch the pod already made. Signing a version 1 Grant now warns, and the class is marked deprecated for type checkers. Verifying one stays quiet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grant is deprecated, so the README, which reads as the module's docs, points new callers at GrantV2 alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uietly git_malformed_request_body matches rpc_malformed_request_body and says which part of the request was wrong. Signing a version 1 Grant no longer warns at runtime, since the class's deprecation already shows in type checkers and IDEs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next to GrantV2 the version 1 grant reads clearer with its version in the name. Grant stays an alias of it, so code that imports Grant keeps working. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push that updates a ref no rule allows is the grant not allowing that operation, so it reuses the existing code, and its detail names the ref and the reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only ever held GrantV1's version, which the class's own field already holds, so GrantV1 stops reading it and the constant goes when GrantV1 does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alias The rules belong to the version 2 grant, so their names say so, and the union is spelled out where it's used rather than exported as a type of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitServer checked a push only when it found push rules and a body, so a push under a GrantV2 with neither would have gone through unread. It now checks every GrantV2 push, refusing one with no body, no rule, a capability git doesn't send, or data after an empty command list. Public authorize_request refuses a GrantV2 push outright, since only GitServer reads the ref updates. Signed, compressed and malformed pushes are read to the end like refused ones, and a signed push gets its reason on side band 3. A refusal's detail shows at most 200 characters of the client's ref. bin/git-server passes a body that ends at Content-Length, as GitServer expects, and checks that the provider refuses a forged create of an existing branch, which is what keeps a create_only rule from moving one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
receive-pack reads capabilities after a NUL on every ref update, so one on a later update would have slipped past the allowlist. A push is now refused unless it looks exactly as git sends it: capabilities on the first update only, branch names git takes, object ids of the push's object format, and each ref updated once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adversarial review of the GrantV2 push path. Each test fails while the check lets its input through: capability values git wouldn't send, a refusal report and detail that grow with a client-chosen capability name, control characters from non-branch refs in the refusal detail, a long branch name that pins a worker in a rule's pattern, and push headers that reach the provider unchecked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each version's rules read on their own, and the version 1 checks stay as they were before rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an/git-grant-rules
A rule's pattern ran on a branch name of up to 64 KiB, so a backtracking pattern could pin a worker. Capability values, refs outside refs/heads/ and push headers went unchecked, so control bytes reached logs and values git never sends reached the provider. A push is refused unless every ref is a name git takes, with branch names at most 255 bytes, its capabilities are printable ASCII, and its Content-Encoding, Content-Type, Accept and Git-Protocol values, under any spelling, are ones git sends. An unknown capability's name is cut short in the reason each refused ref repeats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A sandbox holds one git grant at a time, and a grant names one operation and its repositories. A pod that fetches and pushes can't get both, and a push grant can update any ref, including the default branch, tags and deletions, since
GitServerrelays pushes unread.GrantV2holds a list of rules, each a repository, an operation and the branches it may touch.branchesorany_object, one required, so leaving out branches never reads as full access. Fetch rules listing branches stay refused untilGitServercan keep a fetch to them.branchesor full-matchbranch_patterns, andcreate_onlyallows only new branches. An update passes if any rule allows it, so acreate_onlypattern can sit next to the branch the pod already made.GitServerreads a push's ref updates before the pack and refuses the whole push unless a rule allows every one of them. Anything it doesn't recognise refuses the push too: no body, push options, a signature, or a capability git doesn't send.authorize_requestrefuses aGrantV2push outright, since onlyGitServerreads its ref updates. git reads the answer only after sending the whole push, so the server reads the pack to the end and answers with a status report, which git shows as! [remote rejected] main -> main (reason), where a 403 shows only "RPC failed".Grantis nowGrantV1, withGrantkept as an alias. It still signs and verifies, with pushes relayed unread, so signers can switch later, and it's marked deprecated for type checkers.For reviewers:
GitServerdoesn't know the default branch, so keeping it out of push rules, by name or pattern, is the signer's job.verify_grantnow returnsGrantV1 | GrantV2, so code that readsoperationorrepositoriesfrom its result needs to narrow first. Neither Seer nor Sentry does.Open question:
GrantV1goes away, rather than a minor release that keeps it working while deprecated. Reviewers, please weigh in.Example: a pod's grant once it has pushed its first branch
The pod may fetch
mainand its own branch, start new branches underseer/, and keep pushing toseer/fix-login, which it already created. The fetch rule shows the intended shape, whichGitServerrefuses until it can keep a fetch to listed branches. Until then, Seer's checkout uses a fetch rule with"any_object": truebefore any agent runs.{ "organization_id": 1, "referrer": "seer.coding_job", "rules": [ {"operation": "fetch", "repository": 42, "branches": ["main", "seer/fix-login"]}, {"operation": "push", "repository": 42, "branch_patterns": ["seer/.+"], "create_only": true}, {"operation": "push", "repository": 42, "branches": ["seer/fix-login"]} ], "version": 2, "exp": 1794243666, "iat": 1794242766, "aud": "scm-git-grant" }Under this grant, git shows each refused push like this:
Changelog Entry
GrantV2, a git grant made of repository, operation and branch rules, and keep pushes under it to their rules' branches. RenameGranttoGrantV1, keepingGrantas an alias, and deprecate it.🤖 Generated with Claude Code