Agent Cards as Identity
Every agent in the fleet has an Agent Card — a structured JSON document that encodes identity, org position, capabilities, and authority ceilings, loaded at broker startup and used in every routing decision.
Every agent in the fleet has a JSON file in agents/cards/. This file is that agent's identity document.
Not documentation. Not reference material. Executable configuration.
The broker loads every card in the directory on startup. From that point forward, every routing decision, every authority check, every escalation chain is derived from what is in those cards. If you want to understand why a message went where it went, you read the Agent Cards. If you want to change how an agent is treated by the system, you update its card.
This is one of the most important structural decisions in Principal Broker's design: the org chart is not a diagram on a whiteboard. It is load-bearing infrastructure.
The Pydantic Schema
The card schema is defined in broker/schemas/agent_card.py. Every card must satisfy this structure:
class AgentCard(BaseModel):
id: str
name: str
version: str
type: str # revenue-product | shared-service | content-product | tooling | executive | council
org: AgentOrg
capabilities: list[str]
skills: list[str]
authority: AgentAuthority
endpoints: AgentEndpoints
sla: Optional[AgentSLA]
meta: AgentMeta
notes: Optional[Any]
model_config = {"extra": "allow"}
The extra: "allow" configuration is intentional. Individual agents can embed domain-specific notes (Foresight stores trading assets, timeframes, and risk gate counts) without breaking the schema validation that enforces the required fields.
The Org Hierarchy
The AgentOrg model defines an agent's position in the company:
class AgentOrg(BaseModel):
title: Optional[str] = None
level: str # ic | director | sr-director | vp | c-suite | ceo | founder
reportsTo: Optional[str] = Field(None, description="Agent ID of direct manager")
directReports: list[str]
businessUnit: Optional[str] = None
reportsTo is the field that makes routing work. When the router calls _get_manager(agent_id), it reads card.org.reportsTo. For Foresight, that returns "vp-trading". For VP Trading, it returns "openclaw". For the OpenClaw, it returns None — which the broker interprets as escalate to Knox.
The org hierarchy encoded in Agent Cards reflects the actual company structure:
Knox (human founder)
└── openclaw (CEO)
├── vp-trading
│ ├── foresight (Director of Prediction Markets)
│ ├── sports-agent (Director)
│ └── indecision-engine (Sr. Director of Signals — CRITICAL PATH)
├── vp-engineering
│ ├── memory-service
│ ├── sentinel
│ └── watchdog-service
└── vp-content
└── content-pipeline
This is not organizational theater. When Foresight needs to escalate a decision above its authority ceiling, the broker does not guess where to send it. It reads foresight.org.reportsTo and sends it to VP Trading. The chain of command is machine-readable and enforced in real time.
Foresight's Full Card
Here is Foresight's production Agent Card, which illustrates how all the schema fields work together:
{
"id": "foresight",
"name": "Foresight Trading Bot",
"version": "2.4.1",
"type": "revenue-product",
"org": {
"title": "Director of Prediction Markets",
"reportsTo": "vp-trading",
"directReports": [],
"businessUnit": "trading",
"level": "director"
},
"capabilities": [
"trade-execution",
"signal-scoring",
"risk-management",
"market-monitoring",
"clob-entry",
"path-selection",
"asset-filtering"
],
"authority": {
"maxAutonomousDollars": 500,
"maxRiskTier": "high",
"canModify": ["own-config", "own-database", "retro_overrides.json"],
"cannotModify": [
"indecision-engine",
"shared-foresight-modules",
"wallet-keys"
],
"requiresApprovalFor": [
"trading-halt-all",
"threshold-change-gt-20pct",
"new-asset-addition"
]
},
"meta": {
"host": "trading-server",
"repo": "your-org/foresight",
"launchd": "com.host.foresight",
"logPath": "/tmp/foresight.log",
"status": "active"
}
}
Walk through what each section tells the broker:
type: "revenue-product" — This is a production trading bot. Rule 3 routes all trade.* messages from it to VP Trading. Rule 4 means it receives InDecision Engine signals. Its health and SLA monitoring gets escalated to VP Trading, not VP Engineering.
org.reportsTo: "vp-trading" — Escalations go here. Report messages get copied to VP Trading. If the heartbeat goes stale, VP Trading is the primary notification target.
authority.maxAutonomousDollars: 500 — Foresight can execute trades up to $500 without approval. The authority enforcer checks this on every message before routing. Anything above $500 is converted to an escalation.
authority.cannotModify — This list is the Isolation Rule in machine-readable form. Foresight cannot modify InDecision Engine, shared Foresight modules, or wallet keys. The broker does not need to remember this rule — it is in the card.
authority.requiresApprovalFor — Actions that require explicit human approval. These cannot be auto-escalated and resolved by the manager agent — they require Knox's sign-off.
meta.host: "trading-server" — Foresight runs on the trading server (internal LAN). The broker knows this. When dispatching to Foresight's inbox, it publishes to the NATS topic that the trading server subscribes to.
The Authority Model
class AgentAuthority(BaseModel):
maxAutonomousDollars: float = Field(
default=0,
description="Max $ action without escalation. -1 = no financial authority"
)
maxRiskTier: str = Field(
default="low",
description="low | medium | high | critical"
)
canModify: list[str]
cannotModify: list[str]
requiresApprovalFor: list[str]
The authority model encodes three distinct concepts:
Financial ceiling (maxAutonomousDollars) is a numeric threshold. An agent with -1 has no financial authority. An agent with 500 can act autonomously on transactions up to $500. VP-level agents have higher ceilings. Knox has no ceiling.
Risk ceiling (maxRiskTier) is an ordinal scale: low, medium, high, critical. A revenue-product agent operates at high. A tooling agent operates at low. The authority enforcer maps message subtypes to risk tiers and checks whether the sender's card permits that tier.
Explicit allow/deny lists (canModify, cannotModify, requiresApprovalFor) cover named resources and action types. These are checked by name match — simple string comparison, no pattern matching.
Agent Types Drive Routing Behavior
The type field on an Agent Card is the primary routing signal for all rules except Rule 1 (critical priority). Here is how each type maps to routing behavior:
The router does not have if agent_id == "foresight" conditionals anywhere. It has if message.subtype.startswith("trade.") conditionals. The distinction is significant: type-based routing scales to any number of agents without router changes. ID-based routing requires a router update for every new agent.
Capabilities and Skills
The capabilities and skills lists on a card are not enforced by the broker in the current implementation — they are declared for observability and governance purposes.
Capabilities describe what the agent can do: trade-execution, signal-scoring, market-monitoring. Skills describe the tasks it knows how to perform: trading-pulse, trade-retro, market-bias. When the broker's observability layer detects anomalous behavior, it cross-references the agent's declared capabilities to assess whether the behavior is expected.
More importantly, these lists document intent. When Knox or the OpenClaw needs to know whether the sports prediction agent can perform a specific operation, the answer is in the card. The capabilities list is the contract.
Adding a New Agent
The operational procedure for onboarding a new agent to the fleet:
- Create
agents/cards/{agent_id}.jsonwith a valid Agent Card - Restart the broker (or hot-reload if the registry supports it)
- The registry loads the card, registers the agent, and issues a bearer token
- The new agent connects with its token and starts publishing messages
- Routing, authority enforcement, and audit logging work automatically on the first message
No routing code changes. No conditional updates. No hardcoded ID lists. The card is the code.
The SLA Model
For shared-service agents that other agents depend on, the card includes an SLA declaration:
class AgentSLA(BaseModel):
uptimeTarget: float = 99.0 # percent
responseTimeMs: int = 5000
criticalDependencies: list[str]
InDecision Engine's SLA declares a 99% uptime target and lists Foresight, the sports prediction agent, and the political events prediction agent as critical dependencies. The broker's SLA monitor tracks heartbeat timing and response latency, and escalates when an agent falls below its declared target.
This is the mechanism by which the broker detects that InDecision Engine is offline and initiates the escalation chain before Knox manually notices the bots have stopped trading. The SLA is not a document — it is an active monitoring contract.
Identity Without Permanence
One of the design decisions in the Agent Card system is that capabilities are declared, not discovered. The broker does not probe agents to discover what they can do. It reads the card.
This means an agent can be registered even when it is offline. Foresight's card exists in the registry even when the trading server is powered down. The broker knows Foresight's type, authority ceiling, and reporting chain regardless of whether Foresight is running. This makes cold-start, failover, and incident response cleaner — the system knows who everyone is before any of them show up.
The Agent Card is the source of truth. The running agent is just an instance of what the card describes.