feat(surface,node-sync,centaur): agent commits are authored by the human who drove them#541
Merged
Merged
Conversation
…man who drove them Commits made by agents in Atrium sandboxes were authored `Centaur AI <ai@centaur.local>` — an identity baked into the sandbox image. GitHub attributes commits by author EMAIL, so those commits linked to no GitHub user and the CLA bot reported "seems not to be a GitHub user". The contributor could not sign: signing was unreachable, not merely undone (#536). The gap was structural. Every per-user value in this architecture is injected at the HTTP boundary by the iron-proxy, keyed by a principal bound at claim time. Commit authorship is the one per-user value that lives BELOW that boundary — inside the payload — so the proxy pattern that solves everything else here cannot solve it. Env cannot carry it either: a warm pod's env is baked before the claiming principal is known, and the warm-hit path returns before execute-env is applied. Per-session file materialization is the only channel that reaches a claimed warm pod. - surface: capture GitHub's numeric account id from the /user responses we already parse (no new API calls) and resolve a session's author identity — `<id>+<login>@users.noreply.github.com` (the ID form always links; the plain form only links for pre-2017 accounts), falling back to the Atrium account email. Served on GET /api/internal/sessions/:id/git-identity, or 204 when unresolvable. - node-sync: fetch per claim and materialize ~/.config/git/atrium-identity via `git config --file` so git owns the escaping (a display name is user-controlled and is otherwise a config-injection vector). On 204, remove any stale file. - sandbox: include that path AFTER the baked [user] block, so an identity wins when present and the image identity stands when absent — making this safe to ship dark. commit-msg stamps Atrium-Session/Atrium-Harness trailers for provenance. Identity is sourced from our own records, never from the proxy's token: a user with no personal grant gets a SHARED fallback credential, so deriving authorship from `gh api user` would silently author their commits as a bot. Authorship and transport are different concerns and are kept separate here.
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.
What broke
Agent commits in Atrium sandboxes were authored
Centaur AI <ai@centaur.local>, baked into the sandbox image atcentaur/services/sandbox/Dockerfile:251-252. GitHub attributes commits by author email, so those commits linked to no GitHub user and the CLA bot reported "Centaur AI seems not to be a GitHub user".Signing was unreachable, not merely undone: the bot only records a signature when the commenter's login is in the PR's committer set, and the committer set was
{Centaur AI → login: null}. #536 (a deliberate test PR) found this. It affects every agent-authored commit — it has gone unnoticed only becausegbasinis on the CLA allowlist.Why it was structural
Every per-user value here is injected at the HTTP boundary by the iron-proxy, keyed by a principal bound at claim time. Commit authorship is the one per-user value that lives below that boundary — inside the payload — so the proxy pattern that solves everything else cannot solve it. The push was genuinely authenticated as the user while the commit object said
Centaur AI; transport and payload disagreed.Env can't carry it either: a warm pod's env is baked before the claiming principal is known (
args.rs:1866), the warm-hit path returns beforespec.envis applied (lib.rs:3527vs3540), andvalidate_execution_environmentis a single-name allowlist. Per-session file materialization is the only channel reaching a claimed warm pod.The change
idfrom the/userresponses already parsed (no new API calls); resolve<id>+<login>@users.noreply.github.com(the ID form always links; the plain form only links for pre-2017 accounts), falling back to the Atrium account email. Served onGET /api/internal/sessions/:id/git-identity, or 204 when unresolvable.~/.config/git/atrium-identityviagit config --fileso git owns the escaping (display names are user-controlled — otherwise a config-injection vector). Atomic tmp+rename, 0600. On 204, remove any stale file.include.pathafter the baked[user]block (git config is last-wins), so an identity wins when present and the image identity stands when absent.commit-msgstampsAtrium-Session/Atrium-Harnesstrailers.Safe to ship dark: with no identity file, behavior is byte-identical to today — verified, not assumed.
Design notes
Identity comes from our records, never from the proxy's token. A user with no personal grant gets a shared fallback credential (
iron-control.ts:434-438), so deriving authorship fromgh api userwould silently author their commits as a bot — worse thanCentaur AI, which at least fails loudly. Authorship and transport are different concerns.Not a profile bundle, despite the identical shape. Profile bundles are user-authored, captured, and written back — an identity round-tripping through writeback would re-introduce the clobber class fixed in #97.
Verification
Centaur AIunchanged; claimed →Allan Niemerg <1568680+aniemerg@users.noreply.github.com>+ trailers; revoked → clean fallback;--amend→ no duplicate trailers.-D warnings+ 257 lib + 16 contract + privileged overlay tests in Docker (host clippy is blind tolinux_daemon).pnpm checkgreen (lint 929 files, migrations, typecheck, 891 server tests).pnpm e2egreen — 113 passed, exit 0.[include]cannot inject a config section.Closes the hole #536 found. Does not touch #536 itself — that branch is Allan's.