Summary
Furrow mirroring is one-way today: a run publishes its workspace and a laptop clones it. There is no way to go the other direction — hand a local directory to a remote node and have build / implement_issue run against it.
That is the missing half of "run my code on the fleet". Right now a remote node can only work on code it can git clone, so anything uncommitted, unpushed, private-to-my-laptop, or simply not a git repo cannot be given to a cloud SWE node at all.
Everything needed to close this already exists and is deployed. The gap is one reasoner and one token path.
What exists today
Outbound mirroring works end-to-end. Verified against a live Railway deployment: get_workspace_handle → furrow clone over furrowd → 349 MB of a run's real working tree (sparse checkout, untracked files and all) materialized on a laptop.
go/internal/furrow/manager.go — Attach, Publish, Handle, Detach, Sweep. Every entry point is outbound. Attach(runID, buildID, repoPath) takes a workspace that already exists on the node and starts mirroring it.
go/internal/furrow/types.go — Handle{Version, Remote, Namespace, Key, Token, RepoPath}, HandleVersion = 1. The package doc states the contract explicitly: "The contract with the rest of SWE-AF is deliberately one-way."
go/internal/node/register.go — get_workspace_handle, registered only when SWE_FURROW_ENABLED is truthy.
go/internal/orch/build.go:500 — the handle is attached to the build result.
go/cmd/furrowd/main.go — the server side. AUTH <token> <namespace>, registry-row lookup with a constant-time token compare, validNamespace charset/length rules, then furrow __remote <namespace> as a child.
go/cmd/furrow-dial/main.go — the FURROW_SSH_COMMAND shim: TLS dial, AUTH, stdio pipe. Needed because furrowd speaks TLS, not SSH.
- Client side, already shipped in the pinned
furrow release: clone, pair, sync --push/--pull, work (run a job at a remote snapshot and publish the result to a named ref).
What blocks the inbound direction
- No reasoner input anywhere accepts a handle, namespace, or key.
workspace_handle appears only as an output.
- Tokens are minted only as a side effect of
Attach, i.e. only for a workspace the node already created. There is no way to obtain a token for a namespace a client wants to create.
build requires repo_path or repo_url, and repo_path must already exist on the node — nothing materializes an incoming namespace into a working directory.
Proposed path
Two new reasoners on the node, plus a small manager change. Nothing in the existing outbound flow changes.
1. create_workspace_inbox — mint an inbound namespace
Input: optional label, optional ttl_seconds.
Output: a Handle (same wire shape, HandleVersion = 1) for a namespace that exists but is empty, plus its recovery key and push token.
Implementation: a Manager.Reserve(label) sibling to Attach that allocates a namespace + key + token and writes the registry row furrowd already reads, but points RepoPath at a fresh empty directory under the workspaces root instead of an existing build workspace.
2. Client pushes
No new code — the shipped client already does this:
furrow --repo . watch --no-daemon
FURROW_SSH_COMMAND=furrow-dial FURROW_DIAL_TOKEN=<token> FURROW_DIAL_INSECURE=1 \
FURROW_RECOVERY_KEY=<key> furrow --repo . pair ssh://<host>:<port> --name <namespace> --key <key>
furrow --repo . sync --push
3. import_workspace — materialize it node-side
Input: namespace, key (and remote when the node is not its own furrowd).
Behaviour: clone/pull the namespace into a real directory under the workspaces root, then return {repo_path}.
That repo_path is then a normal input to build, implement_issue, plan, or swe-pro.code_task — no changes needed in any of them. Optionally let build accept workspace_namespace + key directly and do the import inline, so the common case is one call.
4. Publish the result back
The run already mirrors its own workspace, so the caller pulls the finished branch with the normal get_workspace_handle flow. For a stricter contract, furrow work --ref <input> --result-ref <output> already models exactly "run a job at a remote snapshot, publish the result" and could back this directly.
Security — do not copy the current posture inbound
get_workspace_handle performs no per-caller authorization; its own doc comment says so. Anything that can reach the node and guess a run ID gets an answer, which is why key and token are redacted unless SWE_FURROW_EXPOSE_SECRETS=1.
An inbound path is strictly more dangerous, because a push token is write access to something the node will then execute against. Suggested minimums:
- Inbound tokens are single-namespace, single-use, and TTL-bounded (they authorize one push, not a standing channel).
create_workspace_inbox is gated behind its own env flag, off by default, in the same style as SWE_FURROW_ENABLED.
- An imported workspace is quarantined: never trusted as a git remote, no credential inheritance, and swept by the existing
Sweep age/size policy.
- Reject namespaces that collide with a live run's registry row.
What it opens up
- Run the fleet on uncommitted work. Dirty trees, unpushed branches, scratch dirs, anything not yet a git repo. Today all of it has to be pushed to a remote first.
- Private code without a git remote the node can reach. No deploy keys, no PAT on the node, no cloning a private monorepo into a shared cluster.
- Laptop → cloud burst. Develop locally, hand the exact working state to a bigger box for a long build, pull the branch back. The mirror already handles the return leg.
- Closes the loop the docs already imply. The
agentfield-use skill teaches cloning and following a run workspace; users immediately ask why it does not go the other way.
- Reproducible failure handoff. Ship the exact broken workspace — generated files, caches,
.env-shaped state — instead of "clone main and hope it reproduces."
- Multi-repo and non-git inputs. A workspace is a directory, not a repo, so datasets and vendored trees travel too.
Acceptance criteria
Notes
furrow-dial has no committed darwin build (go/bin/ carries linux/amd64 only), so macOS clients build it from go/cmd/furrow-dial. Worth shipping darwin/arm64 alongside this, since the inbound flow makes the client mandatory rather than optional.
- Unrelated but adjacent:
AssetName in the control plane's furrow provisioner has no linux/arm64 case even though the furrow release publishes that asset, so arm64 Linux clients silently get no furrow at all.
Summary
Furrow mirroring is one-way today: a run publishes its workspace and a laptop clones it. There is no way to go the other direction — hand a local directory to a remote node and have
build/implement_issuerun against it.That is the missing half of "run my code on the fleet". Right now a remote node can only work on code it can
git clone, so anything uncommitted, unpushed, private-to-my-laptop, or simply not a git repo cannot be given to a cloud SWE node at all.Everything needed to close this already exists and is deployed. The gap is one reasoner and one token path.
What exists today
Outbound mirroring works end-to-end. Verified against a live Railway deployment:
get_workspace_handle→furrow cloneover furrowd → 349 MB of a run's real working tree (sparse checkout, untracked files and all) materialized on a laptop.go/internal/furrow/manager.go—Attach,Publish,Handle,Detach,Sweep. Every entry point is outbound.Attach(runID, buildID, repoPath)takes a workspace that already exists on the node and starts mirroring it.go/internal/furrow/types.go—Handle{Version, Remote, Namespace, Key, Token, RepoPath},HandleVersion = 1. The package doc states the contract explicitly: "The contract with the rest of SWE-AF is deliberately one-way."go/internal/node/register.go—get_workspace_handle, registered only whenSWE_FURROW_ENABLEDis truthy.go/internal/orch/build.go:500— the handle is attached to the build result.go/cmd/furrowd/main.go— the server side.AUTH <token> <namespace>, registry-row lookup with a constant-time token compare,validNamespacecharset/length rules, thenfurrow __remote <namespace>as a child.go/cmd/furrow-dial/main.go— theFURROW_SSH_COMMANDshim: TLS dial,AUTH, stdio pipe. Needed because furrowd speaks TLS, not SSH.furrowrelease:clone,pair,sync --push/--pull,work(run a job at a remote snapshot and publish the result to a named ref).What blocks the inbound direction
workspace_handleappears only as an output.Attach, i.e. only for a workspace the node already created. There is no way to obtain a token for a namespace a client wants to create.buildrequiresrepo_pathorrepo_url, andrepo_pathmust already exist on the node — nothing materializes an incoming namespace into a working directory.Proposed path
Two new reasoners on the node, plus a small manager change. Nothing in the existing outbound flow changes.
1.
create_workspace_inbox— mint an inbound namespaceInput: optional
label, optionalttl_seconds.Output: a
Handle(same wire shape,HandleVersion = 1) for a namespace that exists but is empty, plus its recovery key and push token.Implementation: a
Manager.Reserve(label)sibling toAttachthat allocates a namespace + key + token and writes the registry row furrowd already reads, but pointsRepoPathat a fresh empty directory under the workspaces root instead of an existing build workspace.2. Client pushes
No new code — the shipped client already does this:
3.
import_workspace— materialize it node-sideInput:
namespace,key(andremotewhen the node is not its own furrowd).Behaviour: clone/pull the namespace into a real directory under the workspaces root, then return
{repo_path}.That
repo_pathis then a normal input tobuild,implement_issue,plan, orswe-pro.code_task— no changes needed in any of them. Optionally letbuildacceptworkspace_namespace+keydirectly and do the import inline, so the common case is one call.4. Publish the result back
The run already mirrors its own workspace, so the caller pulls the finished branch with the normal
get_workspace_handleflow. For a stricter contract,furrow work --ref <input> --result-ref <output>already models exactly "run a job at a remote snapshot, publish the result" and could back this directly.Security — do not copy the current posture inbound
get_workspace_handleperforms no per-caller authorization; its own doc comment says so. Anything that can reach the node and guess a run ID gets an answer, which is whykeyandtokenare redacted unlessSWE_FURROW_EXPOSE_SECRETS=1.An inbound path is strictly more dangerous, because a push token is write access to something the node will then execute against. Suggested minimums:
create_workspace_inboxis gated behind its own env flag, off by default, in the same style asSWE_FURROW_ENABLED.Sweepage/size policy.What it opens up
agentfield-useskill teaches cloning and following a run workspace; users immediately ask why it does not go the other way..env-shaped state — instead of "clone main and hope it reproduces."Acceptance criteria
create_workspace_inboxreturns a usable handle for an empty namespace, gated off by defaultfurrowclient canpair+sync --pushinto it with no new client codeimport_workspacematerializes it and returns arepo_paththatbuildaccepts unchangedbuild→ result branch contains that changeNotes
furrow-dialhas no committed darwin build (go/bin/carries linux/amd64 only), so macOS clients build it fromgo/cmd/furrow-dial. Worth shipping darwin/arm64 alongside this, since the inbound flow makes the client mandatory rather than optional.AssetNamein the control plane's furrow provisioner has nolinux/arm64case even though the furrow release publishes that asset, so arm64 Linux clients silently get no furrow at all.