Authentication
The two cmdlets authenticate in completely different places.
Invoke-Agent
Section titled “Invoke-Agent”Invoke-Agent resolves a credential in a fixed order and prints which step won as the first
transcript row:
- auth: OPEN_ROUTER_KEY in .env.local (via openrouter.ai) -> https://openrouter.ai/api-ApiKeyANTHROPIC_API_KEY, thenANTHROPIC_AUTH_TOKEN- a gateway key, when
-BaseUrlnames one:OPENROUTER_API_KEYorOPEN_ROUTER_KEY, from the environment or the workspace’s.env.local ANTHROPIC_API_KEYin the workspace’s.env.local- an
anthropicAPI key in another CLI’s credential store - the SDK’s own lookup, which ends at an
ant auth loginprofile
An unset environment variable does not mean unauthenticated — a profile may still apply. Steps 3
to 5 are discovery; -NoCredentialDiscovery turns them off.
Two transports
Section titled “Two transports”Invoke-Agent speaks either the Anthropic Messages API or the OpenAI-compatible
/chat/completions shape. -Api selects; auto follows the credential.
-Api | Talks to |
|---|---|
auto (default) | openai when a browser sign-in is in play, otherwise anthropic |
anthropic | the Anthropic Messages API, through the SDK |
openai | any endpoint serving /chat/completions |
auto follows the credential because Anthropic restricts its OAuth to Claude Code. A bearer
token any other client obtained therefore belongs to a service speaking the OpenAI shape, and that
is where a browser sign-in can actually be spent.
Verified against OpenRouter’s OpenAI-compatible endpoint, with real tool calling:
Invoke-Agent "Read calc.py, fix the bug in add(), then run it to prove the fix." ` -Api openai ` -BaseUrl 'https://openrouter.ai/api/v1' ` -Model 'openai/gpt-4o-mini'- auth: OPEN_ROUTER_KEY in .env.local (via openrouter.ai) -> ... [OpenAI-compatible]⚙ read_file calc.py ✔ read calc.py (33 chars)⚙ edit_file calc.py ✔ edit calc.py⚙ run_bash python calc.py ✔ exit 0● The bug in add() has been fixed to use addition.- done · 2563 in / 82 out tokensNote the endpoint differs by transport: the Anthropic path wants the base without a version
segment (the SDK appends /v1/messages), while the OpenAI path wants the full API root
(.../api/v1), to which /chat/completions is appended.
Signing in with a browser
Section titled “Signing in with a browser”Connect-Agent -Provider openrouterInvoke-Agent "fix the bug in calc.py" -Model 'openai/gpt-4o-mini'That works with no setup and no application registration, which is worth explaining because it is not true of every provider.
Do you have to register ps-agent as an OAuth app?
Section titled “Do you have to register ps-agent as an OAuth app?”Depends who you are signing in to.
OpenRouter: no. Its PKCE flow takes no client_id and needs no registered application. The
browser goes to https://openrouter.ai/auth with a callback_url and a code_challenge, and the
exchange at POST /api/v1/auth/keys returns a user-controlled API key. ps-agent ships this as a
built-in profile, so Connect-Agent -Provider openrouter is the whole setup.
OpenAI: yes. “Sign in with ChatGPT” is an interest-form programme rather than self-serve
registration, so a third party needs OpenAI to issue a client_id before it can run the flow.
Free, Plus and Pro accounts are eligible; Enterprise, Edu and Team are not. Until you have one,
an API key is the route.
Anthropic: not available. Its OAuth is restricted to Claude Code, which is why a browser sign-in points at an OpenAI-compatible endpoint rather than the Messages API.
Once you have a client_id from any provider, put it in a profile and the standard flow handles
the rest — nothing about that path is provider-specific.
Connect-Agent runs an authorization-code flow with PKCE (RFC 7636) and a loopback redirect
(RFC 8252): ps-agent starts a listener on localhost, opens the provider’s consent page, and
catches the redirect. The code never transits a clipboard, and no client secret ships with the
module — the flow proves possession of a one-time verifier instead.
Connect-Agent -ShowExample # the profile shape, and where to put itConnect-Agent -Provider my-provider # opens the browser, stores the tokenConnect-Agent -List # what is configured, and what is signed inDisconnect-Agent my-provider # forget the token, keep the profileThen Invoke-Agent uses it. With one sign-in stored it is picked up automatically; with several,
name one with -OAuthProvider. The token refreshes itself when it is within two minutes of
expiry, and the transcript says so.
- auth: my-provider sign-in, valid until 2026-08-23 17:21:32Z -> https://api.example.comA provider is described by a JSON profile in ~/.config/ps-agent/oauth/<name>.json:
{ "name": "my-provider", "authorizeUrl": "https://auth.example.com/oauth/authorize", "tokenUrl": "https://auth.example.com/oauth/token", "clientId": "<the client id the provider issued to your application>", "scopes": ["openid", "offline_access"], "redirectPort": 1455, "redirectPath": "/auth/callback", "baseUrl": "https://api.example.com", "extraHeaders": {}}The clientId is configuration rather than something built in, because a public OAuth client is
an identity a provider issues to a named application. ps-agent ships the mechanism; you supply the
identity you are entitled to use. extraHeaders covers providers that require something alongside
a bearer token — Anthropic’s OAuth path wants anthropic-beta: oauth-2025-04-20, and without it
the token is rejected as though it were invalid.
Tokens are stored beside the profile as <name>.token.json, chmod 600 on POSIX.
Gateways
Section titled “Gateways”Any endpoint serving the Anthropic Messages API works. OpenRouter is verified end to end:
Invoke-Agent "fix the bug in calc.py" ` -BaseUrl 'https://openrouter.ai/api' ` -Model 'anthropic/claude-sonnet-4.6'Give the base without a version segment. The SDK appends /v1/messages itself, so
https://openrouter.ai/api/v1 builds /api/v1/v1/messages and returns an HTML 404 that reads
like an outage rather than a typo.
Whose credentials get used
Section titled “Whose credentials get used”Discovery reads API keys — a key you were issued, spending your quota as you intended. It does not read sign-in tokens. A credential store also holds OAuth tokens that a different application obtained by signing you in to a subscription; those are bound to that application’s client registration. ps-agent lists them by name so it can explain the skip, and never reads them:
Found API keys for deepseek, openrouter, which this cmdlet cannot use: it talks to theAnthropic Messages API. Reach those providers with Invoke-Acp instead. Skipped openaisign-in tokens: those were issued to another application, and are not ps-agent's to spend.Put the key in .env.local and add .env* to .gitignore. A key in a tracked file is a key in
the history.
Invoke-Acp
Section titled “Invoke-Acp”Invoke-Acp never talks to an LLM and holds no API key. The agent subprocess authenticates
itself against its own service — Claude Code against your Claude account, Gemini against yours.
What this client needs is only that the agent binary launches and answers the ACP handshake
within -ConnectTimeoutSeconds (default 60). If the launch fails you get:
Could not launch ACP agent 'npx'. Is it installed?Pass -ShowAgentLog to surface the agent’s stderr as transcript rows; that is where an agent
reports its own auth failures.
To check your setup end to end without credentials of any kind, run the stub agent used by the
test suite — fixtures/stub-acp-agent.js is a dependency-free Node script that performs the full
handshake, or use opencode, which is verified against version 1.18.18:
Invoke-Acp "Read hello.txt and tell me the magic word." -Agent opencode -NoUi -AutoApprove