Repository navigation
Sign in with ChatGPT: serve a person's ChatGPT plan models to Workshop - #35
Draft
sublimator wants to merge 6 commits into
Draft
sublimator wants to merge 6 commits into
sublimator wants to merge 6 commits into
Conversation
|
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
|
sublimator
force-pushed
the
sign-in-with-chatgpt
branch
2 times, most recently
from
November 14, 2026 08:30
3f43d4e to
37d322e
Compare
On macOS, `cargo clippy --locked --workspace --all-targets --all-features` fails with `clippy::large_enum_variant` on `TraySlot`: `Armed` holds a box, and `Headless` held a `GatewayHandle` inline. `Headless` now holds a box too. - CI lints on Linux, where `tray/macos.rs` is not compiled, so the job did not see it. - `run_headless` takes the handle by value as before. The one caller dereferences the box.
A caller always speaks chat completions to the Gateway, and every backend spoke chat completions too. `ResponsesUpstream` is a second implementation of `Upstream`: it rewrites the request as an OpenAI Responses request, always asks the backend to stream, and translates the events back to chat chunks. A caller that did not ask for a stream gets the same chunks folded into one completion. No endpoint selects it yet. The config vocabulary for it comes with the ChatGPT plan routes. - `wire::to_responses_request` builds the request. Its module doc holds the field mapping. A field that only tunes the reply and has no Responses equivalent is dropped. A request whose meaning would change is refused with the new `ProtocolError::UnsupportedRequest` before anything is sent: `n` above 1, a `function` message, or a tool that is not a function. - The request never asks the provider to store the response. - `upstream/responses/` holds the SSE reader and the translator. Text and tool-call arguments are relayed as their deltas arrive. Each `output_item.done` is checked against what was streamed: argument text that never arrived is sent then, and arguments that contradict what was sent fail the response. - A failure the backend reports inside its event stream becomes a `ProtocolError::UpstreamStatus`. It arrives as a `response.failed` event, or as an `error` event with its details as its own fields or nested under `error`. A `code` that names a limit, a quota or a rate maps to 429, an `invalid_request_error` to 400, anything else to 502. - `BearerSource` supplies the credential for each request, and `rejected` tells it that the backend answered one with a 401. `StaticBearer` wraps a fixed secret. `ProtocolError::CredentialUnavailable` is what a source returns when it has none, with a stable `code`. - `http_util::hardened_client` builds a client that follows no redirect, with the timeouts it is given. It returns `None` when the client cannot be built and never falls back to `reqwest::Client::default()`. `http_util::no_redirect_streaming_client` is that client for a streaming backend, and `ResponsesUpstream` refuses each request when it holds `None`. - One change reaches the existing backends: the display of `ProtocolError::UpstreamStatus` now ends with the backend's error `code` when the body has one that is a plain identifier of at most 64 characters, as in `upstream returned 400 (context_length_exceeded)`. The backend's message text is still never shown. - `upstream/openai.rs` shares `check_status` and `escape_control` with the new module. Plan: vibe/2026-11-14-1-sign-in-with-chatgpt.md
`gateway.json` is the discovery contract: any client reads it to attach, and an operator hands its key to other tools. The ChatGPT plan routes need two values that only Runvil's own programs may read. `GatewayPrivateFile` is `gateway-private.json`, written beside the discovery file and named by no public contract. - The file holds `pid`, `epoch`, `plan_proof`, and `owner_proof`. `pid` and `epoch` are the discovery file's, and `names_boot_of` pairs the two files only when both agree, so a proof of an earlier boot is not sent to a later one. - `write_to` uses the discovery file's atomic, owner-only writer. `remove_private_if_mine` withdraws the file at shutdown when the `pid` is this process's. - `read` returns `None` for a missing file and an error for one it cannot read, parse, or validate. `validation_error` refuses a `pid` of 0, an empty proof, a proof that is not printable ASCII without spaces, and two proofs that are equal. - `Debug` prints `[REDACTED]` for both proofs. - Nothing writes the file yet. The Gateway app mints the proofs and writes it with the ChatGPT plan routes. Plan: vibe/2026-11-14-1-sign-in-with-chatgpt.md
The crate holds a person's Sign in with ChatGPT credential inside the Gateway process. It follows OpenAI's documented flow for open-source tools, <https://developers.openai.com/siwc/token-sharing-open-source>, and each module names the page and section it follows. No crate depends on it yet. - `callback` is a one-shot listener on `127.0.0.1:0` for `GET /auth/callback`. Its page is a fixed sentence, and nothing from the request is written to the page or to a log. - `id_token` verifies RS256 against the issuer's JWKS, and then checks `iss`, `aud`, `exp` with 60 seconds of skew, `nonce`, `at_hash`, and `sub`, in that order. - `oauth` makes the dynamic registration, the PKCE authorization URL, the code exchange, the refresh, and the revocation, over bounded requests that follow no redirect. - `plan` is the broker, `ChatGptPlan`. It serializes sign-in, completion, refresh, sign-out, forget, and reset, caches the access bearer until 120 seconds before it expires, and implements `BearerSource`. - The broker's status reports a sign-in as pending from the start of an attempt until that attempt's own work is finished, the exchange, the activation, and the revocation of a token it will not keep. So a client that polls does not read "nothing pending" in between and stop waiting. A sign-out, a reset, or a newer attempt ends the pending state at once, and an attempt whose callback never arrives stops being pending when it expires. - `store` is the `chatgpt-plan.json` record. It is written through a fresh `0600` temp file and renamed, in a directory that must be private to the user. A corrupt record is reported and never guessed at. On Windows the store refuses to save. - `fake_issuer`, which exists in tests and behind the `test-fixtures` feature, is an issuer the tests sign in to. It serves the discovery document, JWKS, the token endpoint, and revocation. - `aws-lc-rs` becomes a workspace dependency, for the RS256 check and for random bytes. It is the crypto backend the Gateway already links, and `cargo tree -i ring` still prints nothing. - `Cargo.lock` gains the crate and `untrusted` 0.7.1, which `aws-lc-rs` needs. Plan: vibe/2026-11-14-1-sign-in-with-chatgpt.md
`PUT /admin/config` replaced the configuration with the document it was given, whatever the file held by then. A settings page that was left open could so overwrite a file that had changed since the page read it. A save now names the revision it was read from, and the Gateway refuses one that is missing or stale. - The revision is the lowercase hex SHA-256 of the file's bytes. `GET /admin/config` returns it in `ETag` and in `x-config-revision` when the configuration came from a file. `GET /admin/config-pending` returns it in both headers and in a `revision` field. - `PUT /admin/config` takes it as `revision` in the body, as `x-config-revision`, or as `If-Match`. With none it returns `428 config_revision_required` and stages nothing. With one that does not match the bytes it would replace it returns `409 config_conflict`. A malformed one, or two carriers that disagree, returns `422`. - A save is compared with the staged draft when there is one, and with the file otherwise. - This changes an existing route. Every client that saves the configuration has to send the revision back. - The settings page does. `config-store.ts` keeps the revision it loaded and sends it with each save. On a `409` it tells the person to reload the page. - The check covers writers that go through the Gateway. It is not a lock against another program that edits the file between the check and the rename. - The second writer in the new tests is a ChatGPT plan entry written to the file. That is the writer the next commit adds: its model setup changes the configuration under the apply lock, and this check stops a page that was open from overwriting the result. - `crates/gateway/README.md` states the contract under "Saving the configuration". Plan: vibe/2026-11-14-1-sign-in-with-chatgpt.md
An endpoint's credential was an API key from the configuration. `auth = "chatgpt-plan"` is a second kind: the Gateway signs a person in to their ChatGPT account, keeps the credential to itself, and serves the plan's models to a caller that presents the plan proof of this boot on top of its ordinary authentication. Every other endpoint keeps `auth = "api-key"`, and a client that names no plan model sees the routes it saw before. A person authorizes one tool to use their plan, so the Gateway serves it to Runvil's own client and does not become a general relay for it. `crates/gateway/README.md` states the contract under "The ChatGPT plan". - `gateway-config`: the vocabulary gains `protocol = "openai-responses"` and `auth = "chatgpt-plan"`. The second requires the first and allows neither `base_url` nor `api_key`. - `gateway-routing`: an endpoint records where its credential comes from, as `CredentialSource`. A request is authorized by that, never by a name. - At start the app mints the owner proof and the plan proof and writes `gateway-private.json`. It withdraws the file at shutdown. A Gateway that shuts down to restart keeps its discovery file for its supervisor and still removes its proofs file: the next child mints its own. - `/admin/chatgpt/sign-in`, `status`, `sign-out`, `forget`, and `reset` take `OwnerCaller`: a loopback peer, ordinary authentication, and the owner proof. No route returns a token. The sign-in exists only on a Gateway that listens on loopback. - The routing table's entry for a plan model holds an upstream that refuses. `ChatGpt::authorize_plan` hands out the one that spends the plan, after it checks the listener, the proof, the broker, and the sign-in. `resolve_routed_model` applies it for chat, embeddings, rerank, and speech. A request without the proof gets `403 chatgpt_plan_runvil_only`, and `GET /v1/models` omits the plan's rows for that caller. - `/admin/chatgpt/models/enable` and `models/forget` add the plan's endpoint and models to `gateway.toml` and to live routing, and remove them. An enable commits only if the sign-in it started under still holds. - The settings page shows a plan endpoint with a badge and without `base_url` and `api_key`, and skips it in a cloud merge. It makes no `/admin/chatgpt` request: the owner proof never reaches a page. - `RUNVIL_CHATGPT_AGENT_NAME` changes the name the consent screen shows. - Not done: the store refuses to save a credential on Windows, so a sign-in does not persist there. The older admin routes do not require the owner proof, so a caller that may save the configuration can still add or remove the plan's entries. It cannot give them a `base_url` or a key, and it cannot use them without the plan proof. Plan: vibe/2026-11-14-1-sign-in-with-chatgpt.md
sublimator
force-pushed
the
sign-in-with-chatgpt
branch
from
November 14, 2026 08:44
37d322e to
653a423
Compare
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, this one and
cppalliance/runvil-workshop#22. Use it or not.
It lets a person sign in with their ChatGPT account and use their plan's
models from Workshop, through OpenAI's documented flow for open-source tools
(https://developers.openai.com/siwc/token-sharing-open-source). The
Gateway is the only process that ever holds the tokens.
Read these three things first
PUT /admin/configmust nowname the revision it was read from. With none it returns
428 config_revision_required, and with a stale one409. Every clientthat saves the config has to change. The settings page is changed here.
It is its own commit, so it can be judged alone.
a credential, so a sign-in does not persist there. Only macOS was run.
/admin/chatgptroutes need the owner proof. The older admin routes do not. So a caller
that may save the config can add or remove the plan's entries. It cannot
point them elsewhere, and it cannot use them without the plan proof.
What it adds
Six commits on
fe136b4. The first five each stand alone.tray/macos.rs.fe136b4failslarge_enum_variantthere. CI lints on Linux, so it did not show. It isindependent of the rest.
gateway-protocol. A Responses upstream, because the plan endpointspeaks only the Responses API, and a bearer source for a credential that
has a lifetime.
gateway-api-discovery.gateway-private.json, a private file fortwo proofs the Gateway mints at each start.
gateway-chatgpt-plan, a new crate. Sign-in (PKCE, a one-shotloopback callback), ID-token checks, an owner-only credential file,
refresh with rotation, sign-out with revocation. Each module names the
OpenAI page and section it follows.
a second writer of
gateway.toml. Without the check, a settings pageleft open could overwrite that result with a later save.
/admin/chatgpt/(status,sign-in,sign-out,forget,reset,models/enable,models/forget). Theyneed the owner proof on top of ordinary authentication. Sign-in exists
only on a Gateway that listens on loopback.
X-Runvil-Plan-Proof. Anyone else gets403 chatgpt_plan_runvil_only.OpenAI's terms restrict a plan to the application its user connected
(https://openai.com/policies/sign-in-with-chatgpt-terms/, section 3.1):
"Use the user's plan only for the application they connected. Do not
provide general-purpose API access for other tools or unrelated
requests." The plan proof is how the Gateway keeps to that.
makes no
/admin/chatgptcall, so the owner proof never reaches apage. Sign-in is driven from Workshop.
The contracts are in
crates/gateway/README.mdunder "The ChatGPT plan" and"Saving the configuration". The note is
vibe/2026-11-14-1-sign-in-with-chatgpt.md.How it was checked
On macOS arm64 only.
check, tidy, docs,
cargo deny, and the settings page's typecheck andtests (218 of 218). Also
cargo audit.crate's tests alone. Commit 5 passes the whole list alone.
named test. The rules are in the gate, the ID-token checks, the fences
around sign-in and refresh, the store, the config revision and the
Responses translation. The runner is outside this repo.
and sign-out with confirmed revocation both work. So do the model list
with and without the proof, a streamed tool round and a token refresh.
The sign-in survives a Gateway restart.
Not done
covered against a fake issuer only.