Skip to content

fix(workspace): provision a per-agent git identity in agent checkouts (BLO-29050) - #1445

Merged
kkroo merged 6 commits into
masterfrom
fix/blo-23894-per-agent-git-identity
Aug 24, 2026
Merged

fix(workspace): provision a per-agent git identity in agent checkouts (BLO-29050)#1445
kkroo merged 6 commits into
masterfrom
fix/blo-23894-per-agent-git-identity

Conversation

@kkroo

@kkroo kkroo commented Aug 20, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the control plane that hands AI-agent runs a Git checkout in which they may commit and push.
  • Git commit attribution must identify the agent that produced the change even though every run shares the same GitHub App write credential.
  • Checkout-local user.name and user.email were not provisioned consistently, leaving existing workspaces unset or stamped as the shared App.
  • A local-config write is not a concurrency boundary: linked worktrees share the parent repository's config, so concurrent agents can overwrite one another.
  • The claude_k8s adapter also clones a fresh checkout inside its Job, so host-local Git config does not cross the pod boundary.
  • Git's per-process GIT_AUTHOR_* and GIT_COMMITTER_* variables override every config scope, remain private to one run, and are inherited by in-pod clone and commit commands.
  • This pull request keeps best-effort local provisioning for standalone checkouts, skips unsafe linked-worktree writes, and applies a final system-owned identity environment immediately before adapter dispatch.
  • The result is deterministic per-agent attribution across concurrent host, worktree, provision-command, and claude_k8s execution paths.

Linked Issues or Issue Description

  • Fixes: BLO-29050 - Agent checkouts get no per-agent Git identity, so git push commits are unattributed or App-stamped.
  • Refs BLO-21416 - Attribution is write-path dependent.
  • Refs BLO-23894 - Commit-attribution enforcement and the original checkout audit.

Related PRs found while searching for duplicates:

What Changed

  • Added ensureCheckoutGitIdentity() for idempotent, fail-open provisioning of standalone checkouts. It rewrites only unset, shared-App, or Paperclip-owned identities and preserves foreign developer identities.
  • Covered managed project workspaces, the project_primary strategy, fresh and reused Git worktrees, and persisted workspace restoration.
  • Added canonical per-agent GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, and GIT_COMMITTER_EMAIL helpers.
  • Applied that identity after agent, environment, project, routine, scratch, and K8s isolation overlays so lower-trust configuration cannot replace it.
  • Passed the same identity environment to workspace provision commands.
  • Stopped writing git config --local from linked worktrees because it mutates shared parent config; those runs use the process-level identity instead.
  • Carried the final runtime env through claude_k8s, whose Job manifest injects config.env into the container that performs the in-pod clone and commits.
  • Fixed author-name sanitization so leading dashes are removed after whitespace normalization.
  • Added focused tests for environment precedence, real commit author/committer identity, linked-worktree config isolation, sanitizer behavior, and updated workspace reuse expectations.

Verification

pnpm exec vitest run --no-file-parallelism --maxWorkers=1 \
  server/src/__tests__/git-checkout-identity.test.ts \
  server/src/__tests__/workspace-runtime.test.ts

Test Files  2 passed (2)
Tests       139 passed | 15 skipped (154)
pnpm exec vitest run --no-file-parallelism --maxWorkers=1 \
  server/src/__tests__/git-checkout-identity.test.ts

Test Files  1 passed (1)
Tests       28 passed (28)
cd vendor/paperclip-adapter-claude-k8s
pnpm exec vitest run --config vitest.config.ts src/server/job-manifest.test.ts

Test Files  1 passed (1)
Tests       189 passed (189)
  • Server TypeScript typecheck (tsc --noEmit): exit 0 with no diagnostics.
  • git diff --check: exit 0.
  • The 15 skipped workspace tests require embedded Postgres, whose initialization is unavailable on this host; all runnable cases passed.

Risks

  • The four Git identity variables intentionally apply to every Git command launched by the run. This is broader and more reliable than one checkout's local config, but means agent-authored commits in additional repositories use the same agent identity.
  • Linked worktrees no longer receive a persistent local identity write. Commands outside Paperclip's run/provision environment fall back to existing Git config; avoiding a shared mutable identity is the deliberate concurrency tradeoff.
  • User-provided values for the four identity variables are overwritten by the system-generated identity. This prevents project, routine, or environment configuration from spoofing another author.
  • Provisioning remains fail-open and no schema, migration, or API contract changes are involved.

Model Used

  • Initial implementation: Anthropic Claude Opus 5 (claude-opus-5[1m], 1M context) via Claude Code with extended reasoning, filesystem, shell, GitHub API, and Paperclip tools.
  • Review remediation and verification: OpenAI Codex based on GPT-5 with reasoning, filesystem, shell, and GitHub tooling. The exact deployment ID and context-window size are not exposed in this session.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have linked the existing Paperclip issues
  • I have run focused tests locally and they pass
  • I have added or updated tests where applicable
  • UI screenshots are not applicable because this change has no UI surface
  • Relevant behavior and tradeoffs are documented in code and in this description
  • I have considered and documented risks above
  • All Paperclip CI gates are green on the updated head
  • Ally has reviewed the updated head with no open important findings
  • I will address all reviewer comments before requesting merge

kkroo and others added 2 commits August 19, 2026 22:21
CHECKPOINT ONLY — UNVERIFIED. Do not review this as a proposal and do not ship it.

The agent that wrote this was killed mid-run by provider capacity (429) before it
could run `pnpm typecheck` or any test, and before any of the three review lenses
executed. Nothing here has been compiled, executed, or reviewed. It is committed
solely so ~185 lines of work survive a worktree prune.

What it is meant to do: paperclip creates git checkouts for agent runs but never
provisions a git author identity in them, so commits are either unattributed or
stamped with the shared `allyblockcast[bot]` App identity. A 2026-08-10 sweep of
71 checkouts under /paperclip/work found 11 App-stamped and 18 with none.

Design decided before implementation:
  - identity = `<normalizeAgentUrlKey(agent.name)>@paperclip.blockcast.net` + agent.name.
    `normalizeAgentUrlKey` (packages/shared) was chosen over `normalizeAgentNameKey`
    because the latter is only trim+lowercase, preserves spaces, and is therefore not
    a legal email local-part.
  - must NOT stamp the App email — paperclip's `policy` job treats that exact address
    on a non-merge commit as a violation (BLO-21416).
  - must be idempotent and must repair ALREADY-EXISTING checkouts, not just fresh
    clones. That is the entire defect: heartbeat.ts's provisioning helper returns
    early on several already-exists paths, and the 29 broken checkouts all take one.
  - deliberately touches neither `check-commit-author-attribution.mjs` nor its test:
    that gate has no local-part opinion, and branch `blockcast/blo-23894` is
    rewriting both files.

What still has to happen before this is worth anything:
  1. typecheck + tests actually run and pass
  2. re-derive every early return in `ensureManagedProjectWorkspace` from source and
     prove each reaches the identity step — a prior enumeration was judged incomplete
  3. confirm the workspace-runtime seams are the shared tails (3991 / 4279), not the
     four `worktree add` lines, and that the `reuseExistingWorktree` path is covered
  4. confirm no write is scoped off `repoRoot` and every write passes `{ cwd }` so it
     can never reach global/system config

Context: penstock #1414 (merged) pins why this matters downstream — a commit authored
by the `allyblockcast` User seat gets that seat's approval demoted as a self-review,
and it is the only identity that can green `review/ally-complete` on an App-authored
PR, so the PR deadlocks with no recovery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (BLO-29050)

paperclip creates git checkouts for agent runs but never provisions a git
author identity in them. A 2026-08-10 sweep of 71 checkouts under
/paperclip/work found 11 stamped with the shared `allyblockcast[bot]` App
identity and 18 with no local identity at all; AGENTS.md compensated by asking
every agent to run `git config` by hand.

`server/src/services/git-checkout-identity.ts` is the shared provisioning step,
imported by both heartbeat.ts and workspace-runtime.ts. Identity is
`<normalizeAgentUrlKey(agent.name)>@paperclip.blockcast.net` + `agent.name`,
via `deriveAgentUrlKey` so an all-punctuation name falls back to the agent id
rather than producing a bare `@paperclip.blockcast.net`.

Wiring covers the already-exists paths, which is the entire defect — a fix that
only ran after a successful clone would never reach any of the 29 broken
checkouts:
  - heartbeat.ts: `ensureManagedProjectWorkspace` is split into an inner
    resolver (4 returns + 1 throw) and a single-exit wrapper that provisions.
    A future early return in the resolver inherits provisioning; it has no way
    to return anywhere else.
  - workspace-runtime.ts: `provisionExecutionWorktree` is the shared tail all
    four worktree paths funnel through (create, attach-existing-branch
    fallback, `reuseExistingWorktree`, persisted restore), and its reuse call
    is now unconditional rather than gated on a configured provisionCommand.
    `project_primary` — the *default* strategy — bypasses that funnel entirely
    and gets its own `stampCheckoutIdentity` seam on all three of its returns.

Provisioning only rewrites addresses paperclip owns: unset, either App form, or
its own @paperclip.blockcast.net namespace. `git config --local` inside a linked
worktree resolves through the gitdir pointer to the *parent repository's*
config, so an unconditional write would retarget a developer's `user.email`
repo-wide — a worse bug than the misattribution being fixed.

It never throws (failures surface as warnings on a provisioning path), no-ops
when the path carries no git metadata (`git config` discovers upwards, so a
repo-less managed dir would stamp an ancestor), and probes with
lstat + isDirectory()||isFile() so a worktree's `.git` FILE is not mistaken for
"not a checkout".

Never writes the App email: `scripts/check-commit-author-attribution.mjs` treats
it as a violation on a non-merge commit (BLO-21416). That script and its test
are deliberately untouched — the gate has no local-part opinion, and branch
blockcast/blo-23894 is rewriting both files.

Verification:
  pnpm typecheck                                          exit 0
  vitest run server/src/__tests__/git-checkout-identity.test.ts   26 passed
  vitest run server/src/__tests__/workspace-runtime.test.ts      126 passed
  vitest run server/src/__tests__/heartbeat-workspace-session.test.ts  231 passed
  node scripts/check-no-git-push.mjs                      exit 0
  node scripts/check-test-undefined-symbols.mjs           exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 20, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-29050
🔗 Paperclip issue: BLO-23894

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 20, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-29050
🔗 Paperclip issue: BLO-23894

@kkroo

kkroo commented Aug 20, 2026

Copy link
Copy Markdown
Author

Two findings from adversarial review — please read before merging

This PR passed review with no critical findings, but two important ones survived verification and they bear on whether BLO-29050 is actually closed. Filing them here so they are not lost.

1. Under git_worktree, the identity write does not achieve per-agent attribution for concurrent runs

git config --local executed inside a linked worktree writes the PARENT repository's .git/config — the reviewer verified this empirically (created a repo, git worktree add, wrote user.email in the worktree, read it back from the parent). Since every agent worktree for a project shares one repoRoot (resolveGitOwnerRepoRoot, workspace-runtime.ts:3791), all agents share a single identity slot.

Failure scenario: agent A realizes its worktree at T0 and is stamped; agent B realizes a different worktree in the same repo at T0+30s and overwrites the shared config; A commits at T0+1h and the commit is authored as agent B. There is no repo-level mutual exclusion — acquireBranchRunClaim (heartbeat.ts:23216) keys on (repoUrl, branchName), and concurrent agents have distinct branches.

The header at git-checkout-identity.ts:33-37 accepts this on the grounds that "provisioning happens immediately before dispatch, so the last writer is the agent about to commit." That only holds for serialized runs. Paperclip runs agents concurrently.

This matters more than it first looks: the result is confidently wrong attribution rather than absent attribution, which is harder to audit than the bug being fixed. The mitigation the header names and declines — extensions.worktreeConfig + git config --worktree — is the only thing that makes the write worktree-private. Worth reconsidering.

2. claude_k8s runs are not covered, so this closes BLO-29050 only for host-executed adapters

heartbeat.ts:861-863 documents that claude_k8s pods bootstrap their own workspace in-pod via git clone --shared from the adapter's resolved cwd. git clone writes a fresh .git/config and never copies the source repo's local config, so the host-side user.email/user.name provisioned here is discarded at the pod boundary. A grep for GIT_AUTHOR_* / GIT_COMMITTER_* / any other user.email plumbing found only this new module — there is no env-based fallback carrying identity into the pod.

Failure scenario: a claude_k8s agent under run isolation clones in-pod, commits with no local identity, and git falls back to whatever global/App identity the image carries — exactly the condition AGENTS.md tells agents to hand-fix. Not a regression, and outside the declared diff scope, but the ticket should not close as fully fixed while this holds.

Also, minor

sanitizeGitAuthorName (git-checkout-identity.ts:145) strips leading dashes before .trim(), so " -foo""-foo" and "<-bad>""-bad", defeating the documented purpose. Because the email write (:280) precedes the name write (:281), such a name leaves the checkout with a correct email and absent name while reporting {status: "failed", email: null, name: null} — the caller is told nothing was provisioned when half was. Fix: .trim().replace(/^-+/, "").trim().


None of this makes the PR worse than the status quo — on every path the reviewer could reach it is a strict improvement, and finding 1's hazard is bounded by the skipped_foreign_identity guard, which is load-bearing precisely because of the shared-config behaviour. But "attribution is confidently wrong under concurrency" should be a deliberate decision, not an accident of the review threshold.

🤖 Generated with Claude Code

@allyblockcast

allyblockcast Bot commented Aug 20, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@github-actions
github-actions Bot requested a review from allyblockcast August 23, 2026 14:24
@github-actions

Copy link
Copy Markdown

@ally head 2304d22 has been awaiting review for 81.6h with no review on either surface (pulls/1445/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 2304d22.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2304d22

Critical Issues (0)

Important Issues (1)

  • [native-codex] server/src/services/workspace-runtime.ts:3779project_primary stamps the shared checkout identity immediately before handing it to a run, but ensureCheckoutGitIdentity writes git config --local at server/src/services/git-checkout-identity.ts:280-281; for linked worktrees, that local config is shared by the common repository as documented at server/src/services/git-checkout-identity.ts:27-35. Concurrent runs can therefore overwrite each other’s identity, causing a later commit from Agent A to be authored as Agent B.
    • Recommendation: serialize identity-sensitive commits for shared/project-primary checkouts, or enable worktree-specific config and write the identity there. Do not rely on “last writer is about to commit” without an enforced lock spanning stamping through commit.

Suggestions (0)

Strengths

  • Centralizes identity provisioning and covers fresh, existing, reused, and persisted checkout paths.
  • Correctly avoids rewriting foreign human identities and keeps failures fail-open with warnings.
  • Adds focused tests for App-stamped, unset, foreign, reused, and linked-worktree cases.

Recommended Action

  1. Fix the Important issue before merge.
  2. Re-run the workspace concurrency tests against the chosen serialization or worktree-specific configuration.
  3. Consider the existing focused test coverage and passing CI as strong regression protection.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 777becb

Prior Findings Dispositioned (1)

  • prior:2304d22 critical 1 — fixed — server/src/services/heartbeat.ts:23675 — the final runtime configuration overlay now applies per-agent GIT_AUTHOR_* and GIT_COMMITTER_* values immediately before adapter dispatch, so linked-worktree commits no longer depend on the shared local config that caused the prior concurrent identity race.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • Applies the identity overlay at the final dispatch boundary, after user, project, routine, scratch, and isolation environment composition.
  • Correctly treats linked worktrees as shared-config hazards and avoids mutating their common repository config.
  • Covers fresh, reused, persisted, project-primary, and managed-workspace paths with focused tests and structural call-site guards.
  • Sanitizes generated author names and keeps provisioning failures fail-open with surfaced warnings.

Recommended Action

  1. No Critical or Important code issues found; the PR is approved.
  2. CI policy currently fails on a stale suite-duration manifest (397 of 442 suites covered), which appears unrelated to this diff; regenerate or rerun the policy lane if the repository requires a green check before merge.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3328783

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • Centralizes per-agent Git identity generation and keeps local checkout provisioning fail-open.
  • Applies the four process-level Git identity variables at the final adapter boundary, after lower-trust environment overlays.
  • Correctly avoids shared local-config writes for linked worktrees while passing the identity into provision commands and claude_k8s Job containers.
  • Adds focused coverage for App-stamped, unset, foreign, reused, persisted, linked-worktree, sanitizer, precedence, and real commit attribution paths.

Recommended Action

  1. No Critical or Important code issues found; approve.
  2. Allow the remaining CI checks to complete.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3328783

Prior Findings Dispositioned (1)

  • prior:2304d22 important 1 — fixed — server/src/services/workspace-runtime.ts:3785 — project_primary now applies the per-agent Git identity at the direct-checkout return, while linked worktrees skip shared local-config writes and receive the authoritative per-run identity overlay at dispatch in server/src/services/heartbeat.ts:24550.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • Centralizes per-agent Git identity generation and keeps local checkout provisioning fail-open.
  • Applies all four process-level Git identity variables after lower-trust environment overlays.
  • Correctly avoids shared local-config writes for linked worktrees while passing identity into provision commands and adapter execution.
  • Adds focused coverage for direct, reused, persisted, linked-worktree, sanitizer, precedence, and commit-attribution paths.

Recommended Action

  1. No Critical or Important code issues found; approve.
  2. Allow the remaining CI checks to complete.

@kkroo
kkroo added this pull request to the merge queue Aug 23, 2026
Merged via the queue into master with commit 9bf533f Aug 24, 2026
21 checks passed
@kkroo
kkroo deleted the fix/blo-23894-per-agent-git-identity branch August 28, 2026 23:30
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