feat: make the workspace handle reachable — provision furrow, fix the skill, expose the address - #890
Merged
Merged
Conversation
The workspace handle needs furrow on the CALLER's machine, and furrow had no distribution channel, so the only instruction anyone could give was "build it from Rust source" — which meant the feature was unreachable in practice. Download the pinned release asset into ~/.agentfield/bin, verified against the release's SHA256SUMS and written atomically. Unsupported platforms (Windows has no asset; furrow uses std::os::unix unconditionally) are a clean no-op, and the installed version is recorded beside the binary so bumping the pin actually upgrades machines that already have it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urrow ensure Provisioning belongs in the install path rather than in documentation that each caller re-implements. Ensure furrow when the agentfield-use skill installs — best-effort, so a failed download never fails the install — and add an explicit `af furrow ensure` for repair, which does surface the error since someone asking for it by name is owed the failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workspace-handle section added in #885 landed in skills/ only, leaving the embedded copy the control plane actually serves 41 lines behind. Three skillkit tests have been failing on main since that merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The desktop's workspace-sync probe (PR #885) reads furrow_public_addr from the health response body, but nothing emitted the field, so the probe could never report availability. Emit it from the shared health handler when the FURROW_PUBLIC_ADDR env var is set; omit the key entirely when it is not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
furrow has no release channel today, so "use it only if furrow is on PATH" silently disabled the workspace handle for every caller. Point the lookup at `~/.agentfield/bin` (where provisioning puts it) and at a node's own vendored copy, and keep the silent-skip when neither exists. Provisioning itself belongs in the install path, not in this document. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
…ownloads Two processes running Ensure at once (a desktop skill sync racing a manual af skill install, possibly different af versions) could interleave the binary rename and marker write, leaving an old binary marked as current — permanently skipping the repair. An flock around the whole check-download- install sequence serializes them, and the loser re-checks under the lock so it skips instead of re-downloading. The 15s client timeout bounded the entire request including the ~7.5MB body, failing spuriously below ~500KB/s. Phase timeouts (dial 10s, TLS 10s, response header 30s) with a 3-minute ceiling replace it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… bump to 0.5.0
The resolver snippet used {bin,go/bin} brace expansion, which dash leaves
literal — any agent running it under sh would never find furrow-dial inside
installed packages. Spell the two package dirs out. It also hardcoded
~/.agentfield while provisioning honors AGENTFIELD_HOME, so a custom home
could install furrow somewhere the skill never looks.
The catalog says to bump Version on every content change; the furrow
sections (here and #885) shipped on 0.4.0, leaving reconcilers no signal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The furrow client is a laptop-side tool — cloud agents get furrowd vendored by their own packages, and nothing in the container clones workspaces. Any skill install run in the container would otherwise pull ~7.5MB from GitHub onto the volume for no consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing skill sync in cloud mode Re-run deploy has been a safe upgrade path since source_image pinning, but nothing said an upgrade existed — users had to know the button doubles as one. The panel now compares the deployed pin from Terraform state against the release tag Docker Hub resolves for :latest, shows 'Control plane vX -> vY available', and relabels the action 'Upgrade & redeploy' while one is pending. The connection test also gets a Workspace sync row, kept neutral when the server predates the health field it reads. syncSkills was skipped whenever a cloud profile was active — a guard the cloud-mode PR added wholesale. Skills (and the furrow client their install provisions) belong to local coding agents regardless of where the control plane runs; a cloud-connected laptop is exactly the machine that needs the workspace client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
golangci-lint is advisory in CI, but the new provisioning code should not ship with its own errcheck noise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
CI's 80% patch-coverage gate measured the furrow provisioning code at 74%: every error branch (unresolvable home, bin-dir collision, lock acquisition, missing or malformed checksums, failed binary download) and the runtime-platform defaulting path were untested. Exercise each of them, plus 'af furrow ensure' end to end through cobra in both its silent-success and surfaced-failure shapes. The lock test lives behind a unix build tag because only the flock implementation can fail; the flock() syscall error itself stays uncovered rather than contorting the code to inject it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Testing #885 + Agent-Field/SWE-AF#130 against a live cloud deployment showed the workspace-sync feature was unreachable end to end, for three independent reasons. This fixes all three, plus a red test on
mainfound on the way.A
workspace_handleis only usable iffurrowis on the caller's machine, and furrow had no distribution channel — so the honest instruction was "install Rust and compile it." Agent-Field/furrow now publishes releases (Agent-Field/furrow#6), soafcan provision the binary like everything else under~/.agentfield/.Provisioning lives in the install path deliberately. The alternative — teaching the skill to
curlit at use time — puts an installer inside a document, with no checksum, no version pin, and no single place to fix it.Changes Made
Five focused commits:
fix(skills): sync the embeddedagentfield-usemirror. feat(desktop): open the workspace-sync port when we deploy a control plane #885 added the workspace-handle section toskills/but not to the embedded copy the control plane serves. Threeinternal/skillkittests have been failing onmainsince that merge; this isscripts/sync-embedded-skills.shand nothing else.feat(control-plane): expose the furrow address in health. The desktop probe from feat(desktop): open the workspace-sync port when we deploy a control plane #885 (furrowAddressFromincloud.ts) readsfurrow_public_addrfrom the health body, but nothing emitted it, sofurrowAvailablecould never be true. Emitted from the shared handler whenFURROW_PUBLIC_ADDRis set; the key is absent when it is not.feat(af): provision the pinned furrow client. Downloads the pinned release asset to~/.agentfield/bin/furrow, verified against the release'sSHA256SUMS, temp-file-and-rename so a failure never leaves a partial binary, size-bounded responses. Unsupported platforms are a clean no-op — there is no Windows asset because furrow usesstd::os::unixunconditionally in 13 files, and Windows users reach it through WSL. The installed version is recorded beside the binary so bumping the pin actually upgrades machines that already have furrow.feat(af): install it with the skill that uses it, best-effort so a failed download never fails the install, plusaf furrow ensurefor explicit repair — which does return the error, since silence is right for an install that merely offers to provision furrow and wrong for someone who asked by name.docs(skill): resolve furrow from where AgentField installs it. The skill said to use the handle "only iffurrowis on PATH" — which, with no release channel, silently disabled the feature for every caller. Now a pure resolver over~/.agentfield/binand a node's vendored copy, keeping the silent skip when neither exists.Test Plan
go test ./internal/furrow ./internal/skillkit ./internal/server— pass (skillkitis red onmainbefore commit 1)go build ./...,go vet ./..., gofmt cleanSupersedes #888 and #889. Pairs with Agent-Field/SWE-AF#130.
Review hardening (second pass)
A five-dimension review swarm over the full upgrade story (provisioning diff, trigger coverage, desktop upgrade path, cloud image, release pipeline) produced five more commits:
fix(furrow): serialize concurrent installs, fix download timeouts. Two processes ensuring at once (worse, two differentafversions) could interleave rename/marker and permanently mark a stale binary current — an flock around the sequence serializes them, with a re-check under the lock. The 15s whole-request timeout failed spuriously on slow links for a ~7.5MB asset; replaced with phase timeouts (dial/TLS/header) under a 3-minute ceiling. Race-clean concurrent test asserts exactly one download.fix(skill): POSIX resolver,AGENTFIELD_HOME, version bump. The resolver used bash-only{bin,go/bin}brace expansion (dash leaves it literal — furrow-dial never found undersh) and hardcoded~/.agentfieldwhile provisioning honorsAGENTFIELD_HOME. The catalog demands a version bump on every content change;agentfield-useis now 0.5.0 so reconcilers have a real signal.fix(deploy):AGENTFIELD_SKIP_FURROW=1in the cloud image. The client is a laptop-side tool; cloud agents get furrowd vendored by their packages. Nothing in the container should pull it onto the volume.feat(desktop): visible cloud upgrades + skill sync in cloud mode. Re-run deploy has been a safe upgrade since image pinning, but nothing said an upgrade existed: the panel now compares the deployed pin against Docker Hub and shows "Control plane vX → vY available" with an "Upgrade & redeploy" action, and the connection test gains a Workspace-sync row (neutral for servers predating the health field). Also removes the!isCloudActive()guard onsyncSkills— skills and the furrow client belong to local coding agents regardless of where the control plane runs, and a cloud-connected laptop is exactly the machine that needs the workspace client.chore(furrow): errcheck-clean provisioning code.Gates run locally: full control-plane suite (UI build →
go build→go test ./...; the only failure isTestDevServiceRunDev, which fails identically onorigin/mainin this environment — the known WSL port-discovery flake),golangci-lint --new-from-rev origin/main= 0 issues, desktopnpm ci+ typecheck + vitest (402 passing) +dist:dir, cloud image built + smoke-tested including the new env.Known follow-ups deliberately not in this PR: release.yml publishes binaries before the cloud image (split-release window if the image push fails);
furrow-dialstill has no standalone channel (fold intoaflater); a bare binary-swap upgrade triggers no skill reconcile (curl installer and desktop are the covered paths); no linux-arm64 furrow asset yet.🤖 Generated with Claude Code