vgi-forge-github 0.12.2

GitHub adapter for VGI git namespaces: per-community GitHub App auth, repo creation and bootstrap, role projection, device-flow account linking and verified webhooks.
Documentation

vgi-forge-github

GitHub adapter for Verifiable Git Infrastructure (VGI) git namespaces: implements vgi-forge's Forge for github.com and GitHub Enterprise Server, acting as one community's own GitHub App.

  • App registration through the manifest flow, with a fixed permission set — repository Administration, Contents, Variables and Checks (write), Metadata, Pull requests and Merge queues (read); organisation Members (read) and Administration (write), nothing else. Organisation Administration is there for one thing, the org ruleset that makes verify-trust a required workflow (below); an owner who declines it gets the owner-review fallback. Checks, Pull requests and Merge queues are there for one thing too, the check the bridge posts itself outside a required workflow (below); an installation without them (or without the pull_request / merge_group events — an App registered before they were in the manifest) keeps the Actions workflow (detect_bridge_checks, set_bridge_checks_ready). The code exchange refuses an App that GitHub registered with more than that, and returns the key and secrets in a type that zeroizes on drop and never prints them.

  • Auth. An RS256 App JWT (iat backdated 60 s, nine-minute lifetime) signed through AppKeySigner — in-process, or an enclave that signs but never exports. Each operation mints its own installation token, scoped to one repository and that operation's permissions, and drops it on return.

  • Binding a namespace via the App's install page and a state nonce, compared in constant time; the installation must be this App's and on the expected owner.

  • Account linking with the OAuth device flow (authorization_pending and slow_down handled); the bridge keeps the numeric id and login and revokes the user token (given the App's client secret) or, without it, discards it. Enable Device Flow on the App's settings page — the manifest format cannot.

  • Repositories: create (organisations; a personal account reports the reduced capability set and gets manual instructions), inspect (people, pending invitations, ruleset), archive, and role convergence keyed on numeric account ids, never logins.

  • Bootstrap (check-then-apply, idempotent), with a ruleset on the default branch — PR required, no force-push, no deletion, no bypass actors — and a guard so that a pull request cannot satisfy its own check. A pull_request workflow runs from the pull request's own files, so a writer could edit it (or add any job named "Verify commit trust") to pass; pinning the check to the Actions App does not help, the forged run is an Actions run too. The guard is chosen per namespace (Capabilities) and per repository (ProtectionState::check_source_guard):

    • Required workflow (organisations with org rulesets, required_workflow: true). The workflow lives in a bridge-managed, public <org>/.vgi repository, itself protected by a ruleset (PRs only, no force-push, no deletion, no bypass), and an org ruleset (VGI required workflow: workflows rule, pinned commit SHA, enforced on creation, default branch, the bridge's managed repositories by numeric id, enforcement active, no bypass actors) requires it on every managed repository. Nothing is committed to the repository itself; a repository that had the fallback guard gets its workflow, keyring, variables and status-check rule cleaned up. The DIDs are literals in the workflow (a repository variable would override an org one) and the web-flow keyring is written from it, not read from the repository. The pin only moves to a commit whose workflow the bridge has read back as its own. The org ruleset is read, changed and written under a per-namespace lock, lists exactly the managed set the bridge hands the adapter (set_managed_repositories; archived repositories drop out), and is read back afterwards — a concurrent edit that lost the repository is a retryable error.
    • Owner review (personal accounts and organisations without org rulesets, repositories with two or more owners). The workflow and keyring are committed, with the DIDs as literals and the check required and pinned to the GitHub Actions App. The CODEOWNERS GitHub reads (.github/, else root, else docs/; an adopted file keeps its rules and its place) ends with a managed block giving /.github/ — and the file itself, if it is not under .github/ — to every owner (logins looked up from numeric ids at run time), and the ruleset requires one approving review from a code owner, dismissed by later pushes and never the last pusher's own.
    • Bridge-posted check (the same namespaces, with GitHubConfig::with_bridge_checks, which the bridge sets): no workflow at all. The bridge receives pull_request / merge_group webhooks, runs verify-trust against the commits itself (never the pull request's code) and posts the "Verify commit trust" check run as the App (checks module: parse_check_trigger — pull request opened, synchronize, reopened and base edits, merge groups, this App's rerequests — default_branch, pull_request, compare_commits (every page), contents_read_token, start_check_run, finish_check_run). The caller posts only for the protected base: a check run attaches to a commit, so a success against any other base would count for the protected one too. The ruleset pins the required check to the App's own integration id, which no workflow can post as, so this closes the forged-check-run gap below. inspect counts only a check pinned to the App and reports CheckSourceGuard::BridgePosted; an old in-repo workflow is removed. The bridge becomes a merge dependency, as the registry already is.
    • Solo (the same namespaces, a repository with one owner): the check alone, no review requirement — the owner could weaken their own workflow, which is accepted since they control the repository anyway. single_owner_repos_unreviewed lets the UI say "solo: workflow edits not review-protected". A change of owner count across one ↔ two is a Drift::ReplanNeeded.
  • Availability of the required workflow is probed at bind (the result is also returned in NamespaceBinding::capabilities, for the bridge to persist) or with detect_required_workflow: GET /orgs/{org}/rulesets with an organisation Administration token. GitHub offers org rulesets on Team and Enterprise plans only, so a Free organisation (403), or an owner who declined the permission (422 on the token), falls back. GitHub documents the workflows rule for Enterprise Cloud: if creating the org ruleset is refused because of the plan (a 403/422 whose message or documentation link says so), the step returns ForgeError::CapabilityChanged for the bridge to persist and re-plan; any other refusal is returned as it is and changes nothing.

  • Drift is reported by inspect as critical (ProtectionGap::CheckSourceUnprotected) and put back by the bootstrap:

    • required workflow: the org ruleset missing, not active, with bypass actors, pinning another commit, not enforced on creation, selecting repositories by name or property, or no longer including the repository; .vgi missing, not public, unprotected, or without the pinned commit; the pin unknown to the bridge (fail closed);
    • owner review (against the projection's owners): CODEOWNERS gone, not ending with the managed block, naming other accounts than the owners, or with an error GitHub reports on the managed lines (GET /repos/{o}/{r}/codeowners/errors); the review rule weakened in any of its four parts;
    • either: GitHub Actions disabled, or an allowed-actions policy that blocks actions/checkout or the verify-trust action.

    Limits.

    • Organisation owners can still edit or delete the org ruleset, and repository admins their ruleset — inherent to GitHub; the drift monitor catches it and re-applies.
    • Outside a required workflow, repository writers are trusted not to forge check runs. Anyone who can push a branch can add a workflow there whose job is named "Verify commit trust"; its run is a GitHub Actions check run like the real one, and GitHub cannot tell them apart for the required status check. Owner review stops edits to this workflow, not that. The required workflow closes it, and so does the bridge-posted check.
    • What GitHub does with a required workflow in a repository whose Actions are disabled has not been verified against a live organisation yet; inspect reports disabled Actions as drift either way.
    • Once a branch is protected, the bridge's own rewrite of a protected file (CODEOWNERS, the in-repo workflow, a clean-up, or .vgi's workflow) is refused and lands through a pull request a human merges; .vgi's new commit is pinned once merged.
    • After a restart the bridge must hand back the pin (required_workflow_pin / set_required_workflow_pin) and the managed set, or inspect reports the pin as unverified and the step refuses to run.
  • Webhooks: X-Hub-Signature-256 verified in constant time over the raw body before parsing; repository, member, team membership, organization membership, ruleset, branch protection and installation events become ForgeEvents.

Why not octocrab

octocrab 0.54 covers rulesets and the device flow, but not what the security model needs: its installation tokens are requested with an empty body (no per-repository or per-permission scoping) and cached for reuse; its App JWT takes an in-memory jsonwebtoken key, so the key cannot live in an enclave signer; it has no Actions-variables API; and it follows redirects. It would also add hyper-rustls with a second crypto provider, tower, snafu and jsonwebtoken to a graph that already carries reqwest. The adapter instead uses a thin reqwest client (~300 lines) over the dozen endpoints it calls, with redirects off and a configurable base URL (GHES, tests).

License

Apache-2.0.