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-81

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.author column (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, then GET /repos/{o}/{r} permissions.push).
    • app-automation: an automation started it (automation_id or automation_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}/pulls on 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: 0 or retry-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).
  • 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 ready and 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, never gh pr create, to say so in the final message if the App opened it in the developer's place, and to use gh for 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 of dd133561.
  • 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-unlinked or app-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.sh green. (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 as type assertion outside the allowlisted files.
  • No Effect.runPromise/runSync/runFork outside the runtime edges (the tool runs through apps/dev/host.ts's runAgent, 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.md sentence naming a changed part is edited (service README).
  • PRIVACY.md changed. 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