| Agent | Owner | Status | Granted tools | Recently used | Last seen | |
|---|---|---|---|---|---|---|
| Loading… | ||||||
input_guardrails + output_guardrails config — click to expandNo custom input policies configured.
No custom output policies configured.
Create Custom Policy
message, stage, user_role, session_id, agent_id, tool_name, tool_input.
Modifiers: contains, startswith, endswith, all, cased, exists, re. A keyword list searches message.
Loading…
X-API-Key header.
Header: X-API-Key: <your-key>
your-x-votal-tenant-key-here
https://your-domain.com/v1/
—
X-API-Key header for all requests.POST /v1/agents/registry
PUT /v1/tenant/me/policies
PUT /v1/agents/tools/policies
POST /v1/shield/tool/check · /output
GET /v1/tenant/me/usage · /audit
│
├─▶ LiteLLM Proxy (https://litellm.your-company.com)
│ ├── Input guardrails run automatically (adversarial, PII, toxicity)
│ ├── LLM call → returns tool_calls
│ └── Output guardrails run automatically (PII leakage, bias)
│
├─▶ Shield (https://shield.votal.ai) /v1/shield/tool/check
│ ├── RBAC: can this role use this tool?
│ ├── Data policy: do input rules allow these args?
│ └── allowed → execute tool locally
│
└─▶ Shield (https://shield.votal.ai) /v1/shield/tool/output
├── allow → output unchanged
├── mask → partial masking (j***@****.com)
├── redact → full replacement ([REDACTED])
└── block → output rejected entirely
/guardrails/input or /guardrails/output directly.
LiteLLM handles those via Shield middleware. Your app only adds ~10 lines around the tool execution loop.
If tools/agents are not registered in the portal, they are blocked by default and tracked as shadow agents.
pip install langchain langchain-openai openai requests
"""
LangChain + LLM Shield (Production Architecture)
LiteLLM proxy handles input/output guardrails automatically.
App only calls Shield for tool RBAC + data policy enforcement.
"""
import os, json, requests
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
# LiteLLM proxy — your LLM gateway (guardrails run here automatically)
LITELLM_URL = os.getenv("LITELLM_URL", "https://litellm.your-company.com")
# Shield — tool RBAC + data policies (separate service)
SHIELD_URL = os.getenv("LLM_SHIELD_URL", "https://shield.votal.ai")
API_KEY = os.getenv("API_KEY")
AGENT_ID = os.getenv("AGENT_ID", "my-langchain-agent")
USER_ROLE = os.getenv("USER_ROLE", "user")
shield = requests.Session()
shield.headers.update({
"X-API-Key": API_KEY,
"X-Agent-Key": AGENT_ID,
"X-User-Role": USER_ROLE,
"Content-Type": "application/json",
})
# ── 1. Register Agent (do this once) ──────────────────────────────
def register_agent():
"""Register the agent so Shield knows about it.
If you skip this, the agent appears as a SHADOW AGENT."""
resp = shield.post(f"{SHIELD_URL}/v1/agents/registry", json={
"agent_id": AGENT_ID,
"name": "LangChain Support Agent",
"tools": ["search_faq", "create_ticket", "check_order_status"],
"role_permissions": {
"user": ["search_faq", "check_order_status"],
"support": ["search_faq", "create_ticket", "check_order_status"],
"admin": ["search_faq", "create_ticket", "check_order_status"],
},
})
print("Registered:", resp.json())
# ── 2. Define LangChain tools ─────────────────────────────────────
@tool
def search_faq(query: str) -> str:
"""Search the FAQ knowledge base for answers."""
return f"FAQ result for '{query}': Check our help center at /help."
@tool
def create_ticket(subject: str, description: str) -> str:
"""Create a support ticket for the customer."""
return f"Ticket created: {subject} — ID: TKT-{hash(subject) % 10000}"
@tool
def check_order_status(order_id: str) -> str:
"""Check the current status of a customer order."""
return f"Order {order_id}: Shipped, arriving in 2 days."
lc_tools = [search_faq, create_ticket, check_order_status]
# ── 3. Shielded LangChain agent ──────────────────────────────────
def run_agent(user_message: str):
# LangChain LLM pointed at LiteLLM proxy
# Input/output guardrails are applied automatically by LiteLLM
llm = ChatOpenAI(
model="default",
base_url=f"{LITELLM_URL}/v1",
api_key=os.getenv("OPENAI_API_KEY", "not-needed"),
default_headers={
"X-API-Key": API_KEY,
"X-Agent-Key": AGENT_ID,
"X-User-Role": USER_ROLE,
},
)
llm_with_tools = llm.bind_tools(lc_tools)
# Step 1: LLM call via LiteLLM (guardrails automatic)
messages = [HumanMessage(content=user_message)]
try:
response = llm_with_tools.invoke(messages)
except Exception as e:
return f"Blocked by guardrails: {e}"
if not response.tool_calls:
return response.content
# Step 2: Process tool calls with Shield RBAC + data policies
messages.append(response)
tool_map = {t.name: t for t in lc_tools}
output_parts = []
for tc in response.tool_calls:
name, args = tc["name"], tc["args"]
# 2a. Shield RBAC + data policy check on tool args
check = shield.post(f"{SHIELD_URL}/v1/shield/tool/check", json={
"tool_name": name,
"tool_input": args,
"agent_key": AGENT_ID,
"user_role": USER_ROLE,
}).json()
if not check.get("allowed", True):
reason = check.get("reason", "denied by RBAC")
messages.append(ToolMessage(content=f"DENIED: {reason}",
tool_call_id=tc["id"]))
output_parts.append(f"BLOCKED: {name} — {reason}")
continue
# 2b. Execute the tool locally
raw_output = tool_map[name].invoke(args)
# 2c. Sanitize tool output (mask/redact/block per data policy)
sanitized = shield.post(f"{SHIELD_URL}/v1/shield/tool/output", json={
"tool_name": name,
"tool_output": str(raw_output),
"agent_key": AGENT_ID,
}).json()
if sanitized.get("action") == "block":
messages.append(ToolMessage(
content="Tool output blocked by data policy.",
tool_call_id=tc["id"]))
else:
clean = sanitized.get("sanitized_output", str(raw_output))
messages.append(ToolMessage(content=clean, tool_call_id=tc["id"]))
output_parts.append(f"[{name}] {clean}")
# Step 3: Final LLM call (output guardrails automatic via LiteLLM)
try:
final = llm_with_tools.invoke(messages)
return final.content
except Exception:
return "\n".join(output_parts)
if __name__ == "__main__":
# register_agent() # Uncomment on first run
print(run_agent("What is your return policy?"))
print(run_agent("Check status of order ORD-555"))
print(run_agent("Create a ticket: billing issue"))
AGENT_ID is not registered:1. Every API call with that
X-Agent-Key is tracked by the middleware2. Any tools the LLM calls are flagged as shadow tools
3. The tenant portal Agents tab shows a "Shadow Discovery" panel
4. Metadata captured: call count, endpoints hit, roles used, first/last seen
5. Admin can dismiss or register the agent from the UI
pip install crewai crewai-tools openai requests
"""
CrewAI + LLM Shield (Production Architecture)
LiteLLM proxy handles input/output guardrails automatically.
App only calls Shield for per-agent tool RBAC + data policies.
"""
import os, json, requests
from crewai import Agent, Task, Crew, Process
from crewai.tools import BaseTool
from typing import Type
from pydantic import BaseModel, Field
# LiteLLM proxy — your LLM gateway (guardrails run here automatically)
LITELLM_URL = os.getenv("LITELLM_URL", "https://litellm.your-company.com")
# Shield — tool RBAC + data policies (separate service)
SHIELD_URL = os.getenv("LLM_SHIELD_URL", "https://shield.votal.ai")
API_KEY = os.getenv("API_KEY")
shield = requests.Session()
shield.headers.update({
"X-API-Key": API_KEY,
"Content-Type": "application/json",
})
# ── 1. Register all crew agents (do once) ─────────────────────────
def register_crew():
agents = [
{
"agent_id": "research-agent",
"name": "Research Analyst",
"tools": ["web_search", "document_search"],
"role_permissions": {
"analyst": ["web_search", "document_search"],
"viewer": ["web_search"],
},
},
{
"agent_id": "writer-agent",
"name": "Report Writer",
"tools": ["generate_report", "send_email"],
"role_permissions": {
"analyst": ["generate_report", "send_email"],
"viewer": ["generate_report"],
},
},
]
for a in agents:
r = shield.post(f"{SHIELD_URL}/v1/agents/registry", json=a)
print(f"Register {a['agent_id']}: {r.status_code}")
# ── 2. Shield-aware tool wrapper ──────────────────────────────────
class ShieldTool(BaseTool):
"""Wraps tool calls with Shield RBAC + data policy checks.
Input/output guardrails are handled by LiteLLM automatically."""
agent_key: str = ""
user_role: str = "analyst"
def _run(self, query: str) -> str:
# PRE-EXECUTION: RBAC + data policy check on tool args
tool_check = shield.post(f"{SHIELD_URL}/v1/shield/tool/check", json={
"tool_name": self.name,
"tool_input": {"query": query},
"agent_key": self.agent_key,
"user_role": self.user_role,
}).json()
if not tool_check.get("allowed", True):
return f"BLOCKED: {self.name} — {tool_check.get('reason', 'denied')}"
# Execute tool (your actual implementation here)
raw_output = f"[{self.name}] Results for: {query}"
# POST-EXECUTION: Sanitize tool output (mask/redact/block)
sanitized = shield.post(f"{SHIELD_URL}/v1/shield/tool/output", json={
"tool_name": self.name,
"tool_output": raw_output,
"agent_key": self.agent_key,
}).json()
if sanitized.get("action") == "block":
return f"BLOCKED: {self.name} output violated data policy"
return sanitized.get("sanitized_output", raw_output)
# ── 3. Build the crew ─────────────────────────────────────────────
class WebSearchInput(BaseModel):
query: str = Field(description="Search query")
class WebSearchTool(ShieldTool):
name: str = "web_search"
description: str = "Search the web for information"
args_schema: Type[BaseModel] = WebSearchInput
agent_key: str = "research-agent"
class GenerateReportInput(BaseModel):
query: str = Field(description="Report topic and data")
class GenerateReportTool(ShieldTool):
name: str = "generate_report"
description: str = "Generate a formatted report"
args_schema: Type[BaseModel] = GenerateReportInput
agent_key: str = "writer-agent"
def run_crew(topic: str, user_role: str = "analyst"):
# CrewAI uses LiteLLM as its LLM backend
# Input/output guardrails are applied automatically by LiteLLM
researcher = Agent(
role="Research Analyst",
goal=f"Research: {topic}",
backstory="Senior analyst with deep domain expertise",
tools=[WebSearchTool(user_role=user_role)],
llm=f"litellm/{LITELLM_URL}/v1/default",
verbose=True,
)
writer = Agent(
role="Report Writer",
goal="Write a clear, actionable report",
backstory="Technical writer specializing in research reports",
tools=[GenerateReportTool(user_role=user_role)],
llm=f"litellm/{LITELLM_URL}/v1/default",
verbose=True,
)
crew = Crew(
agents=[researcher, writer],
tasks=[
Task(description=f"Research {topic}", expected_output="Findings", agent=researcher),
Task(description="Write report", expected_output="Report", agent=writer),
],
process=Process.sequential,
verbose=True,
)
return str(crew.kickoff())
if __name__ == "__main__":
# register_crew() # Uncomment on first run
print(run_crew("Analyze Q1 2026 market trends in AI"))
1. Each CrewAI agent maps to a separate
agent_id in Shield2. If any crew member is not registered, it is tracked as a shadow agent
3. Tools used by unregistered agents are tracked as shadow tools
4.
GET /v1/agents/unregistered returns all shadow items5. Admin can register missing agents or dismiss false positives
pip install openai requests
"""
OpenAI Agents SDK + LLM Shield (Production Architecture)
LiteLLM proxy handles input/output guardrails automatically.
App only calls Shield for tool RBAC + data policy enforcement.
"""
import os, json, requests
from openai import OpenAI
# LiteLLM proxy — your LLM gateway (guardrails run here automatically)
LITELLM_URL = os.getenv("LITELLM_URL", "https://litellm.your-company.com")
# Shield — tool RBAC + data policies (separate service)
SHIELD_URL = os.getenv("LLM_SHIELD_URL", "https://shield.votal.ai")
API_KEY = os.getenv("API_KEY")
AGENT_ID = os.getenv("AGENT_ID", "my-openai-agent")
USER_ROLE = os.getenv("USER_ROLE", "user")
# OpenAI client pointed at LiteLLM (guardrails automatic)
client = OpenAI(base_url=f"{LITELLM_URL}/v1",
api_key=os.getenv("OPENAI_API_KEY", "not-needed"),
default_headers={"X-API-Key": API_KEY,
"X-Agent-Key": AGENT_ID,
"X-User-Role": USER_ROLE})
shield = requests.Session()
shield.headers.update({
"X-API-Key": API_KEY,
"X-Agent-Key": AGENT_ID,
"X-User-Role": USER_ROLE,
"Content-Type": "application/json",
})
# ── 1. Register Agent (do this once) ──────────────────────────────
def register_agent():
shield.post(f"{SHIELD_URL}/v1/agents/registry", json={
"agent_id": AGENT_ID,
"name": "My OpenAI Agent",
"tools": ["lookup_order", "cancel_order", "get_refund_status"],
"role_permissions": {
"user": ["lookup_order", "get_refund_status"],
"admin": ["lookup_order", "cancel_order", "get_refund_status"],
},
})
# ── 2. Define tools (OpenAI function calling format) ──────────────
tools = [
{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up order details by order ID",
"strict": True,
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
"additionalProperties": False,
},
}},
{"type": "function", "function": {
"name": "cancel_order",
"description": "Cancel an existing order",
"strict": True,
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}, "reason": {"type": "string"}},
"required": ["order_id", "reason"],
"additionalProperties": False,
},
}},
]
# ── 3. Shielded agent loop ────────────────────────────────────────
def run_agent(user_message: str):
# Step 1: LLM call via LiteLLM (input guardrails automatic)
messages = [
{"role": "system", "content": "You are a helpful support agent."},
{"role": "user", "content": user_message},
]
response = client.chat.completions.create(
model="default", messages=messages,
tools=tools, tool_choice="auto",
)
msg = response.choices[0].message
if not msg.tool_calls:
return msg.content # Output guardrails already applied by LiteLLM
# Step 2: Process tool calls with Shield RBAC + data policies
for tc in msg.tool_calls:
name = tc.function.name
args = json.loads(tc.function.arguments)
# 2a. Shield RBAC + data policy check on tool args
check = shield.post(f"{SHIELD_URL}/v1/shield/tool/check", json={
"tool_name": name,
"tool_input": args,
"agent_key": AGENT_ID,
"user_role": USER_ROLE,
}).json()
if not check.get("allowed", True):
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": f"DENIED: {check.get('reason', 'RBAC')}"})
continue
# 2b. Execute tool (your implementation)
raw_output = execute_tool(name, args)
# 2c. Sanitize output (mask/redact/block per data policy)
sanitized = shield.post(f"{SHIELD_URL}/v1/shield/tool/output", json={
"tool_name": name,
"tool_output": str(raw_output),
"agent_key": AGENT_ID,
}).json()
if sanitized.get("action") == "block":
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": "Output blocked by data policy."})
else:
clean = sanitized.get("sanitized_output", str(raw_output))
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": clean})
# Step 3: Final LLM call (output guardrails automatic via LiteLLM)
final = client.chat.completions.create(model="default", messages=messages)
return final.choices[0].message.content
if __name__ == "__main__":
# register_agent() # Uncomment on first run
print(run_agent("What's the status of order ORD-12345?"))
print(run_agent("Cancel order ORD-12345, I changed my mind"))
AGENT_ID is not registered:1. Every API call with that
X-Agent-Key is tracked by the middleware2. Any tools the LLM calls are flagged as shadow tools
3. The tenant portal Agents tab shows a "Shadow Discovery" panel
4. Metadata captured: call count, endpoints hit, roles used, first/last seen
5. Admin can dismiss or register the agent from the UI
pip install anthropic requests
"""
Anthropic Claude + LLM Shield (Production Architecture)
LiteLLM proxy handles input/output guardrails automatically.
App only calls Shield for tool RBAC + data policy enforcement.
"""
import os, json, requests
import anthropic
# LiteLLM proxy — your LLM gateway (guardrails run here automatically)
LITELLM_URL = os.getenv("LITELLM_URL", "https://litellm.your-company.com")
# Shield — tool RBAC + data policies (separate service)
SHIELD_URL = os.getenv("LLM_SHIELD_URL", "https://shield.votal.ai")
API_KEY = os.getenv("API_KEY")
AGENT_ID = os.getenv("AGENT_ID", "my-claude-agent")
USER_ROLE = os.getenv("USER_ROLE", "user")
# Claude client via LiteLLM (guardrails automatic)
claude = anthropic.Anthropic(base_url=f"{LITELLM_URL}/v1",
api_key=os.getenv("ANTHROPIC_API_KEY", "not-needed"))
shield = requests.Session()
shield.headers.update({
"X-API-Key": API_KEY,
"X-Agent-Key": AGENT_ID,
"X-User-Role": USER_ROLE,
"Content-Type": "application/json",
})
# ── 1. Register Agent ─────────────────────────────────────────────
def register_agent():
shield.post(f"{SHIELD_URL}/v1/agents/registry", json={
"agent_id": AGENT_ID,
"name": "Claude Support Agent",
"tools": ["search_knowledge_base", "create_ticket", "escalate_to_human"],
"role_permissions": {
"user": ["search_knowledge_base"],
"support": ["search_knowledge_base", "create_ticket"],
"admin": ["search_knowledge_base", "create_ticket", "escalate_to_human"],
},
})
# ── 2. Define tools (Anthropic format) ────────────────────────────
claude_tools = [
{"name": "search_knowledge_base",
"description": "Search the support knowledge base",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
}},
{"name": "create_ticket",
"description": "Create a support ticket",
"input_schema": {
"type": "object",
"properties": {
"subject": {"type": "string"},
"description": {"type": "string"},
"priority": {"type": "string", "enum": ["low", "medium", "high"]},
},
"required": ["subject", "description", "priority"],
}},
]
# ── 3. Shield-wrapped Claude agent ────────────────────────────────
def run_claude_agent(user_message: str):
# Step 1: Call Claude via LiteLLM (input guardrails automatic)
response = claude.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="You are a helpful support agent. Use tools when needed.",
messages=[{"role": "user", "content": user_message}],
tools=claude_tools,
)
# Step 2: Process response blocks
results = []
tool_results = []
for block in response.content:
if block.type == "text":
results.append(block.text)
elif block.type == "tool_use":
name = block.name
# 2a. Shield RBAC + data policy check on tool args
check = shield.post(f"{SHIELD_URL}/v1/shield/tool/check", json={
"tool_name": name,
"tool_input": block.input,
"agent_key": AGENT_ID,
"user_role": USER_ROLE,
}).json()
if not check.get("allowed", True):
tool_results.append({"type": "tool_result", "tool_use_id": block.id,
"content": f"DENIED: {check.get('reason', 'RBAC')}"})
continue
# 2b. Execute tool (your implementation)
raw_output = execute_tool(name, block.input)
# 2c. Sanitize output (mask/redact/block per data policy)
sanitized = shield.post(f"{SHIELD_URL}/v1/shield/tool/output", json={
"tool_name": name,
"tool_output": str(raw_output),
"agent_key": AGENT_ID,
}).json()
if sanitized.get("action") == "block":
tool_results.append({"type": "tool_result", "tool_use_id": block.id,
"content": "Output blocked by data policy."})
else:
clean = sanitized.get("sanitized_output", str(raw_output))
tool_results.append({"type": "tool_result", "tool_use_id": block.id,
"content": clean})
# Step 3: If tools were called, send results back to Claude
if tool_results:
final = claude.messages.create(
model="claude-sonnet-4-20250514", max_tokens=1024,
messages=[
{"role": "user", "content": user_message},
{"role": "assistant", "content": response.content},
{"role": "user", "content": tool_results},
],
tools=claude_tools,
)
return final.content[0].text
return "\n".join(results)
if __name__ == "__main__":
# register_agent() # Uncomment on first run
print(run_claude_agent("Search for how to reset my password"))
print(run_claude_agent("Create an urgent ticket: billing error"))
| Tool | Agents | Status | Reason | Action |
|---|---|---|---|---|
| Loading tools... | ||||
monitor mode, review what would be blocked, then switch to enforce.runtime_profile). Agents without a profile are unaffected.examples/runtime/shield_runtime_sync.py rather than a copy of this file.Laptops without the Votal agent
shield_runtime_sync.py --watch) and what agent tokens attest at capability mint. A sandbox that needs a restart, or whose policy was changed outside Shield, is shown here.action_profile). Robots pull the profile as a signed bundle with packages/shield-embodied.proposed_action is what the model proposes; context is what the robot knows.embodied_guard) and uploaded from robots' audit logs (runtime:action).PinnedPublicKey is the key laptops check every policy bundle against; copy it from here into MDM, never from anywhere else.mode is monitor or enforce, per fleet in fleet_modes. enforcement sets which categories the model may act on; source code, financial data and exfiltration intent are monitor-only by default because Tev1 0.8B misses too many of them. Rules come from your Data Policies.Pending Approvals
Circuit Breakers
How to put your MCP server behind Shield 3 steps
Set $SHIELD to your Shield data-plane URL and $KEY to your tenant API key. These are gateway API calls; this portal is the admin plane, so run them from your terminal or CI.
isolation_ack:true asserts your server only accepts traffic from the gateway — set it false if it is still publicly reachable, and Shield will warn.curl -X PUT "$SHIELD/v1/tenant/me/mcp-gateway/upstreams/my-server" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{
"transport": "http",
"url": "https://your-mcp-server/mcp",
"enforcement_backend": "inprocess",
"isolation_ack": true
}'tools/list.curl -X POST "$SHIELD/v1/agents/registry" \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{
"agent_id": "my-agent",
"tools": ["search", "get_record"],
"role_permissions": { "reader": ["search", "get_record"] }
}'tools/call, then forwards to your server.# MCP endpoint for the "my-server" route: $SHIELD/gateway/my-server/mcp # Identity headers on every request: X-API-Key: $KEY X-Agent-Key: my-agent X-User-Role: reader
delete_record). A correctly-wired gateway returns isError:true with a "Blocked by Shield" message. If it succeeds, enforcement is not engaging — check isolation_ack and that the agent is registered.
match is san_uri, san_dns or subject_cn; pattern is an exact value, or contains {agent_id} once, for example spiffe://acme.com/agent/{agent_id}. First match wins; a rule never maps to an unregistered agent.A certificate, and its fingerprint, is public. What proves an agent is who it says is the TLS handshake in which it uses the certificate's private key. That handshake happens at your proxy (Envoy, nginx or a load balancer), which forwards the certificate to Shield. Shield accepts a forwarded certificate only from that proxy: it must send SHIELD_TRUSTED_PROXY_SECRET as X-Shield-Proxy-Token, with SHIELD_TRUSTED_PROXY_ONLY=true set on Shield. Without that, certificate headers are ignored.
Trust levels: high for a certificate verified through your proxy, or an agent token whose key proof (DPoP) verified. medium for an agent named by key or a bearer agent token. low for no agent. The cert_identity guardrail can require a minimum trust per tool (min_trust_for_tools) and blocks callers below it. Agents can also be required to prove their identity under Agent Registry (identity.require).
Step 1: Issue the agent a client certificate from your CA, and either register the certificate above or add your CA as a trust anchor with a mapping rule.
Step 2: Terminate mTLS at your proxy and forward the certificate, overwriting any value the client sent. Envoy:
forward_client_cert_details: SANITIZE_SET
set_current_client_cert_details: { cert: true, chain: true, uri: true }
# and add the header X-Shield-Proxy-Token: <SHIELD_TRUSTED_PROXY_SECRET>
nginx:
ssl_verify_client on; proxy_set_header X-Forwarded-Client-Cert $ssl_client_escaped_cert; proxy_set_header X-Shield-Proxy-Token "<SHIELD_TRUSTED_PROXY_SECRET>";
Step 3: Check it with Test a certificate above, and call Shield through the proxy. The agent never sends its fingerprint itself.
Step 4: Revoke a compromised certificate above or with POST /v1/shield/agent/identity/revoke. It stops identifying the agent at once on the server that handled the request, and within 5 minutes on every other server.
| Time | Event | Agent | User | Tool | Resource | Reason |
|---|---|---|---|---|---|---|
| Loading… | ||||||
curl -X POST $BASE/v1/shield/auth/agent-token \
-H "X-Admin-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_sub": "alice",
"agent_id": "billing-bot",
"agent_instance_id": "inst-1",
"tenant_id": "",
"build_hash": "sha256:<agent-build>",
"model_version": "claude-opus-4.7",
"session_id": "s1",
"ttl_seconds": 600
}'
curl -X POST $BASE/v1/shield/cap/mint \
-H "X-Agent-Token: $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tool":"send_email","resource":"alice/inbox","ttl_seconds":30}'
403 with reasons — visible in the table above.curl -X POST $BASE/v1/shield/cap/verify \
-H "Content-Type: application/json" \
-d '{"cap_token":"$CAP","expected_tool":"send_email"}'
cap_replay in red.- Agent tokens ≤ 15 minutes. Capability tokens ≤ 60 seconds.
- Caps are single-use (nonce burned at verify). Replay returns
valid: false. - Verify uses a local public key — no round-trip to Shield needed at the tool.
- If a build hash isn't on the allowlist, the token is rejected at the middleware.
- Revoke an instance / user / token via
POST /v1/shield/auth/revoke(admin-gated).