EQTY LabDocs

Agents and identities

How each agent gets its own key and agent ID, how one Viper proxy signs for several agents, and how sub-agents and instances fit in.

View as Markdown

Every agent you run with the SDK has its own identity in the Guardian Platform, so its requests can be told apart from every other agent’s, and checked against what it said it was.

An agent’s identity

An agent’s identity has two parts:

  • A key, held by Viper on the machine the agent runs on. Its public half is a DID (did:key:...), and every request the agent makes is signed with it. No key ever enters your Python process.
  • An agent ID, given by the Guardian Platform when the key is registered. It names the agent across keys and machines.

You create both with Viper, once per agent:

sh
viper agent add researcher
GUARDIAN_API_KEY=ak_... viper agent register researcher

viper agent add creates the key and records it under the agent’s name in Viper’s config.toml. It takes the same signer options as viper did add, so the key can live on a YubiKey or a vComp Notary instead of on disk (Key storage and signers). viper agent register registers the key with the Guardian Platform, as an agent of app type custom with its name as the display name, and saves the agent ID it gets.

The name is how your code refers to the agent, in guardian.agent("researcher", ...): 1 to 64 characters of a-z, 0-9, ., _ or -, starting with a letter or digit.

Viper’s agent commands manage the agents on a host:

  • viper agent add <name> creates a key for a new agent and records it under the name.
  • viper agent register <name> registers the agent’s key with the Guardian Platform, with an API key (GUARDIAN_API_KEY) or a registration token (VIPER_PROVISIONING_TOKEN).
  • viper agent list lists the named agents on this host.
  • viper agent renew <name> renews a registered agent’s credential.
  • viper agent remove <name> removes the agent from this host’s configuration, so proxies started afterwards don’t sign as it. A proxy that’s already running keeps signing as it until it’s stopped or restarted. The key stays in the keyring, and the agent stays registered, with its credential valid, until it’s revoked in the Guardian Platform; revoke it there to stop it being used anywhere.

One proxy, many agents

A single viper proxy holds the key of every agent enrolled on its machine. Each request your agent’s model client sends through it names the agent in an X-Viper-Agent header, which the SDK adds for you, and the proxy signs it with that agent’s key. So several agents, in one process or several, can share a proxy and still be told apart.

The proxy reads its enrolled agents when it starts: start it after enrolling an agent, and restart it after enrolling or removing one. guardian.agents() lists the agents a running proxy knows.

A proxy fronts one provider’s gateway endpoint (--gateway-endpoint, such as …/v1/openai_api or …/v1/anthropic_api). An agent that uses two providers runs through two proxies, one for each endpoint: make the agent from a Guardian for each, with the same name and definition.

A request that names no agent is signed with the proxy’s own key (the one viper did add --assign proxy creates) and reaches the Guardian Platform as generic proxy traffic, with no agent’s identity or definition. Run the proxy with --require-agent-header to refuse such requests instead, so that nothing goes out under the proxy’s name.

The proxy only answers callers on its own machine. Any process on that machine that can reach it can name any agent enrolled on it, so keep the machine, or the container, to the agents it runs.

Sub-agents

A sub-agent, such as a Deep Agents sub-agent or a reviewer your agent hands work to, isn’t enrolled on its own. It’s declared in its parent’s definition and signs as its parent: its requests carry the parent’s key, and an X-Viper-Subagent header records which sub-agent made each one.

python
from openai import OpenAI

import guardian_agent as ga

WRITER = "You write reports."
CRITIC = "You review drafts."

guardian = ga.Guardian()

critic = ga.SubAgent(
    "critic",
    description="Reviews drafts for unsupported claims.",
    model=ga.Model("openai", "gpt-4.1-mini"),
    instructions=CRITIC,
)
# Enrolled as report-writer; its sub-agents aren't enrolled separately
writer = guardian.agent(
    "report-writer",
    model=ga.Model("openai", "gpt-4.1"),
    instructions=WRITER,
    subagents=[critic],
)

# Signed as report-writer
drafting = writer.bind(OpenAI())
# Signed as report-writer, recorded as its critic
reviewing = writer.subagent("critic").bind(OpenAI())

draft = drafting.chat.completions.create(
    model="gpt-4.1",
    messages=[
        {"role": "system", "content": WRITER},
        {"role": "user", "content": "Draft the quarterly summary."},
    ],
)
text = draft.choices[0].message.content
review = reviewing.chat.completions.create(
    model="gpt-4.1-mini",
    messages=[
        {"role": "system", "content": CRITIC},
        {"role": "user", "content": f"Review this draft:\n{text}"},
    ],
)
print(review.choices[0].message.content)

Both requests are signed as report-writer; the Guardian Platform records the second as its critic’s. Nested sub-agents are named by path, as in writer.subagent("critic/fact-checker"), and a name the definition doesn’t declare raises ga.SubagentUnknown when you call subagent(), not on the first request.

Several instances of one agent

An agent running in several containers is still one agent. Each container has its own key, and joins the existing agent with its agent ID, so the Guardian Platform sees one agent with several keys:

sh
viper agent add researcher
GUARDIAN_API_KEY=ak_... viper agent register researcher --agent-id <agent ID>

Joining needs an API key, held by an operator of the agent. VIPER_AGENT_ID sets the agent ID in place of --agent-id, which suits a container image.