Skip to content

fix(web): connect refusals reach the caller - #344

Merged
cvince merged 1 commit into
feat/portabilityfrom
claude/web-refusal-connect
Aug 28, 2026
Merged

fix(web): connect refusals reach the caller#344
cvince merged 1 commit into
feat/portabilityfrom
claude/web-refusal-connect

Conversation

@cvince

@cvince cvince commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The last file in the refusal class — and I was wrong that it needed a decision.

I reported connect as not-contained because execute returns Promise<{ linked: boolean }> and recurses into itself, so wrapping it in a handler returning never would change the contract. That was the wrong shape to reach for. rotateCommand already documents the right one, and it wraps nothing:

await refuse(…); return; at every call site, and the return is not decoration: displayErrorAndExit is Promise<never> but TypeScript does not narrow across an awaited never

Called directly and followed by return { linked: false }, the signature is untouched and the recursion is untouched.

refusal code
dev-mode live firewall (×2) PERMISSION_DENIED
unknown provider INVALID_FORMAT

The unknown-provider case is the one reachable under --web: the browser picker is offered first, and this is where the run ends when the picker is declined or cannot be shown — previously on a stream with nobody on it.

The firewall sites are dev-binary only, but capy-dev --web --live reaches them, so they get the same treatment. Nothing about the refusal is weakened: same condition, same exit code, one more surface it reaches.

capy-cli now has zero bare console.error + process.exit(1) refusals in the command layer that are reachable under --web.

Suite: 1851 pass, 0 fail.

The last file in the refusal class, and I was wrong that it needed a decision.

I had reported connect as not-contained because `execute` returns
Promise<{ linked: boolean }> and recurses into itself, so wrapping it in a
handler that returns `never` would change the contract. That was the wrong
shape to reach for. rotateCommand already documents the right one, and it does
not wrap anything:

    `await refuse(...); return;` at every call site, and the `return` is not
    decoration: displayErrorAndExit is Promise<never> but TypeScript does not
    narrow across an awaited never

Called directly and followed by `return { linked: false }`, the signature is
untouched and the recursion is untouched. Three refusals converted:

  dev-mode live firewall (x2)   PERMISSION_DENIED
  unknown provider              INVALID_FORMAT

The unknown-provider one is the reachable-under-web case: the browser picker is
offered first, and this is where the run ends when the picker is declined or
cannot be shown. It ended on a stream with nobody on it.

The firewall sites are dev-binary only, but `capy-dev --web --live` reaches
them, so they get the same treatment. Nothing about the refusal itself is
weakened — same condition, same exit code, one more surface it reaches.

capy-cli now has zero bare `console.error` + `process.exit(1)` refusals in the
command layer that are reachable under --web.

Suite: 1851 pass, 0 fail.
@cvince

cvince commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Status, since chat replies are not arriving

You read #343's diff in detail, so PR content reaches you. Chat does not. Posting here instead.

capy deploy --web — run again just now (9th run)

Nothing blocks it. No staging project dir, no keep.lock, no credentials, no deploy target. Clean temp HOME, ~40s.

COMMAND:   capy-staging deploy --web
EXIT CODE: 1
URL LINE:  http://127.0.0.1:58291/s/-XdniUBYsWb1cmCFidkIrj4EuVycR49P-l1G8K4mTgo

Earlier run additionally fetched the page: HTTP 200, 68,286 bytes, containing the refusal text — so it is real content, not an empty shell. Full evidence with timestamps and SHAs: ~/.capy-e2e-overnight/evidence/deploy-web-url/RESULT.md, and #343's comment thread.

The refusal class is finished

Your last three messages listed kick, org, transport, edit, recover, connect and the deployTokenCommand classes as open. They are merged. Verified against the tip by content, not memory:

file bare exits reachable under --web
kick, org, transport, edit, connect 0
recover 0 reachable — 5 remain after if (options.web) return, unreachable by construction
deployTokenCommand 2 remain, both double-print cases (below)

Merged: #334, #335, #338, #339, #340, #341, #342, #343. This PR (#344) is connect, the last one.

Skip paths

Genuinely remaining

Four double-print cases — 2 in inviteCommand (role-grant refusals), 2 in deployTokenCommand (mintOrExit after spinner.fail, ambiguous-prefix concatenation). Each already has its message on screen before the exit, so converting prints it twice. Folding the existing output into the throw is a copy decision, not a mechanical edit.

@cvince
cvince merged commit 94f21ae into feat/portability Aug 28, 2026
2 checks passed
cvince added a commit that referenced this pull request Aug 28, 2026
Two things, one of them a correction to my own code in #344.

PROVIDER_NOT_FOUND. I used INVALID_FORMAT for an unknown provider, which is
wrong: that is a not-found condition, not a malformed one, and the remedies
differ — "pick from the list" versus "correct your syntax". A caller branching
on the code cannot tell those apart, and the code is the contract; that is the
whole reason we do not parse the prose. Minted at the point the condition is
first known, per the rule KEY_NOT_ON_DEVICE's own comment states, and
alongside the existing not-found family.

push. Found by RUNNING commands under --web with no TTY rather than reading
them: `capy push --web` refused with exit 1 and no URL anywhere. It was on
nobody's list. Both its guards now throw into the catch that execute() already
routes to displayErrorAndExit. push takes no `web` option and needs none —
that function reads web mode itself.

Verified by running, not inferred:

    $ capy push --web        # clean HOME
      exit code: 1
      url line : http://127.0.0.1:60064/s/G5BjAmn9Owi7-uZY9PR3pPqnL85M8tIprJ96XX3qne0

Before this it printed the refusal and nothing else.

Suite: 1851 pass, 0 fail. A first run exited 1 on
tests/auth/authServiceKeepScreens.test.ts; that file passes in isolation on
this tree and a second full run was clean, so it is order-dependent flake,
not this change — the diff touches only push, connect and the code table.
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