Skip to content

feat(surface,node-sync,centaur): agent commits are authored by the human who drove them#541

Merged
gbasin merged 2 commits into
masterfrom
int-20260716112836-f551ffbf
Jul 16, 2026
Merged

feat(surface,node-sync,centaur): agent commits are authored by the human who drove them#541
gbasin merged 2 commits into
masterfrom
int-20260716112836-f551ffbf

Conversation

@gbasin

@gbasin gbasin commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

What broke

Agent commits in Atrium sandboxes were authored Centaur AI <ai@centaur.local>, baked into the sandbox image at centaur/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 because gbasin is 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 before spec.env is applied (lib.rs:3527 vs 3540), and validate_execution_environment is a single-name allowlist. Per-session file materialization is the only channel reaching a claimed warm pod.

The change

  • surface — capture GitHub's numeric id from the /user responses 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 on GET /api/internal/sessions/:id/git-identity, or 204 when unresolvable.
  • node-sync — fetch per claim, materialize ~/.config/git/atrium-identity via git config --file so git owns the escaping (display names are user-controlled — otherwise a config-injection vector). Atomic tmp+rename, 0600. On 204, remove any stale file.
  • sandboxinclude.path after the baked [user] block (git config is last-wins), so an identity wins when present and the image identity stands when absent. commit-msg stamps Atrium-Session/Atrium-Harness trailers.

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 from gh api user would silently author their commits as a bot — worse than Centaur 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

  • Sandbox simulation with the real hook and real image gitconfig, proving the three parts compose: unclaimed → Centaur AI unchanged; claimed → Allan Niemerg <1568680+aniemerg@users.noreply.github.com> + trailers; revoked → clean fallback; --amend → no duplicate trailers.
  • Linux clippy -D warnings + 257 lib + 16 contract + privileged overlay tests in Docker (host clippy is blind to linux_daemon).
  • pnpm check green (lint 929 files, migrations, typecheck, 891 server tests). pnpm e2e green — 113 passed, exit 0.
  • Injection proven neutralized: a hostile display name with embedded newlines + [include] cannot inject a config section.

Closes the hole #536 found. Does not touch #536 itself — that branch is Allan's.

gbasin added 2 commits July 16, 2026 11:30
…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.
@gbasin gbasin added this pull request to the merge queue Jul 16, 2026
Merged via the queue into master with commit 6b057fb Jul 16, 2026
16 checks passed
@gbasin gbasin deleted the int-20260716112836-f551ffbf branch July 16, 2026 17:42
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.

1 participant