packages/adapters/AUTHORING.md
In-repo notes for adapter authors. The user-facing guide lives at
docs/adapters/creating-an-adapter.md;
this file holds invariants that are easy to violate from inside the adapter
package itself.
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:
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.@paperclipai/adapter-utils:
prepareWorkspaceForSshExecution bundles the
local cwd to the remote dir before the run, and
restoreWorkspaceFromSshExecution syncs
remote-side changes (including new git commits) back into the local cwd
after the run. Both run with no git remote configured.execute() returns.workspace_finalize=failed on the execution workspace, which gates
dependent issue wakes until the next successful finalize. Do not swallow
restore errors.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,
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 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.