Verifiable Git Infrastructure (VGI)

Repo: github.com/OpenVTC/verifiable-git-infrastructure

VGI is the git layer of the OpenVTC stack — “the git-layer sibling of verifiable-trust-infrastructure” — and the most direct realization so far of the Know Your Developer problem the whole ecosystem was started to solve (see first-person-network). It has two halves. The first, from July 2026: a contributor signs git commits with a key held in their VTA, and a project’s CI asks a community’s Trust Registry whether that DID is authorized to sign for this repository right now — “VGI verifies, the VTC decides.” The second, from late September 2026: a community’s VTC governs whole GitHub organizations or Forgejo owners — who may create, own, maintain or sign for each repository — and VGI’s per-community bridge turns those rights into forge roles, repository settings and the required check, reporting back what changes on the forge anyway. “The VTC decides, the bridge acts, the registry answers.” The rights model itself is described in community-git-namespaces; this page covers what VGI does with it.

Why It Exists

Conventional git signing (GPG, SSH keys) binds a commit to a key, not to an identity that anyone governs. Onboarding or revoking a contributor means editing allowed-signers files in every repo, and nothing ties the key to a real, accountable person — exactly the gap a sock-puppet or a compromised account slips through. VGI moves the policy out of the repo and into the community: the signing key lives in a VTA (and since October 2026 stays there — the VTA signs, the key is never exported), the commit names its signer by DID, and authorization is a live Trust Registry query scoped to the repo. Take a contributor out of the registry and their next commit fails CI everywhere at once.

The forge layer exists for the same reason one level up. A forge’s own permission model is a flat list of accounts and roles that someone edits by hand, and nothing connects it to who is actually a member in good standing. With a bridge, the forge roles are a projection of rights the community records and governs — grants with a granter, a reason, an expiry, separation of duties and break-glass — and a right a member loses when they leave is taken off the forge by the next projection pass. Nobody edits the forge by hand; what changes there anyway is reported as drift.

It is deliberately not a generic git-signing library or a generic forge-management tool: you need a VTA to sign, a registry to verify against, and a VTC to decide.

Components

Rust workspace (edition 2024, MSRV 1.95), eight crates versioned in lockstep and all published to crates.io. The workspace is at 0.18.2 (2026-10-06), the version VTI-Eucalyptus (2026-10-07) points at; it was 0.4.5 at Cypress, 0.4.7 at VTI-Dogwood and 0.4.12 at VTI-Eucalyptus-RC-0 (coordinated-releases) — twenty-nine releases in the nineteen days between, two of them (0.6.0 and 0.8.0) breaking. Its dependency line follows the VTA stack: vta-sdk 0.63, affinidi-tdk 0.22, trql-client 0.22, affinidi-messaging-sdk ≥ 0.32 (the stated security floor is 0.27.2 with didcomm 0.15.9 — the release that binds an authcrypt message’s reported sender to the key the key agreement actually used, which the bridge’s DIDComm registry queries believe), and affinidi-did-resolver-cache-sdk ≥ 0.8.39 (0.8.37 refused non-public hosts; 0.8.39 makes a deactivated did:webvh fail to resolve). trust-tasks-rs is one 0.26.3 node across the workspace plus a deliberately contained second copy, 0.27.3 under the alias trust-tasks-git-ns, for the git-ns/bridge/event 0.4 and job 0.5 payload types that no published vta-sdk or trql-client can take yet; the manifest records the rename that collapses it.

CrateRole
vgi-coreDependency-light primitives: SSH-signature (PROTOCOL.sshsig) encoder — since 0.17 split into sshsig_message_hash / sshsig_signed_data / assemble_ssh_signature, so a signature made remotely by the VTA assembles byte-equal to a local one — git commit-object handling (including the git-exact removal of every gpgsig-* header, VGI-08), Ed25519 signing-key extraction from DID documents (assertionMethod only, VGI-02), the Signed-by-DID: trailer reader that ports git’s own trailer-block rules, the forge-qualified resource grammar (<forge-host>/<owner>[/<repo>], lowercased, whole-segment containment) and the Trust Registry referral reader. No network, keyring, or VTA. OpenVTC depends on it directly since 0.5.0.
did-git-signThe signer, installed as git’s gpg.ssh.program. Since 0.14 init only provisions: it resolves the VTA, mints a temporary admin did:key, prints the pnm contexts create … --admin-handoff or pnm acl create … --handoff command for the operator to authorize, connects over the VTA’s DIDComm mediator (TSP leg built by default; REST only when no mediator is advertised), finds persona DIDs across the chosen context and its sub-contexts (where OpenVTC keeps them), offers to mint a did:webvh into an empty context, and writes the identity, the hook directory, allowed_signers and a gitconfig/<name>.gitconfig holding every setting a repository needs — gpg.format=ssh, commit.gpgsign=true, did-git-sign.key = <DID#key-id> (the single identity selector) and core.hooksPath — into its own directory. It writes no git configuration. enable [--profile <name>] [--dir <dir>] adds one include.path line to a repository’s .git/config (or one includeIf "gitdir:…" line globally), disable removes it, and both refuse a core.hooksPath that belongs to husky / lefthook / pre-commit. Named profiles (0.13) hold several identities on one machine; profiles, use, health --profile. The hook directory holds a commit-msg hook (version 2: writes the Signed-by-DID: trailer into the final trailer block, past any --- line) plus delegating stubs for every other standard hook. Signer modes (0.17/0.18.1, did-git-sign.signer / DID_GIT_SIGN_SIGNER): auto (default) sends the VTA the commit’s SHA-512 over keys/sign-sshsig/0.1 and the VTA signs — the private key never reaches the machine — falling back to the old export with a warning when the VTA lacks the task or the credential lacks the capability; vta never falls back; export is the old behavior. The credential init provisions can then be narrowed to sign-sshsig alone, so a stolen one can sign commits until revoked but never take the key. Refuses to sign when the claimed DID and the selected key disagree; signs only in the git sshsig namespace and only for a git / git-* / ssh-keygen parent (an accident guard, not a boundary); writes every attempt to audit.log. Since 0.18.2 it keeps its credential in the same OS store OpenVTC uses (vta_sdk::keyring_init; on Linux Secret Service rather than the kernel keyring), copying an older kernel-keyring credential across on a miss.
verify-trustThe CI verifier binary, and the library the bridge’s own check runs. Depends on vgi-core, a DID resolver (affinidi-tdk, HostPolicy::PublicOnly stated explicitly), trql-client and vta-sdk with only the agent-names feature for display names. Never opens a VTA session or keyring. Since 0.5.0 it queries the registry over TSP, then DIDComm, then HTTPS — whichever the registry’s DID document advertises first — as a per-run did:peer:2 minted in memory, believing only an answer the binding authenticated as the registry’s DID; REST is optional and there is no automatic fallback. Resources are forge-qualified only since 0.8.0 (the bare owner/repo slug was removed). A platform (web-flow) signature earns exempt only on a clean merge commit whose parents each pass; everything else GitHub signs on someone’s behalf — web-UI edits, squash merges, Dependabot commits — must be DID-signed. It refuses an empty repository or range (VGI-01).
vgi-forgeThe forge-neutral seam for VTC-governed namespaces: the object-safe Forge trait and ForgeHooks, Capabilities, Resource on the vgi-core grammar, EffectiveRights (own ⇒ maintain ⇒ commit), rights → role projection that rounds onto a forge’s ladder (the configurable role map; a namespace admin gets no forge role under any map, and a map that gives maintain or commit forge-admin is refused), check-then-apply bootstrap plans, ProtectionSpec (required check, code-owner review, required_approvals), check-source guards (required workflow / owner review / bridge-posted / unreviewed), ForgeEvent and Drift. No forge I/O.
vgi-forge-githubOne community’s own GitHub App: manifest registration with a fixed permission set, RS256 App JWTs through an enclave-ready AppKeySigner, per-operation installation tokens scoped to one repository and that operation’s permissions, state-checked namespace binding, device-flow account linking, repository create / inspect / archive, role convergence keyed on numeric ids, the bootstrap (pinned workflow, committed web-flow keyring, a ruleset with no bypass actors), constant-time verified webhooks. Where the organization supports a required workflow the check comes from a workflow held outside the repository; where it does not (personal accounts, older plans) the bridge posts the check itself as the App, because a check pinned to the GitHub Actions App is forgeable from another branch. A personal account is the same adapter reporting a reduced capability set.
vgi-forge-forgejoThe Forgejo (and Gitea) adapter: a scoped bot user with token rotation (mint → persist → retire), version-probed capabilities, OAuth2 + PKCE binding and account linking, repository creation and bootstrap (fast-forward-only merges, protected workflow paths, DIDs written into the protected workflow), merge allow-list role projection, verified webhooks, a configurable runner label.
vgi-bridgeThe per-community bridge service (cargo install vgi-bridge, a release binary for Linux x86-64 and macOS, or a container): its own DID, exactly one VTC whose git-ns/bridge/jobs it takes over TSP or DIDComm through the mediator and whose results and events it signs with its authentication key, a durable job ledger (idempotent jobId, exactly one result per job, results and events retried until acknowledged), the adapters, and either a sealed redb store under a master key (self-contained mode, did:peer:2 identity) or VTA mode — identity, secrets and state in the bridge’s own trust context of the VTC’s VTA, only a context credential on the host, the local file a rebuildable cache. It runs the bridge-posted check, re-signs Dependabot pull requests with its own DID on push provenance, enforces the pull-request gate (reports pullRequestOpened, closes on the VTC’s order) and required approvals, reports the effective role map, and answers trust-task-discovery. vgi-bridge setup --vtc <DID> --vta <DID> --dir <folder> writes a runnable VTA-mode deployment — config, credential, systemd/launchd/Compose unit — in one command. Operator guide docs/BRIDGE.md; end-to-end docs/SETUP-GITHUB-VTC.md.
vgi-cliThe vgi command. vgi repo init --vtc <did> --resource <host>/<owner>/<repo> turns commit trust on for one repository where no bridge acts — a manual-mode namespace, or a personal account without the App — running the adapters’ own bootstrap plan as you, through gh on GitHub or FORGEJO_TOKEN on Forgejo, with per-repository guards (never the organization required workflow, which only a bridge sets; an organization repository with one owner is refused without --solo, since any other writer could edit the workflow in the pull request it judges), and prints the cnm git adopt command that finishes the job.

Where the DID claim lives — the Dogwood change

Until Cypress the commit’s identity claim was user.email = <DID#key-id>: the only place a commit could name its signer, since an sshsig blob carries a raw key and no identity. It worked, but it made every commit unattributable on GitHub and GitLab, which match commits to accounts by email — and it forced people in several communities to keep two settings (user.email and the key selector) in step by hand, with a silent wrong-community commit as the failure mode. Dogwood moved the claim into a Signed-by-DID: git trailer (PR #25, Robert Kwolek, 2026-08-30). The trailer sits inside the commit message, which is part of the signed payload, so it is exactly as tamper-evident as the committer header was; verify-trust reads it first and falls back to a DID committer for older commits. A commit carrying both a trailer and a DID committer that disagree is a fail-closed verdict, conflictingSignerDids. Hook version 2 (0.4.14, #72) writes the trailer into the final trailer block, past any --- line — Dependabot-style messages had been getting the trailer where neither git log --format='%(trailers)' nor verify-trust reads it, failing noSignerDid; did-git-sign health reports an OUTDATED hook, since upgrading the binary does not replace it. user.email and user.name are never written.

The GitHub Action

.github/actions/verify-trust (composite) downloads a prebuilt verify-trust (Linux x64, macOS arm64/x64, Windows x64 — no Rust toolchain on the runner; version: defaults to latest, the README example pins v0.11.0) and runs it over a commit range. Since #61 it downloads anonymously with curl — no gh, no token — so it also runs on Forgejo Actions runners, referenced by full URL; on GitHub runners the tarball’s build-provenance attestation is verified, elsewhere only its SHA-256. For every commit: it must carry a gpgsig; it must claim a signer DID (trailer, else DID committer); that DID must resolve — on a public host, and not be deactivated — and publish, under assertionMethod, the Ed25519 key that signed; the signature must verify; and the registry must hold a grant for the TRQP tuple (entity = DID, authority = VTC DID, action = git.commit.sign, resource = <forge-host>/owner/repo). Inputs of note: registry-did (the endpoint is discovered from the registry’s DID document; transport: auto|tsp|didcomm|https, registry-url as the explicit HTTPS override), vtc-did, resource (defaults to the repo, forge-qualified — it alone scopes a signer), fallback-resource (the namespace, so namespace-wide grants and admins’ implied commit rights count), resource-format (qualified; legacy is refused since 0.8.0), exempt-keyring (the committed web-flow keyring, .github/trusted-platform-keys.asc), max-signers, resolve-agent-names, json, verify-attestation. Every input reaches the script through env:, never ${{ }} in a run: body. Verdicts trusted / exempt pass; unsigned, noSignerDid, conflictingSignerDids, unresolvedSigner (including BlockedHost and a deactivated DID), unknownKey, badSignature, unauthorized, registryUnavailable, pgpRejected, platformSignedEdit, platformMergeUnverifiedParent, platformMergeAltered fail — fails closed at every layer, including registry outage. The docs insist the check be a required status check, and the bridge and vgi repo init write the workflow with the DIDs as literals rather than vars.*, because any repository admin can change a variable.

Community git namespaces, from the forge side

A namespace binds a VTC to one owner on one forge — github.com/acme, codeberg.org/acme. Inside it the community records rights held by member DIDs on forge-qualified, lowercase resources (normative: git-ns/right/grant/0.3 in the ToIP Trust Tasks; the model is community-git-namespaces). VGI’s reading of them:

RightOnWhat VGI does with it
git.ns.adminthe namespaceexercised through the VTC and the bridge (bind, adopt, reseat, grants); no forge role — making someone an organization owner is left to the community
git.repo.createthe namespacethe bridge creates the repository and runs the bootstrap plan; the creator becomes owner only when the right is an explicit grant (0.3)
git.repo.owna repositoryprojected to the forge’s top repository role (admin on GitHub by default)
git.repo.maintaina repositoryprojected to maintain (GitHub) or the merge allow-list (Forgejo): merge and triage
git.commit.signa repository or the namespaceno forge role by default — committers contribute through forks and the required check decides what lands; it is the one right verify-trust asks the registry about, so the implied commit right of every own, maintain and ns.admin is published explicitly

own implies maintain implies commit.sign; the VTC evaluates implication and publishes one TRQP authorization record per live right, under its own authority, naming impliedBy but never who granted it or why. The bridge projects only rights held in a person’s own name, per linked forge account (device flow on GitHub, OAuth2 + PKCE on Forgejo), and each repository’s desiredRoles carries one role per account — the highest of own, maintain, commit.sign — under the bridge’s role map, configurable per bridge, forge, namespace and repository and reported back to the VTC (git-ns/bridge/event 0.3 roleMapReported) so the VTC can re-project when it changes.

What the bridge adds beyond projection:

  • The check that cannot be forged. Inside an organization with required workflows, the workflow that runs verify-trust is held outside the repository and pinned; a pull request cannot satisfy its own check (F3). Where GitHub has no required workflow, the bridge fetches the commits as objects into a throwaway bare repository with a read-only token, runs verify-trust as a library with the namespace as fallback resource, and posts “Verify commit trust” as the community’s App, with the ruleset pinned to the App’s own integration id. The bridge holds a service grant of git.commit.sign on the namespace, granted by the VTC itself, because it re-signs Dependabot pull requests with its own DID on push provenance — only a branch still at the head it read, and only commits GitHub’s push delivery attributes to Dependabot.
  • The pull-request gate (event 0.4 / job 0.5, trust-tasks #723). A forge lets anyone open a pull request on a public repository; a community can say who may (members, committers, maintainers, or named roles). The bridge reports every pull request opened — number, author, actor, draft, from-fork, never title or diff — the VTC decides, and sends a closePullRequest job that posts the community’s message and closes it. It is hygiene, not the merge gate: it fails open, and nothing the required check refuses can be merged through it. A reopen by an owner or maintainer is an override.
  • Required approvals (0.18.0): required_approvals = N puts N approving reviews from accounts with write access — the rights holders the bridge itself projected — into every governed repository’s protection, with stale approvals dismissed; a rule asking for fewer is drift.
  • Drift (reported to the VTC, shown in openvtc’s Repos panel, resolved with git-ns/drift/resolve): a role added or raised on the forge, a weakened protection, a missing check, disabled Actions. The policy’s role_drift setting chooses report or enforce.
  • Manual mode: a VTC with no bridge still records rights and publishes commit rights to the registry; vgi repo init sets each repository up by hand, and roles, drift, the re-sign and the gate do not exist.

The authority chain is deliberately short: the bridge accepts a job only from the one VTC it serves, by issuer and proof before any DID is resolved; the VTC accepts results and events only from the bridge the namespace records; a bridge speaks only for the namespace it serves, and an event naming any resource outside it is refused whole.

Supply-chain posture of the repo itself

The September hardening (tracked as SEC-4045) treats VGI’s own CI as part of the trust boundary, because the Action is published for third-party use and the workflows hold crates.io and Release credentials: every uses: is pinned to a commit SHA with Dependabot (weekly, 7-day cooldown) moving pins; tokens are contents: read by default; publish.yml is split into a package job (compiles every crate tarball, no OIDC) and a publish job (tag-only, crates-io environment, --no-verify, so no dependency build script runs beside the registry credential) — resumable, publishing the forge crates and the bridge from the tag; release tarballs (now verify-trust on four targets and vgi-bridge on three) carry build-provenance attestations; a CODEOWNERS routes .github/, policy.rs, verify-trust/ and the manifests to @OpenVTC/openvtc-admins; CI scans dependencies for advisories (#107) and builds the bridge container (#108). Two regression gates guard the shape: action-selftest.yml runs the Action with shell syntax in its inputs on every PR and fails if any of it executes, and a structural test parses every workflow and asserts no ${{ }} inside a run: scalar and no unpinned uses:. The SEC-4045 findings numbered VGI-01 through VGI-08 are each closed with a regression test: VGI-01 (the platform-merge check ran in the caller’s working directory — now from the fetch, and an empty range is refused), VGI-02 (any Ed25519 key in a DID document could sign — now assertionMethod only), VGI-04 (a deactivated did:webvh kept verifying — now unresolvedSigner), VGI-08 (gpgsig-sha256 headers left in the payload).

Relationship to OpenVTC

did-git-sign was born inside openvtc — auto-configured by its setup wizard since v0.1.6, per-repo persona selection under multi-community (T8) — and the verify side landed there first too (openvtc #152 verify-trust, #154 PGP exemption keyring + Action packaging, #155 org-fallback grants + committed web-flow keyring, July 2026). VGI was then extracted with history (first commit 2026-07-18) and openvtc #159 dropped the vendored crate to consume the published one (0.4.5 at Cypress), pinning the Action for its own dogfooding CI (#160). A standing obligation is recorded in both repos: VGI’s vta-sdk line must track OpenVTC’s, or two vta-sdk copies land in the OpenVTC binary — and the obligation is discharged only by a published release, not by main. In the Eucalyptus window it bit on every vta-sdk minor from 0.43 to 0.63, which is why releases 0.4.13, 0.5.2, 0.6.0, 0.8.1, 0.9.0, 0.10.0 and 0.11.0 are each one commit long, and why OpenVTC’s own dependency PRs name the did-git-sign version they waited for.

The relationship inverted in October. OpenVTC 0.5.0’s Repos panel (#425, #475) provisions did-git-sign itself: it mints a did:key, grants it — through OpenVTC’s admin session — admin of the persona’s own sub-context narrowed to sign-sshsig (or key-export for an older VTA), proves the grant by asking the VTA for a real SSHSIG signature over a vgi-core-encoded message, stores the identity as a did-git-sign named profile with the library’s own calls, and runs did-git-sign enable --profile in each checkout — no init, no pnm grant. Three VGI releases were cut for that path in two days: 0.15.1 (init finds personas in sub-contexts), 0.18.1 (auto falls back when a key-export-only credential is refused permissionDenied, so OpenVTC-provisioned identities kept signing when the VTA was upgraded) and 0.18.2 (did-git-sign reads the same OS store OpenVTC writes to, so a Linux signer finds its credential). OpenVTC also consumes vgi-core directly (0.18.1) and ships the member-facing half of git namespaces — the Repos panel over git-ns/view, grants, drift resolve, break-glass — that the bridge’s projection is the other end of.

Recent Development

Eucalyptus — v0.4.13 → v0.18.2 — 2026-09-23 → 10-06 (#60–#149)

v0.4.12..VTI-Eucalyptus: 133 commits, 151 files, +76.9k / −1.4k, twenty-nine releases. Three weeks earlier VGI was two binaries and a GitHub Action. By the Eucalyptus tag it had a forge-adapter layer, a per-community service with its own DID that can live inside a VTA, and a signer that never sees the key. Almost all of the forge layer landed on one day, 2026-09-23 — eleven PRs — against the VTI side’s git-ns work and the ToIP git-ns/* Trust Task specs, with the rest of the window spent on the security findings the new surface raised, the VTA stack’s move to DTG Credentials v1, and the signer.

The forge layer and the bridge (09-23 → 09-28: #60–#101, v0.4.14 → v0.5.2)

  • Forge-qualified resources (#60, 09-23): --resource-format legacy|qualified, qualified deriving <forge-host>/<owner>/<repo> from FORGEJO_* or GITHUB_* — because github.com/acme and codeberg.org/acme must never be confused, and the VTC writes only the qualified form. The Action installs without gh or a token (#61), so Forgejo runners can run it.
  • vgi-forge and the GitHub adapter (#62), the Forgejo adapter and the check-source guards (#64 — RequireNamespaceWorkflow, RequireOwnerReview, CheckSourceUnprotected as a critical gap), the PR can no longer satisfy its own check (#65, F3), and the bridge (#66/#67): one per community, its own DID, the only holder of the forge credentials, jobs checked issuer → transport sender → recipient → freshness → proof and only then parsed, a durable ledger, AES-256-GCM-sealed secrets under a mounted master key, DIDComm through the mediator keeping the authcrypt sender and never flushing the inbox, axum for App registration and webhooks, and the bridge-posted check where no required workflow can. 0.4.14 (#73) is the first release of the three forge crates.
  • Platform signatures narrowed (#68, breaking): GitHub’s web-flow key signs every commit GitHub writes on someone’s behalf — web-UI edits, Contents-API commits, squash merges, Dependabot commits — so a single-parent web edit had bypassed DID signing entirely. exempt now survives only on a merge commit whose parents each pass and whose tree is exactly what git merge-tree --write-tree computes (the web conflict editor lets whoever can push the head branch write arbitrary text into a web-flow-signed merge); three new verdicts. Dependabot is deliberately not exempted — a flaw in exactly that author check once yielded web-flow-signed commits with arbitrary authors — hence the Dependabot re-sign (#70): the bridge re-signs a Dependabot PR with its own DID on push provenance. #69 pins the merge recomputation to no attributes (merge=union in a worktree had turned a conflict into a “clean” merge; git ≥ 2.40). #72 fixes the hook for --- lines.
  • 09-24/25: forge status in ext (#76); bridge/job 0.2 removeAccounts and transfers reported from inside the namespace they concern (#77); re-sign only a branch still at the head it read (#78); bridge/event 0.2 (#79); the end-to-end setup guide for a VTC-managed GitHub or Forgejo namespace (#80); vgi repo init (#81) and the namespace as the check’s fallback-resource (#82) — 0.4.15.
  • 09-25/26: configurable Forgejo runner label and the role map (#84, #87); the bridge mints a did:peer:2 the VTC can reach — a did:key advertises no service, so every job had failed noMatchingProtocol — and exports the identity for backup (#85, #86); verify-trust queries the registry over TSP or DIDComm as a per-run did:peer, REST optional, no fallback (#88) — closing the “still only compiles HTTPS” note the previous cycle of this page recorded; VTA mode (#89): identity, secrets and state in the bridge’s own trust context of the VTC’s VTA, the local store a cache, a second writer stopping the mirror fail-closed; the effective role map reported (event 0.3, #91); bridge/job 0.4 only, with a trust-task-discovery answer (#92); the bridge queries the registry over DIDComm as its own DID, and several organizations on one bridge with one private App each (#95); did-git-sign requires an authenticated client identity (#93) — 0.5.0 (vta-sdk 0.53), 0.5.1 (trql-client 0.20: cargo tree -i trust-tasks-rs one node across the whole workspace for the first time), 0.5.2 (vta-sdk 0.56, 09-28).

SEC-4045 continued, and the v1 line (09-29 → 10-02: #102–#125, v0.6.0 → v0.11.0)

  • VGI-02 / VGI-08 (#102): verify-trust had accepted a commit signed by any Ed25519 key a DID document contained — a key-agreement, recovery or custodian key the controller never authorized to sign; now assertionMethod only, and the payload strips every gpgsig-* header as git does. VGI-04 (#104, cache-sdk 0.8.39): a did:webvh whose log ends in a deactivation had resolved Ok with its last document, so a retired or compromised signer kept verifying indefinitely. VGI-01 (#106, #112): the bridge verified platform-signed merges from the fetch, not its working directory, and the git path refuses an empty repository or range. Also: VTA URL check, legacy resources, the bridge listener and image hardened (#103); advisories scanned in CI and did-git-sign’s input bounded (#107); the container builds again, in CI (#108).
  • 0.6.0 (#110, 10-01, breaking): the DTG Credentials v1 line — vta-sdk 0.58, tdk 0.21, messaging-sdk 0.31, trust-tasks 0.25, trql-client 0.21 — clearing a second vta-sdk from OpenVTC’s graph; bridge jobs require the authentication proof purpose and results and events are signed with it (#111). 0.7.0 (#114): --resource-format defaults to qualified (breaking; the Action now always passes the flag). TSP to the VTC, VTA and registry (#115): one mediator socket carrying both, inbound only with a verified sender VID, replies on the transport the request came in on; 0.7.1 (#117) resets a stranger’s TSP invite before dispatch. 0.8.0 (#119): the legacy resource format removed — “the VTC never wrote owner/repo grants; a leftover one authorizes nothing”. 0.8.1 / 0.9.0 / 0.10.0 / 0.11.0 (#121–#125, 10-01/02): vta-sdk 0.59, 0.60, 0.61 (tdk 0.22, trql-client 0.22) and 0.62, each so OpenVTC resolves one copy; 0.11.0 is the version the setup guide names as the floor for the whole of VGI.

did-git-sign grows up (10-03 → 10-06: #126–#132, #135, #141, #143, #146, #148; v0.11.1 → v0.14.0, v0.15.1, v0.17.0, v0.18.1–0.18.2)

  • The runbook and guides brought up to 0.11.0 and current VTC behavior (#126); --admin-handoff in the printed grant command (#127). init connects over the VTA’s mediator and builds TSP by default (#128, 0.12.0) — a VTA publishing no REST service had answered the hardcoded REST handshake with “is this a VTA?“. init creates a DID in an empty context (#129, 0.12.1), the normal path once the printed pnm contexts create makes an empty one. health asks the VTA itself when reached over a mediator (#130, 0.12.2). Named profiles (#131, 0.13.0) for more than one signing identity on one machine — testing as BOB and CAROL in one repository had no path.
  • init writes no git config; enable signs only where asked (#132, 0.14.0, breaking). init --global had written eight settings into the global git config, replacing the contributor’s existing signing key and taking over every repository on the machine, and the repository-local init wrote .did-git-sign.json into the working tree and provisioned a new identity on every run. Now init only provisions; everything a repository needs sits in gitconfig/<name>.gitconfig under did-git-sign’s own directory, and enable adds one include line. init --global is refused.
  • init finds persona DIDs across the context and its sub-contexts (#135, 0.15.1), where OpenVTC keeps them, pinning no primary DID to any.
  • The VTA signs; the key never leaves it (#141, 0.17.0, 10-05). did-git-sign had exported the persona’s Ed25519 seed with keys/export-secret on every commit. It now sends the commit’s SHA-512 over keys/sign-sshsig/0.1; the VTA builds the SSHSIG signed data, signs, and returns the raw signature, checked here against the public half and armored. A credential narrowed to sign-sshsig “signs commits until revoked and never takes the key”; the VTA audits each signature with the namespace and commit digest. auto falls back on permissionDenied too (#146, 0.18.1) — otherwise every credential OpenVTC had provisioned before the task existed would have stopped signing the moment its VTA was upgraded. The same credential store as OpenVTC (#148, 0.18.2): on Linux the binary had registered the kernel keyring while OpenVTC wrote the credential to Secret Service, one store over.

The bridge as a product (10-04 → 10-06: #133–#134, #137–#140, #144–#145; v0.15.0, v0.16.0, v0.18.0)

  • The PR-open gate (#133, 0.15.0; vta-sdk 0.63, trust-tasks 0.27.3 for the generated types): event 0.4 pullRequestOpened, opt-in with event_version = "0.4" because the spec forbids sending it to a VTC that has not listed it in discovery; job 0.5 closePullRequest with comment then close, declining a close of a pull request reopened after the job was issued.
  • vgi-bridge on crates.io, with release binaries (#137, 0.16.0): Linux x86-64 and both macOS targets with attestations; no Windows build, since the owner-only checks on the master key and sealed credentials are Unix-only. vgi-bridge setup (#138): one command, no root, reads the registry referral and mediator from the VTC’s DID document, pins [verify_trust] to this binary’s tag and commit, provisions the context credential the way did-git-sign init does, writes bridge.toml and a systemd --user / launchd / Compose unit, mints the bridge’s did:webvh, and prints the VTC’s [git_ns] bridges line. The App registration outcome is logged (#140).
  • required_approvals (#144, 0.18.0): N approving reviews, from write-access accounts only — the rights holders the bridge projected — with stale approvals dismissed and the last push approved by someone other than its pusher; on Forgejo the branch protection’s required_approvals. A rule asking for fewer is an ApprovalsBelow drift gap inside the existing protectionWeakened item, so no wire change.

Versions at the tag. VTI-Eucalyptus = v0.18.2 (did-git-sign, verify-trust, vgi-core, vgi-forge, vgi-forge-github, vgi-forge-forgejo, vgi-bridge, vgi-cli), on vta-sdk 0.63 / tdk 0.22 / trql-client 0.22 / trust-tasks 0.26.3 (+ 0.27.3 contained). OpenVTC 0.5.0 pins did-git-sign 0.18.2 and vgi-core 0.18.1. The README’s Action example and the setup guide’s floor say v0.11.0; the bridge’s example config pins the current release.

Post-Dogwood — v0.4.8 → v0.4.12 — 2026-09-08 → 09-17 (#37–#57)

The month after Dogwood had two threads: finishing the release that Dogwood had half-shipped, and a security pass (SEC-4045) that turned findings against the CI and the signer into permanent gates.

  • Un-sticking crates.io (#37–#40, v0.4.8, 09-10): the v0.4.7 publish had gone up partially — vgi-core 0.4.7 landed, then verify-trust was rejected because it took trql-client by git rev. The registry publishing trql-client 0.16 (then 0.17, matching vta-sdk 0.35’s trust-tasks line) let the pin go back to a plain version, and the 0.4.8 bump exists so vgi-core republishes with its trailer code rather than being skipped by the resumable loop. cargo tree -i trust-tasks-rs is one node again.
  • SEC-4045 hardening (v0.4.9, 09-11/12): the composite Action pasted ${{ inputs.* }} into its shell (#42 — an input holding $(…) ran as code; now env: + argument array, plus the git --end-of-options guard); the DID_GIT_SIGN_BYPASS_POLICY=1 escape hatch was compiled out of released binaries and the parent-process gate is now documented honestly as an accident guard, not a boundary — the key is protected by the VTA credential in the OS keyring, not by the gate (#44); actions pinned by SHA, tokens narrowed, publish split so no dependency build script runs beside the crates.io credential (#45).
  • Fail-closed, but also honest (#47, #48, v0.4.10 09-13): a property test against git’s own two trailer views found seven places where vgi-core’s trailer reader disagreed with git — never exploitable (every decision already fails closed) but a display differential, where the DID a reviewer reads in git log was not the DID verified. commit.rs now ports git’s find_trailer_block_start / find_separator / unfolding rules; the last Signed-by-DID wins and only if it is a DID. And because every DID the verifier resolves is chosen by the PR author, did:webvh resolution is now guarded by the resolver’s public-host policy (didwebvh-rs 0.7 via cache-sdk 0.8.37) — closing an SSRF primitive on self-hosted runners; an internal-host DID fails unresolvedSigner / BlockedHost without a connection being made (#48). Structural regression gates for the YAML shape (#52); CODEOWNERS; Dependabot holds rand at 0.8 until pgp supports 0.9 (#49); a hook-test ETXTBSY race serialized (#54).
  • Follow the SDK (v0.4.11 09-16, v0.4.12 09-17): vta-sdk 0.38 (drops the duplicate didwebvh-rs 0.6.1, #51) → 0.39 (#55/#56) → 0.42 with TDK 0.16 and trql-client 0.18 (#57), each a no-source-change bump so OpenVTC’s graph holds one sdk. None of the intervening vta-sdk breaking changes (generic verify_trust_task_proof_with, ML-DSA KeyType variants, #[non_exhaustive] wire bodies) touches this workspace — it reaches the VTA only through connect_auto and display names.
  • The README, crate READMEs and RUNBOOK still pinned the Action example at @v0.4.6; the self-test pinned v0.4.8; the Action’s own default is latest.

VTI-Dogwood — v0.4.6 / v0.4.7 — 2026-08-22 → 08-30 (#25, #32–#35)

Dogwood was a “silent” release across the stack, and in VGI it carried the one user-visible change of the period: the identity claim moved from user.email to a Signed-by-DID: trailer, so commits are attributable on forges and there is nothing left to keep in step by hand (see above). PR #25 (Robert Kwolek, Affinidi — “git trailer for git-host email compatibility”) merged 08-30 with a review pass from Glenn Gore that made the hook portable (GNU sed had hung git commit on Linux; a one-line message got the trailer glued to its subject where git’s parser saw nothing), made Signed-off-by: opt-in (did-git-sign.signoff — “a signing tool must not assert DCO on the user’s behalf”), had init refuse to steal core.hooksPath in both scopes, and replaced tests that grepped the hook’s source with tests that run it. did-git-sign health --did-jsonl arrived in the same PR.

  • VTI-Dogwood-RC-1 = v0.4.6 (08-22): vta-sdk 0.25 → 0.27, “the line the VTA now deploys” — the third time the standing obligation was found from OpenVTC’s cargo tree -d, so the manifest note was rewritten to say it is discharged only by a published release. It left a tolerated trust-tasks 0.9 / 0.11 duplicate confined to verify-trust.
  • VTI-Dogwood and VTI-Dogwood-R1 both point at the same commit (#35, 08-30): v0.4.7 (vta-sdk 0.32, TDK 0.10, trql-client on a git rev for the trust-tasks 0.17 line, #33) plus a resumable publish.yml (#35) that skips crates already on crates.io — written because the v0.4.7 tag had half-published. At Dogwood crates.io therefore held vgi-core 0.4.7 beside verify-trust / did-git-sign 0.4.6; that was only reconciled by v0.4.8.
  • Client identity on VTA auth (#36, 09-01/02, Robert Kwolek): the cached-token path now builds VtaClient with a ClientIdentity::did_key(...), matching the identity-bearing authentication the VTA moved to. Notably, that commit is itself carrying a Signed-by-DID: trailer — the first dogfooded one in the history.

Cypress and before — 2026-07-18 → 08-17

  • v0.1.1 — 2026-07-18 — workspace import, release pipeline, download-based Action, crates.io trusted publishing, per-crate READMEs. v0.1.2 — 07-25 — signers reported by agent name.
  • v0.4.0 — 2026-07-26 — three breaking PRs (#12–#14): derive the signer set from the commits themselves and drop the .did-signers file; discover the registry endpoint from its DID and name the VTC; write user.email as the signing DID with a sign-time guard. v0.4.1 (same day) — security hardening: atomic allowed_signers writes, host parsing before any cleartext-to-loopback decision, a triage record under docs/security/.
  • v0.4.2 → v0.4.5 — 2026-08-10 → 08-17 — dependency lockstep with the VTA stack: trust-tasks-rs 0.4, vta-sdk 0.23 / 0.24 / 0.25, trql-client 0.13 → 0.14 (collapsing three trust-tasks copies). v0.4.5 is the Cypress snapshot; the repo also carries the shared VTI-Cypress-RC-* tags.

Direction

Stay in lockstep with vta-sdk — now a daily rather than weekly obligation, since OpenVTC and VGI share both vta-sdk and vgi-core. Collapse the contained trust-tasks-rs 0.27 copy once a vta-sdk and trql-client on that line publish. The setup guide’s own “not implemented, or not yet consistent” list: namespace admins get no organization role (there is no namespace-level projectRoles job); committers get no GitHub role at all under the default map; a GitHub required workflow with Actions disabled in the target repository is unverified against a live organization. Re-attaching a namespace after losing the bridge’s store is still documented as a limit. No Windows bridge build. The README example and the setup guide still name v0.11.0 while the workspace is at 0.18.2.

See also: openvtc, community-git-namespaces, verifiable-trust-agent, verifiable-trust-community, trust-registries, first-person-network, coordinated-releases