Repository navigation
Sign in with ChatGPT in Workshop, with the Gateway config page on its own origin - #22
Draft
sublimator wants to merge 3 commits into
Draft
sublimator wants to merge 3 commits into
sublimator wants to merge 3 commits into
Conversation
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
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
|
Member
|
This needs design work and it will have to wait until we have implemented model-shaped tool catalogs and provider-specific chat APIs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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.@workshop/lookdropdown. Generic, and used bythe picker in commit 3.
current boot and presents it on model calls. It also contacts a loopback
Gateway with no proxy and no redirect.
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.
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.
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.
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.
page, reach IPC or navigate the top window, and the bridge refuses
/admin/chatgpt.Three things fail on this Mac without this change too:
Ctrl+labels (run on6b19f792: thesame 60 assertions fail there and here);
realtime_relay::overload::stalled_browser_cleanup_is_bounded_after_gateway_disconnect(run on
6b19f792);on
1d6d2697). The Workshop set was run two at a time.Short reports on each are ready if they are wanted.
Not done
origin isolation in particular needs checking there.