---
title: "Guardian and Agent"
description: "Guardian, the connection to the local Viper proxy, and Agent, an enrolled agent you route model clients through."
source: "Guardian Agent SDK"
---
A `Guardian` is your connection to the local Viper proxy. From it you make each `Agent`: an enrolled agent, described by its definition, whose model clients you route through the proxy so that every request is signed as that agent.

## Guardian

{/* generated api guardian_agent.Guardian */}

Your connection to the local Viper proxy.

The proxy holds each enrolled agent's key and signs every model request as
the agent that made it. A `Guardian` talks to its local agent API: it
checks the proxy is compatible, and creates the agents you route model
clients through.

It holds no open connections, so there is nothing to close, and one
`Guardian` can be shared freely between threads and async tasks. Use
one per proxy.

Example:

```python
import guardian_agent as ga

guardian = ga.Guardian()                # reads VIPER_PROXY_URL
print(guardian.capabilities().agents)   # ('analyst', 'researcher')
```

**Parameters:**

- **proxy_url** (`str | None`) – The proxy's base URL, as `viper proxy` printed it, such
  as `"http://127.0.0.1:8080"`. Defaults to `VIPER_PROXY_URL`.
  The proxy answers only callers on its own machine that address it
  as `localhost` or a loopback IP.
- **timeout** (`float`) – Seconds a call to the proxy may take. An async call ends
  within it. A sync call waits at most this long for each step
  (connecting, the answer, each read of its body) and stops reading
  once it has passed overall.

**Raises:**

- `ValueError` – Neither `proxy_url` nor `VIPER_PROXY_URL` is set, the
  one given isn't an `http(s)://host[:port]` base URL, or
  `timeout` isn't a positive number of seconds.

**Functions:**

- [**capabilities**](#guardiancapabilities) – What the proxy supports, checked on first use and then remembered.
- [**acapabilities**](#guardianacapabilities) – What the proxy supports; the async form of `capabilities`.
- [**agent**](#guardianagent) – An enrolled agent, described here, to route model clients through.
- [**agents**](#guardianagents) – The agents enrolled on the proxy, sorted.
- [**skill**](#guardianskill) – A skill for a definition's `skills`, its files stored on the proxy.
- [**askill**](#guardianaskill) – A skill, its files stored on the proxy; the async form of `skill`.
- [**instructions**](#guardianinstructions) – Memory the agent reads but can't change, its files stored on the proxy.
- [**ainstructions**](#guardianainstructions) – Fixed memory, its files stored; the async form of `instructions`.

**Attributes:**

- [**proxy_url**](#guardianproxy_url) (`str`) – The proxy's base URL, without a trailing slash.

### `Guardian.proxy_url`

```python
proxy_url: str
```

The proxy's base URL, without a trailing slash.

### `Guardian.capabilities`

```python
capabilities(*, refresh: bool = False) -> Capabilities
```

What the proxy supports, checked on first use and then remembered.

Calls made at the same moment, before any answer is remembered, may
each ask the proxy; they get the same answer.

**Parameters:**

- **refresh** (`bool`) – Ask the proxy again. The proxy reads its enrolled agents
  when it starts, so this matters after the proxy restarts.

**Returns:**

- `Capabilities` – The proxy's API version, Viper version, features, schemas,
  enrolled agents and limits.

**Raises:**

- `ProxyUnreachable` – Nothing answered at the proxy's address in time.
- `ProxyIncompatible` – The proxy doesn't serve this SDK's agent API,
  accept its definition schema, or offer a feature it relies on.
- `UnexpectedResponse` – Something answered that isn't a Viper proxy, or
  its answer couldn't be read.

### `Guardian.acapabilities`

```python
acapabilities(*, refresh: bool = False) -> Capabilities
```

What the proxy supports; the async form of `capabilities`.

**Parameters:**

- **refresh** (`bool`) – Ask the proxy again, as for `capabilities`.

**Returns:**

- `Capabilities` – The proxy's API version, Viper version, features, schemas,
  enrolled agents and limits.

**Raises:**

- `ProxyUnreachable` – Nothing answered at the proxy's address in time.
- `ProxyIncompatible` – The proxy doesn't serve this SDK's agent API,
  accept its definition schema, or offer a feature it relies on.
- `UnexpectedResponse` – Something answered that isn't a Viper proxy, or
  its answer couldn't be read.

### `Guardian.agent`

```python
agent(name: str, *, definition: Definition | None = None, **description: Any) -> Agent
```

An enrolled agent, described here, to route model clients through.

Takes the same keyword arguments as `Definition` (`model`,
`instructions`, `tools`, `exclude` and the rest), or a
`definition` built already. The definition is checked now; nothing
is sent until the agent's first request, or `Agent.register`.

Example:

```python
researcher = guardian.agent(
    "researcher",
    model=ga.Model("openai", "gpt-4.1"),
    instructions="You research.",
    tools=[search_docs],
)
client = researcher.bind(OpenAI())
```

**Parameters:**

- **name** (`str`) – The agent's name, as enrolled with `viper agent add`.
- **definition** (`Definition | None`) – The agent's definition, instead of keyword arguments.
- **description** (`Any`) – The definition's keyword arguments.

**Raises:**

- `ValueError` – `name` isn't a valid agent name.
- `TypeError` – Both `definition` and keyword arguments were given.
- `DefinitionInvalid` – The definition breaks a rule.
- `DefinitionContainsSecret` – It holds what looks like a credential.

### `Guardian.agents`

```python
agents(*, refresh: bool = False) -> tuple[str, ...]
```

The agents enrolled on the proxy, sorted.

**Parameters:**

- **refresh** (`bool`) – Ask the proxy again; it reads its agents when it starts.

**Returns:**

- `tuple[str, ...]` – The enrolled agents' names.

**Raises:**

- `ProxyUnreachable` – Nothing answered at the proxy's address in time.
- `ProxyIncompatible` – The proxy doesn't serve this SDK's agent API.
- `UnexpectedResponse` – Something answered that isn't a Viper proxy, or
  its answer couldn't be read.

### `Guardian.skill`

```python
skill(name: str, directory: str | os.PathLike[str]) -> Skill
```

A skill for a definition's `skills`, its files stored on the proxy.

Reads every regular file in the skill's directory, hidden ones
included, and uploads them. The proxy stores them and returns their
CID, the one Viper gives a coding agent's skill of the same files, so
the Agent Gateway checks both alike and editing any file gives the agent a new
definition. Symlinks inside the directory are skipped; the directory
itself may be a link.

Example:

```python
triage = guardian.skill("triage", "skills/triage")
agent = guardian.agent("planner", model=..., skills=[triage])
```

**Parameters:**

- **name** (`str`) – The skill's name, as the agent knows it.
- **directory** (`str | PathLike[str]`) – The skill's directory, the one holding its `SKILL.md`.

**Returns:**

- `Skill` – The skill, with the CID of its stored files.

**Raises:**

- `CollectionInvalid` – `directory` isn't a skill (a directory with a
  `SKILL.md`), holds no files, or has one the proxy can't take.
- `CollectionTooLarge` – The files are over the proxy's limits
  (`capabilities().limits`).
- `DefinitionContainsSecret` – A file, or a file's path, looks like it
  holds a credential.
- `OSError` – A file or directory couldn't be read.
- `ProxyUnreachable` – Nothing answered at the proxy's address in time.

### `Guardian.askill`

```python
askill(name: str, directory: str | os.PathLike[str]) -> Skill
```

A skill, its files stored on the proxy; the async form of `skill`.

**Parameters:**

- **name** (`str`) – The skill's name, as the agent knows it.
- **directory** (`str | PathLike[str]`) – The skill's directory, the one holding its `SKILL.md`.

**Returns:**

- `Skill` – The skill, with the CID of its stored files.

**Raises:**

- `CollectionInvalid` – As for `skill`.
- `CollectionTooLarge` – As for `skill`.
- `DefinitionContainsSecret` – As for `skill`.
- `OSError` – A file or directory couldn't be read.
- `ProxyUnreachable` – Nothing answered at the proxy's address in time.

### `Guardian.instructions`

```python
instructions(path: str, local_path: str | os.PathLike[str], *, backend: str) -> Memory
```

Memory the agent reads but can't change, its files stored on the proxy.

For a definition's `memory`: an `AGENTS.md`, say, or a directory
of notes. They are configuration, so they are recorded by their
files' CID, and the Agent Gateway checks them as it checks a coding agent's
`CLAUDE.md`. A file is stored under the name the agent reads it by,
the last part of `path`, whatever it's called here; a directory is
read as a skill's is.

Example:

```python
agents_md = guardian.instructions(
    "/AGENTS.md", "docs/agent-notes.md", backend="filesystem"
)
agent = guardian.agent("planner", model=..., memory=[agents_md])
```

**Parameters:**

- **path** (`str`) – Where the agent reads it, as the agent sees it.
- **local_path** (`str | PathLike[str]`) – The file or directory to read it from.
- **backend** (`str`) – The kind of storage it's in, as the framework names it.

**Returns:**

- `Memory` – The memory, with the CID of its stored files.

**Raises:**

- `CollectionInvalid` – `local_path` isn't a file or directory, holds
  no files, or has one the proxy can't take.
- `CollectionTooLarge` – The files are over the proxy's limits
  (`capabilities().limits`).
- `DefinitionContainsSecret` – A file, or a file's path, looks like it
  holds a credential.
- `OSError` – A file or directory couldn't be read.
- `ProxyUnreachable` – Nothing answered at the proxy's address in time.

### `Guardian.ainstructions`

```python
ainstructions(path: str, local_path: str | os.PathLike[str], *, backend: str) -> Memory
```

Fixed memory, its files stored; the async form of `instructions`.

**Parameters:**

- **path** (`str`) – Where the agent reads it, as the agent sees it.
- **local_path** (`str | PathLike[str]`) – The file or directory to read it from.
- **backend** (`str`) – The kind of storage it's in, as the framework names it.

**Returns:**

- `Memory` – The memory, with the CID of its stored files.

**Raises:**

- `CollectionInvalid` – As for `instructions`.
- `CollectionTooLarge` – As for `instructions`.
- `DefinitionContainsSecret` – As for `instructions`.
- `OSError` – A file or directory couldn't be read.
- `ProxyUnreachable` – Nothing answered at the proxy's address in time.

{/* end generated */}

## Agent

{/* generated api guardian_agent.Agent */}

An enrolled agent, ready to route model clients through the proxy.

Made by `Guardian.agent`. Nothing is sent until the first request
(or `register`); then the definition is registered once, and every
request names the agent so the proxy signs it with the agent's key.

An `Agent` is safe to share between threads and async tasks, and holds
no open connections of its own.

**Attributes:**

- **name** – The agent's name, as enrolled with `viper agent add`.
- **definition** – What the agent is.
- **guardian** – The connection to the proxy it routes through.
- **subagent_path** – For a route made by `subagent`, the sub-agent's
  path, such as `"critic/fact-checker"`; `None` for the agent.

**Functions:**

- [**register**](#agentregister) – Register the definition now, rather than on the first request.
- [**aregister**](#agentaregister) – Register the definition now; the async form of `register`.
- [**subagent**](#agentsubagent) – A route through one of the agent's sub-agents, which signs as the agent.
- [**bind**](#agentbind) – A copy of a model client that routes through the proxy as this agent.
- [**httpx_client**](#agenthttpx_client) – An `httpx.Client` whose requests go through the proxy as this agent.
- [**httpx_async_client**](#agenthttpx_async_client) – An `httpx.AsyncClient`; the async form of `httpx_client`.

### `Agent.definition_cid`

```python
definition_cid: str | None
```

The registered definition's content ID, or `None` before registration.

### `Agent.register`

```python
register() -> str
```

Register the definition now, rather than on the first request.

Call it at start-up so a misconfigured deployment fails there, with a
typed error, rather than on its first user. Once registered, calling
it again returns the content ID without contacting the proxy; after a
proxy restart, the agent's next model request registers it again.

**Returns:**

- `str` – The definition's content ID, as the proxy stored it.

**Raises:**

- `ProxyUnreachable` – Nothing answered at the proxy's address.
- `ProxyIncompatible` – The proxy doesn't speak this SDK's agent API.
- `AgentNotEnrolled` – The proxy has no agent of this name; run
  `viper agent add` and `viper agent register`, then
  restart the proxy.
- `DefinitionError` – The proxy refused the definition.
- `UnexpectedResponse` – Something answered that isn't a Viper proxy,
  or its answer couldn't be read.

### `Agent.aregister`

```python
aregister() -> str
```

Register the definition now; the async form of `register`.

However many threads and tasks ask at once, the proxy sees one
registration and they all share its outcome. Waiting holds no worker
thread, and cancelling the wait (`anyio.move_on_after`,
`asyncio.wait_for`) works.

**Returns:**

- `str` – The definition's content ID, as the proxy stored it.

**Raises:**

- `ProxyUnreachable` – As for `register`.
- `ProxyIncompatible` – As for `register`.
- `AgentNotEnrolled` – As for `register`.
- `DefinitionError` – As for `register`.
- `UnexpectedResponse` – As for `register`.

### `Agent.subagent`

```python
subagent(path: str) -> Agent
```

A route through one of the agent's sub-agents, which signs as the agent.

Example:

```python
reviewing = writer.subagent("critic").bind(OpenAI())
checking = writer.subagent("critic/fact-checker")
```

**Parameters:**

- **path** (`str`) – The sub-agent's name, or a `/`-separated path to a nested
  one, relative to this route.

**Returns:**

- `Agent` – A route that shares this agent's registration.

**Raises:**

- `SubagentUnknown` – The definition declares no sub-agent at `path`.

### `Agent.bind`

```python
bind(client: Any) -> Any
```

A copy of a model client that routes through the proxy as this agent.

Your client is left as it was. A client still aimed at the provider's
own API is pointed at the proxy; one aimed anywhere else is refused.

Example:

```python
client = researcher.bind(OpenAI())
client.chat.completions.create(model="gpt-4.1", messages=[...])

llm = researcher.bind(ChatOpenAI(model="gpt-4.1"))
llm.bind_tools([search_docs]).invoke("...")
```

A LangChain chat model's copy routes every call it makes, sync or
async, streamed or not, and through `bind_tools()` and
`with_structured_output()`; bind the model before those.

**Parameters:**

- **client** (`Any`) – `OpenAI`, `AsyncOpenAI`, `Anthropic` or
  `AsyncAnthropic`; or LangChain's `ChatOpenAI` or
  `ChatAnthropic`.

**Returns:**

- `Any` – A client or chat model of the same type.

**Raises:**

- `ModelNotRoutedThroughProxy` – The client is aimed somewhere other
  than the proxy or the provider's own API, or sets
  `websocket_base_url`.
- `ValueError` – The client is aimed at the proxy with the wrong path
  (OpenAI's clients need `/v1`, Anthropic's none); or a
  LangChain model isn't one the definition describes exactly
  (for a sub-agent route, one the sub-agent's does), or sends a
  setting no definition can record, as its requests would be
  signed under a definition that doesn't describe them.
- `TypeError` – It isn't a client `bind()` knows (use
  `httpx_client` for others), or is a LangChain model
  that can't be routed: a model name, one wrapped by
  `bind_tools()`, `with_structured_output()`,
  `with_retry()`, `with_fallbacks()` or `with_config()`,
  one made with `configurable_fields`, one given its own
  `client` or `async_client`, or another kind of chat
  model.

### `Agent.httpx_client`

```python
httpx_client(**options: Any) -> httpx.Client
```

An `httpx.Client` whose requests go through the proxy as this agent.

For any SDK that takes an `httpx` client. Its base URL is the
proxy's, and a request anywhere else is refused rather than sent.

**Parameters:**

- **options** (`Any`) – `httpx.Client` options: `headers`, `timeout`,
  `auth`, `follow_redirects`, `event_hooks` and the like,
  and connection options (`verify`, `cert`, `http2`,
  `limits`, `retries`, `local_address`). Options that
  could send requests elsewhere (`transport`, `mounts`,
  `proxy`) are refused, as is `trust_env`, which wouldn't
  apply. OpenAI's Realtime (WebSocket) connections don't use
  this client, so an SDK given it routes only its HTTP requests.

**Raises:**

- `TypeError` – An option isn't one the agent's client takes.

### `Agent.httpx_async_client`

```python
httpx_async_client(**options: Any) -> httpx.AsyncClient
```

An `httpx.AsyncClient`; the async form of `httpx_client`.

{/* end generated */}