Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/agent/agent_swarm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.venv/
.auths-demo/
__pycache__/
*.pyc
.swarm-keys.json
audit.jsonl
report.md
dist/
.ruff_cache/
1 change: 1 addition & 0 deletions examples/agent/agent_swarm/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
149 changes: 149 additions & 0 deletions examples/agent/agent_swarm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Agent Swarm — Delegation Chain

Every sub-agent in a governed swarm. Every action traceable to a human.

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.

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.

**Why this matters:** This is the first real answer to "how do you govern a swarm."

---

## Quick start

```bash
# 1. Install dependencies
uv sync

# 2. Set your OpenAI key
export OPENAI_API_KEY=sk-...

# 3. Run the swarm
uv run run-swarm "read data/sales.csv, analyze it, and notify the team"
```

Expected output:

```
Swarm identity chain:
Human did:key:z6MkuXq...
└─ Orchestrator did:key:z6MkpRn... [delegate, read_data, analyze, notify]
├─ DataAgent did:key:z6MkiHa... [read_data]
├─ AnalysisAgent did:key:z6MktBc... [analyze]
└─ NotifyAgent did:key:z6MkvWe... [notify]

Scope enforcement demo:
Attempting to use DataAgent for a 'notify' action it was never granted...
✓ Blocked: 'DataAgent' lacks 'notify' capability (granted: ['read_data'])

Task: read data/sales.csv, analyze it, and notify the team

[DataAgent] read_csv(path='data/sales.csv') ✓ signed
[AnalysisAgent] summarize(data='month, product...') ✓ signed
[NotifyAgent] send_notification(channel='team') ✓ signed

✓ 3 action(s) across 3 agent(s)
Run verify-swarm to verify the full delegation chain.
```

## Verify the delegation chain

```bash
uv run verify-swarm
```

```
Registered identities:
Human did:key:z6MkuXq...
Orchestrator did:key:z6MkpRn...
DataAgent did:key:z6MkiHa...
AnalysisAgent did:key:z6MktBc...
NotifyAgent did:key:z6MkvWe...

Verifying action audit trail...

# Agent Tool Sig Delegation Capabilities
1 DataAgent read_csv ✓ ✓ read_data
2 AnalysisAgent summarize ✓ ✓ analyze
3 NotifyAgent send_notification ✓ ✓ notify

✓ 3/3 action signatures valid
✓ 3/3 delegation chains valid

✓ Audit trail intact — every action is authorized and verifiable.
```

---

## How it works

```
Human
│ generates keypair, holds root authority
└─ Orchestrator ← delegation token: signed by Human
│ capabilities: [delegate, read_data, analyze, notify]
├─ DataAgent ← delegation token: signed by Orchestrator
│ capabilities: [read_data]
│ Each tool call envelope embeds the delegation token
├─ AnalysisAgent ← delegation token: signed by Orchestrator
│ capabilities: [analyze]
└─ NotifyAgent ← delegation token: signed by Orchestrator
capabilities: [notify]
```

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.

**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.

## What's in `audit.jsonl`

Each line is a signed `tool_call` envelope. The sub-agent's delegation token is embedded in the payload:

```json
{
"version": "1.0",
"type": "tool_call",
"identity": "did:key:z6MkiHa...",
"payload": {
"tool": "read_csv",
"args": {"path": "data/sales.csv"},
"delegation_token": {
"type": "delegation",
"identity": "did:key:z6MkpRn...",
"payload": {
"delegate_to": "did:key:z6MkiHa...",
"capabilities": ["read_data"]
},
"signature": "..."
}
},
"timestamp": "2026-04-07T09:14:22Z",
"signature": "..."
}
```

Tampering with the tool name, args, or delegation token breaks the signature.

---

## Run the tests

```bash
uv run pytest
```

Tests cover the full identity tree, delegation token validity, capability enforcement, tamper detection, and cross-agent scope isolation — no LLM or network required.

---

## Next steps

- **[Demo #1: Single agent audit log](../single_agent/)** — simpler starting point
- **[Demo #3: Verifiable AI-generated code](../verifiable_codegen/)** — LangChain + GitHub Actions
- **[auths Python SDK docs](https://docs.auths.dev/sdk/python)**
- **[auths init --profile agent](https://docs.auths.dev/guides/agent-identity)** — replace in-memory keypairs with persistent, revocable agent identities
7 changes: 7 additions & 0 deletions examples/agent/agent_swarm/data/sales.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
month,product,revenue
Jan,Widget A,12500
Feb,Widget A,14200
Mar,Widget B,9800
Apr,Widget B,11100
May,Widget A,15600
Jun,Widget B,13400
40 changes: 40 additions & 0 deletions examples/agent/agent_swarm/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[project]
name = "agent-swarm"
version = "0.1.0"
description = "Multi-agent delegation chain demo using auths + PydanticAI"
requires-python = ">=3.12"
dependencies = [
"pydantic-ai[openai]>=0.1",
"auths>=0.1",
"rich>=13",
"cryptography>=42",
]

[project.scripts]
run-swarm = "agent_swarm.agents:main"
verify-swarm = "agent_swarm.verify_swarm:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/agent_swarm"]

[dependency-groups]
dev = [
"ruff>=0.4",
"pytest>=8",
"pytest-asyncio>=0.23",
]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Empty file.
151 changes: 151 additions & 0 deletions examples/agent/agent_swarm/src/agent_swarm/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Orchestrator + sub-agent swarm with cryptographically signed tool calls.

The orchestrator is a PydanticAI agent. Each of its tools delegates to a
specialized sub-agent, which signs the action with its own key and embeds its
delegation token. Every action in the audit log is traceable to the human
who bootstrapped the swarm.
"""

from __future__ import annotations

import sys
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext
from rich.console import Console
from rich.text import Text

from agent_swarm import tools
from agent_swarm.audit import append_envelope, clear, read_all, save_swarm_keys
from agent_swarm.identities import SwarmIdentity, make_swarm
from agent_swarm.signing import CapabilityError, sign_tool_call

console = Console()


@dataclass
class OrchestratorDeps:
data_agent: SwarmIdentity
analysis_agent: SwarmIdentity
notify_agent: SwarmIdentity


_orchestrator = Agent(
"openai:gpt-4o-mini",
deps_type=OrchestratorDeps,
system_prompt=(
"You are a data analyst orchestrator. "
"Coordinate your specialized sub-agents to fulfill the user's request. "
"Use read_data to fetch data, analyze_data to summarize it, "
"and send_notification to deliver results. "
"Be concise — one short paragraph max."
),
)


def _record(agent: SwarmIdentity, tool_name: str, args: dict, cap: str) -> None:
"""Sign the tool call and append it to the audit log; print status line."""
envelope = sign_tool_call(agent, tool_name, args, cap)
append_envelope(envelope)

display_args = ", ".join(
f"{k}={repr(v[:40] + '...' if isinstance(v, str) and len(v) > 40 else v)}"
for k, v in args.items()
)
line = Text()
line.append(f" [{agent.name}] ", style="bold cyan")
line.append(f"{tool_name}({display_args})", style="dim")
line.append(" ✓ signed", style="green bold")
console.print(line)


@_orchestrator.tool
def read_data(ctx: RunContext[OrchestratorDeps], path: str) -> str:
"""Read data from a CSV file (delegated to DataAgent)."""
_record(ctx.deps.data_agent, "read_csv", {"path": path}, "read_data")
return tools.read_csv(path)


@_orchestrator.tool
def analyze_data(ctx: RunContext[OrchestratorDeps], data: str) -> str:
"""Summarize the provided data (delegated to AnalysisAgent)."""
_record(ctx.deps.analysis_agent, "summarize", {"data": data}, "analyze")
return tools.summarize(data)


@_orchestrator.tool
def send_notification(ctx: RunContext[OrchestratorDeps], channel: str, message: str) -> bool:
"""Send a notification to a channel (delegated to NotifyAgent)."""
args = {"channel": channel, "message": message}
_record(ctx.deps.notify_agent, "send_notification", args, "notify")
return tools.send_notification(channel, message)


def _print_identity_tree(
human: SwarmIdentity,
orchestrator: SwarmIdentity,
sub_agents: list[SwarmIdentity],
) -> None:
console.print("\n[bold]Swarm identity chain:[/bold]")
console.print(f" [yellow]{human.name}[/yellow] [dim]{human.did[:40]}...[/dim]")
caps = ", ".join(orchestrator.capabilities)
did_abbrev = orchestrator.did[:40]
console.print(f" └─ [cyan]{orchestrator.name}[/cyan] [dim]{did_abbrev}...[/dim] [{caps}]")
for i, agent in enumerate(sub_agents):
prefix = "└─" if i == len(sub_agents) - 1 else "├─"
caps = ", ".join(agent.capabilities)
console.print(
f" {prefix} [green]{agent.name}[/green]"
f" [dim]{agent.did[:40]}...[/dim] [{caps}]"
)
console.print()


def _demo_scope_violation(sub_agents: list[SwarmIdentity]) -> None:
"""Show that a sub-agent cannot exceed its granted capabilities."""
data_agent = sub_agents[0]
console.print("[bold]Scope enforcement demo:[/bold]")
console.print(
f" Attempting to use [green]{data_agent.name}[/green] for a"
" [red]'notify'[/red] action it was never granted..."
)
try:
sign_tool_call(data_agent, "send_notification", {"channel": "team", "message": "hi"}, "notify") # noqa: E501
console.print(" [red]ERROR: scope check did not fire[/red]")
except CapabilityError as e:
console.print(f" [green]✓ Blocked:[/green] [dim]{e}[/dim]\n")


def main() -> None:
prompt = (
" ".join(sys.argv[1:])
or "Read data/sales.csv, analyze it, and send a summary notification to the team channel."
)

clear()
human, orchestrator, sub_agents = make_swarm()
data_agent, analysis_agent, notify_agent = sub_agents

save_swarm_keys(human, orchestrator, sub_agents)
_print_identity_tree(human, orchestrator, sub_agents)
_demo_scope_violation(sub_agents)

console.print(f"[bold]Task:[/bold] {prompt}\n")

result = _orchestrator.run_sync(
prompt,
deps=OrchestratorDeps(
data_agent=data_agent,
analysis_agent=analysis_agent,
notify_agent=notify_agent,
),
)

console.print(f"\n[bold]Result:[/bold] {result.output}\n")

entries = read_all()
signed_dids = {e.get("identity") for e in entries}
agent_count = len(signed_dids)

console.print(f"[green]✓[/green] {len(entries)} action(s) across {agent_count} agent(s)")
console.print("[dim]Run [bold]verify-swarm[/bold] to verify the full delegation chain.[/dim]\n")
Loading
Loading