Skip to content

feat: make the workspace handle reachable — provision furrow, fix the skill, expose the address - #890

Merged
AbirAbbas merged 11 commits into
mainfrom
feat/af-provisions-furrow
Aug 7, 2026
Merged

feat: make the workspace handle reachable — provision furrow, fix the skill, expose the address#890
AbirAbbas merged 11 commits into
mainfrom
feat/af-provisions-furrow

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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 main found on the way.

A workspace_handle is only usable if furrow is 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), so af can provision the binary like everything else under ~/.agentfield/.

Provisioning lives in the install path deliberately. The alternative — teaching the skill to curl it 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:

  1. fix(skills): sync the embedded agentfield-use mirror. feat(desktop): open the workspace-sync port when we deploy a control plane #885 added the workspace-handle section to skills/ but not to the embedded copy the control plane serves. Three internal/skillkit tests have been failing on main since that merge; this is scripts/sync-embedded-skills.sh and nothing else.
  2. 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 (furrowAddressFrom in cloud.ts) reads furrow_public_addr from the health body, but nothing emitted it, so furrowAvailable could never be true. Emitted from the shared handler when FURROW_PUBLIC_ADDR is set; the key is absent when it is not.
  3. feat(af): provision the pinned furrow client. Downloads the pinned release asset to ~/.agentfield/bin/furrow, verified against the release's SHA256SUMS, 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 uses std::os::unix unconditionally 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.
  4. feat(af): install it with the skill that uses it, best-effort so a failed download never fails the install, plus af furrow ensure for 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.
  5. docs(skill): resolve furrow from where AgentField installs it. The skill said to use the handle "only if furrow is on PATH" — which, with no release channel, silently disabled the feature for every caller. Now a pure resolver over ~/.agentfield/bin and a node's vendored copy, keeping the silent skip when neither exists.

Test Plan

  • go test ./internal/furrow ./internal/skillkit ./internal/server — pass (skillkit is red on main before commit 1)
  • Behaviors covered: platform→asset mapping incl. the Windows no-op, idempotent skip, upgrade when the pin changes, marker recorded so the next run skips, checksum mismatch rejected with nothing written, download failure non-fatal, rename failure leaves no partial file, opt-out honored, health key present/absent
  • go build ./..., go vet ./..., gofmt clean
  • No network in any test (httptest via a base-URL override)
  • The resolver was run verbatim on a real machine against an already-vendored binary
  • End-to-end clone of a live cloud run's workspace verified against a Railway deployment

Supersedes #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:

  1. fix(furrow): serialize concurrent installs, fix download timeouts. Two processes ensuring at once (worse, two different af versions) 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.
  2. 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 under sh) and hardcoded ~/.agentfield while provisioning honors AGENTFIELD_HOME. The catalog demands a version bump on every content change; agentfield-use is now 0.5.0 so reconcilers have a real signal.
  3. fix(deploy): AGENTFIELD_SKIP_FURROW=1 in 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.
  4. 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 on syncSkills — 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.
  5. chore(furrow): errcheck-clean provisioning code.

Gates run locally: full control-plane suite (UI build → go buildgo test ./...; the only failure is TestDevServiceRunDev, which fails identically on origin/main in this environment — the known WSL port-discovery flake), golangci-lint --new-from-rev origin/main = 0 issues, desktop npm 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-dial still has no standalone channel (fold into af later); 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

AbirAbbas and others added 2 commits August 6, 2026 14:07
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>
@AbirAbbas
AbirAbbas requested a review from a team as a code owner August 6, 2026 18:07
AbirAbbas and others added 3 commits August 6, 2026 14:25
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>
@AbirAbbas AbirAbbas changed the title feat(af): provision the furrow client at install time feat: make the workspace handle reachable — provision furrow, fix the skill, expose the address Aug 6, 2026
AbirAbbas and others added 5 commits August 6, 2026 15:13
…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>
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.10% 87.40% ↓ -0.30 pp 🟡
sdk-go 92.70% 92.00% ↑ +0.70 pp 🟢
sdk-python 93.82% 93.73% ↑ +0.09 pp 🟢
sdk-typescript 91.17% 90.42% ↑ +0.75 pp 🟢
web-ui 84.75% 84.79% ↓ -0.04 pp 🟡
aggregate 85.62% 85.75% ↓ -0.13 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 179 89.00%
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every 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>
@AbirAbbas
AbirAbbas merged commit 1fc0eb7 into main Aug 7, 2026
29 checks passed
@AbirAbbas
AbirAbbas deleted the feat/af-provisions-furrow branch August 7, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant