Files
PaperClipAI/doc/execution-github-identity.md
T
Devin FoleyandPaperclip 5442f2d869 fix: repair managed Git launchers in sandbox projects (#13588)
## Thinking Path

> - Paperclip runs agents in local and remote execution environments.
> - Managed GitHub launchers select credentials for each Git operation.
> - Remote launchers are written inside the project checkout as
extensionless CommonJS scripts.
> - An ES module project makes Node interpret those launchers as ESM, so
they crash before credential resolution.
> - When the launcher can start, empty identity variables also override
valid repository and command-line Git configuration.
> - This change gives the launchers their own CommonJS scope and clears
empty identity overrides while preserving managed credential isolation.

## Linked Issues or Issue Description

**What happened?**

In a repository with `"type": "module"`, the managed `git` and `gh`
launchers fail immediately with `ReferenceError: require is not defined
in ES module scope`. The launchers use CommonJS but inherited the
enclosing project's module type.

Sandbox agents also report empty `GIT_AUTHOR_NAME` and
`GIT_COMMITTER_NAME` variables and try to unset them for each command.
With no managed identity available, the Git launcher recreated those
empty values. `git commit` failed with `fatal: empty ident name`, even
with explicit `user.name` and `user.email` configuration.

**Expected behavior**

Managed `git` and `gh` start in both ES module and CommonJS projects.
Local commits with an explicitly configured identity work without manual
environment cleanup. Managed credentials and captured identity continue
to take precedence. Missing identity does not silently select the host
user's details.

**Steps to reproduce**

1. Create a sandbox project whose `package.json` contains `"type":
"module"`.
2. Stage the managed GitHub launchers and run `git --version` or `gh
--version`. Before this fix, the launcher fails at its first
`require()`.
3. In a CommonJS project with no available managed identity, configure
repository `user.name` and `user.email`, or supply them with `git -c`.
4. Run `git commit --allow-empty -m test`. Before this fix, both
identity configuration forms fail with empty identity.

**Paperclip version or commit**

Reproduced from master commit `165b10bd9`.

**Deployment mode**

Sandbox execution. The shared launcher is also used for managed local
and SSH execution.

Related work: #13094 introduced the local-operation fallback; #13053
changes launcher discovery on Windows. Neither fixes empty identity
overrides. Related identity work in #8945 and #8946 configures worktree
authorship and does not remove these environment overrides.

## What Changed

- Stage `package.json` with `"type": "commonjs"` in the launcher
directory before the Node scripts. Keep the project's package
configuration unchanged.
- Leave inherited author and committer variables unset in the real Git
process. When credentials are absent, require explicit Git identity
configuration with `user.useConfigOnly`.
- Clear empty identity merge overrides in staged shell profiles after
environment merging. Preserve nonempty captured identity values.
- Exercise real Git commits with repository and command-line identity,
broker failures, and managed-user switching. Verify startup in ES module
and CommonJS projects, shell cleanup, and captured identity
preservation.
- Document launcher module scope and local identity behavior in the
execution GitHub identity contract.

## Verification

- Confirmed both new local-commit regression cases fail before the fix
with `fatal: empty ident name`.
- Confirmed the new ES module project regression fails before the fix
with `require is not defined in ES module scope`.
- Focused launcher and shell tests: 28 passed.
- `pnpm exec vitest run --project @paperclipai/adapter-utils --exclude
'**/dist/**'`: 1,216 passed, 11 skipped across 58 files.
- `pnpm --filter @paperclipai/adapter-utils typecheck` and `pnpm
--filter @paperclipai/adapter-utils build`: passed.
- `pnpm -r typecheck` and `pnpm build`: attempted; both stop in the
unchanged native runner because Cargo is not installed on this machine.
- Full `pnpm test:run`: started locally; stopped the duplicate run after
the complete CI suite passed. No local full-suite success is claimed.
- CI on `99ea8050e`: all 53 checks passed (2 skipped), including full
tests, typecheck, build, native runner checks, and browser checks.
- Greptile reviewed `99ea8050e`: 5/5 with no findings or unresolved
comments. GitHub reports no merge conflicts with master.
- No live sandbox or GitHub push probe performed.

## Risks

- The new package scope is confined to the run-specific launcher
directory. It does not change the project's module type, launcher names,
or credential selection.
- Without a managed identity, an explicitly configured repository author
can now create local commits. GitHub access remains subject to the
existing credential broker. Global/system Git configuration, ambient
credentials, and SSH identity remain isolated.
- Managed identity still wins over repository settings. Missing local
identity still fails instead of guessing host details.
- New or resumed executions must stage the updated launcher and shell
profiles. Existing processes retain their prior files and environment
until refreshed. No database migration or sandbox image rebuild is
required.
- Revert this change to restore the prior behavior.

## Model Used

- OpenAI GPT-6 via Codex, with code inspection, implementation, and
local test execution. The hosted model variant and context window were
not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (the affected adapter-utils
package)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-17 18:40:22 -07:00

12 KiB
Raw Blame History

GitHub identity during agent execution

Shared agents use the GitHub connection of the person whose accepted instructions they are executing. Task ownership remains unchanged. GitHub is optional: ordinary work can start without a connection; a private checkout, authenticated API call, or commit can fail when that operation needs credentials or author metadata.

Accepted instructions and continuations

run_identity_contexts records ordered revisions, stored message authors, originating causes, parent contexts, acceptance state, and redacted GitHub outcomes. heartbeat_runs.active_identity_context_id selects the current revision. Existing historical runs are not backfilled with inferred authorship.

Human messages use their stored authenticated author. Queued messages retain their delivery order. Accepting steering reserves a pending revision before delivery, then activates it after the provider acknowledgement. Rejected delivery leaves the prior revision active. An uncertain acknowledgement holds new credential acquisition; a later acknowledgement or its authenticated native event receipt reconciles the reservation. Replays cannot reactivate an older revision. Activation locks the task before the run, matching task and queue mutations so concurrent status changes cannot deadlock identity initialization.

Delegated work and interactions persist their originating context. Retries retain the originating run's active context. Background continuations carry their source run; dependency wakes use the task's continuation context, independently of its owner. Scheduled and webhook routines use the routine's responsible person; manual invocations use the caller, and edits preserve the routine's responsible person.

Managed GitHub operations

Executions with managed GitHub configured receive token-free git and gh launchers and a run-scoped capability. Each launcher invocation requests the active context through the authenticated runtime transport and resolves one eligible credential at operation start. A gh command's child Git processes inherit that command's captured identity. Later steering does not change already-started operations. When a subsequent run resumes a settled native conversation, the controller starts a fresh provider process with that run’s capability and rebinds its token-free launcher paths. The durable conversation and protected provider settings remain unchanged. Local and remote durable runners complete their bounded suspension before the controller releases the session for the next run, so a queued continuation cannot race unfinished cleanup.

The broker endpoint rejects browser origins and session cookies, validates a distinct signed runtime scope, and rechecks the company, agent, and live run. Sandboxes relay the capability through the existing authenticated callback bridge. Tokens are returned only to the managed command process. They are not persisted in identity history or injected into the long-lived provider process.

Low-trust executions cannot receive raw GitHub credentials, including dedicated agent tokens. The broker rechecks current agent, project, task, and retained run policies before credential resolution. An external guest's internal sponsor is accountable for the task, but does not authorize using the sponsor's account. Read-only access must use separately authorized tools that enforce that boundary.

Server-side Git operations and GitHub gateway calls follow the same selection rules. Approved gateway operations retain their signed originating identity. Connection audience and tool policies continue to apply to the selected person's connection. Native catalogs remain stable across identity changes, but each invocation resolves the selected grant again. Personal OAuth secret declarations survive connection pauses and metadata edits.

Managed commands disable ambient Git credential helpers, Git global/system configuration, host GitHub CLI configuration, and host SSH identity access. Per-operation GitHub CLI configuration is isolated in a writable configuration directory beneath the managed launcher directory. Missing credentials clear previous author and token values; no teammate, standing delegation, host token, or company-default user's account is substituted. Anonymous/local operations remain available where supported.

Without a managed identity, local commits can use an explicitly configured repository identity or git -c user.name=... -c user.email=.... The launcher leaves author/committer environment variables unset and requires configured identity instead of guessing the host user's details. Managed shell profiles remove empty identity overrides after environment merging, so agents do not need to unset them per command. A captured managed identity still takes precedence over repository configuration.

Remote launchers prepend their directory to the execution target's effective PATH. An explicit remote PATH override is preserved; otherwise Paperclip reads the provider's environment before staging the launcher shell files. This keeps legacy NVM and user-local agent installations available alongside newer images with system-wide CLIs. The generated shell files retain that combined path with managed git and gh first. The launcher directory has its own CommonJS package scope, so the extensionless Node launchers work inside repositories that declare "type": "module" without changing the project's package configuration. Sandbox command checks use the same sanitized environment as execution, so a CLI visible only in the provider's default environment cannot pass the launch check. Failed path discovery stops startup instead of silently falling back to a minimal path.

Scripts that previously read a persistent GH_TOKEN must use managed git, gh, or GitHub gateway tools. Managed execution skips legacy GitHub token bindings in agent, environment, project, and routine configuration before secret preflight. Configure personal or dedicated access through the GitHub connection instead. Directly invoking an unmanaged executable or retaining a token obtained during an earlier invocation is outside the managed invocation contract.

Legacy hosts and networking

When no managed GitHub connection is installed for an agent, standard-trust local and SSH executions retain that execution host's existing Git and GitHub CLI credentials, configuration, credential helpers, and SSH agent. Paperclip does not import controller credentials into an SSH target. Sandbox, plugin, and low-trust executions do not receive this compatibility fallback. Once a managed connection is configured, unavailable or revoked access never falls back to host authentication. Switching modes replaces the provider process while preserving the settled conversation.

Runner network access is independent of GitHub credentials. The controller enables networking for standard-trust execution. Low-trust runs and runners without a controller network decision retain a restricted default. An operator can set PAPERCLIP_RUNNER_NETWORK_ACCESS=disabled to restrict normal execution; user environment bindings cannot override that decision. Outer execution- environment network restrictions still apply. The controller projects the assigned worktree's Git metadata paths so Git can operate without exposing unrelated workspace or provider state. The sandbox also receives read access to validated provider executable resources and the target host's DNS and CA files, including resolver symlink targets outside /etc. Provider credential directories remain isolated.

A managed broker outage does not prevent local Git operations. Launchers clear credentials and run the command without authentication, with a redacted error category identifying configuration setup, transport, or capability rejection. They do not retain a previous operation's token or replay a GitHub operation.

Healthy eligible grants for the same stable GitHub account ID take precedence over duplicates with failed health checks. Credential acquisition can retry once against another grant for that same principal and account, before any GitHub operation begins. Run identity diagnostics include the selected connection and grant IDs, without credential values. Access-refresh conflicts retry once against current state and never turn a concurrency conflict into a reconnect requirement.

Dedicated accounts and diagnostics

An explicit dedicated-agent grant overrides personal selection. Revoked, disabled, unavailable, or ambiguous dedicated grants do not fall back to a person's account. Removing the dedicated configuration restores personal selection.

Connection setup and permissions display: “This agent uses this GitHub account for everyone's work, instead of the person giving instructions.”

The GitHub permissions page shows repositories across all connected accounts in one scrollable list. It has no account filter or repository search. Repository icons, private-repository indicators, refresh, and GitHub configuration links remain available. The “Add More Repos on GitHub” button opens GitHub’s app installation and repository-access setup.

Multiple eligible connections for the same GitHub account are treated as one identity, using GitHub's stable account ID rather than its login. The resolver selects an available grant, preferring the newest authorization with a stable ID tie-breaker. Duplicate eligibility includes an active credential record with the correct owner, the OAuth access-token reference, and repository access metadata. It keeps that grant's credential and connection policy together; it does not combine repository access or bypass connection audiences. Distinct accounts or unidentifiable duplicate grants remain ambiguous. Managed commands print the redacted reason when GitHub access is unavailable, while unrelated local operations can still proceed without credentials.

Run details show identity revisions and redacted GitHub results: responsible person, selected login when available, personal/dedicated source, and an unavailable reason. Tasks do not receive an additional identity indicator or takeover action.

Deployment and verification

Deploy the schema, server broker, launcher staging, and runtime environment contract together. Already-running processes retain their original environment; only newly dispatched processes receive the broker contract. Run-scoped capabilities remain valid only while their bound run is active.

Focused coverage lives in run-identity.test.ts, github-operation-credentials.test.ts, and github-launcher.test.ts, alongside the native steering, gateway, routine, and callback-bridge suites. Live acceptance additionally requires two authenticated Paperclip users, two authorized GitHub accounts, and a designated disposable repository for push verification. Local commit metadata and mocked API results do not replace that live push test.

Release procedure

  1. Back up the instance database using the normal deployment procedure.
  2. Build and deploy one revision containing migrations 0240–0245, the server broker, managed launchers, and the runner artifacts. Run the standard pending-migration check before admitting new runs. These additive migrations are safe to replay and do not infer authorship for historical runs.
  3. Let pre-rollout executions finish with their original runtime contract. New executions must have an active identity context and the managed launcher capability before provider startup.
  4. Check one ordinary run without a GitHub connection, then an authenticated GitHub operation. Inspect the run details for the responsible person and credential outcome. Verify a queued continuation on the same conversation.
  5. If rollback is needed, finish or explicitly stop executions using the new broker before removing its endpoint. Keep the additive schema and identity history. Do not drop identity columns or tables to roll back application code.

Remote acceptance uses the existing paid runner workflow with a narrow selection. Run it against the same immutable revision as the release; a successful local test does not qualify a different remote runner artifact.

Identity history survives deletion of the originating agent or run, so surviving subtasks and approvals retain their responsible person. The company foreign key and company-deletion service remove these company-scoped records when their company is deleted. Completed runs remove their managed launcher files before releasing a remote environment; same-run recovery retains them until the terminal boundary. Cleanup failures are logged and do not change the run result.