Skip to content

Tell a connected worker to reconnect, not to connect - #554

Merged
ralyodio merged 1 commit into
masterfrom
worktree-coinpay-reconnect
Sep 9, 2026
Merged

Tell a connected worker to reconnect, not to connect#554
ralyodio merged 1 commit into
masterfrom
worktree-coinpay-reconnect

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Reported from the agenticjobs apply flow: an agent tried to invoice $0.25 and ugig answered

Connect your CoinPay account before sending an invoice

CoinPay was connected. It tried again and got the same sentence.

The gate is right, the sentence is wrong

A CoinPay link authorised before ugig needed wallet:read cannot read global wallet addresses, so it cannot back an invoice, and getConnectedCoinpayAccessToken correctly returns null for it.

But null carries no reason, so the route reported the only case it could name: not connected. That is an instruction to do something the worker has already done, which is exactly why it loops — they check, see a connection, and try again.

#553 fixed this on the connections page, which now shows Reconnect required instead of Connected. The API never learned the difference, and the API is what an agent gets: it never sees that page, so the sentence in the 409 is its only instruction.

The reason now travels with the token

getCoinpayLink() returns none | needs_reconnect | connected, and the invoice route picks its error and its setup_instructions from that. For a stale link the steps lead with reconnecting and say what it does and does not change — "reconnecting re-authorises the same account, your gigs are untouched" is the question anyone hesitates on before clicking.

Deliberately unchanged:

  • getConnectedCoinpayAccessToken stays as a wrapper, so the wallets route and the bounty payout route are untouched.
  • oauth_required still reports true for a worker in both cases. Reconnecting is the same authorise round trip, so a client keying on that flag to offer the button keeps working.
  • New coinpay_link_state field is what a caller should branch on.

Tests

Two, and the second is the one that keeps the fix honest:

  • a stale link must not receive the exact sentence that sent the reporter in circles, and its first instruction must name reconnecting
  • a genuinely absent link must still be told to connect — so this cannot be "fixed" by wording everything as reconnect

Checks

2104 tests pass across 219 files, 0 fail. tsc --noEmit clean. Pre-commit hooks (lint + full Next build) pass.

Not included

src/app/api/coinpay/wallets/route.ts carries the same connect-only wording. It is a different surface and nobody has reported hitting it, so I left it rather than widen this. Say the word and it is a two-line follow-up.

🤖 Generated with Claude Code

Reported from the agenticjobs apply flow: an agent tried to invoice $0.25 and
ugig answered "Connect your CoinPay account before sending an invoice". CoinPay
was connected. It tried again, and got the same sentence.

The gate is right and the sentence is wrong. A CoinPay link authorised before
ugig needed wallet:read cannot read global wallet addresses, so it cannot back
an invoice, and getConnectedCoinpayAccessToken correctly returns null for it.
But null carries no reason, so the route reported the one case it could name:
not connected. That is an instruction to do something the worker has already
done, which is why it loops. They check, see a connection, and try again.

#553 fixed this on the connections page, which now says "Reconnect required"
rather than "Connected". The API never learned the difference, and the API is
what an agent gets: it never sees that page, so the sentence in the 409 is its
only instruction.

So the reason travels with the token now. getCoinpayLink returns
none | needs_reconnect | connected, and the invoice route picks its message and
its setup_instructions from that. The steps for a stale link lead with
reconnecting and say what reconnecting does and does not change, because "your
gigs are untouched" is the question anyone hesitates on.

getConnectedCoinpayAccessToken stays, as a wrapper, so the wallets route and
the bounty payout route are untouched. oauth_required still reports true for a
worker in both cases: reconnecting is the same authorise round trip, so a
client that keys on the flag to offer the button keeps working. The new
coinpay_link_state field is what a caller should branch on.

Two tests: the stale link must not receive the exact sentence that sent the
reporter in circles, and a genuinely absent link must still be told to connect,
so this cannot be "fixed" by wording everything as reconnect.

2104 tests pass across 219 files. Typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxWrsbLuaSaZqQ7FFTrdVW
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

45 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 36

Severity Rule Location
HIGH js-ssrf-outbound-request scripts/scan-all-skills.ts:38
MEDIUM js-open-redirect src/app/agent-login/AgentLoginForm.tsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:79
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:90
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:113
MEDIUM js-open-redirect src/app/dashboard/subscription/page.tsx:144
MEDIUM js-open-redirect src/components/funding/FundingClient.tsx:137
MEDIUM js-dynamic-code-execution src/lib/skills/metadata-extract.ts:300
MEDIUM js-dynamic-code-execution src/lib/skills/security-scan.ts:48
LOW secret-generic-credential cli/src/commands/auth.test.ts:66
LOW secret-generic-credential cli/src/commands/auth.test.ts:85
LOW secret-generic-api-key docs/agents/integration-guide.md:893
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:53
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:68
LOW secret-generic-credential src/app/api/auth/login/route.test.ts:87
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:158
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:182
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:193
LOW secret-generic-credential src/app/api/auth/signup/route.test.ts:232
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:212
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:223
LOW js-dynamic-code-execution src/app/api/skills/[slug]/scan/route.test.ts:239
LOW secret-generic-credential src/lib/api.test.ts:126
LOW secret-generic-credential src/lib/api.test.ts:131
LOW js-dynamic-code-execution src/lib/skills/composite-scanner.test.ts:106
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:36
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:44
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:66
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:81
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:94
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:103
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:118
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:144
LOW js-dynamic-code-execution src/lib/skills/secureclaw-scanner.test.ts:162
LOW js-dynamic-code-execution src/lib/skills/security-scan.test.ts:28
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:178
LOW js-dynamic-code-execution src/lib/skills/url-import.test.ts:191
LOW secret-generic-credential src/lib/validations.test.ts:148
LOW secret-generic-credential src/lib/validations.test.ts:512
LOW secret-generic-credential src/lib/validations.test.ts:523
LOW secret-generic-credential src/lib/validations.test.ts:538
LOW secret-generic-credential src/lib/validations.test.ts:548
LOW secret-generic-credential src/lib/validations.test.ts:557
LOW secret-generic-credential src/lib/validations.test.ts:567
LOW secret-generic-credential src/lib/validations.test.ts:582

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 27ca744 into master Sep 9, 2026
6 checks passed
@ralyodio
ralyodio deleted the worktree-coinpay-reconnect branch September 9, 2026 11:10
@ralyodio ralyodio mentioned this pull request Sep 9, 2026
ralyodio added a commit that referenced this pull request Sep 9, 2026
Marks the CoinPay reconnect fix (#554): a worker whose link predated the
wallet:read scope was told to connect an account they had already connected,
and the invoice API now tells them to reconnect instead.

Note for whoever cuts the next one: the tags have drifted a long way from the
branch. v1.1.3 is 882 commits behind master, and package.json had already been
moved to 1.1.4 by 1c4e430 without a tag ever being cut for it. So v1.1.5 is the
first tag in a long while and it covers far more than its own line above. The
deploy has never depended on this - Railway builds from the GitHub connection
on merge - so nothing was broken by the drift, but the tag has not been a
useful marker of what is live and this is a step back towards it.


Claude-Session: https://claude.ai/code/session_01WxWrsbLuaSaZqQ7FFTrdVW

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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