Skip to content

docs(adr): propose FTW Cloud as the single remote entry - #1632

Draft
frahlg wants to merge 1 commit into
masterfrom
cursor/cloud-single-entry-c077
Draft

frahlg wants to merge 1 commit into
masterfrom
cursor/cloud-single-entry-c077

Conversation

@frahlg

@frahlg frahlg commented Oct 15, 2026

Copy link
Copy Markdown
Member

Problem and result

Remote access is two outbound dials: the readable copy goes to FTW Cloud in Stockholm (cloud.ftw.energy, #1595), and commands go to the blind relay in Frankfurt (relay.ftw.energy). An agent has no identity on the box.

ADR 0010 is a proposed sketch, docs only. Fredrik's target: the box keeps one outbound connection to Stockholm, carrying the readable copy and end-to-end Noise command frames. The iOS app (srcfl/ftw-app), the webapp (srcfl/ftw-webapp), the home box and a later MCP connector all use that entry. The Frankfurt relay is retired after they have moved. The cloud forwards command frames and cannot read them. This revises ADR 0008's read-only command rule; the service still does not dispatch.

The box issues an agent device when the owner approves it with the passkey in the iOS app: a scoped, expiring, revocable delegated key, checked on every MCP call. The first agent reads live data and sends control. Statistics and opt-in redacted logs (ftw.logs.read) come later. MCP clients use OAuth 2.1, one token per home. A separate GitHub App may open a redacted issue or pull request and cannot merge.

Open: (a) port the relay to Go in srcfl/ftw-cloud or keep it as a loopback container; (b) how the cloud admits a command session; (c) the allowlist, caps and iOS push that stand in for a passkey on a sensitive agent command; and the webapp questions (escrow and Cloudflare hosting, the browser Noise session, the app.ftw.energy passkey origin, migration order and DNS).

Cross-links point here from ADRs 0006, 0008 and 0009 and from docs/architecture.md.

Scope and safety

Documentation. No runtime change. The sketch keeps the box as the authority: commands keep an expiry, core revalidates against fresh state, planner output never reaches hardware, and stale site-meter data still stops dispatch. After the relay is retired, a home with the FTW Cloud grant off has no remote command path. Local control is unchanged.

No overlapping open PR touches docs/adr/.

Verification

Not applicable. This is a proposal. Implementation evidence is the proof column in the ADR's phase table, starting with a box that accepts a command while the Frankfurt relay is firewalled.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.
Open in Web Open in Cursor 

ADR 0010 records one outbound box connection to Stockholm for the
readable copy and Noise command frames, with the Frankfurt relay
retired after the iOS app, webapp and MCP connector have moved.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg

frahlg commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Review, and Fredrik's decisions on it (15 October 2026). The target in this ADR stands. These answer the open questions and change four points.

Decisions

  1. FTW Cloud is one service: one codebase (srcfl/ftw-cloud), one deployment, one domain (cloud.ftw.energy). It may have parts inside; no second server or system. Frankfurt goes.
  2. The app reads and writes through the same server. Answer (b) with a tunnel, and drop option (a): do not port the relay.
    • The phone sends tun {sid, d} on its existing /v1/app session. d is the existing ftw.session.v1 box session, byte for byte.
    • The cloud adds the device id from the roster entry its own handshake matched, never from the client, and sends tun {dev, sid, d} down /v1/box.
    • The cloud can drop or delay. It cannot read or forge. Lane 0 keeps its buckets inside the session, so the cloud sees timing, not command sizes.
    • The watch sends commands as POST /v1/app/x with the ADR 0009 exchange bytes. The cloud keeps a pending table.
    • In the iPhone app this is one new Carrier under NoiseCarrier(inner:). RelayCarrier, the rendezvous code, trial decryption and the relay's close codes then go.
  3. Remote control does not depend on the readable grant.
    • The box links to the cloud when the first app is paired. Its roster gets a "may open a session" bit, and it sends no readings until the owner turns the readable copy on.
    • The QR carries the home id.
    • While the QR is shown, the roster lists a short-lived pending pairing entry, so a new phone can be admitted.
    • The notice must say that the cloud sees a stable home id, the box's address and traffic timing for every home with remote access.
  4. MCP lives in FTW Cloud, as the one way in, at https://cloud.ftw.energy/mcp.
    • It follows the MCP authorization spec 2026-07-28: OAuth 2.1 with PKCE, RFC 9728, RFC 8414, RFC 9207, RFC 8707, CIMD, with DCR kept for ChatGPT, and no token passthrough.
    • Agent keys live in a separate keys process on the same host, sealed with KMS. It is the only part that can open an agent session.
    • The guide line "reads and never controls" becomes: "forwards ciphertext it cannot read; acts on a home only as an agent its owner approved, within limits the box enforces."
  5. The box checks every approval. stepUp is the app's word today (Vault.swift:289, apiauth.go:43-47).
    • Each owner phone gets a Secure Enclave P-256 key behind Face ID. The box stores its public half and verifies the signature on agent.add, on wider rights, and on confirmation of a pending request.
    • The owner can also approve on the box's own page on the home network.
    • Revoke needs no ceremony. The passkey stays for unlock and recovery.
  6. Agents read by default. The owner can grant control.
    • Control means capped temporary holds, so it ends when the agent stops renewing.
    • Durable changes (mode, planning style, schedules, goals) are requests that wait for the owner.
    • The box enforces role, scopes within the approving owner's, an op allowlist (dispatch.write alone also covers loadpoint.soc.set), caps, and an expiry that fails closed with no clock.
    • Each agent gets its own command log root, and each call an audit row the owner can see.
  7. Support access, new.
    • From "Get help", an owner can grant sourceful-support: read and diagnostics, plus control only if ticked. It lasts until the ticket closes, at most 14 days.
    • Sourceful's agents and staff debug that box through the same MCP. Staff sign in with a hardware key, and each call carries the person's id into the audit.
    • There is still no staff path without the owner's grant.
  8. Release checks, new.
    • Beta owners can opt in to a health summary, with no readings and no names.
    • Core gets a staged rollout: the signed manifest carries a share, and each box picks itself from a hash of its home id.
    • An agent compares updated boxes with the rest through fleet.health before the share rises.

#1633 and srcfl/ftw-webapp#87. Please pause both. They move the relay's room protocol (/r/{epoch}/{handle}, rooms, broadcast, ready/gone, /x/, /deadman) onto the cloud host. They add a second and third box socket and two fallbacks. That locks in what this ADR retires. A box moved to the cloud door would also be unreachable from every shipped iPhone build. Worth keeping from #87, as one small PR: the api.req field alignment and its tests, and the CSP entry.

Order. The box keeps its relay socket until every client has moved, so nothing breaks.

Step Content Proof
1 ftw + ftw-cloud: tunnel, the link at pairing, home id in the QR, pairing admission With the relay blocked, a command is answered
2 iOS: tunnel carrier, the relay as fallback for one release, then deleted TestFlight with the relay blocked
3 Watch The watch's direct path
4 Android The same carrier
5 Web app Browser session through the cloud
6 Roles, rights, expiry, caps, approval key, approval on the box page, audit Revoke and expiry fail at the box even when the cloud admits
7 MCP and OAuth with keys, read only ChatGPT, Claude and Grok connect; a token for one home is refused at another
8 Agent control Holds end without renewal; durable changes wait for the owner
9 Support A test ticket worked end to end
10 Release checks A beta at 5 %, then 25 %, with a written verdict
11 Frankfurt off; /fleet to an anonymous host No box dials eu-central-1

Before commands move to the cloud, fix these in ftw-cloud:

  • reload the signed roster at start (uplink.go:276-285; only its number is read back);
  • keep box.unreachable timers across a restart;
  • fix the watch-before-hello_ok race (uplink.go:201-221);
  • give tun its own queue and byte budget, apart from the 20 MB a day budget that also stops hb;
  • set per-home caps;
  • lock outbound traffic and turn on SSM session logs, as ADR 0008 says;
  • plan a deploy drain.

Box session slots should become one per device rather than four anonymous ones (appuplink/session.go:21).

Small fixes to the text: the iPhone's passkey code is appleApp/FTW/Platform/Passkeys.swift, not Passkey.kt. Under "One socket", command sizes are not visible inside the tunnel.

🤖 Generated with Claude Code

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.

2 participants