Skip to content

Accept brokered provider credentials - #177

Merged
CoreyShupe merged 30 commits into
mainfrom
feat/brokered-provider-credentials
Oct 1, 2026
Merged

CoreyShupe merged 30 commits into
mainfrom
feat/brokered-provider-credentials

Conversation

@QuillDev

Copy link
Copy Markdown
Owner

What

Adds a brokered OAuth credential kind, oauth_brokered, for Claude and Codex accounts whose sign-in is owned by an external broker (the FStack hosted dashboard).

  • A brokered credential carries an access token, its expiry and optional account/organization identity. It never includes a refresh token.
  • Nakode never refreshes a brokered credential. If it expires before the broker supplies a new one, the request fails with an error prefixed credential_refresh_required.
  • SetProviderAccountCredential with a new brokered token reaches live Claude and Codex sessions through UpdateCredential, so a token replacement doesn't interrupt running work.
  • Codex derives the account ID from the access token's JWT when the broker doesn't send one.
  • Documented in docs/provider-account-routing.md.

Why

Providers rotate refresh tokens, so two refreshers invalidate each other. With a broker as the only refresher, every machine and VM can share one account sign-in without anyone signing in on each machine.

Checks

  • cargo fmt and Clippy (pedantic, -D warnings) are clean.
  • cargo test --all-targets --all-features passes except tools::tests::bash_skips_unchanged_successful_validation_until_reason_or_state_change, which also fails on main.

Not for merge yet. FStack is testing this locally first.

@QuillDev
QuillDev force-pushed the feat/brokered-provider-credentials branch from b7ae591 to 143356d Compare September 23, 2026 18:51
A broker that serves many machines from one sign-in, such as the FStack
dashboard, can now supply Claude and ChatGPT access tokens through
SetProviderAccountCredential with kind oauth_brokered. Providers rotate
refresh tokens, so the broker must be the only refresher: a brokered
credential carries no refresh token, Nakode never refreshes it, and once it
expires commands fail with a message beginning credential_refresh_required.

Each adapter validates its own brokered shape. Replacement credentials now
reach running Claude sessions in place through UpdateCredential, as they
already did for ChatGPT, so a periodic token replacement never interrupts
work.

cargo fmt and clippy (pedantic, -D warnings) are clean. The all-target suite
passes except tools::tests::bash_skips_unchanged_successful_validation_until_
reason_or_state_change, which fails identically on origin/main.
With several eligible accounts and none in automatic routing, session
creation refused outright even though one account was the owner's default.
A machine holding its own sign-in beside a brokered account could not start
a Claude session at all. The eligible default is now the choice; without one
the owner must still pick.
A session no longer belongs to a provider account. Where a turn begins the
runtime keeps the session's last or requested account while it is eligible
and otherwise routes to any eligible one; commands inside a running turn stay
on the account running it. Codex, Claude, Kimi and GLM take the new account's
credential into the live handle, and a start or resume loads into a fresh
handle on it. sessions.account_id records where a session last ran and is no
longer enforced: the affinity conflicts, the historical-session refusal and
the in-use account refusal are gone, and removing an account frees its
sessions. Codex reasoning is tagged with the ChatGPT account that produced it
and replayed only to that account, since its encrypted content opens for no
other.
@QuillDev
QuillDev force-pushed the feat/brokered-provider-credentials branch from b6f1dc0 to 765701c Compare September 23, 2026 22:21
QuillDev and others added 25 commits September 23, 2026 18:36
Every backend event of an ordinary session logged "durable inbound replay
deferred" because the missing bridge record surfaced as an error.
The Claude SDK owns Claude history, so after a restart a read of a session
that is not loaded had nothing to project and failed until the session was
reopened. The bridge now leaves its history after every turn and resume,
and the retained read projects it.
Account variables such as GH_TOKEN and the GitHub credential helper reached
only Nakode's own shell tools; Claude's Bash ran with the bridge's
environment, so gh and git push could not authenticate.
One short-lived bridge reads each snapshot-less Claude session's transcript
without resuming it, so sessions from before snapshots existed read back
too. An unreadable transcript keeps no snapshot rather than an empty one.
Every mutation, including each streamed chunk, cleared the prefix proofs,
and the next page read serialized and hashed every entry again. Entry
digests are now kept beside the exact entry they came from and reused while
it is unchanged; each proof chains the previous one with the next entry's
digest. A rebuilt unloaded session reuses its last digests. Proof values
change once (v2 tag); clients only compare them.
History rebuilt from the Claude SDK kept a call's input and output only in
its body, so resumed and unloaded transcripts showed raw JSON instead of the
command, edit and search rows live calls get.
History rebuilt from the Claude SDK had no provider, so restored thinking
fell back to a generic notice instead of the thinking trace a live turn
shows. Owner messages stay unattributed.
Where the machine gives agents a desktop (DISPLAY, xdotool, ffmpeg), three
tools appear: desktop_screenshot, desktop_action (click, drag, scroll,
type, keys) and screen_record. Recordings drop repeated frames, hold the
last one, and compress to H.264 within 8 MiB into the stack Gallery with a
poster frame. Claude sees each screenshot, including one after every
action; native tools save it for the vision tool.
The bridge always launched `claude` from PATH, so a machine without Claude
Code installed, such as an environment VM, listed no Claude models and
could not start a turn. It now uses a configured or installed Claude Code
and otherwise the platform build the Agent SDK installed.
…r-credentials

# Conflicts:
#	docs/coordination-messages.md
Claude Code offers one model twice, with its standard and its 1M-token context window (opus and
opus[1m]); names leave the window out, so both read "Opus 5.5". Where names would repeat, the row
whose identifier asks for a window now says so: "Opus 5.5 (1M context)".
A Chat on a machine and the agent it started inside a stack VM run in two
runtimes, so no session_child_links row joins them and the child's final
report never reaches the parent. AdmitExternalChildReport lets the
authenticated integration that recorded the relationship relay the report:
it enters the parent's durable inbox exactly as a linked child's report
does (durable_child_evidence, inert, never owner instruction), deduplicated
by message identity, and the idle parent is activated to process it. The
runtime advertises ExternalChildReports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parent in another runtime needs the same report a linked parent receives.
The runtime already captures each terminal turn's bounded completion and
persists it with the session; keep it on LastTurn and project it as
last_turn.completion through the protocol view and gRPC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admit a child's report relayed from another runtime as child evidence
@CoreyShupe
CoreyShupe marked this pull request as ready for review October 1, 2026 17:55
@CoreyShupe
CoreyShupe merged commit 2a2edda into main Oct 1, 2026
4 checks passed
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