mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-09 16:35:27 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - When an agent runs on a different host (sandbox or SSH), the adapter transport copies the local git execution workspace to that host and syncs changes back after the run > - The transport materializes the remote copy with `git init` plus a depth-1 or bundle fetch, so the copy has no `origin` remote and its head reads as a parentless snapshot commit > - An agent asked to publish its branch (push it, open a pull request) sees "no remote, root snapshot" and must hand the publish step back to a human operator, even when the branch base is a commit the upstream remote already holds > - This pull request carries the workspace's `origin` URL (credential-scrubbed) onto the transported copy as metadata > - The benefit is that branches produced in transported workspaces stay publishable by any actor with credentials, while the transport itself still never fetches or pushes ## Linked Issues or Issue Description No public issue exists. Description follows the enhancement template: **What existing behavior does this improve?** The workspace transport in `@paperclipai/adapter-utils` already copies a git workspace to the execution host and back. This change improves the fidelity of that copy: the transported repo keeps the workspace's `origin` remote instead of losing it. **Subsystem affected** Adapter utilities — the sandbox transport (`withShallowGitWorkspaceClone` in `packages/adapter-utils/src/git-workspace-sync.ts`) and the SSH transport (`importGitWorkspaceToSsh` in `packages/adapter-utils/src/ssh.ts`). **Current behavior** The transported copy is built with `git init` plus a depth-1 (sandbox) or bundle (SSH) fetch. It has no remotes. `git remote -v` is empty and the head commit reads as a root snapshot with no visible ancestry. Agents and operators inside the execution host cannot fetch real ancestry or push a branch, even when the branch base is a commit the upstream remote already holds. **Proposed behavior** The transport reads the source workspace's `origin` URL, scrubs credentials from it, and configures it on the transported copy. The sandbox path adds the remote to the fresh clone. The SSH path sets or adds the remote in the remote setup script, which also covers reused workspace directories. A workspace with no `origin` transports exactly as before. **Reason and benefit** A branch committed in a transported workspace becomes publishable in place: the shallow boundary commit already exists on the remote, so a push pack closes without full local ancestry (a new test locks in this property). Fetching real ancestry also becomes possible for whoever holds credentials. Without this, agents must describe their change in a handoff document and a human must reconstruct the branch by hand. **Breaking changes** None. The URL copy is best-effort and metadata-only. The transport never fetches from or pushes to the remote. The no-remote-git contract holds: sync-back through the local cwd stays the only cross-run persistence path, and `packages/adapters/AUTHORING.md` gains a paragraph that makes the carried-remote nuance explicit. ## What Changed - `packages/adapter-utils/src/git-workspace-sync.ts`: new `sanitizeGitRemoteUrl` (strips http(s) userinfo, where tokens can be embedded; scp-like/ssh forms and filesystem paths pass through) and `readSanitizedOriginRemoteUrl`; `withShallowGitWorkspaceClone` configures the scrubbed `origin` on the fresh clone, best-effort. - `packages/adapter-utils/src/ssh.ts`: `importGitWorkspaceToSsh` sets or adds the scrubbed `origin` in the remote setup script, non-fatal under `set -e`. - `packages/adapter-utils/src/git-workspace-sync.test.ts`: four new integration cases (remote copied, credentials scrubbed, no-origin unchanged, push from the shallow clone to an origin that holds the base commit) plus `sanitizeGitRemoteUrl` unit tests. - `packages/adapters/AUTHORING.md`: documents that a transported copy may carry a credential-scrubbed `origin` as metadata, and why this does not weaken the no-remote-git contract. ## Verification - `npx vitest run packages/adapter-utils/src/git-workspace-sync.test.ts` — 12/12 pass (4 new integration cases + sanitizer unit tests). - `npx vitest run packages/adapter-utils/src/sandbox-managed-runtime.test.ts` — 24/24 pass. - `npx vitest run packages/adapter-utils/src/ssh-fixture.test.ts` — 16/16 pass, including the `no-remote-git contract` case (a workspace without `origin` still round-trips with no remote introduced at any point). - `node scripts/check-no-git-push.mjs` — passes; this change adds no push or fetch to adapter/runtime code. - `pnpm typecheck` in `packages/adapter-utils` — clean. ## Risks - Low risk. The change is additive metadata on the transported copy only; failure to record the remote never fails the transport. - Credential exposure is the real hazard and is handled: http(s) userinfo is stripped before the URL leaves the host. Non-http forms (scp-like, `ssh://`) carry no secret in the URL and pass through. - A reused SSH workspace whose project `origin` changed now gets the current URL via `set-url` instead of keeping a stale one. ## Model Used Claude Fable 5 (`claude-fable-5`), Anthropic — extended thinking, agentic tool use via Claude Code CLI. ## 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 `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [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
71 lines
4.1 KiB
Markdown
71 lines
4.1 KiB
Markdown
# Adapter Authoring Notes
|
|
|
|
In-repo notes for adapter authors. The user-facing guide lives at
|
|
[`docs/adapters/creating-an-adapter.md`](../../docs/adapters/creating-an-adapter.md);
|
|
this file holds invariants that are easy to violate from inside the adapter
|
|
package itself.
|
|
|
|
## No-remote-git contract (cross-run persistence)
|
|
|
|
The local execution-workspace cwd is the only persistence boundary across
|
|
runs. No adapter may depend on a git remote for cross-run state.
|
|
|
|
Why: Paperclip resolves a local execution workspace (a worktree) for each
|
|
heartbeat. Code state is carried forward by syncing that local cwd to wherever
|
|
the agent actually runs — over ssh, into a sandbox, into a managed runtime —
|
|
and then syncing changes back when the run finishes. Treating a `git remote`
|
|
as the source of truth (`git push` from inside the agent, fetch on the next
|
|
wake) breaks dependent issues that are gated on the local worktree being
|
|
caught up, and breaks isolated execution workspaces that have no remote
|
|
configured at all.
|
|
|
|
How to apply:
|
|
|
|
- Never `git push` from adapter runtime code. Never assume the local worktree
|
|
has any `git remote` configured. If you need data from the previous run,
|
|
read it from the local cwd Paperclip handed you.
|
|
- If your adapter runs the agent on a different host (ssh, sandbox, remote
|
|
container), use the round-trip helpers in `@paperclipai/adapter-utils`:
|
|
[`prepareWorkspaceForSshExecution`](../adapter-utils/src/ssh.ts) bundles the
|
|
local cwd to the remote dir before the run, and
|
|
[`restoreWorkspaceFromSshExecution`](../adapter-utils/src/ssh.ts) syncs
|
|
remote-side changes (including new git commits) back into the local cwd
|
|
after the run. Both run with no `git remote` configured.
|
|
- If your adapter runs the agent locally, you can read and write the cwd
|
|
directly — same invariant applies: changes that future runs need must live
|
|
in the local cwd by the time `execute()` returns.
|
|
- A failed sync-back is a run-level error. The heartbeat records
|
|
`workspace_finalize=failed` on the execution workspace, which gates
|
|
dependent issue wakes until the next successful finalize. Do not swallow
|
|
restore errors.
|
|
- A transported workspace copy *may* carry the local workspace's `origin`
|
|
remote URL so that branches in the copy stay publishable by the agent or an
|
|
operator who holds credentials — the transport helpers copy the URL as
|
|
metadata only. The copy is allowlist-based and fails closed
|
|
(`sanitizeGitRemoteUrl`): http(s) URLs are stripped of userinfo, query, and
|
|
fragment; `ssh:`/`git:` scheme URLs are stripped of password and query;
|
|
scp-like `user@host:path` passes through (the syntax has no password slot);
|
|
every other shape — filesystem paths, unknown schemes — is dropped rather
|
|
than risk persisting an embedded secret. This does not weaken the contract:
|
|
sync-back through the local cwd remains the only cross-run persistence
|
|
path, the helpers never fetch from or push to that remote, and a workspace
|
|
without an `origin` transports exactly as before.
|
|
|
|
The invariant is pinned by the `no-remote-git contract` case in
|
|
[`packages/adapter-utils/src/ssh-fixture.test.ts`](../adapter-utils/src/ssh-fixture.test.ts),
|
|
which asserts that a remote-only commit propagates to the local worktree
|
|
through `prepareWorkspaceForSshExecution` → `restoreWorkspaceFromSshExecution`
|
|
with no git remote configured at any point.
|
|
|
|
A static check enforces the rule before runtime ever sees it:
|
|
[`scripts/check-no-git-push.mjs`](../../scripts/check-no-git-push.mjs) scans
|
|
adapter and runtime source (`packages/adapters/`, `packages/adapter-utils/`,
|
|
`server/src/`, `cli/src/`) and fails the `policy` CI job if any unapproved
|
|
`git push` invocation is added. If you are building an operator-configured
|
|
path that legitimately must push, add a
|
|
`// paperclip:allow-git-push: <reason>` comment on the line (or the line
|
|
above) so the opt-in shows up in code review.
|
|
|
|
For the architecture-level write-up of cross-run persistence, see
|
|
[`docs/guides/board-operator/execution-workspaces-and-runtime-services.md`](../../docs/guides/board-operator/execution-workspaces-and-runtime-services.md#cross-run-persistence-no-remote-git-contract).
|