Protocol 04
Spec v0.1Call any API. Never hold its key.
Two ways in, secured the same way. Ask for an outcome with a provider action, or write the HTTP request yourself — either way a proxy adds the credential in transit, after a human has connected the account, and the agent never sees it.
# ask for the outcome$npx interagentic providers gmail \ messages list --query "is:unread"✓ 3 unread — alice@acme.dev: Q3 planning, … # or write the request yourself$npx interagentic curl -G \ https://gmail.googleapis.com/gmail/v1/users/me/messages \ -d q=is:unread \ -H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}"200 OK — 3 message ids # same account, same proxy, no key in eitherTwo ways in
Ask for the outcome, or write the request.
Provider actions are for getting something done. Direct HTTP is for when the agent wants access of its own. They sit on the same proxy, so choosing between them is a question of convenience, never of security.
Provider actions
outcome-orientedAsk for the outcome.
Name what you want done and pass typed inputs. A maintained implementation deals with the API's pagination, quirks and limits — and when the API changes, the network repairs it.
npx interagentic providers gmail \ messages list --query "is:unread"
returns the messages, with sender, subject and snippet
Use it when
- You want the result, not a tour of the API
- The API is one you would rather not learn
- The call has to keep working as the API evolves
Direct HTTP
full controlWrite the request yourself.
Any endpoint, any method, exactly the bytes you write, sent through the same proxy. For when the agent wants direct access of its own.
npx interagentic curl -G \
https://gmail.googleapis.com/gmail/v1/users/me/messages \
-d q=is:unread \
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}"returns exactly what the API returns — here, a list of message ids
Use it when
- No action covers the endpoint yet
- The agent already knows the API well
- Every header and field has to be yours
One security model for both
- A human connects each account once, in a browser — OAuth where the provider has it, a pasted key where it does not.
- The proxy adds the credential as the request leaves, whether the request came from a curl the agent wrote or from an action's code.
- The agent only ever holds a placeholder, so there is nothing in its context for a prompt injection to steal.
Provider actions
Outcomes that keep working.
Named actions with typed inputs, implemented in open-source TypeScript that anyone can read, run and fix — maintained by the agents that use them, so they follow the API as it changes.
$npx interagentic providers gmail ls messages list List messages messages send Send a message threads get Read a thread $npx interagentic providers gmail messages list \ --query "from:alice is:unread"✓ 3 messages # or describe it and let the network write it$npx interagentic providers gmail \ "archive everything older than a year" no action matches — generating one…✓ gmail/messages-archive-older — publishedWritten once, for everyone
The first caller who needs an endpoint pays to have it written, with their own model key. The implementation is published to the registry, and everyone after that runs it for free.
Readable, not magic
Every action is plain TypeScript with a declared input and output schema, pinned to a content hash. You can read exactly what will run before you run it.
It repairs itself
When a remote API changes or the action's code hits a bug, an agent is spawned with the caller's model key to work out what went wrong. It opens a pull request, the fix is checked against the action's benchmarks, and once merged every caller gets it.
opened by an agent after a failed run:
TypeError: Cannot read properties of undefined (reading 'map')
- return res.messages.map(toMessage);
+ // Gmail omits "messages" entirely when nothing matches.
+ return (res.messages ?? []).map(toMessage);
✓ 12 benchmark inputs pass · merged as v_9a2e…Direct HTTP
Write everything except the secret.
interagentic curl takes the flags you already know. Method, URL, headers, body — all of it is the agent's to write. The credential is left as a named placeholder, and the proxy fills it in on the way out.
{{UPPER_CASE}}Resolved by the proxy
The proxy looks up which provider owns the target host, fetches the secret the human connected, and substitutes it as the request leaves. The value never travels back toward the agent.
-H "Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}"
-H "X-Api-Key: {{STRIPE_SECRET_KEY}}"What a placeholder name means
{{OAUTH2_ACCESS_TOKEN}}OAuth- The provider's OAuth access token. Against a provider that only issues API keys, its default key — so one name works across the whole catalogue.
{{OAUTH2_REFRESH_TOKEN}}OAuth- The refresh token. Never released to a self-hosted proxy.
{{ANY_OTHER_NAME}}API key- A named credential field of that provider, such as
STRIPE_SECRET_KEY.
Shared by both
Whichever way a call starts, it ends here.
An action's code carries the same placeholders a curl does, and goes through the same proxy. Six steps, all of them boring on purpose — the interesting part is what does not happen: the agent never fetches, stores or forwards a credential.
- 1agentproxy
Send the request with blanks in it
From a curl the agent wrote or from a provider action's code — the proxy cannot tell the difference, and does not need to. The agent's identity token goes inX-Proxy-Authorization, which leavesAuthorizationfree to carry the placeholder.GET https://proxy.interagentic.dev/gmail.googleapis.com/gmail/v1/users/me/messages X-Proxy-Authorization: Bearer <agent jwt> Authorization: Bearer {{OAUTH2_ACCESS_TOKEN}}
- 2proxyregistry
Resolve host to provider
gmail.googleapis.combelongs to Google, which supports OAuth — soOAUTH2_ACCESS_TOKENmeans an access token here. Against an API-key-only provider the same placeholder means that provider's default credential. - 3proxyproxy
Work out the scopes
Derived from the method and path, or stated outright withX-Proxy-Scopes. Scopes are writtengithub.com::repofor OAuth andlifx.com::key::LIFX_TOKENfor API keys. - 4proxyagent
If nothing is connected, refuse usefully
A 403 that names the missing scopes and carries a link a human can act on — not a generic failure the agent has to guess at.403 Forbidden { "error": "permission_denied", "missingScopes": [{ "provider": "google.com", "scopes": ["gmail.readonly"] }], "authorizationUrl": "https://id.interagentic.dev/link/9f2c…" }
- 5humanprovider
Connect the account
OAuth where the provider supports it, a pasted key where it does not. The secret is stored encrypted against the human's account, never against the agent's. - 6proxyprovider
Substitute and forward
Placeholders are replaced in headers, query string and body, everyX-Proxy-*header is stripped, and the request goes upstream. The response comes back untouched.
Proxy headers
X-Proxy-Authorizationheaderrequired- The agent's identity token. Separate from Authorization so the real header stays available for the placeholder.
X-Proxy-Scopesheader- Override the inferred scopes when you know better than the heuristic.
X-Proxy-Accountheader- Pick between several connected accounts for one provider. Without it, an ambiguous call is refused rather than guessed.
Why this is the safe shape
A prompt injection can make an agent do many things. It cannot make it reveal a secret it never had. The agent's context contains the string{{OAUTH2_ACCESS_TOKEN}} and nothing else — exfiltrating it gets an attacker a placeholder.Run the proxy yourself
The substitution step is small and open source. Self-host it and request bodies never leave your infrastructure — the hosted side then only ever sees which provider and which scopes, never your traffic.Every protocol here is a spec you can implement yourself.
There is no gatekeeper. Run your own broker, publish your own service manifest, or point the CLI at a network that has nothing to do with us. The specs are the product.