Skip to content

fix(setup): stop --skip-mount prompting for a mount dir; name the unbound-workspace failure - #461

Merged
kjgbot merged 2 commits into
mainfrom
fix/setup-workspace-binding-and-skip-mount-prompt
Sep 5, 2026
Merged

fix(setup): stop --skip-mount prompting for a mount dir; name the unbound-workspace failure#461
kjgbot merged 2 commits into
mainfrom
fix/setup-workspace-binding-and-skip-mount-prompt

Conversation

@miyaontherelay

Copy link
Copy Markdown
Contributor

Two verified production failures in relayfile setup 0.10.53.

1. --skip-mount still prompted and blocked on stdin

runSetupWithOptions prompted Local mount directory [./relayfile-mount]: whenever --local-dir was omitted, regardless of --skip-mount. --skip-mount is the headless contract — setup finishes without starting the mount process — so a run with --no-open had no terminal to answer and the read blocked forever.

Under --skip-mount, setup now takes the value the prompt's default would have produced instead of asking. The directory is still used (the mirror layout is created there, and the printed relayfile mount <id> <dir> hint names it), so nothing else about the run changes. Interactive setup is untouched.

2. setup cannot create a usable workspace — the root cause is cloud-side

resolveCloudWorkspaceForRelayfile collapsed two different Cloud states into one message:

Cloud did not return cloudWorkspaceId and relayfileWorkspaceId; workspace provisioning did not complete

For a CLI-created workspace that message is wrong. Provisioning did complete. What actually comes back is relayfileWorkspaceId: "rw_…" and cloudWorkspaceId: null, because nothing binds a CLI-created relay workspace to a Cloud workspace:

  • POST /api/v1/workspaces without createAppWorkspace: true calls registry.create(...), which writes only the relay-workspace row. It never sets workspaces.relayWorkspaceId on the app workspace.
  • GET /api/v1/workspaces/{id}/resolve reads that binding. On the rw_* branch it returns cloudWorkspaceId: binding.appWorkspaceId, which is null for an unbound workspace. Only the app-workspace-UUID branch provisions (resolveOrProvisionRelayWorkspace).
  • The CLI cannot send createAppWorkspace: true: that branch requires requireSessionAuth, and the CLI authenticates with a cli:auth bearer (source: "token"), so it would 403.

This PR splits the two checks so the error names the real state instead of blaming provisioning.

Why this PR does not just tolerate a null cloudWorkspaceId

Tolerating it would not produce a usable workspace — it would only move the failure one step later, to a message that cannot explain itself. The next call setup makes is POST /api/v1/workspaces/{rw_*}/relayfile/delegated-token, whose hasWorkspaceAccess gate is:

if(rawWorkspaceId===auth.workspaceId)returntrue;// rw_* != app UUID
...
constbinding=awaitresolveAppWorkspaceByRelayWorkspaceId(rawWorkspaceId);if(!binding.appWorkspaceId)returnfalse;// unbound -> false

which returns 404 workspace_not_found. So the CLI fails fast at the step that can name the cause.

The fix that makes relayfile setup work is cloud-side: POST /api/v1/workspaces (the cli:auth, non-createAppWorkspace path) must bind the new relay workspace to the caller's app workspace, or resolve's rw_* branch must provision that binding the way the UUID branch already does. That change belongs in AgentWorkforce/cloud and is out of scope here.

Worth noting for whoever picks that up: the cloud tests that cover this path all mock resolveAppWorkspaceByRelayWorkspaceId to return a non-null appWorkspaceId (workspaces/create-resolve.integration.test.ts, [workspaceId]/resolve/route.test.ts incl. "resolves a direct rw_ workspace id without app provisioning"). No test exercises the unbound case, which is why this shipped.

Tests

New file cmd/relayfile-cli/setup_headless_test.go:

  • TestSetupSkipMountDoesNotPromptForLocalMountDirectory — headless --skip-mount --no-open run with a stdin reader that errors on any read. Asserts setup succeeds, never reads stdin, never prints the prompt, prepares ./relayfile-mount, and issues exactly create → resolve → delegated-token.
  • TestSetupNamesUnboundCloudWorkspaceOnResolve — replays the production resolve payload (cloudWorkspaceId: null, relayfileWorkspaceId: "rw_1234abcd"). Asserts the error names the unbound binding and that setup stops before the delegated-token call.
  • TestResolveCloudWorkspaceForRelayfileSeparatesUnprovisionedFromUnbound — the two states now produce distinct errors.

All three were confirmed to fail against the unfixed code (the --skip-mount case reproduces Local mount directory [./relayfile-mount]: and the blocking read).

go build ./..., go vet ./cmd/relayfile-cli/, gofmt -l, go test ./... (full repo, -count=1 on cmd/relayfile-cli, 126s) and scripts/check-contract-surface.sh all pass. No HTTP surface changed, so the OpenAPI spec is untouched.

Not run: no live run against production Cloud. The cloud-side mechanism above is established by reading AgentWorkforce/cloud@origin/main, not by executing it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kk8xnrAeA3QfS4A9n6dAd3

…ailure
Two production failures in `relayfile setup` 0.10.53.
1. `--skip-mount` still prompted "Local mount directory [./relayfile-mount]: "
whenever --local-dir was omitted. --skip-mount is the headless contract —
setup finishes without starting the mount process — so a run with --no-open
has no terminal to answer, and the read blocked forever. Under --skip-mount,
take the value the prompt's default would have produced instead of asking.
The directory is still used (mirror layout, and the printed
`relayfile mount <id> <dir>` hint), so nothing else changes.
2. `resolveCloudWorkspaceForRelayfile` collapsed two different Cloud states
into one message: "Cloud did not return cloudWorkspaceId and
relayfileWorkspaceId; workspace provisioning did not complete". For a
CLI-created workspace, provisioning DID complete — Cloud returns
relayfileWorkspaceId rw_* and cloudWorkspaceId: null, because nothing binds
a CLI-created relay workspace to a Cloud workspace. Split the checks so the
error names the actual state: the workspace is not bound to a Cloud
workspace, and that binding is Cloud's to establish.
Setup deliberately still fails on the unbound case rather than tolerating a
null cloudWorkspaceId. Continuing does not produce a usable workspace: the next
call, POST /api/v1/workspaces/{rw_*}/relayfile/delegated-token, authorizes
through the same app-workspace binding (hasWorkspaceAccess) and answers
404 workspace_not_found. Tolerating null would only relocate the failure to a
step that cannot explain it. The fix that makes `relayfile setup` work is
cloud-side.
Tests: all three new cases were confirmed to fail against the unfixed code —
the --skip-mount case reproduces the prompt and the stdin read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk8xnrAeA3QfS4A9n6dAd3
Session-Id: 0b0827b8-9619-4a88-9943-f5d30aa95869
@coderabbitai

coderabbitaiBot commented Sep 4, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 601421fa-1ce2-4a57-b108-494f7d7d6e52


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actionsBot commented Sep 4, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-09-05T11-07-13-241Z-HEAD-provider
Mode: provider
Git SHA: 758b0c9

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

The new tests called testing.T.Chdir, which landed in Go 1.24. go.mod targets
go 1.22, CI pins GO_VERSION 1.22 and Contract builds with go-version-file:
go.mod, so both checks failed to compile:
cmd/relayfile-cli/setup_headless_test.go:35:4: t.Chdir undefined
FAIL github.com/agentworkforce/relayfile/cmd/relayfile-cli [build failed]
It passed locally because the author's toolchain was newer than the module's
target — a green local run could not have caught this.
Replaced with a chdirForTest helper doing the pre-1.24 dance (os.Getwd,
os.Chdir, restore in t.Cleanup), rather than raising go.mod's Go version,
which would change the module's minimum for every consumer to buy one test
convenience.
Verified with GOTOOLCHAIN=go1.22.12 go vet ./cmd/relayfile-cli/ — CI's exact
toolchain, and vet compiles test files, which is where this broke. Behaviour
verified on go1.26 (tests pass). Running a 1.22-built binary on this macOS is
not possible (dyld: missing LC_UUID load command), so the run itself is left
to CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV
Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmd/relayfile-cli/setup_headless_test.go">
<violation number="1" location="cmd/relayfile-cli/setup_headless_test.go:46">
P3: In the cleanup registered by chdirForTest, t.Fatalf calls FailNow (runtime.Goexit), which aborts the test goroutine from inside a cleanup. That stops the remaining cleanup functions (LIFO) and marks the test failed in a way that the testing framework does not reliably attribute to a normal cleanup failure. Use t.Errorf in the cleanup so the restore failure is reported without aborting the cleanup chain.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
t.Cleanup(func() {
if err := os.Chdir(previous); err != nil {
t.Fatalf("restore working directory: %v", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: In the cleanup registered by chdirForTest, t.Fatalf calls FailNow (runtime.Goexit), which aborts the test goroutine from inside a cleanup. That stops the remaining cleanup functions (LIFO) and marks the test failed in a way that the testing framework does not reliably attribute to a normal cleanup failure. Use t.Errorf in the cleanup so the restore failure is reported without aborting the cleanup chain.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/relayfile-cli/setup_headless_test.go, line 46:
<comment>In the cleanup registered by chdirForTest, t.Fatalf calls FailNow (runtime.Goexit), which aborts the test goroutine from inside a cleanup. That stops the remaining cleanup functions (LIFO) and marks the test failed in a way that the testing framework does not reliably attribute to a normal cleanup failure. Use t.Errorf in the cleanup so the restore failure is reported without aborting the cleanup chain.</comment>
<file context>
@@ -28,11 +28,31 @@ func (r *refusingStdin) Read([]byte) (int, error) {
+	}
+	t.Cleanup(func() {
+ if err := os.Chdir(previous); err != nil {
+ t.Fatalf("restore working directory: %v", err)
+ }
+	})
</file context>

@kjgbot
kjgbot merged commit c5c80fc into mainSep 5, 2026
11 checks passed
@kjgbot
kjgbot deleted the fix/setup-workspace-binding-and-skip-mount-prompt branch September 5, 2026 11:18
Sign up for freeto 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.

3 participants

@miyaontherelay@kjgbot@prpmdev-bot