From 017fcb0ea0958d08dae719b87e6c874a3497187b Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 27 May 2026 20:11:29 -0700 Subject: [PATCH] docs(adapters): document the no-remote-git contract (PAPA-441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The architectural invariant — "the local execution-workspace cwd is the only persistence boundary across runs; no adapter may depend on a git remote for cross-run state" — was previously pinned only by the ssh-fixture test added in 1acf2f107. This change adds prose so adapter authors can find it before reaching for a git remote. - packages/adapters/AUTHORING.md (new): in-repo authoring notes with the no-remote-git contract section, linking the prepare/restore round-trip and the pinning test. - packages/adapter-utils/README.md (new): describes the SSH round-trip API surface (prepareWorkspaceForSshExecution, restoreWorkspaceFromSshExecution, prepareRemoteManagedRuntime) and the pinning test. - packages/adapters/openclaw-gateway/README.md: adds a no-remote-git contract note covering the only sibling adapter README. - docs/adapters/creating-an-adapter.md: adds the public adapter-author section covering the round-trip and the workspace_finalize gating behaviour. - docs/guides/board-operator/execution-workspaces-and-runtime-services.md: adds the architecture-level "Cross-run persistence" section. All five entry points reference packages/adapter-utils/src/ssh-fixture.test.ts (the no-remote-git contract case) so future readers can see exactly what enforces the invariant. Co-Authored-By: Paperclip --- docs/adapters/creating-an-adapter.md | 17 +++++++ ...ecution-workspaces-and-runtime-services.md | 11 +++++ packages/adapter-utils/README.md | 37 ++++++++++++++ packages/adapters/AUTHORING.md | 49 +++++++++++++++++++ packages/adapters/openclaw-gateway/README.md | 13 +++++ 5 files changed, 127 insertions(+) create mode 100644 packages/adapter-utils/README.md create mode 100644 packages/adapters/AUTHORING.md diff --git a/docs/adapters/creating-an-adapter.md b/docs/adapters/creating-an-adapter.md index c81274f7559..2f4711aa150 100644 --- a/docs/adapters/creating-an-adapter.md +++ b/docs/adapters/creating-an-adapter.md @@ -249,6 +249,23 @@ Make Paperclip skills discoverable to your agent runtime without writing to the 3. **Acceptable: env var** — point a skills path env var at the repo's `skills/` directory 4. **Last resort: prompt injection** — include skill content in the prompt template +## Cross-run workspace persistence (no-remote-git contract) + +The local execution-workspace cwd is the **only** persistence boundary across runs. No adapter may depend on a git remote for cross-run state. + +The supported round-trip: + +- **Per-run, on the remote side.** `prepareWorkspaceForSshExecution` (in `packages/adapter-utils/src/ssh.ts`) git-bundles the local worktree and ships it to the run's remote dir. No `git remote` is set anywhere; the bundle is the transport. +- **End-of-run, in the adapter's `finally` block.** The adapter invokes `restoreRemoteWorkspace` (e.g. claude-local's `execute.ts`), which calls `restoreWorkspaceFromSshExecution` → `exportGitWorkspaceFromSsh` → `integrateImportedGitHead`. Remote commits made during the run land back in the local Mac worktree with no `git push` and no remote configured. + +The invariant adapters must preserve: + +- **Never `git push`** from adapter or runtime code. Operator-supplied configuration may opt in, but the default contract is no remote operations. +- **Never assume a remote exists.** The local cwd is the source of truth between runs. +- **Surface restore failures.** A failed sync-back must propagate as a run-level error, not a silent warning. The heartbeat records a `workspace_finalize` row (`succeeded`/`failed`) around `adapter.execute` so dependent issues do not wake on a stale worktree. + +The invariant is pinned by the "no-remote-git contract" case in `packages/adapter-utils/src/ssh-fixture.test.ts`: it asserts `git remote` is empty before and after the round-trip and that a remote-only commit still lands locally via restore alone. + ## Security - Treat agent output as untrusted (parse defensively, never execute) diff --git a/docs/guides/board-operator/execution-workspaces-and-runtime-services.md b/docs/guides/board-operator/execution-workspaces-and-runtime-services.md index cd8a31c8cb1..8e3d1ddfd33 100644 --- a/docs/guides/board-operator/execution-workspaces-and-runtime-services.md +++ b/docs/guides/board-operator/execution-workspaces-and-runtime-services.md @@ -64,6 +64,17 @@ Heartbeat still resolves a workspace for the run, but that is about code locatio 4. Heartbeat passes the resolved code workspace to the agent run. 5. Workspace runtime services remain manual UI-managed controls rather than automatic heartbeat-managed services. +## Cross-run persistence (no-remote-git contract) + +Code state moves between runs through the local execution-workspace cwd alone — not through a git remote. + +- Each run's prepare step bundles the local worktree to the run's remote dir over ssh, with no `git remote` configured. +- The adapter's restore step at the end of the run writes any new remote commits back into the local worktree directly. +- Adapters must never `git push` from runtime code, and must never assume a remote exists. +- A failed restore is a run-level error and records `workspace_finalize=failed` on the execution workspace, which gates dependent issue wakes until the next successful finalize. + +The invariant is enforced by the "no-remote-git contract" case in `packages/adapter-utils/src/ssh-fixture.test.ts`, which asserts a remote-only commit reaches the local worktree with no remote configured at any point. + ## Current implementation guarantees With the current implementation: diff --git a/packages/adapter-utils/README.md b/packages/adapter-utils/README.md new file mode 100644 index 00000000000..a51b3ebef9d --- /dev/null +++ b/packages/adapter-utils/README.md @@ -0,0 +1,37 @@ +# @paperclipai/adapter-utils + +Shared utilities for Paperclip adapters: process spawning, environment +injection, sandbox/SSH transport, workspace sync, and the round-trip helpers +that move code between the local execution-workspace cwd and wherever the +agent actually runs. + +For the adapter-author guide see +[`docs/adapters/creating-an-adapter.md`](../../docs/adapters/creating-an-adapter.md) +and the in-repo notes at [`packages/adapters/AUTHORING.md`](../adapters/AUTHORING.md). + +## No-remote-git contract + +The local execution-workspace cwd is the only persistence boundary across +runs. No adapter may depend on a git remote for cross-run state. + +Adapters that run the agent on a different host should use the SSH round-trip +helpers in [`src/ssh.ts`](./src/ssh.ts): + +- `prepareWorkspaceForSshExecution({ spec, localDir, remoteDir })` — bundles + the local cwd (tracked files, dirty edits, untracked additions, and the git + history needed to reconstruct it) to `remoteDir` before the run starts. Runs + with no `git remote` configured. +- `restoreWorkspaceFromSshExecution({ spec, localDir, remoteDir, ... })` — + syncs the remote cwd back into `localDir` after the run, including any new + commits the agent created. Also runs with no `git remote` configured. + +`prepareRemoteManagedRuntime` in +[`src/remote-managed-runtime.ts`](./src/remote-managed-runtime.ts) wraps both +calls for adapters that want a per-run remote workspace and an automatic +`restoreWorkspace()` finally hook. + +The invariant is pinned by the `no-remote-git contract` case in +[`src/ssh-fixture.test.ts`](./src/ssh-fixture.test.ts), which asserts that a +remote-only commit propagates to the local worktree through the +prepare → restore round-trip with no git remote configured at any point. Do +not regress that test. diff --git a/packages/adapters/AUTHORING.md b/packages/adapters/AUTHORING.md new file mode 100644 index 00000000000..27a7059591f --- /dev/null +++ b/packages/adapters/AUTHORING.md @@ -0,0 +1,49 @@ +# 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. + +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. + +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). diff --git a/packages/adapters/openclaw-gateway/README.md b/packages/adapters/openclaw-gateway/README.md index ba3edde24e4..677d31833e5 100644 --- a/packages/adapters/openclaw-gateway/README.md +++ b/packages/adapters/openclaw-gateway/README.md @@ -70,3 +70,16 @@ Structured gateway event logs use: - `[openclaw-gateway:event] run= stream= data=` for `event agent` frames UI/CLI parsers consume these lines to render transcript updates. + +## No-remote-git contract + +Like every Paperclip adapter, this one must treat the local execution-workspace +cwd as the only persistence boundary across runs — no `git push` from runtime +code, no assuming a `git remote` exists. The gateway transport here doesn't +touch the workspace directly, but if you extend the adapter to ship code to +the OpenClaw side, use the round-trip helpers in `@paperclipai/adapter-utils` +(`prepareWorkspaceForSshExecution` → `restoreWorkspaceFromSshExecution`) +rather than reaching for a git remote. See +[`packages/adapters/AUTHORING.md`](../AUTHORING.md#no-remote-git-contract-cross-run-persistence) +for the full contract and the pinning test at +[`packages/adapter-utils/src/ssh-fixture.test.ts`](../../adapter-utils/src/ssh-fixture.test.ts).