Proxy API
The local agent API that viper proxy serves, for agents in other languages: capabilities, agents, registering definitions, storing files, the model-request headers, retries, and errors.
viper proxy serves a small HTTP API to processes on its own machine. The Python SDK is built on it, and an agent in another language can use it directly: store any skill files, register the agent’s definition, then name the agent and definition on each model request. This page describes version v1.
Where it is
The API is at the proxy’s own address, the base URL viper proxy prints when it starts, under /__viper__/v1/. Paths under /__viper__/ are the proxy’s own and are never forwarded to the gateway.
- Local only: the proxy listens on
127.0.0.1. The API also refuses a call that addresses it by anything butlocalhostor a loopback IP, or that carries anOriginheader, as a web page’s requests do. Both are refused withnot_local. - JSON in, JSON out: request and response bodies are
application/json. - Any endpoint can also answer
405method_not_allowedfor the wrong method, with anAllowheader, and an unknown path under/__viper__/v1/is404not_found.
Errors
Every error from this API, and from a model request that names an agent, has this body and the X-Viper-Error: 1 header, which marks errors the proxy made itself rather than ones it passed on:
{
"error": {
"code": "agent_not_enrolled",
"message": "agent `researcher` is not enrolled on this proxy",
"hint": "enroll it with `viper agent add researcher`, then restart the proxy"
}
}code is what a client branches on, message is for people, and hint, when present, says what to do. A message may name the agent, definition or path the request named, but never repeats a value from a definition, prompt or file. An error for a model request that names no agent keeps the older {"error": "<message>"} shape, still marked. Errors lists every code, with its status.
GET /__viper__/v1/capabilities
What this proxy supports. Ask once, at start-up, and check it before relying on the proxy.
api: the API version,"v1".viper: the proxy’s Viper version, for messages only. Never compare it: checkfeatures.features: what the proxy does beyond the API version, by name, such as"agent-headers". A name is added when its feature ships, and never removed (Compatibility).schemas: the definition schemas the proxy accepts, such as"viper/agent-definition/v1".strict: whether model requests must name an agent (viper proxy --require-agent-header).agents: the agents enrolled on this proxy, sorted. The proxy reads them when it starts.limits: the bounds a definition must stay within, in camelCase, such asdefinitionBytesandsubAgentDepth(Limits).
Responses: 200, or 403 not_local.
GET /__viper__/v1/agents/{agent}
One enrolled agent, as this proxy knows it.
name: the agent’s name.did: the DID its requests are signed as.definitions: the content IDs of the definitions registered for it since the proxy started, least recently registered first.latestDefinitionCid: the most recently registered definition, ornull.
Responses: 200, 403 not_local, or 404 agent_not_enrolled.
POST /__viper__/v1/collections
Stores the files of one skill, or one source of memory the agent can’t change, so a definition can name them by content ID. Store them before registering a definition that names them.
files: each file’s contents, base64-encoded (the standard alphabet, padded, with no line breaks), keyed by its path inside the skill or memory source, with/separators. Key a skill’s files from its own directory, with itsSKILL.mdat the root, as a coding agent’s skill is laid out, so the content ID matches Viper’s for the same skill. A single memory file is keyed by the name the agent reads it by, such asAGENTS.md.
The response:
cid: the collection’s content ID, for a skill’s or memory’scidin the definition.filesandbytes: how many files it holds, and their size together.
The same files always give the same content ID. Responses: 201, 403 not_local, 413 collection_too_large, 422 collection_invalid or definition_contains_secret, or 500 internal_error if the files couldn’t be stored.
POST /__viper__/v1/agents/{agent}/definitions
Registers a definition for an enrolled agent. The body is the definition, as JSON in the schema viper/agent-definition/v1: the schema is published at /schemas/agent-definition.v1.schema.json, and Definitions and Definition fields describe each field. This one has a prompt, a tool, a skill and a sub-agent:
{
"schema": "viper/agent-definition/v1",
"name": "report-writer",
"description": "Writes reports from the internal docs.",
"framework": {
"name": "custom"
},
"models": [
{
"provider": "openai",
"model": "gpt-4.1",
"settings": {
"temperature": 0
}
}
],
"prompts": [
{
"role": "system",
"content": "You write reports from the internal docs."
}
],
"tools": [
{
"name": "search_docs",
"description": "Search the internal documentation.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string"
}
},
"required": ["query"]
}
}
],
"skills": [
{
"name": "triage",
"cid": "<the cid POST /__viper__/v1/collections returned>"
}
],
"subAgents": [
{
"schema": "viper/agent-definition/v1",
"name": "critic",
"description": "Checks a draft's claims against the docs.",
"framework": {
"name": "custom"
},
"models": [
{
"provider": "openai",
"model": "gpt-4.1",
"settings": {
"temperature": 0
}
}
],
"prompts": [
{
"role": "system",
"content": "You critique drafts and name each unsupported claim."
}
]
}
]
}- Required:
schema,name,frameworkand at least the agent’s model inmodels;{"name": "custom"}names no framework. The definition’snamemust be the agent’s. - Names: an agent’s or sub-agent’s name is 1 to 64 lowercase letters, digits,
.,_and-, starting with a letter or digit. - Sub-agents are whole definitions, with their own
schema,name,frameworkandmodels, and adescription, which a sub-agent must have. - Skills and memory name their files by the
cidthe collections endpoint returned. A definition naming files the proxy doesn’t hold is refused. - Prompts: send every prompt’s text inline. The proxy stores a prompt over the inline limit apart, and the definition it registers refers to it by
contentCid. Prompts still count toward the prompt and registration limits (Limits). - Checked against the schema, the proxy’s rules, its limits and its credential scan, before anything is stored. A body that isn’t valid JSON is
422definition_invalidtoo.
The response:
definitionCid: the definition’s content ID as stored. Name it inX-Viper-Definitionon model requests. Use this one rather than one you computed, since the proxy may have moved prompts out.created: whether this agent registered it for the first time since the proxy started.
The same definition always gives the same content ID, so registering it again is safe. Registrations last until the proxy restarts (After a restart).
Responses: 200, 403 not_local, 404 agent_not_enrolled, 413 definition_too_large, 422 definition_invalid, definition_contains_secret or schema_version_unsupported, or 500 internal_error if the definition couldn’t be stored or read.
Model requests
Model requests go to the same proxy address, at the provider’s own paths, such as /v1/chat/completions for OpenAI or /v1/messages for Anthropic. A proxy fronts one provider’s gateway endpoint, so an agent that calls both runs through two proxies (One proxy per provider). Send your provider key in the provider’s usual header (Authorization for OpenAI, x-api-key for Anthropic): the proxy passes it on unchanged.
Three headers name who’s asking:
X-Viper-Agent: the enrolled agent. The request is signed with its key.X-Viper-Definition: thedefinitionCidits registration returned. Required withX-Viper-Agent: without it, the request is409definition_unknown.X-Viper-Subagent: for a sub-agent, its path in the definition, such ascriticorcritic/fact-checker. The request is signed as the agent, and the sub-agent is recorded.
Each request is signed as matching the definition it names, so send what the definition records: its prompts as the system or developer messages, its tools, and its model and settings. The proxy doesn’t compare them (Send what the definition says).
The proxy checks the headers before signing, and refuses rather than sign as someone else:
- Each header once: a repeated, empty or unreadable header is
400agent_header_invalid. - The agent must be enrolled (
409agent_not_enrolled), its definition registered (409definition_unknown), and the sub-agent declared in it (409subagent_unknown). - No definition or sub-agent without an agent:
403agent_header_required. - No requests from web pages: a request naming an agent with an
Originheader is403not_local. - A request naming no agent is signed as the proxy itself, unless the proxy runs with
--require-agent-header, which refuses it with403agent_header_required.
The proxy removes the three headers before forwarding the request. Errors on the way are signing_failed, upstream_unreachable, upstream_response_lost, request_unreadable and internal_error (Model requests). Refusals from the Guardian Platform and the provider are passed on unchanged, without X-Viper-Error.
Retrying
Every error the proxy makes with a code carries an x-should-retry header: true for the codes it’s safe to retry, and false for the rest. Clients that follow it, such as OpenAI’s and Anthropic’s generated SDKs in Python, TypeScript, Go and Java, don’t retry what they shouldn’t; without it they retry every 409 and 5xx. Check that yours reads it. A client of your own should follow the header, or branch on code:
- Safe to retry:
signing_failed,internal_errorandupstream_unreachable. Nothing reached the model. - Never retry
upstream_response_lost: the request was sent and the model may have run, so a retry may run it twice. - Don’t retry the rest as they are:
4xxcodes need something fixed first.definition_unknownneeds the definition registered again (below).
A few errors carry no x-should-retry, since the proxy can’t tell what caused them: one for a request naming no agent that the proxy couldn’t sign, and one raised before the agent headers are read (relay-mode dispatch, HTTPS interception). The Python SDK marks each refusal the same way itself, for proxies older than the header (Retries).
After a restart
A restarted proxy has forgotten every registration, so the next model request is 409 definition_unknown, with X-Viper-Error: 1. Nothing reached the model. Recover as the Python SDK does:
POSTthe same definition to/__viper__/v1/agents/{agent}/definitions. It gets the samedefinitionCidback.- Send the refused request once more.
- If that’s refused with
definition_unknownagain, stop and report it: something else is wrong.
Files stored through the collections endpoint persist across restarts.
A whole exchange
With the proxy at http://127.0.0.1:8080 and the agent researcher enrolled:
# 1. Check the proxy
curl -s http://127.0.0.1:8080/__viper__/v1/capabilities
# 2. Store a skill's files, if it has any; keep the cid
curl -s http://127.0.0.1:8080/__viper__/v1/collections \
-H 'Content-Type: application/json' \
-d "{\"files\": {\"SKILL.md\": \"$(base64 < skills/triage/SKILL.md | tr -d '\n')\"}}"
# 3. Register the definition, and keep its definitionCid
curl -s -X POST http://127.0.0.1:8080/__viper__/v1/agents/researcher/definitions \
-H 'Content-Type: application/json' \
-d '{"schema": "viper/agent-definition/v1", "name": "researcher",
"framework": {"name": "custom"},
"models": [{"provider": "openai", "model": "gpt-4.1"}]}'
# 4. Make model requests as the agent
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H 'Content-Type: application/json' \
-H 'X-Viper-Agent: researcher' \
-H "X-Viper-Definition: $DEFINITION_CID" \
-d '{"model": "gpt-4.1", "messages": [{"role": "user", "content": "Hello"}]}'Viper itself is installed and configured as the Guardian Platform docs describe (Install Viper).