EQTY LabDocs

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.

View as Markdown

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 but localhost or a loopback IP, or that carries an Origin header, as a web page’s requests do. Both are refused with not_local.
  • JSON in, JSON out: request and response bodies are application/json.
  • Any endpoint can also answer 405 method_not_allowed for the wrong method, with an Allow header, and an unknown path under /__viper__/v1/ is 404 not_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:

json
{
  "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: check features.
  • 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 as definitionBytes and subAgentDepth (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, or null.

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 its SKILL.md at 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 as AGENTS.md.

The response:

  • cid: the collection’s content ID, for a skill’s or memory’s cid in the definition.
  • files and bytes: 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:

json
{
  "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, framework and at least the agent’s model in models; {"name": "custom"} names no framework. The definition’s name must 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, framework and models, and a description, which a sub-agent must have.
  • Skills and memory name their files by the cid the 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 422 definition_invalid too.

The response:

  • definitionCid: the definition’s content ID as stored. Name it in X-Viper-Definition on 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: the definitionCid its registration returned. Required with X-Viper-Agent: without it, the request is 409 definition_unknown.
  • X-Viper-Subagent: for a sub-agent, its path in the definition, such as critic or critic/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 400 agent_header_invalid.
  • The agent must be enrolled (409 agent_not_enrolled), its definition registered (409 definition_unknown), and the sub-agent declared in it (409 subagent_unknown).
  • No definition or sub-agent without an agent: 403 agent_header_required.
  • No requests from web pages: a request naming an agent with an Origin header is 403 not_local.
  • A request naming no agent is signed as the proxy itself, unless the proxy runs with --require-agent-header, which refuses it with 403 agent_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_error and upstream_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: 4xx codes need something fixed first. definition_unknown needs 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:

  1. POST the same definition to /__viper__/v1/agents/{agent}/definitions. It gets the same definitionCid back.
  2. Send the refused request once more.
  3. If that’s refused with definition_unknown again, 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:

sh
# 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).