feat(service,database,features,dev): a coding agent commits as the developer who started it, with Luke as co-author, and the service opens its pull request as them
Merged
DA
3d ago+11424+11424+01234567890123456789012345678901234567890123456789-81-81-01234567890123456789
Summary
Dean asked that Luke's coding agents' PRs be the developer's, with Luke as co-author, the way Claude Code attributes its commits. This is PR 1 of 3; the decisions are Dean's, including option 1 after Devin's security finding.
- Who an agent writes as. It is settled once, as the agent's first session starts or its sandbox opens, whichever comes first. It is kept on the new
coding_agent.authorcolumn (CODING_AGENT_AUTHOR), so the instructions and the checkout agree for the agent's life:developer: the agent was started by hand or by voice, the developer signed in with GitHub, and their own token may push to the repo (GitHubApp.userPusher:GET /user, thenGET /repos/{o}/{r}permissions.push).app-automation: an automation started it (automation_idorautomation_origin). Its owner's GitHub is never read.app-unlinked: no GitHub sign-in, or GitHub refused the token.app-no-push: the developer's token can't push to the repo.app-unread: GitHub didn't answer.
- Commits. Where the agent writes as the developer, commits are authored as their GitHub account at its noreply address. The agent is told to end each one on
Co-authored-by: luke[bot] <id+luke[bot]@users.noreply.github.com>. Otherwise commits are luke[bot]'s, as today. - The pull request is opened by the service through a new hosted tool,
open_pull_request(coder-host/pull-request.ts):- One
POST /repos/{o}/{r}/pullson the agent's own repository, made with the developer's GitHub App user token where the agent writes as them, and with the App's token otherwise. - The description always ends on "Made with Luke".
- If GitHub refuses the developer's token (401), or their account's access (403/404), the App opens the PR instead. The agent's row records why (
app-unlinked/app-no-push), and the agent is told so it can tell the developer. - If GitHub refuses the PR itself (422), rate-limits the request (403 with
x-ratelimit-remaining: 0orretry-after), or the developer's token can't be read for an outage, the call is refused to be made again, and nothing opens or moves. - The tool never settles the author, so an agent from before this column opens as the App, as its standing session and checkout are (Devin's finding).
- One
- Credentials never leave the service. The sandbox's firewall carries only the App's repo-scoped installation token, exactly as on main, so no developer token reaches the sandbox, the agent, or a teammate's terminal (Devin's security finding). Pushes, comments,
gh pr readyand CI reads all go as luke[bot], so the automations' loop guard is unchanged. - Instructions. Rule 4 names the trailer only for a developer-authored agent; otherwise it keeps today's wording. Rule 7 says to open the PR with
open_pull_request, nevergh pr create, to say so in the final message if the App opened it in the developer's place, and to useghfor everything after the opening. - Docs: PRIVACY.md (the user token is used only server-side, to open the agent's PR on that one repository) and the service README.
Known consequence (accepted by Dean): GitHub doesn't let a PR's author approve it, so a developer can't approve their own agent's PR.
Follow-ups in this stack:
- #2963: a PR an agent opened as its developer still reads as "From Luke's agents" in automation triggers.
- #2976: the Changes tab names Luke among the commit authors and says why an agent's PR was opened as the App.
Evidence
pnpm check:affected: exit 0, on top ofdd133561.pnpm exec vitest run packages/service/src/agents/coder-host/ packages/service/src/github/plus the store and migrations tests: all pass.- New tests:
pull-request.test.ts:- Opens as the developer on their token, once, against the default branch, with the footer.
- On a 401 or 403 for the developer, the App opens instead and the row records
app-unlinkedorapp-no-push. - An App-authored or unsettled agent opens on the App token and never reads the developer's.
- A 422 is relayed with nothing moved, and bad input reaches no GitHub.
checkout.test.ts:- A developer who may push gets their identity and the trailer, and the sandbox gets only the App token; no policy carries the developer token.
- A developer who may not push, an automation's agent, and a Google-only account each write as the App, with the reason on the row.
- Store: the author settles once, a recorded one replaces it, and an outsider writes nothing.
- Instructions: the trailer is told only to a developer-authored agent, and every agent opens through the tool.
- Not verified live: a real Vercel Sandbox and GitHub. A Preview run with a GitHub-linked account should show the PR as the member and the commits co-authored by luke[bot].
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files. - No
Effect.runPromise/runSync/runForkoutside the runtime edges (the tool runs throughapps/dev/host.ts'srunAgent, as the other tools do). - No new raw async primitives.
- No Effect, Stream, or Layer written as a statement and dropped.
- Test count: +8.
- Nothing hand-rolled replaced.
- Every
CLAUDE.md/AGENTS.mdsentence naming a changed part is edited (service README). -
PRIVACY.mdchanged. This is a product decision (Dean, 2026-10-15): the developer's GitHub token is used on the service alone, to open their agent's PR on that one repository, and commits name their GitHub noreply address. - Package deps match (new subpath
@luke/service/agents/coder-host/pull-request). - Barrel/door rule.
Notes
- Migration
20261015233544_coding_agent_author: one nullable column,ADD COLUMN IF NOT EXISTS, regenerated after main's newest.
š¤ Generated with Claude Code