Skip to content

Sign in with ChatGPT in Workshop, with the Gateway config page on its own origin - #22

Draft
sublimator wants to merge 3 commits into
cppalliance:masterfrom
sublimator:sign-in-with-chatgpt
Draft

sublimator wants to merge 3 commits into
cppalliance:masterfrom
sublimator:sign-in-with-chatgpt

Conversation

@sublimator

@sublimator sublimator commented Nov 14, 2026 •

Copy link
Copy Markdown

Draft, for reference. One of three drafts: cppalliance/runvil-harness#14,
cppalliance/runvil-gateway#35 and this one. It needs both: it pins the Harness draft's
commit and talks to the Gateway draft's routes. Use it or not.

The UI is a placeholder

What is here is the least UI that makes the flow usable and testable. It is
text only and it is not a design proposal:

  • a Sign in with ChatGPT row in the model picker when signed out;
  • the plan's models under one ChatGPT plan heading in the picker;
  • a ChatGPT plan item in the status bar, left of the meter and LEDs.
    Its popup has one button for the current state, such as Sign out.

Not built, and left for whoever owns the UI. OpenAI's guidelines
(https://developers.openai.com/siwc/ui-ux-guidelines) ask for three things:
the wording "Using ChatGPT plan", a "Manage usage" link, and a one-time
welcome after the first sign-in. There is no logo: OpenAI's terms say to
use its logos only as authorized, and that is not settled for this tool. Nothing was modelled on Cursor.

What it adds underneath

Three commits on 6b19f792.

  1. Group headings in the @workshop/look dropdown. Generic, and used by
    the picker in commit 3.
  2. The plan proof. Workshop reads the proof the Gateway publishes for its
    current boot and presents it on model calls. It also contacts a loopback
    Gateway with no proxy and no redirect.
  3. The sign-in, and the config page's own origin.
    • Native account commands (status, sign in, sign out, reset, enable
      models). The owner proof they use stays in the native side and never
      reaches the page. The commands answer only to a capability the main
      page binds when it loads. A reload gets a new one.
    • The Gateway config page was framed from the same origin as the page
      that can call those commands. It is now served from a second loopback
      listener and framed with a sandbox. The bridge takes messages only from
      that origin and that frame.

The origin change is in the same commit as the commands on purpose.
Startup holds every config frame until the page's bind attempt has settled,
and a reload of the main page clears the capability. Those orderings span
both parts. The commit
message and the plan note list the origin change's files, for anyone who
wants it alone.

The note is plan/2026-11-14-1-sign-in-with-chatgpt.plan.md.

How it was checked

On macOS arm64 only.

  • Every command in AGENTS.md, CI's page job, cargo deny, cargo audit.
    Tests: 718 of 718 outside Workshop's own crates, 358 of 359 inside them,
    and 213 of 223 page tests. The failures are the three things listed
    below.
  • Each commit passes the checks for what it touches alone.
  • The whole flow in the real window, driven without hands against a fake
    OpenAI. It signs in from the picker, sees the models appear, does one chat
    round, signs out, sees the models vanish and signs in again. The driver is
    a debug-only hook that is not in this branch, and it skips the system
    browser. Also used by hand with my own plan, through the real browser.
  • 26 checks in the real window around the config frame. It cannot read the
    page, reach IPC or navigate the top window, and the bridge refuses
    /admin/chatgpt.

Three things fail on this Mac without this change too:

  • nine page test files that assert Ctrl+ labels (run on 6b19f792: the
    same 60 assertions fail there and here);
  • realtime_relay::overload::stalled_browser_cleanup_is_bounded_after_gateway_disconnect
    (run on 6b19f792);
  • at full parallelism, most of the eleven tests that start a Gateway (run
    on 1d6d2697). The Workshop set was run two at a time.

Short reports on each are ready if they are wanted.

Not done

  • Windows and WebView2 were not run at all, nor a packaged build. The
    origin isolation in particular needs checking there.
  • The Harness pin names a commit that exists only in the Harness draft.

A dropdown menu was a flat list of items. `DropdownGroup` is an entry that holds a heading and its own items, so a menu can name a set of items once instead of tagging each one.

- A group renders as `.menu-group[role="group"]` with an `aria-label`, and its heading as `.menu-heading`.
- The heading is not an item, so the arrow keys skip it. It is hidden from assistive technology, which reads the group's label instead. A group with no items renders nothing.
- `dropdown.css` styles the heading from `@workshop/look` tokens.
- Nothing uses a group in this commit. The model picker uses one in the commit that adds the ChatGPT sign-in.
- `test/workshop-dropdown.mjs` pins the markup and the arrow-key order across a group.

Plan: /plan/2026-11-14-1-sign-in-with-chatgpt.plan.md
The ChatGPT plan extension proposed for `runvil-gateway` lists and serves a plan model only to a caller that presents the plan proof of the Gateway's current boot. The Gateway writes that proof to `gateway-private.json`, beside the discovery file. Workshop now reads it and sends it as `X-Runvil-Plan-Proof` on the model list and on model rounds. Without the file, or with a file that names another boot, Workshop sends what it sent before.

- `workshop-gateway-discovery`: `GatewayPrivateFile::read` reads and validates the file, and `names_boot_of` pairs it with `gateway.json` by `pid` and `epoch`. `Debug` prints `[REDACTED]` for both proofs. `CONTRACT.md` describes the file.
- `workshop-gateway`: a `ResolvedGateway` and a `GatewaySnapshot` carry the proof of the boot they name, or none. `GatewayClient` sends it on `GET /v1/models` and on no other request.
- `workshop-server`: `WorkshopBroker` passes the proof to the Harness's `GatewayBroker`, which sends it on model rounds. `one_plan_round` and `plan_model_ids`, behind `test-fixtures`, let a test list the models and run a round through that broker.
- The four Harness crates move to the commit that adds `GatewayBroker::with_plan_proof`.
- For a loopback base URL, `GatewayClient` builds its HTTP client with no proxy and no redirect, so the bearer key and the proof cannot leave the machine through `HTTP_PROXY` or a redirect. Any other URL keeps reqwest's default proxy and redirect policy.
- `workshop_support::is_loopback_http_url` is the one predicate for an http or https URL whose host is `localhost`, in `127.0.0.0/8`, or `::1`. `GatewayClient` and the server's cross-site guard each had a copy. The client's copy looked at the host alone, so it also accepted `ws://127.0.0.1`. Its caller passes an http or https API root.
- `tests/loopback_direct.rs` runs the model list in a child process with `HTTP_PROXY` and `ALL_PROXY` set to a counting listener, after a plain reqwest client proves the listener is consulted, and gives it a redirect whose target must receive nothing.

Plan: /plan/2026-11-14-1-sign-in-with-chatgpt.plan.md
Workshop had no way to sign a person in to their ChatGPT account. The Gateway holds that credential and manages the sign-in only for a caller that presents its owner proof. That proof must not reach the page, so the calls are native commands, and the page can call them only with a capability it bound when it loaded. The Gateway config page was framed from the page's own origin, where a script has the page's DOM and its IPC. It moves to its own origin, so it cannot reach those commands. The page side is one status bar item and two kinds of picker row, all text.

The account:

- `workshop-gateway`: `ChatgptAccount` makes the five `/admin/chatgpt` calls. Each call reads `gateway.json` and `gateway-private.json` again and checks that they name one boot. The request goes to `127.0.0.1` with no proxy and no redirect. A call returns the Gateway's status JSON, or for `enable_models` the status and body, as received. `ChatgptAccountError` is `Copy` and holds closed categories and integers only.
- The desktop app: `chatgpt_bind_capability` accepts the first non-empty capability while none is bound. `chatgpt_status`, `chatgpt_sign_in`, `chatgpt_sign_out`, `chatgpt_reset`, and `chatgpt_enable` require it. `chatgpt_sign_in` opens the system browser itself, and the authorization URL is in no reply.
- `AccountView` is what the page gets. The Gateway's reply is read into typed fields, so an unknown field is dropped. An opener error is discarded, and a JSON or transport failure reaches the page as a category.
- The server's config forwarder refuses `/admin/chatgpt` for every method. Its relay appends a `chatgpt_` error code to the failure label, and a `refresh_models` socket frame re-reads the catalog.

The page:

- `chatgpt-account.ts` seals the capability in a closure without replacing `invoke`, so a frozen `invoke` does not stop startup. It polls once a second only while a sign-in is waiting, enables the plan's models once per flow when the plan is granted and no plan model is listed, and drops a late reply from an older flow.
- The status bar gets a `ChatGPT plan` item, to the left of the meter and the LEDs. Its popup has one action for the current state.
- The model picker gets a `Sign in with ChatGPT` row when signed out, and lists the plan's models under one `ChatGPT plan` heading.
- A round that fails with a `chatgpt_` code makes the service read the status again and refresh the models.

The config page's origin:

- A second listener on `127.0.0.1:0` serves only the proxied Gateway config documents, for `GET` and `HEAD` with a loopback `Host`. The main listener answers `404` on the old path and gains `GET /gateway/config-origin`.
- The main page may frame only the config origin and may be framed by nothing. The config page may be framed only by the main origin, and its policy has no IPC source.
- The window's IPC grant names the main origin alone. The config origin is allowed only at the panel's URL, and a top-level load of any other http or https origin is opened in the system browser while the window returns to the main page.
- A top-level load of the main page clears the capability, and startup waits for the new bind attempt to settle before it restores the layout. So no config frame exists while a bind is in flight. A bind that is refused leaves the account commands refusing.
- The frame has `sandbox="allow-scripts allow-same-origin"` and no top-navigation permission. When the config origin cannot be read, no frame is mounted.
- The bridge takes a message only when its origin is the config origin and its source is the live frame, and it checks both again before it replies.
- Shutdown waits for both listeners and the registered cleanup. Its watchdog can still force termination after the grace period.

Not built, and left for whoever owns the UI: the wording `Using ChatGPT plan`, a `Manage usage` link, and a one-time welcome after the first sign-in, which OpenAI's guidelines ask for (<https://developers.openai.com/siwc/ui-ux-guidelines>), and any logo.

Not run: Windows, WebView2, and a packaged build.

Plan: /plan/2026-11-14-1-sign-in-with-chatgpt.plan.md
@coderabbitai

coderabbitai Bot commented Nov 14, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@vinniefalco

Copy link
Copy Markdown
Member

This needs design work and it will have to wait until we have implemented model-shaped tool catalogs and provider-specific chat APIs

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