Connect agent accounts (keys and subscriptions)
An agent run thinks with an account that belongs to its owner. MIOSA never powers your agents with its own model keys. So before a run can start, a model provider has to be connected: an API key, or a subscription plan such as Claude or ChatGPT.
You can do that in the console. You can also do it from your own code, for your own workspace or for each of your end users. That second case is what a platform needs: your customer connects their own Claude plan inside your product, and every run on their machines uses it.
This guide covers both.
What you can connect
| Connection | What it is | How you connect it | Name in the SDK |
|---|---|---|---|
| Claude plan | A Claude Pro or Max subscription. | Sign-in, the user pastes a code back. | claude |
| ChatGPT plan | A ChatGPT Plus, Pro or Team subscription (Codex). | Sign-in, the user enters a code. | chatgpt |
| Kimi plan | A Kimi Code subscription. | Sign-in, the user enters a code. | kimi |
| Mistral Vibe plan | A Mistral Vibe subscription. | Sign-in in the browser. | mistral |
| Anthropic key | An Anthropic API key. | Set it. | anthropic |
| OpenAI key | An OpenAI API key. | Set it. | openai |
| OpenRouter key | An OpenRouter API key. | Set it. | openrouter |
| Other keys | DeepSeek, LLM Gateway, Moonshot, Mistral, Amazon Bedrock. | Set it. | deepseek, llmgateway, moonshot, mistral, bedrock |
| Tool-server keys | The key a tool server needs: Apify, Refero, any MCP server. | Set it. | toolKeys |
A key never leaves the platform.
Every read returns a status, a redacted preview such as sk-ant...1234, and for a plan the account email and plan name.
Never the secret.
Who the connection belongs to
Every call takes the same two optional identifiers.
| You pass | The connection belongs to | A run uses it |
|---|---|---|
| Nothing | Your organization. | In every workspace, unless a narrower one exists. |
workspaceId | One workspace. | For runs in that workspace. |
externalUserId | One of your end users: your own id for them. | For runs on that end user’s machines, before the workspace’s. |
A run looks for the account in this order, and stops at the first one it finds:
- The end user’s own connection.
- The workspace’s connection.
- The organization’s connection.
The end user is the one on the machine the run uses.
You tag a machine with externalUserId when you create it, the same id you use everywhere else in your platform (Ownership and attribution).
Writes and deletes always touch exactly the scope you name. Setting a key for one end user never changes the workspace’s key, and removing it falls back to the workspace’s. An end user’s connection is never visible to another end user, to the workspace, or to another organization.
Your own workspace
Setting a key again replaces it. That is how you rotate one.
Your end users
Pass externalUserId and the connection is theirs.
An API key
An end user’s key is for agents only. It is never offered to the AI Gateway.
A Claude plan or a ChatGPT plan
Connecting a plan is a sign-in the user completes in their own browser. Your code starts it, shows the user a link, and waits. The call returns at once with what to show. Nothing is connected yet.
There are two shapes of sign-in:
| Mode | Plans | What the user does | What you do |
|---|---|---|---|
paste_code | Claude | Approves at the link, then copies the code the page shows. | Pass that code to submitCode. |
device_code | ChatGPT, Kimi | Opens the link and types userCode. | Call wait(), or poll() on a timer. |
wait() resolves when the plan is connected.
It rejects with SubscriptionSignInError if the sign-in failed, expired or was cancelled. poll() never rejects for a failed sign-in: read its status. cancel() ends the sign-in.
A wrong or expired pasted code is rejected with SIGNIN_CODE_REJECTED (422).
The sign-in stays open, so the user can paste again.
The sign-in link and the code carry no credentials. The plan’s tokens are stored encrypted on the platform, refreshed in the background, and never returned by any call.
Check, list and remove
test makes one cheap authenticated request to the provider, such as listing models.
For a plan it checks the stored state instead.
It answers ok: true, ok: false (the provider rejected the key) or ok: null (the provider did not answer, which is not the key’s fault).
It never returns the key or the provider’s response.
Tool-server keys
A tool server is an MCP server your agents use: Apify, Refero, your own.
You define the server once, with its credential as a placeholder such as {{secret:APIFY_TOKEN}}.
Each end user then supplies their own value.
An agent run for that end user reaches the tool server with their key, and falls back to the workspace’s.
Only a name that a tool server references can be set.
Anything else is UNKNOWN_TOOL_KEY (404), so this is not a general-purpose secret store.
Which harness uses which connection
A harness is the agent that runs on the machine: Claude Code, Codex, OpenCode and the others. Each accepts a set of connections.
Harness (runner) | Accepts |
|---|---|
Claude Code (claude-code) | Claude plan, Anthropic key, OpenRouter key, DeepSeek key, Amazon Bedrock |
Codex (codex) | ChatGPT plan, OpenAI key, DeepSeek key, Amazon Bedrock |
OpenCode (opencode) | ChatGPT plan, Anthropic key, OpenAI key, OpenRouter key, LLM Gateway key, Mistral key, Mistral Vibe plan |
Pi (pi) and Prime Agent (prime) | Claude plan, ChatGPT plan, Anthropic key, OpenAI key, OpenRouter key, LLM Gateway key, Mistral key, Mistral Vibe plan |
Hermes Agent (hermes) | ChatGPT plan, Claude plan, Anthropic key, OpenAI key, OpenRouter key |
Kimi Code (kimi) | Kimi plan, Moonshot key |
Mistral Vibe (mistral) | Mistral Vibe plan, Mistral key |
OSA (osa) | An Anthropic, OpenAI or OpenRouter key, or the model key you set in Agent Studio |
Two limits come from the vendors, not from MIOSA:
- OpenCode does not take a Claude plan. Anthropic prohibits it, and OpenCode removed the support.
- Hermes takes a Claude plan only on a Claude Max plan with extra usage purchased. A Pro plan is refused by Anthropic.
You connect once, and any run on a compatible harness picks the connection up. You do not pick a connection per run.
To see what a given user can run right now:
The harness settings (which method is active, the default model) belong to you, the builder. They are set per workspace and are the same for all of your end users. The connections are theirs. A key minted for an end user cannot change the harness settings.
Choose which connection a harness uses
A harness that takes one method at a time (Claude Code, Codex) uses one connection. A harness that takes several (OpenCode, pi, Prime Agent, Kimi Code, Mistral Vibe, Hermes Agent) uses the ones you enable. These are the builder’s settings, set per organization or per workspace.
Then run on any harness, with the model you want:
miosa prompt --harness opencode --model gpt-6.1-sol --sandbox my-box "add a health check" miosa prompt --harness takes every harness the platform lists, including ones added after your CLI was released. miosa connections harnesses opencode lists that harness’s models and which are available with what is connected.
Build it into your product
A “Connect your Claude plan” button in your settings page is two requests to your backend. The sign-in id travels through the browser, and your backend names the end user from its own session.
// 1. Start. Send the browser only what it needs.
app.post("/api/connect/claude", async (req, res) => {
const signin = await miosa.connections.connectSubscription({
provider: "claude",
externalUserId: req.user.id, // from YOUR session, never from the request body
})
res.json({ signinId: signin.id, url: signin.verificationUrl })
})
// 2. The user approves in a new tab and pastes the code into your page.
app.post("/api/connect/claude/code", async (req, res) => {
const signin = miosa.connections.resumeSubscription({
id: req.body.signinId,
provider: "claude",
externalUserId: req.user.id,
})
const status = await signin.submitCode(req.body.code)
res.json({ connected: status.connected, email: status.accountEmail, plan: status.accountPlan })
}) resumeSubscription picks a sign-in up by its id.
Because it carries the end user, a sign-in id belonging to someone else is NOT_FOUND, even with your organization-wide key.
The worked example, with a runnable server, is in the SDK repository under examples/connect-agent-accounts.
It connects a Claude plan, a ChatGPT plan, a key and a tool-server key for two customers, and shows that one customer’s connection never reaches the other’s runs.
Security
- Server only. Call these methods from your backend with your
msk_*key. Never put that key in a browser. - Mint an end-user key for anything that runs near your customer’s code. A key minted with an
externalUserIdalways acts as that end user. It cannot read or change the workspace’s or the organization’s connections (403 SCOPE_FORBIDDEN), cannot change harness settings, and cannot see another end user’s sign-ins. - No secret is returned, by any call. Keys and plan tokens are encrypted at rest, bound to the end user they belong to, and decrypted only to start a run.
- Audit. Every connect, replace, remove and use is written to the audit log with the end user’s id and never the value.
- Isolation. A connection is checked against your organization on every read. An end user’s id means nothing to another organization.
- Scopes. Reading needs
integrations:read(plusconnections:readfor the connections list). Changing needsintegrations:write(andconnections:writefor sign-ins). - Plans are the user’s. A plan connected for an end user is used only for runs on that end user’s machines. It is never used to serve your other customers.
- Rate limits apply as everywhere (Rate limits).
Errors
| Code | Status | Meaning |
|---|---|---|
SCOPE_FORBIDDEN | 403 | The key acts for one end user and asked for something wider. |
MISSING_EXTERNAL_USER_ID | 422 | scope: "external_user" without an externalUserId. |
CONFLICTING_SCOPE | 422 | scope does not agree with the identifiers. |
INVALID_EXTERNAL_USER_ID | 422 | The id is longer than 255 characters or has control characters. |
USE_SIGNIN | 422 | A plan cannot be set; connect it with connectSubscription. |
VALIDATION_FAILED | 422 | A field is missing or wrong, for example an empty key. |
SIGNIN_CODE_REJECTED | 422 | The pasted code was wrong or expired. Paste again. |
SIGNIN_PROVIDER_ERROR | 502 | The provider’s sign-in service did not answer. Start again. |
NOT_FOUND | 404 | Nothing is connected at exactly that scope. |
UNKNOWN_CREDENTIAL | 404 | Not a credential MIOSA knows. |
UNKNOWN_TOOL_KEY | 404 | No tool server references that secret name. |
OWN_CREDENTIALS_REQUIRED | 422 | A run was started with nothing connected for its harness. |
The API underneath
The SDK and CLI are thin wrappers.
Every route accepts workspace_id, external_user_id and an optional scope.
| Route | Does |
|---|---|
GET /agents/credentials | Every credential and its state at the scope. |
PUT /agents/credentials/{credential} | Set or replace a key. |
DELETE /agents/credentials/{credential} | Remove it at exactly the scope. |
POST /agents/credentials/{credential}/test | Does it work? |
POST /agent-signins | Start a plan sign-in. Returns the link and the code. |
GET /agent-signins/{id} | Poll it. |
POST /agent-signins/{id}/code | Hand over a pasted code. |
DELETE /agent-signins/{id} | Cancel it. |
GET /agents/tool-keys | The keys your tool servers need. |
PUT /agents/tool-keys/{name} | Set one. |
DELETE /agents/tool-keys/{name} | Remove one. |
GET /connections | Models, apps and tool servers in one list. |
GET /agents/settings | Harness readiness at the scope. |