Repository navigation
feat(web): the box page lets an assistant in with no phone app - #1682
Conversation
Settings has an Access tab. The owner types the code from FTW Cloud's consent page; the browser fetches the signed offer and the box reads it, so the owner sees what the box checked. Reads are the default, control is opt-in, and the grant is never wider than the offer. Granting, renewing, narrowing and approving need the house password, and the page says so before a code is spent. The tab lists who has access and lets the owner renew, narrow, remove and answer waiting requests. Core: POST /api/agents/offer reads an offer for the page and grants nothing. It and POST /api/agents say why the box will not take an offer. GET /api/agents carries the box's facts and the registry's agent ops. A box with only an assistant now links to FTW Cloud, which learns of the assistant from the roster. Paired with the consent page's words in srcfl/ftw-cloud. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Fredrik approved the 🤖 Generated with Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 73bb9f6773
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ? "A client FTW Cloud knows, from " + (v.clientHost || "an address it checked") + "." | ||
| : "FTW Cloud does not know this client. The name is what it calls itself" + | ||
| (v.clientHost ? "; it comes from " + v.clientHost : "") + ". Let it in only if you just connected it.", |
There was a problem hiding this comment.
Sanitize the client host before displaying it
For an unverified client, clientHost originates from its metadata document or redirect URI, but it is inserted without the cleanText processing applied to the client name and request text. A crafted host containing Unicode bidi or other formatting controls can visually reorder or conceal parts of this identity warning immediately before the owner grants access; sanitize the host before rendering it, including hosts derived for existing rows.
Useful? React with 👍 / 👎.
| if (state.note) setNote(state.note.text, state.note.kind); | ||
| draw(ctx); | ||
| refresh(ctx); |
There was a problem hiding this comment.
Refresh requests while the Access tab remains open
When an agent creates a durable-change request after this one-time refresh(ctx) completes, nothing reloads /api/agents, so the request remains invisible for as long as the owner leaves the Access tab open; this is especially problematic in the new no-phone flow, where this page is the only approval surface. Poll while the tab is active or subscribe to an existing update signal so pending requests and expirations stay current.
AGENTS.md reference: AGENTS.md:L22-L25
Useful? React with 👍 / 👎.
…mes harder Review of #1682: - A box that takes no tun (app link off) cannot serve an agent, so an agent row no longer links it to FTW Cloud. - On loopback the house password never counts, so the grant failed after the code was spent. GET /api/agents says box.widen for this request, and the page stops with plain words first. Docs and comments no longer imply the password works on the box itself. - Power caps are exact to the watt: 3,680 W read back as 3.7 kW and the page refused its own default. - Names lose double quote marks, soft hyphens, joiners, variation selectors and tag characters, and are cut by code point. An unknown argument's name is cleaned. A request shows the client's host beside its name. - A grant lasts a week unless the owner picks another length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
On 17 October Fredrik asked that an owner can connect an assistant over MCP with no app involved. Many owners have no iOS app: Android, web only, Home Assistant. FTW Cloud's consent page already tells them to "open Access" on the box's own page and type a code, but there was no Access page. Core's API for it shipped in #1646; this is the page, and the few things the page needed from Core.
Paired with srcfl/ftw-cloud#25: the consent page's words, and a code lookup whose refusals the box page can read. Either can merge first.
What changes
Settings → Access (
web/settings/tabs/access.js), beside FTW app:clientHost, whether FTW Cloud verified it, the scopes and ops asked for, caps and length.Core, kept small:
POST /api/agents/offer {offer}reads an offer for the page and grants nothing. It andPOST /api/agentsnow say why the box will not take an offer:{error, reason, boxMs}withreasonone ofno_offer_keys,unsigned,expired,malformed,other_home. A clock the box cannot trust answersE_UNAVAILABLE {subsystem: "clock"}first, since every offer would otherwise read as expired. Before, all of these were one sentence.GET /api/agentsalso carriesbox {offers, lookup, clock, nowMs, markMs}andregistry {agentOps, writeScopes, agentArgs}. The page checks the first before asking for a code, and takes scope, op and argument names from the second instead of writing them by hand.lookupfollowscloudlink.Endpoint, so a local build with its own FTW Cloud finds its own lookup.cloudlink.wantsLinklooked only at phones with a tunnel and at a pairing code. An agent row has no tunnel, so a box with no phone never linked, and FTW Cloud never saw the assistant in a roster: the grant did nothing. An idle link would also have ended with an assistant still on it.TestAnAgentRowLinksTheBoxfails without the one-line fix.docs/agents.mdanddocs/protocol-cloud.mdsay both.The offer key: #1680, now merged, pins FTW Cloud's real offer key (kid
s0xLrCPjFa-AoMMl) incloudlink.OfferKeys, and this branch carries master. A release build with this PR takes real offers. The "cannot let assistants in yet" message stays for a build that pins none.After review
cloud.key, no roster naming the client.wantsLinknow asksd.Tun || (d.Agent != nil && l.opts.Tunnel != nil), asTakesTundoes.TestAnAgentRowLinksNoBoxThatTakesNoTunfails without it.resolveLANSecretdoes not read the sign-in from loopback, so throughssh -Lor a browser on the box the page read as ready, the lookup spent the code, and the grant got 403.GET /api/agentsnow saysbox.widenfor this very request, and the page stops first: "Open this page from another device on your home network, and sign in with the house password there." Renew and Approve use the same answer.docs/agents.mdand the comments inapi_agents.gono longer imply that a password works on loopback.TestTheListSaysWhetherThisRequestCanWidenruns both cases through the real middleware.step="any"; a test round-trips 1 W to 99,999 W. Checked in the browser: granted and renewed at 3,680 W.Facts()reads as ready on a build that pins an offer key but no service key. No release build is like that: every release names the service.Tests
web/settings/tabs/access.test.mjs, 32 tests: the grant is never wider than the choice and has no caps without a hold; reads in the page's order; each block and refusal has its own words, including the clock-skew case where FTW Cloud and the browser still count the offer as good; request arguments in the registry's order; the page's words cover every agent scope and op incontract/registry.yamland nothing else.TestTheBoxPageReadsAnOfferBeforeGranting(each reason, grants nothing, the clock first),TestTheAgentListCarriesTheBoxFacts,TestTheCodeLookupFollowsTheUplink,TestAnAgentRowLinksTheBox, and the new route in the local-route and sign-in sweeps.make verifypasses.Reviewed in a browser
Against a locally running Core from this branch, built with a test offer key pinned through
-ldflags -X cloudlink.OfferKeys, as Core's tests do, andServiceKeysempty so it never dialled FTW Cloud. A small stand-in for FTW Cloud's code lookup signed fresh offers with that key. The browser pane refuses a page's requests to another origin (ERR_BLOCKED_BY_CLIENT), so the stand-in also served the box from one origin; the box saw a LAN client with no forwarding headers, as from any device on the Wi-Fi. Signed in with a house password, the page:The pending request in shot 10 was added to the box's real answer in the page, since this rig has no agent session to ask; Decline then reached the real box and got
E_NOT_FOUND, which the page now answers by reloading the list. The page fits 375 px with no sideways scroll and reads in the light theme.For the owner
minor: a visible new capability. Fredrik approved it in chat (quoted on this PR).Left for later
contract/registry.yamlwriteslabel: Ready by, in minutes after midnightinside a flow mapping, so YAML ends the label at the comma and the label reads "Ready by". The page shows that argument as a time of day.ftw.dispatch.holdstill say "Hold the battery", butbattery.holdis no agent op.🤖 Generated with Claude Code