Skip to content

Commit a38a3f8

Browse files
authored
Merge pull request #163 from auths-dev/dev-agentExamples
docs/tests: add agent examples, fix cli flags in tests
2 parents 9c0b60d + 53ecf31 commit a38a3f8

33 files changed

Lines changed: 7996 additions & 172 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.venv/
2+
.auths-demo/
3+
__pycache__/
4+
*.pyc
5+
.swarm-keys.json
6+
audit.jsonl
7+
report.md
8+
dist/
9+
.ruff_cache/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Agent Swarm — Delegation Chain
2+
3+
Every sub-agent in a governed swarm. Every action traceable to a human.
4+
5+
Multi-agent systems have no identity model today. Sub-agents run with whatever credentials the orchestrator passes them. There's no way to scope what a sub-agent can do, or prove that its outputs were authorized by anyone in particular.
6+
7+
This demo builds a three-layer identity tree — human → orchestrator → sub-agents — where each level's authority is cryptographically delegated. Every tool call is signed by the sub-agent that made it, and every sub-agent carries a delegation token proving the orchestrator authorized it. The full chain is verifiable offline.
8+
9+
**Why this matters:** This is the first real answer to "how do you govern a swarm."
10+
11+
---
12+
13+
## Quick start
14+
15+
```bash
16+
# 1. Install dependencies
17+
uv sync
18+
19+
# 2. Set your OpenAI key
20+
export OPENAI_API_KEY=sk-...
21+
22+
# 3. Run the swarm
23+
uv run run-swarm "read data/sales.csv, analyze it, and notify the team"
24+
```
25+
26+
Expected output:
27+
28+
```
29+
Swarm identity chain:
30+
Human did:key:z6MkuXq...
31+
└─ Orchestrator did:key:z6MkpRn... [delegate, read_data, analyze, notify]
32+
├─ DataAgent did:key:z6MkiHa... [read_data]
33+
├─ AnalysisAgent did:key:z6MktBc... [analyze]
34+
└─ NotifyAgent did:key:z6MkvWe... [notify]
35+
36+
Scope enforcement demo:
37+
Attempting to use DataAgent for a 'notify' action it was never granted...
38+
✓ Blocked: 'DataAgent' lacks 'notify' capability (granted: ['read_data'])
39+
40+
Task: read data/sales.csv, analyze it, and notify the team
41+
42+
[DataAgent] read_csv(path='data/sales.csv') ✓ signed
43+
[AnalysisAgent] summarize(data='month, product...') ✓ signed
44+
[NotifyAgent] send_notification(channel='team') ✓ signed
45+
46+
✓ 3 action(s) across 3 agent(s)
47+
Run verify-swarm to verify the full delegation chain.
48+
```
49+
50+
## Verify the delegation chain
51+
52+
```bash
53+
uv run verify-swarm
54+
```
55+
56+
```
57+
Registered identities:
58+
Human did:key:z6MkuXq...
59+
Orchestrator did:key:z6MkpRn...
60+
DataAgent did:key:z6MkiHa...
61+
AnalysisAgent did:key:z6MktBc...
62+
NotifyAgent did:key:z6MkvWe...
63+
64+
Verifying action audit trail...
65+
66+
# Agent Tool Sig Delegation Capabilities
67+
1 DataAgent read_csv ✓ ✓ read_data
68+
2 AnalysisAgent summarize ✓ ✓ analyze
69+
3 NotifyAgent send_notification ✓ ✓ notify
70+
71+
✓ 3/3 action signatures valid
72+
✓ 3/3 delegation chains valid
73+
74+
✓ Audit trail intact — every action is authorized and verifiable.
75+
```
76+
77+
---
78+
79+
## How it works
80+
81+
```
82+
Human
83+
│ generates keypair, holds root authority
84+
85+
└─ Orchestrator ← delegation token: signed by Human
86+
│ capabilities: [delegate, read_data, analyze, notify]
87+
88+
├─ DataAgent ← delegation token: signed by Orchestrator
89+
│ capabilities: [read_data]
90+
│ Each tool call envelope embeds the delegation token
91+
92+
├─ AnalysisAgent ← delegation token: signed by Orchestrator
93+
│ capabilities: [analyze]
94+
95+
└─ NotifyAgent ← delegation token: signed by Orchestrator
96+
capabilities: [notify]
97+
```
98+
99+
Each **delegation token** is itself a signed `ActionEnvelope` (`type: "delegation"`) whose payload records the grantee's DID and capabilities. When a sub-agent signs a tool call, it embeds this token in the payload — so both the action signature and the authorization chain are verifiable from a single JSON object.
100+
101+
**Scope enforcement happens at signing time:** calling `sign_tool_call` with a required capability the agent doesn't hold raises `CapabilityError` before the action is signed or the tool executes.
102+
103+
## What's in `audit.jsonl`
104+
105+
Each line is a signed `tool_call` envelope. The sub-agent's delegation token is embedded in the payload:
106+
107+
```json
108+
{
109+
"version": "1.0",
110+
"type": "tool_call",
111+
"identity": "did:key:z6MkiHa...",
112+
"payload": {
113+
"tool": "read_csv",
114+
"args": {"path": "data/sales.csv"},
115+
"delegation_token": {
116+
"type": "delegation",
117+
"identity": "did:key:z6MkpRn...",
118+
"payload": {
119+
"delegate_to": "did:key:z6MkiHa...",
120+
"capabilities": ["read_data"]
121+
},
122+
"signature": "..."
123+
}
124+
},
125+
"timestamp": "2026-04-07T09:14:22Z",
126+
"signature": "..."
127+
}
128+
```
129+
130+
Tampering with the tool name, args, or delegation token breaks the signature.
131+
132+
---
133+
134+
## Run the tests
135+
136+
```bash
137+
uv run pytest
138+
```
139+
140+
Tests cover the full identity tree, delegation token validity, capability enforcement, tamper detection, and cross-agent scope isolation — no LLM or network required.
141+
142+
---
143+
144+
## Next steps
145+
146+
- **[Demo #1: Single agent audit log](../single_agent/)** — simpler starting point
147+
- **[Demo #3: Verifiable AI-generated code](../verifiable_codegen/)** — LangChain + GitHub Actions
148+
- **[auths Python SDK docs](https://docs.auths.dev/sdk/python)**
149+
- **[auths init --profile agent](https://docs.auths.dev/guides/agent-identity)** — replace in-memory keypairs with persistent, revocable agent identities
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
month,product,revenue
2+
Jan,Widget A,12500
3+
Feb,Widget A,14200
4+
Mar,Widget B,9800
5+
Apr,Widget B,11100
6+
May,Widget A,15600
7+
Jun,Widget B,13400
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[project]
2+
name = "agent-swarm"
3+
version = "0.1.0"
4+
description = "Multi-agent delegation chain demo using auths + PydanticAI"
5+
requires-python = ">=3.12"
6+
dependencies = [
7+
"pydantic-ai[openai]>=0.1",
8+
"auths>=0.1",
9+
"rich>=13",
10+
"cryptography>=42",
11+
]
12+
13+
[project.scripts]
14+
run-swarm = "agent_swarm.agents:main"
15+
verify-swarm = "agent_swarm.verify_swarm:main"
16+
17+
[build-system]
18+
requires = ["hatchling"]
19+
build-backend = "hatchling.build"
20+
21+
[tool.hatch.build.targets.wheel]
22+
packages = ["src/agent_swarm"]
23+
24+
[dependency-groups]
25+
dev = [
26+
"ruff>=0.4",
27+
"pytest>=8",
28+
"pytest-asyncio>=0.23",
29+
]
30+
31+
[tool.ruff]
32+
line-length = 100
33+
target-version = "py312"
34+
35+
[tool.ruff.lint]
36+
select = ["E", "F", "I", "UP", "B", "SIM"]
37+
38+
[tool.pytest.ini_options]
39+
asyncio_mode = "auto"
40+
testpaths = ["tests"]

examples/agent/agent_swarm/src/agent_swarm/__init__.py

Whitespace-only changes.
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Orchestrator + sub-agent swarm with cryptographically signed tool calls.
2+
3+
The orchestrator is a PydanticAI agent. Each of its tools delegates to a
4+
specialized sub-agent, which signs the action with its own key and embeds its
5+
delegation token. Every action in the audit log is traceable to the human
6+
who bootstrapped the swarm.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import sys
12+
from dataclasses import dataclass
13+
14+
from pydantic_ai import Agent, RunContext
15+
from rich.console import Console
16+
from rich.text import Text
17+
18+
from agent_swarm import tools
19+
from agent_swarm.audit import append_envelope, clear, read_all, save_swarm_keys
20+
from agent_swarm.identities import SwarmIdentity, make_swarm
21+
from agent_swarm.signing import CapabilityError, sign_tool_call
22+
23+
console = Console()
24+
25+
26+
@dataclass
27+
class OrchestratorDeps:
28+
data_agent: SwarmIdentity
29+
analysis_agent: SwarmIdentity
30+
notify_agent: SwarmIdentity
31+
32+
33+
_orchestrator = Agent(
34+
"openai:gpt-4o-mini",
35+
deps_type=OrchestratorDeps,
36+
system_prompt=(
37+
"You are a data analyst orchestrator. "
38+
"Coordinate your specialized sub-agents to fulfill the user's request. "
39+
"Use read_data to fetch data, analyze_data to summarize it, "
40+
"and send_notification to deliver results. "
41+
"Be concise — one short paragraph max."
42+
),
43+
)
44+
45+
46+
def _record(agent: SwarmIdentity, tool_name: str, args: dict, cap: str) -> None:
47+
"""Sign the tool call and append it to the audit log; print status line."""
48+
envelope = sign_tool_call(agent, tool_name, args, cap)
49+
append_envelope(envelope)
50+
51+
display_args = ", ".join(
52+
f"{k}={repr(v[:40] + '...' if isinstance(v, str) and len(v) > 40 else v)}"
53+
for k, v in args.items()
54+
)
55+
line = Text()
56+
line.append(f" [{agent.name}] ", style="bold cyan")
57+
line.append(f"{tool_name}({display_args})", style="dim")
58+
line.append(" ✓ signed", style="green bold")
59+
console.print(line)
60+
61+
62+
@_orchestrator.tool
63+
def read_data(ctx: RunContext[OrchestratorDeps], path: str) -> str:
64+
"""Read data from a CSV file (delegated to DataAgent)."""
65+
_record(ctx.deps.data_agent, "read_csv", {"path": path}, "read_data")
66+
return tools.read_csv(path)
67+
68+
69+
@_orchestrator.tool
70+
def analyze_data(ctx: RunContext[OrchestratorDeps], data: str) -> str:
71+
"""Summarize the provided data (delegated to AnalysisAgent)."""
72+
_record(ctx.deps.analysis_agent, "summarize", {"data": data}, "analyze")
73+
return tools.summarize(data)
74+
75+
76+
@_orchestrator.tool
77+
def send_notification(ctx: RunContext[OrchestratorDeps], channel: str, message: str) -> bool:
78+
"""Send a notification to a channel (delegated to NotifyAgent)."""
79+
args = {"channel": channel, "message": message}
80+
_record(ctx.deps.notify_agent, "send_notification", args, "notify")
81+
return tools.send_notification(channel, message)
82+
83+
84+
def _print_identity_tree(
85+
human: SwarmIdentity,
86+
orchestrator: SwarmIdentity,
87+
sub_agents: list[SwarmIdentity],
88+
) -> None:
89+
console.print("\n[bold]Swarm identity chain:[/bold]")
90+
console.print(f" [yellow]{human.name}[/yellow] [dim]{human.did[:40]}...[/dim]")
91+
caps = ", ".join(orchestrator.capabilities)
92+
did_abbrev = orchestrator.did[:40]
93+
console.print(f" └─ [cyan]{orchestrator.name}[/cyan] [dim]{did_abbrev}...[/dim] [{caps}]")
94+
for i, agent in enumerate(sub_agents):
95+
prefix = "└─" if i == len(sub_agents) - 1 else "├─"
96+
caps = ", ".join(agent.capabilities)
97+
console.print(
98+
f" {prefix} [green]{agent.name}[/green]"
99+
f" [dim]{agent.did[:40]}...[/dim] [{caps}]"
100+
)
101+
console.print()
102+
103+
104+
def _demo_scope_violation(sub_agents: list[SwarmIdentity]) -> None:
105+
"""Show that a sub-agent cannot exceed its granted capabilities."""
106+
data_agent = sub_agents[0]
107+
console.print("[bold]Scope enforcement demo:[/bold]")
108+
console.print(
109+
f" Attempting to use [green]{data_agent.name}[/green] for a"
110+
" [red]'notify'[/red] action it was never granted..."
111+
)
112+
try:
113+
sign_tool_call(data_agent, "send_notification", {"channel": "team", "message": "hi"}, "notify") # noqa: E501
114+
console.print(" [red]ERROR: scope check did not fire[/red]")
115+
except CapabilityError as e:
116+
console.print(f" [green]✓ Blocked:[/green] [dim]{e}[/dim]\n")
117+
118+
119+
def main() -> None:
120+
prompt = (
121+
" ".join(sys.argv[1:])
122+
or "Read data/sales.csv, analyze it, and send a summary notification to the team channel."
123+
)
124+
125+
clear()
126+
human, orchestrator, sub_agents = make_swarm()
127+
data_agent, analysis_agent, notify_agent = sub_agents
128+
129+
save_swarm_keys(human, orchestrator, sub_agents)
130+
_print_identity_tree(human, orchestrator, sub_agents)
131+
_demo_scope_violation(sub_agents)
132+
133+
console.print(f"[bold]Task:[/bold] {prompt}\n")
134+
135+
result = _orchestrator.run_sync(
136+
prompt,
137+
deps=OrchestratorDeps(
138+
data_agent=data_agent,
139+
analysis_agent=analysis_agent,
140+
notify_agent=notify_agent,
141+
),
142+
)
143+
144+
console.print(f"\n[bold]Result:[/bold] {result.output}\n")
145+
146+
entries = read_all()
147+
signed_dids = {e.get("identity") for e in entries}
148+
agent_count = len(signed_dids)
149+
150+
console.print(f"[green]✓[/green] {len(entries)} action(s) across {agent_count} agent(s)")
151+
console.print("[dim]Run [bold]verify-swarm[/bold] to verify the full delegation chain.[/dim]\n")

0 commit comments

Comments
 (0)