Skip to content
Coming, not released. Built and audited, with a soak running and a release decision from 2026-11-07. The Alice and Bob nodes named in its rollout were removed on 2026-10-31.

ADR-010: Connecting assistant apps with sign-in and Allow

  • Status: Accepted on 2026-10-31. The owner accepted every recommended answer in “Decisions for the owner”.
  • Date: 2026-10-31

The owner wants other people, starting with his sister who uses the Muse AI app, to onboard without a terminal.

Today’s last step is “paste one line into your agent” (ADR-008, ADR-007 onboarding). It suits agents that can make POST requests and keep a secret. A consumer assistant app has nowhere to paste a line.

ChatGPT and the Claude apps connect to remote MCP servers through an OAuth sign-in: the user adds a server, signs in in a browser window, taps Allow, and the app keeps tokens the user never sees. Which plans allow it, which redirect addresses each vendor uses, and whether each registers dynamically are unverified.

Muse. An unofficial third-party directory says the following. None of it is verified.

  • The consumer app has no setting for a custom MCP URL. Its connectors come from Meta’s reviewed catalog; developers submit an MCP server URL or API at muse.ai/platform. A “custom connector from a service’s own API” exists, with an unknown auth method.
  • Muse Code supports remote MCP through mcp_servers in ~/.config/muse/settings.json.

This ADR does not depend on Muse. It makes Stelloz a standard OAuth service, so any app that follows the MCP authorization model can connect.

The node becomes an OAuth 2.1 authorization server for its own remote MCP endpoint. The app sends the owner to the dashboard, the owner picks or creates an agent and taps Allow, and the app gets tokens. Join links and alc_ credentials stay.

The node follows the MCP authorization model. The build confirms the current MCP text first.

  • Protected resource metadata (RFC 9728). /mcp answers a missing or bad token with 401 and a WWW-Authenticate header whose resource_metadata points at /.well-known/oauth-protected-resource.
  • Authorization server metadata (RFC 8414). /.well-known/oauth-authorization-server lists GET /authorize (the consent screen), POST /oauth/token, POST /oauth/register and POST /oauth/revoke (RFC 7009). It sets code_challenge_methods_supported to ["S256"], authorization_response_iss_parameter_supported to true, and client_id_metadata_document_supported to true.
  • PKCE (RFC 7636), S256 only, for every client. Only authorization_code and refresh_token grants exist. A client that lists other grant types (Claude’s document lists jwt-bearer) has them ignored; it must list authorization_code. At /oauth/token the client_id may come from the form, from HTTP Basic auth or, when left out, from the code’s or refresh token’s own grant; a different explicit one is invalid_client. client_assertion is ignored.
  • Resource indicator (RFC 8707). Tokens are for this node’s /mcp and agent routes only.
  • Redirects (RFC 8252). Exact match for https addresses. Loopback http on 127.0.0.1 or [::1] matches any port, because native apps pick one at run time. A native app’s custom scheme (such as com.example.app:/callback) matches exactly. No wildcards. Responses carry state and iss (RFC 9207). Errors before the redirect is validated (unknown client, bad redirect) show on the node’s own page and never redirect.
  • Cross-origin policy. The metadata and /oauth/* token endpoints answer any Origin and use no cookies. /authorize and the consent POST are same-origin only and refuse framing. The sign-in pages (/authorize, /connect-app/, /login/) send Cross-Origin-Opener-Policy: unsafe-none, so a host’s sign-in popup keeps its link to the host; every other response keeps same-origin. /mcp refuses a disallowed Origin and accepts only bearer tokens.

Clients are known in three ways. All are public, with no client secret.

  • Dynamic registration (RFC 7591). The name is the app’s own claim, shown as unverified. A request that names client_secret_post, client_secret_basic or private_key_jwt is registered as a public client and answered with token_endpoint_auth_method: none (RFC 7591 section 3.2.1); a request that carries a client_secret is refused.
  • Client ID Metadata Documents. The client_id is an https address of a document naming the app and its redirects. The reviewer recalls them in the MCP text of 2025-11-25; to confirm in rollout step 1. The node fetches it safely: https, public addresses, no redirects, size and time caps.
  • Pre-registered clients. For catalog listings, the operator adds a verified client by hand, with its own name and logo.

The node never fetches or shows a logo from registration or a metadata document; only an operator-added logo appears.

Token lifetimes.

  • Authorization code: 60 seconds, single use, bound to the PKCE challenge, redirect and client.
  • Access token (ala_): 15 minutes, as the agent routes already accept, with the client and grant added.
  • Refresh token (alr_): 30 days idle, 180 days absolute. It rotates on every use.

/mcp accepts ala_ tokens as well as alc_ credentials. After authentication nothing changes: scopes, mode, grants, approvals, rate limits and audit.

It is a dashboard page in the owner’s session. A signed-out owner signs in and returns to the same request. On a sealed node they must also be unlocked: password entered in this tab (ADR-007 A1).

The node holds the request, not the browser. GET /authorize validates it all (client, redirect, PKCE challenge and method, scopes, resource, state) and stores it as a request row with a random ID, single use, expiring in 10 minutes. The page carries only that ID. Allow is a POST with a CSRF token that names the request by ID and adds the owner’s choices: the agent (or a new username) and the scope switches. The node takes client, redirect, challenge and resource from the stored row, treats the switches as a subset of the requested scopes, and mints the grant ID. Nothing security-relevant comes from the browser’s POST.

Muse wants to act as your agent on Stelloz. Muse says its name is “Muse”. We have not checked that. You will return to muse.ai. Muse, and the company behind it (Meta), will see everything your agent sees through this connection, including your messages. They become part of who you trust with this agent.

  1. Which agent. Create a new agent, with a username field that checks availability, or use an agent I already have.
  2. What Muse will be able to do, one plain line per scope group (section 4), each with a switch. On by default: everything except asking for approvals and running actions.
  3. What stays yours. “Your agent’s mode and sharing settings still apply, and anything consequential still waits for you. Disconnect Muse any time under Your agents, Connected apps.”
  4. Allow or Cancel (which sends access_denied).

A new agent is created as a join link creates it (ADR-008): active at once, in Assisted mode. For an unverified app, “create a new agent” is the default, so a lookalike app starts with an empty address book. The owner never sees a credential. For an unverified app the vendor line says “the company behind this app”.

Today an agent’s keyring is wrapped under a secret inside its credential. The node stores a hash of that secret and the wrapped keyring, and unlocks it per request (ADR-007 A2). An OAuth client holds tokens, not a credential, yet the node must still store nothing that opens the keyring alone.

Two facts shape the answer. Only the owner’s unlocked browser can open the owner copy of a keyring, and an OAuth client gets one secret from consent: the code.

A. Code carries a secret, refresh token rotates it B. Code carries a static credential C. Node holds the wrap key D. Owner-online delegation
Node stores anything that opens the keyring alone No No Yes No
Works when the owner is away Yes Yes Yes No
Stolen refresh token Stale at next refresh, reuse detected Works until revoked, unnoticed As B, and the operator can use it n/a

A: the code is a one-time secret, swapped for a refresh secret at each exchange. B: the browser mints an ordinary credential whose secret rides in the code and is the refresh token. C: the node wraps the keyring under its own key. D: the owner’s browser re-wraps short-lived copies while it is open.

Recommended: A. C breaks the Stage A promise. D fails when the owner is away. B is the fallback.

How A works. Formats follow the Stage A build plan docs/world/stage-a-build-plan.md, section 2; new salts start with AL1-OAUTH-. The keyring is wrapped once, under a random per-grant key K_g. Only K_g is resealed later.

  1. The node creates the agent (new agent only). It generates the keys as a join link does, seals the keyring to the owner copy only, publishes the bundle in the same request, and drops the keyring from memory. The node then holds nothing that opens it. The agent-create POST and the keyring-blob POST (step 2) both reference the same stored authorize request (section 2); either is refused if it is gone, used, or another owner’s. Code: create (agents.ts:103) refuses on a sealed node. A new createForConsent calls insert and start and issues no credential; the holdFresh microtask drop (:196) is unchanged. issueSealedCredential (:457) is not used and still throws sealed_unsupported. A new method stores the grant’s keyring row from the browser’s blob. An agent whose consent is unfinished after 10 minutes is deleted. A credential row of kind oauth is refused at /v1/agent-auth/token, so a grant is reachable only through /oauth/token.
  2. The owner’s browser wraps it. It opens the owner copy with the owner key in memory, draws K_g and 32 random bytes c, wraps the keyring as an ak1. blob under K_g (grant ID in the AAD), and seals K_g under a key from c. The node records a grant (a credential row of kind oauth) with the blob, the sealed K_g and a pending code keyed by codeId. It never sees c or K_g.
  3. Redirect. The app gets code = alo_ plus b64u(c).
  4. First exchange. The app posts the code and PKCE verifier. The node checks the binding, unseals K_g with c, draws r, stores K_g sealed under a key from r as generation 1, and deletes the c-sealed copy. It stores no r. It keeps a 30-second replay copy of the response sealed under a key from c, so a lost first response needs no re-consent. c travels in the redirect URL, so the replay path MUST re-check the PKCE verifier, client_id and redirect_uri against the stored authorize binding before it returns the copy. A code reused after its 30 seconds revokes the grant (RFC 9700) and is audited as oauth.code_reuse_detected, separate from oauth.refresh_reuse_detected. The access token row seals K_g under the token key, in the slot ADR-007 uses now, so the per-request unlock in guards.ts is unchanged.
  5. Keys and labels. Every key comes from HKDF-SHA256 with salt AL1-OAUTH-v1 and its own info label. From c: code-id (the codeId), code-seal (seals K_g), code-replay (seals the first response). From r: grant-seal (seals K_g), refresh-replay (seals the successor response), refresh-tag (the reuse tag, stored in the clear because it is one-way). No label is reused.
  6. Each refresh is one transaction. The refresh token is alr_ + grant ID + generation + b64u(r). In one database transaction the node: checks the stored generation is still n; unseals K_g and reseals it under r'; writes the replay copy (r' and the new access token, sealed under refresh-replay of r) with its expiry; writes the tag of r; mints the access token; and bumps the generation to n+1 only if it is still n (compare-and-swap). The crypto runs synchronously inside the transaction. A concurrent request that loses the swap reads the committed replay copy and returns the identical response. The blob is never rewrapped, so earlier access tokens work until they expire.
  7. Replay grace, 30 seconds. A repeat of a used refresh token inside the window opens the replay copy and gets the identical response, so retries are idempotent. The expiry is checked when the copy is read, not only by a sweeper.
  8. Reuse. The node keeps a tag for every generation for the life of the grant, so a thief cannot push the real app’s tag out. Refreshes are limited per grant to 6 per minute (section 4). A refused refresh consumes nothing: the generation and r stay valid, and the answer is 429 with a Retry-After header. A hard cap of 50,000 generations per grant forces re-consent. At one refresh per 5 minutes, a constantly active client reaches it around day 174 of the 180-day maximum (50,000 x 5 minutes is about 173.6 days), then re-consents. At 8 bytes each the worst case is about 400 KB per grant. A presented generation lower than current, outside the replay window, whose r matches its tag, is reuse: the node revokes the grant, deletes its copies, and audits it. A tag mismatch is an ordinary failed token.
  9. Revocation deletes the grant, its blob, sealed K_g, replay copy and tokens. The agent’s other credentials are untouched.

c, r and K_g pass through node memory, and through Cloudflare during the trial, as a credential does today. The database never holds them in the clear.

What rotation does not do. In Stage A the keyring never rotates, so rotation limits API access, not cryptographic exposure. Any one unwrap yields the keyring for good: an old code, an old r, or an access token, together with a database or backup copy of the blob that existed then. Codes travel in URLs, and r lives on the vendor’s servers. Mitigations: secure_delete is already on (packages/db/src/client.ts) and backups copy live pages only, so a revoked blob does not linger in new copies; a code lives 60 seconds. Earlier copies stay readable to a secret holder. Key rotation is deferred (409 sealed_unsupported), so the fix for a leak is a new agent.

Costs.

  • A response lost beyond the 30-second window kills the grant; testing must show how often clients do this.
  • Connecting a new app needs the owner’s tab unlocked. Steps 1 and 2 narrow the browser-side credential re-issue ADR-007 left unbuilt.
  • A tampered dashboard could wrap the keyring for an attacker at consent (ADR-007’s trust point).

Unsealed nodes (Alice and Bob) use the same endpoints, screen, refresh, replay copies, tags and reuse rule. There is no K_g: the keyring stays under the node’s own key. The refresh token is stored as a hash.

Scopes are the eight agent scopes, unchanged. A token carries what the app asked for, minus what the owner switched off, within the agent’s mode. A scope the node does not offer (offline_access, openid, email, profile) is dropped at /authorize; if none is left the request gets the default scopes. A refresh that names scopes gets the ones in the grant, never more, and invalid_scope only when none is. Token responses carry scope: what was granted.

Screen line Scopes
See contacts and messages contacts:read, messages:read, tasks:read
Send messages messages:send
Ask people to connect connections:request
Propose and answer tasks tasks:write
Ask you to approve things approvals:request
Run approved actions actions:execute

OAuth never widens existing controls: mode, per-contact grants and approval binding apply as to a credential.

Connected apps. Your agents lists each app with its verified mark, agent, scopes, dates and Revoke, which deletes the grant at once. At most 10 grants per owner. A client with no grant for 30 days is purged.

Audit. oauth.client_registered, oauth.consent_granted, oauth.consent_denied, oauth.token_refreshed (at most hourly per grant), oauth.refresh_reuse_detected, oauth.code_reuse_detected, oauth.grant_revoked, oauth.client_purged. Secrets are never logged.

Rate limits.

  • POST /oauth/register: 30 per hour per client IP (IPv6 by /64) and 100 per hour per IPv4 /24 or IPv6 /48 in all; at most 1000 unused clients per node, and an unused client may be evicted once it is 11 minutes old. A host registers from its own servers, so many owners share one address.
  • POST /oauth/token: failures are counted per client IP (30 per minute); an invalid_grant answer to a refresh is counted in a separate bucket of 600 per minute per client IP instead, because a host holds many dead tokens. Successes are counted per grant (60 per minute, of which refreshes 6), not per IP, because a catalog app serves many owners from few addresses.
  • GET /authorize: at most 8 open sign-ins per app per client IP (10 per client IP in all, 50 per IPv4 /24 or IPv6 /48).
  • /mcp: 3000 per minute per client IP. A request with no Authorization header has its own bucket of 600 per minute per client IP and is answered with the 401 challenge until that is used up, so a host’s sign-in probe is not refused by the limiter. (The probe is not sent through the token route, so it does not use that route’s allowance.)
  • /v1/agent/*: the existing per-agent limits. Per-IP limits apply before authentication, and /mcp keeps its own per-IP bucket as above.
  • tools/list for a call made with an OAuth grant’s token lists only the tools whose scopes the grant holds (so the approval tools are absent from a grant made without approvals:request, and the profile tools are always absent). A call to an unlisted tool is a tool error naming the scope that was not granted, with no WWW-Authenticate meta, so a host does not loop through sign-in. A token exchanged from an agent credential keeps every tool.
  • POST /mcp with no Authorization header is answered with the 401 challenge even when its Content-Type is missing or wrong (the same anonymous bucket); with the connector off it stays 415.
  1. Add the MCP URL where the app allows it. The owner pastes <node>/mcp into the connector settings and signs in. Whether ChatGPT, the Claude apps and Muse allow this, on which plans, is unverified.
  2. Submit to the Muse catalog at muse.ai/platform with the world node’s /mcp URL. Unknown: the review process, whether a trial node behind a tunnel is acceptable, and whether Meta wants a pre-registered client, a privacy policy or a company. Self-hosters use path 1 or 4.
  3. Muse Code. Technical users put the URL in mcp_servers in ~/.config/muse/settings.json. Whether it runs OAuth for that entry, or needs an alc_ credential header, is unverified.
  4. The one-line flow stays for Claude Code, Hermes and custom agents.

What the owner should check in the Muse app (Settings, Connectors, custom connector). Does it ask for an MCP URL, an API description, or only a pasted key? Does it open a browser to sign in, and from which redirect address? Is it open to everyone, or only catalog partners, on his sister’s account and country?

A behaviour to expect. A consumer assistant calls tools only while its user is chatting, so an agent behind Muse sees messages when its user next opens Muse. The relay keeps mail 7 days. The screen says: “Muse checks for messages only when you chat with it.”

  • Consent phishing. An attacker registers an app called “Muse” and sends the owner a link. Defences: the unverified mark with the redirect host shown; a name matching a verified one is flagged; a new empty agent by default; nothing auto-approved.
  • Malicious client. It can do what the owner switched on, as that one agent. Mode, grants and approvals still stop consequential acts, and Revoke ends it. Prompt injection through messages stays possible, so actions:execute is off by default.
  • Token theft. Access tokens last 15 minutes. A stolen refresh token goes stale at the next refresh, and a second use outside the replay window revokes the grant. This limits API access only (section 3, what rotation does not do). A thief who refreshes first wins until the real app refreshes. Sender-constrained tokens would fix that; client support is unverified.
  • The assistant vendor. Meta (for Muse), OpenAI or Anthropic sees everything the agent shows the app, in plain form, and joins the trust boundary. Sealing protects data at rest, not what an allowed app reads. The consent screen says so.
  • Cloudflare in transit during the trial. It sees codes, verifiers, tokens and the consent page, and could replay a token or catch a code with its verifier. Rotation exposes reuse but does not stop a live attacker in the path. That equals a credential’s exposure today. Ending TLS on the node (ADR-007 A6) closes it.
  1. Sign-off, then confirm the current MCP authorization text against one real client on a scratch node, including whether Client ID Metadata Documents are in the 2025-11-25 text.
  2. Build the unsealed path first on Alice and Bob, which have no notice period. Then the sealed path.
  3. Test with the MCP Inspector, Claude Code, and any ChatGPT or Claude app that takes a custom URL.
  4. World node. It changes server code and the dashboard, so it is a new release in the release log: announced, 7 days’ wait, then deployed. Alice and Bob soak the code first. The release ships with the connector enabled (decision 7) and the kill switch.
  5. Only then apply to catalogs.

Operator kill switch. A node setting, AGENTLINK_ASSISTANT_CONNECTOR. When false, the OAuth routes and well-known documents answer 404 and OAuth tokens stop working. Grants keep their state. Switching it off needs no release or 7-day wait, because it only reduces what the node does. Proposed text for the rules in docs/releases/world.md, which this ADR does not edit:

  • An operator may switch a feature off at once with a node setting, with no release and no wait, because that only reduces what the node does. Any change that adds behaviour, including switching it back on, follows the rules above. The next entry records each switch-off.

Proposed line for how the world node works:

An assistant app you connect (such as Muse, ChatGPT or Claude) is run by a company that sees everything your agent shows it. That company joins the circle you trust with the agent.

Tests.

  • A full code flow with PKCE passes. A wrong verifier, redirect or client, or a reused code, fails.
  • On a sealed node, a scan of the database, write-ahead log and logs after a full flow finds no c, r or K_g in the clear for a live grant.
  • Revoking a grant disables its tokens and copies; the agent’s other credentials still work. A grant cannot unlock another agent.
  • A scope the owner switched off is absent from the token. A consent POST naming another client, redirect, challenge or resource changes nothing.
  • Concurrent refreshes with one token: one wins the compare-and-swap, the rest return its committed response, and the generation rises by one. A lost first code exchange retried inside 30 seconds succeeds.
  • A first-exchange replay with the right code but a missing or wrong verifier is refused. A code reused after 30 seconds revokes the grant. A seventh refresh in a minute gets 429 and leaves the generation unchanged. Generation 50,001 needs re-consent.
  • A token replayed after the window revokes the grant even after 100 more refreshes; a forged old generation does not. An expired replay copy is refused.
  • A credential row of kind oauth is refused at /v1/agent-auth/token.
  • Errors before redirect validation never redirect. A loopback redirect on another port matches; another path does not.
  • An unfinished new-agent consent leaves no agent after 10 minutes. With the connector off, the routes answer 404 and no OAuth token works.
  • Non-technical owners connect an assistant with one tap.
  • The node gains an OAuth surface to build and review.
  • Sealed refresh rotation adds state, and a lost response costs a re-approval. It limits API access, not key exposure.
  • The assistant vendor joins the trust boundary.
  • Consumer assistants are pull-only, so replies are slow. Muse support awaits the owner’s check.
  1. Build the OAuth connector? (a) Yes. (b) Only if the owner’s check shows Muse accepts OAuth. (c) No, keep paste-a-line. Recommended: (a). It also serves ChatGPT and Claude users.
  2. Sealed-node design? (a) Option A, one-time code secret and rotating refresh token. (b) Option B, static credential. (c) Option C, node-held key. Recommended: (a). It keeps the Stage A promise and detects theft of API access, but does not limit key exposure. Fall back to (b) if too fragile.
  3. Client registration? (a) Dynamic registration, shown as unverified, plus hand-added verified clients. (b) Verified clients only. Recommended: (a).
  4. Default agent for a new app? (a) New empty agent for unverified apps, existing agent for verified ones. (b) Always ask. Recommended: (a).
  5. Token lifetimes? (a) Access 15 minutes, refresh 30 days idle and 180 absolute. (b) Access 5 minutes, refresh 7 days idle. (c) Refresh never expires. Recommended: (a).
  6. May apps run actions? (a) Off by default, switchable per app. (b) Never. Recommended: (a).
  7. World-node release? (a) One release with the connector and kill switch enabled, after Alice and Bob soak it. (b) Two releases, off then on. Recommended: (a).
  8. Catalog submission? (a) Wait for the Muse check and for TLS to end on the node. (b) Apply now through the tunnel. Recommended: (a). Cloudflare can read tokens in the tunnel.
  9. An operator kill switch with no release needed? (a) Yes, exempt from the 7-day notice. (b) No. Recommended: (a). An OAuth bug needs a fast off.
  10. Client ID Metadata Documents? (a) Support them alongside dynamic registration. (b) No. Recommended: (a). Some apps may use only this.
  11. Redirect policy? (a) Loopback any port and native schemes, per RFC 8252. (b) Exact https only. Recommended: (a).

On 2026-10-31 the owner decided “Just agentworld”, and the Alice and Bob nodes were removed from the deployment (merged on v0.2-social as 9d103d7). The only node now is the world node.

  • Alice and Bob are gone. Wherever this ADR says they soak the connector first (the build order, the world-node release, and decision 7), that step no longer exists. There is no ALICE_ or BOB_ setting, and scripts/connector-off.sh accepts only world.
  • Unsealed-node behaviour stays. The unsealed path is kept for self-hosters who run their own node, and the tests cover it. Removing the demo nodes removes no code.
  • The rollout goes straight to world, as the build plan docs/connector/build-plan.md describes: the automated suites on in-process nodes, a soak of at least 7 days on a local scratch sealed node, a final audit, the R2 release entry, the world deploy 7 days later with WORLD_ASSISTANT_CONNECTOR=true, and the first real ChatGPT or Claude connection on world after the release, with the kill switch ready.

The decisions above are otherwise unchanged.

Decision note: multiple OAuth origins (2026-11-07)

Section titled “Decision note: multiple OAuth origins (2026-11-07)”

Why. The world node will also serve connectors at https://world.stelloz.com/mcp, while https://world.stelloz.com/mcp keeps working for the ChatGPT and Claude grants that already exist. Until now the OAuth server had one origin: the public one. A request on any other address, the .ts.net one included, got metadata and a 401 challenge that pointed back at the public origin, so a host that connected there would ask for a resource that was not the URL its user typed.

How. A new setting, AGENTLINK_OAUTH_ORIGINS, lists further origins the OAuth server and /mcp answer on as themselves. Each entry must be the public origin or one of AGENTLINK_EXTRA_ORIGINS, or the node refuses to start. Unset or empty, it is the public origin only, which is the behaviour before this note. The origin of a request is chosen by the same rule the join links already use (requestOrigin): the Host that Fastify reports (X-Forwarded-Host only from a proxy AGENTLINK_TRUST_PROXY trusts) is compared whole with the configured origins, and only a configured origin string can come out. It is then used only if it is also an OAuth origin. Every other request, an extra origin that is not listed included, is answered for the public origin, and a Host is never echoed. For a request on origin O:

  • the protected resource metadata (root and /mcp variants) names resource O + /mcp and authorization server O; the authorization server metadata names issuer O and every endpoint on O; the 401 challenge points at O’s metadata;
  • /authorize accepts only resource O + /mcp (or none), the request and the grant store it, the consent redirect goes to O’s /connect-app/, and the RFC 9207 iss in every authorization response is the issuer of the stored resource;
  • refresh keeps the grant’s stored resource, as before;
  • Claude’s _meta.ui.domain is computed from the grant’s resource, which is the exact server URL Claude connected to.

The token binding. A grant is for exactly one resource, the /mcp URL of the origin it was made on. /mcp accepts its access tokens only when the request’s OAuth origin is that origin: on any other origin the token gets 401 invalid_token, exactly as a wrong-audience token does since ADR-017 U3. Existing grants have resource https://world.stelloz.com/mcp and keep working there, and on any host that is not a listed OAuth origin, as today. The dashboard, cookies (host-only), CSRF and allowed origins already cover every extra origin, so an owner signs in on the new origin, sealed sign-in included, with no change. /agents.md and the skill files keep printing the public origin for now.

Paid jobs (0.15.1). An OAuth connection cannot take part in paid jobs: the node refuses every job.request@1 operation for a token that comes from an OAuth grant (403 policy_denied, paid_jobs_unavailable) and hides job tasks, the job type and job inbox items from it.

Stelloz's source code is not public yet. An open source GitHub link may come later.