Skip to content

feat(ui): confirm your computer inside the Final step panel - #918

Merged
allisonmahmood merged 5 commits into
mainfrom
feat/inline-confirm
Oct 24, 2026
Merged

allisonmahmood merged 5 commits into
mainfrom
feat/inline-confirm

Conversation

@allisonmahmood

@allisonmahmood allisonmahmood commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

What changed

Once the person's agent starts a login with the setup line, the Final step panel shows that login's confirmation right there, so the person never opens a second tab. The link the agent prints keeps working exactly as before.

Closes #911. Part of #906. Stacked on #917.

Look at

  • The form is the device page's own. confirmForm in packages/auth/src/DevicePages.ts is now shared by the device page and the panel. In the panel it reads "Name this computer", prefilled from the login's hint, with Confirm and Not me, and carries the setup link as a hidden field.

  • The server decides, not the page. A form with setup posts to the existing POST /login/device, which sends it to DeviceLogins.confirmInline / denyInline.

    • Confirm rechecks, under the login's lock then the user lock then the line's row lock, that the line is the signed-in person's, live, not retired, still pending (so not contested) and bound to this code. Anything else confirms nothing.
    • Every outcome goes back Home, where the panel shows where things stand. A refreshed sign-in replays nothing, and a form rendered for another account answers nothing.
    • It keeps the device route's same-origin check, account binding, rate limit and replacement warning.
    • Confirm locks the line before checking it against a fresh clock, so a wait for the lock can't outlast the line.
    • The panel's fragment can't be framed (frame-ancestors 'none') and posts only to Patchy. It names the account it was rendered for, and the script reloads the page rather than swap in another account's form after an account switch.
  • Not me settles the login by where it actually is, in one transaction:

    • Pending: denies it.
    • Confirmed, key not collected: deletes it, so the terminal's next poll finds nothing and no key is made.
    • Key already made: says "That computer is already connected" and points to Your machines. It never says nothing was connected.

    The line ends denied, and the panel says "Nothing was connected. If you didn't just paste the line, you're fine: no computer can publish as you." with Get a new line.

  • Polling never eats a half-typed name. The panel's script leaves the form alone while it has focus or an edited name.

In the panel After Confirm, key not yet collected Connected Not me
inline confirm after confirm connected not me

Where this differs from #911's wording:

  • Refusals (a stale account, an expired code, an emptied name) go back Home instead of rendering inside the panel. The panel then shows the current state, which is usually the same form or the fallback. The name field is required, so an empty name rarely reaches the server.
  • Not me ends the line denied instead of retiring it. The effect is the same: the line can't bind another login, and Get a new line starts over.
  • The "already connected" answer is a small page with a link to Your machines, not panel copy, because the panel is gone once a key exists.

How it was verified

  • pnpm exec vitest run across packages/auth, packages/portal, packages/sdk, packages/limits, packages/core, the CLI login and the server's setup-pickup and migration tests: 491 tests pass. New ones cover:
    • confirming from the panel, and both entry points answering the same login;
    • a stale panel form once its line is contested, or for another account;
    • Not me for pending, confirmed-but-uncollected (the CLI's poll then gets nothing, so no key is made) and already-minted logins;
    • the panel rendering the form only for a live, uncontested, bound line.
  • pnpm typecheck and pnpm lint pass.
  • Screenshots above are from a dev instance (pnpm dev up team): an agent's login started with the line, answered in the panel.
  • GPT 6.1 Sol security review at xhigh, three rounds, signed off on a758add. It found clickjacking, an account-switch swap, two lock-order deadlocks (fixed in feat(auth): setup links tie the agent's fetch, login and key to the person's Final step panel #917), an expiry-during-lock-wait approval, and two ways to block Not me. All are fixed with tests, red then green where it's a race; see the review comments.
  • The account-switch reload and Not me with an emptied name were checked in Chromium on a dev instance.

Release impact

  • Breaking — requires action from anyone already running this
  • Not breaking — no action required

No schema change: an optional setup field on the device form, and new queries over #917's tables.

  • What does a revert leave behind? Nothing. A panel form open at the time posts a setup field the old route ignores, so it acts as an ordinary device confirmation.
  • Does the release before it still work with that? Yes.

Made by Claude Opus 5.5 in Claude Code, running in T3 Code.

@github-actions github-actions Bot added area:server Touches the server area area:docs Touches the docs area area:ui Portal and first-party browser pages, shared rendering and styles size:m 100-500 changed lines labels Oct 23, 2026
@allisonmahmood

Copy link
Copy Markdown
Owner Author

Security review (GPT 6.1 Sol, xhigh) on c540633: four findings, all fixed

Sol's verdict on that commit: the server-side guards on confirmation, and the serialization of Not me against the mint, hold up; the framing and account-switch gaps did not.

  1. [blocker] Clickjacking: fixed in b8d05ca. /guide/steps now carries the inline form, so it gets the device page's frame-ancestors 'none', plus form-action 'self' and base-uri 'none'. Home was already frame-ancestors 'none' through the shared page response. Asserted in PortalPages.test.ts.
  2. [blocker] An account switch under a waiting panel: fixed in b8d05ca. The panel's steps (and Home's watch region) carry the viewer they were rendered for. When a poll answers for another account, the script reloads the page instead of swapping in that account's form, before anything else (including the busy check). In Chromium on a dev instance:
    • Ben's panel was waiting while Cleo's line had a pending login, and another tab switched the browser to Cleo.
    • Ben's tab reloaded into Cleo's own page: header, panel and form all Cleo's (region viewer: usr_env_acme_cleo | form userId: usr_env_acme_cleo, page names ['Cleo Shah']).
  3. [should-fix] Lock order: fixed in feat(auth): setup links tie the agent's fetch, login and key to the person's Final step panel #917 (4a03a91), where the reversed order was introduced. start's cleanup now locks expiring logins before writing their lines, so it can't deadlock an inline Not me or a poll. Red (40P01) then green, with a controlled test.
  4. [should-fix] Not me blocked by an emptied name: fixed in b8d05ca. Not me and Deny carry formnovalidate. In Chromium, with the name cleared, Not me posted, the panel said "Nothing was connected." and the CLI's poll got denied.
After the switch: Cleo's whole page Not me with the name cleared
after switch not me empty

Checks: typecheck, lint, and packages/auth plus packages/portal (337 tests) pass. A second Sol round on the fixed head is running.

@github-actions github-actions Bot added size:l 501-1,000 changed lines and removed size:m 100-500 changed lines labels Oct 24, 2026
@allisonmahmood

Copy link
Copy Markdown
Owner Author

Security review round 2 (GPT 6.1 Sol, xhigh) on b8d05ca: three should-fixes and a nit, all fixed

Sol confirmed the round-one fixes hold, including the reload on an account change before the busy check in both panel and watch mode. It found no blocker and no way to get a key, and judged the defences against a stolen line and phishing sound, provided the person matches their agent's code. It held sign-off for these:

  1. [should-fix] A long name hint could block Not me: fixed in a758add. The hint has no length limit in the API. 1,400 &s fit the request but URL-encode past the form's 4 KiB cap, so Deny got a 413. The device page had the same problem before this PR. DeviceLogins.start now keeps the hint to the 64 characters a name may have, so every prefill fits. The device page still shows a person's own invalid name back as typed.
    • Test: DevicePages.test.ts › "keeps a long name hint to 64 characters…". It starts a login with the long hint, reads the prefill, posts Deny with it, and the poll gets denied.
  2. [should-fix] Pruning old lines locked a login after its line: fixed in feat(auth): setup links tie the agent's fetch, login and key to the person's Final step panel #917 (2455397), where the pruning lives. A line any login still names is no longer pruned. Red then green.
  3. [should-fix] Inline confirmation could approve after the line ran out while it waited for the lock: fixed in a758add. confirmInline now locks the line, then checks it in a second statement against clock_timestamp().
    • Red then green: SetupLinks.test.ts › "refuses an inline confirmation whose line ran out while it waited for it". Another transaction holds the line unchanged past its last second.
    • On the old code, the login ended up confirmed (expected false to be true). On the fix, it answers SetupLineChanged and the login stays pending.
  4. [nit] Missing viewer markers compared equal: fixed in a758add. The guide script doesn't poll a region with no data-viewer. Any reply whose viewer differs, including none, reloads the page.

Checks: typecheck, lint and 42 affected test files (491 tests) pass locally. A third Sol round is checking these for sign-off.

@allisonmahmood

Copy link
Copy Markdown
Owner Author

Security review round 3 (GPT 6.1 Sol, xhigh) on a758add: sign-off for #911's threat: yes.

Sol found no blocker or should-fix, and all four round-2 fixes hold. Besides the suites, it checked the polling script in isolation: missing markers, an account change before the busy check, form preservation, and steady polling without repeated reloads.

One nit, fixed in the next commit: cutting the hint made the start-overflow tests in AuthApi.socket.test.ts look for the full long hint, which can no longer be stored, so that assertion could never fail. They now look for any stored hint starting with the test's prefix.

Base automatically changed from feat/setup-links to main October 24, 2026 01:07
allisonmahmood and others added 5 commits October 24, 2026 03:07
With setup links the panel knows when the person's agent has started a
login, so it now shows that login's own confirmation in place: the code
to check against the agent's, the computer's suggested name, Confirm and
Not me. It is the device page's form, posting to its route with the
line; the server rechecks the line (own, live, uncontested, bound to
this login) under the login's locks before answering, and every outcome
goes back Home. Not me denies a pending login, cancels a confirmed one
whose key isn't collected, and says so when a key was already made. The
printed link keeps working.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…unt switch

/guide/steps now carries the inline confirmation, so it gets frame-ancestors 'none',
form-action 'self' and base-uri 'none' like the device page. The panel's steps carry the viewer
they were rendered for, and the guide script reloads the page when a poll answers for another
account instead of swapping that account's form under the first one's header. Not me and Deny
answer even with the name field emptied.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ng hints can't block Deny

confirmInline now locks the setup line and then checks it against a fresh clock: a wait for the
lock could outlast the line, and now() stays at the transaction's start. A terminal's name hint is
kept to the 64 characters a name may have, since it prefills the form and a longer one could push
Deny or Not me past the form's size cap. The guide script doesn't poll a region that doesn't say
which account it was rendered for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@allisonmahmood
allisonmahmood merged commit ac6a0c1 into main Oct 24, 2026
14 checks passed
@allisonmahmood
allisonmahmood deleted the feat/inline-confirm branch October 24, 2026 01:19
allisonmahmood added a commit that referenced this pull request Oct 24, 2026
## What changed

This PR adds release 18's What's new entry, written from `b23e6ad`. It
covers everything merged since release 17's `through` (`5dd94f3`), and
it sets the `patchy` release to `0.18.0` in
`packages/patchy/package.json` and `CURRENT_RELEASE`. With this release,
onboarding ends in a Final step on Home that follows the person's agent
through setup and lets them confirm it right there (#906).

| Change | PRs |
| --- | --- |
| New: A Final step to connect your AI agent | #916, #917 |
| New: Confirm your computer without leaving Home | #918 |
| Improved: Setting up ends with your first page | #914 |
| Improved: The logo takes you Home | #913 |
| Behind the scenes: the first time someone opens a colleague's tool is
counted, for activation | #915 |

One merged PR gets no line: #905 is release 17's own entry.

## Look at

- **The entry**, at the top of `releases` in
`packages/core/src/whatsNew.ts`. Its labels are the pages' own: Final
step, Later, Not me, Home, Patches.
- **Rollback safety, from the live commit `a049a3c` to `b23e6ad`.**
- No PR in the range is labelled `one-way`, and every PR answers both
Release impact questions with "Yes".
- Migrations 0026 and 0027 are additive: the `setup_prompt_sign_ins` and
`setup_links` tables, and a nullable `device_logins.setup_link_id` with
`ON DELETE SET NULL`. Release 17 never reads them.
  - `rollback-rehearsal` exercised both migrations on the landed heads:
- #916: `Exercised rollback paths passed: a049a3c → 6037d86 → a049a3c →
6037d86, in 234s.`
- #917: `Exercised rollback paths passed: a049a3c → d03a5bf → a049a3c →
d03a5bf, in 217s.`
- #918 and #915 change no migration or handler descriptor, so they had
nothing to rehearse.
- A rollback takes the Final step panel and setup links away. Home goes
back to the old guide, and a CLI at `0.18.0` stays usable against
release 17: `--setup` is only sent when `/llms.txt` prints it, and
release 17's `/llms.txt` doesn't.

## How it was verified

- `pnpm typecheck` and `pnpm lint` pass, and so do `packages/core`,
`packages/api`, `packages/portal` and the skill lock test (245 tests),
including `whatsNew.test.ts`, `schemas.test.ts` and
`WhatsNewPages.test.ts`.
- The live commit was pinned from `/healthz/deep`: `a049a3c`, release
17. The Deploy run updated last succeeded, and none is unfinished.
- Each PR in the range was read, including its Release impact answers.
All six landed with the reviewed trees: each squash left `main`'s tree
identical to its PR's reviewed head.

## Release impact

- [ ] **Breaking** — requires action from anyone already running this
- [x] **Not breaking** — no action required

This PR changes only the changelog text and the `patchy` version. As
with every release, a CLI installed before this deploy is told to
upgrade.

- **What does a revert leave behind?** Nothing. People whose seen marker
reached 18 would just see no new release.
- **Does the release before it still work with that?** Yes.

If this release misbehaves, roll back to
`a049a3cf544ae89691eec547dea46773febfdf05` (release 17).

Made by Claude Opus 5.5 in Claude Code, running in T3 Code.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Touches the docs area area:server Touches the server area area:ui Portal and first-party browser pages, shared rendering and styles size:l 501-1,000 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ui): confirm your computer inside the Final step panel

1 participant