Scope credentials for sub-agents with delegated passes
Python library that gives sub-agents a narrowed, signed credential (a Pigeon Pass) instead of a copy of your API key, and verifies it before tools run.
0.1.0Add to Favorites
Why it matters
Prevent privilege escalation when AI agents spawn sub-agents by issuing narrowed, signed credentials (Pigeon Passes) that restrict capabilities, resources, and constraints instead of copying full API keys.
Outcomes
What it gets done
Grant scoped capabilities and resources to parent agents with custom constraints
Delegate narrower passes to child agents that cannot escalate privileges or widen permissions
Verify agent authority before executing tools, deployments, or database operations
Enforce MCP tool-call boundaries by minting and validating per-tool passes
Source
Get it from source
Spark does not host a copy of it.
Open sourceReports
Agent outcome reports
No reports yet
Overview
Pigeon addresses an agent handing a sub-agent the same API key, so the child can deploy to production, read the payments database and merge to main. Instead you hand the child a Pigeon Pass: a narrowed, signed credential for what it may do, not a copy of everything you can do. It is a small Python primitive with no server to connect to. Use it where an agent spawns sub-agents or calls tools with side effects: delegate a narrower Pass at spawn and verify it at the tool. It only helps if your runner actually calls verify before the side effect, and it does not stop prompt injection.
What it does
Your agent spawned a sub-agent and handed it the same API key. That sub-agent can now deploy to production, read the payments database and merge to main. Pigeon stops that: you hand the child a Pigeon Pass, a narrowed, signed credential for what it may do, not a copy of everything you can do.
grant creates authority for a subject with capabilities, resources and optional constraints, for example subject="agent:deployer", capabilities=["deploy"] and resources=["environment:staging"]. verify(authority, action="deploy", resource="environment:staging") comes back allowed, while the same action on environment:production is denied with the reason code RESOURCE_NOT_ALLOWED. verify never returns a bare boolean: a denial includes a reason code, a message, and the comparison that failed (requested vs allowed).
delegate takes a parent Pass plus a subject, capabilities and resources, and produces a child Pass. For example, an orchestrator holding deploy and open_pr on environment:staging and repo:acme/api, with the constraint max_deploys_per_hour: 3, delegates only open_pr on repo:acme/api to agent:pr-bot and passes the constraint again, because a child cannot drop a parent constraint. The child is allowed to open_pr, and deploy on environment:staging is denied with CAPABILITY_NOT_GRANTED. A child cannot add capabilities, widen resources, raise a bound, or drop a parent constraint. If Pigeon cannot prove the child is narrower, it rejects: delegating open_pr and deploy from that child raises DelegationError with the reason code PRIVILEGE_ESCALATION.
When to use - and when NOT to
There is no Pigeon server to connect to. You change two places you already have:
- Spawn: where you would have copied an API key into a sub-agent, call
delegate(...)and give the child a Pass. - Tool: where the side effect happens (deploy, query, MCP tool), call
verify(...)and do not run the tool if it is denied.
Keep the real secret on the runner; the child carries the Pass. If the runner never calls verify, the Pass is decoration.
Pigeon is a small primitive. It is not a platform, a policy engine, an identity provider, or a key custodian. It does not stop prompt injection. It bounds blast radius along the dimensions you put on the Pass, and only those. Identity tells you who the agent is; authority tells you what it may do.
Inputs and outputs
grant takes a subject, capabilities, resources and optional constraints such as {"max_deploys_per_hour": 3}, and returns a Pass. delegate(parent, subject, capabilities, resources) returns a narrower child Pass and can take constraints too. verify(authority, action=..., resource=...) returns a result with allowed, reason_code, message and details. A rejected delegation raises DelegationError, which carries a reason_code. The CLI provides pigeon keygen and pigeon inspect pass.json.
Integrations
For MCP the middleware is an enforcement point, not part of the protocol. The client mints a narrower Pass per tool call and the server verifies it before the tool runs. Import pass_for_tool and execute_tool from pigeon.integrations.mcp. pass_for_tool(parent, "create_issue", "mcp:github") mints the per-call Pass from a parent granted create_issue and merge_pr on mcp:github. execute_tool takes that Pass, the tool name, the resource, the tool arguments as a dict and the tool function. It returns a dict with allowed, and a denial carries reason_code, for example CAPABILITY_NOT_GRANTED when merge_pr is attempted with a Pass minted for create_issue.
Pigeon needs Python 3.12 or newer. Install:
git clone https://github.com/pigeonlabsHQ/pigeon.git
cd pigeon
pip install .
Source README
Pigeon
Your agent spawned a sub-agent and handed it the same API key. That sub-agent can now deploy to production, read the payments database, and merge to main.
Pigeon stops that. You hand the child a Pigeon Pass: a narrowed, signed credential for what it may do, not a copy of everything you can do.
Install
Python 3.12 or newer.
git clone https://github.com/pigeonlabsHQ/pigeon.git
cd pigeon
pip install .
The whole idea, in 20 lines
from pigeon import grant, verify
authority = grant(
subject="agent:deployer",
capabilities=["deploy"],
resources=["environment:staging"],
)
allowed = verify(authority, action="deploy", resource="environment:staging")
assert allowed.allowed
denied = verify(authority, action="deploy", resource="environment:production")
assert not denied.allowed
assert denied.reason_code == "RESOURCE_NOT_ALLOWED"
print(denied.reason_code, denied.message, denied.details)
verify never returns a bare boolean. A denial includes a reason code, a message, and the comparison that failed (requested vs allowed).
Try it without writing that yourself:
python examples/01_infrastructure.py
python demo/agent.py
Where it goes in an agent
There is no Pigeon server to connect to. You change two places you already have:
- Spawn. Where you would have copied an API key into a sub-agent, call
delegate(...)and give the child a Pass. - Tool. Where the side effect happens (deploy, query, MCP tool), call
verify(...)and do not run the tool if it is denied.
Keep the real secret on the runner. The child carries the Pass.
from pigeon import delegate, grant, verify, DelegationError
parent = grant(
subject="agent:orchestrator",
capabilities=["deploy", "open_pr"],
resources=["environment:staging", "repo:acme/api"],
constraints={"max_deploys_per_hour": 3},
)
worker = delegate(
parent,
subject="agent:pr-bot",
capabilities=["open_pr"],
resources=["repo:acme/api"],
constraints={"max_deploys_per_hour": 3}, # cannot drop a parent constraint
)
result = verify(worker, action="open_pr", resource="repo:acme/api")
assert result.allowed
denied = verify(worker, action="deploy", resource="environment:staging")
assert denied.reason_code == "CAPABILITY_NOT_GRANTED"
try:
delegate(worker, "agent:rogue", ["open_pr", "deploy"], ["repo:acme/api"])
except DelegationError as exc:
assert exc.reason_code == "PRIVILEGE_ESCALATION"
A child cannot add capabilities, widen resources, raise a bound, or drop a parent constraint. If Pigeon cannot prove the child is narrower, it rejects.
If the runner never calls verify, the Pass is decoration.
MCP middleware
This is an enforcement point, not part of the protocol. The client mints a narrower Pass per tool call. The server verifies it before the tool runs.
from pigeon import grant
from pigeon.integrations.mcp import execute_tool, pass_for_tool
parent = grant(
subject="agent:github",
capabilities=["create_issue", "merge_pr"],
resources=["mcp:github"],
)
tool_pass = pass_for_tool(parent, "create_issue", "mcp:github")
def create_issue(*, title, body):
return {"created": True, "title": title}
ok = execute_tool(tool_pass, "create_issue", "mcp:github",
{"title": "bump deps", "body": "automated"}, create_issue)
assert ok["allowed"]
no = execute_tool(tool_pass, "merge_pr", "mcp:github",
{"title": "nope", "body": "nope"}, create_issue)
assert no["reason_code"] == "CAPABILITY_NOT_GRANTED"
Identity tells you who the agent is. Authority tells you what it may do.
CLI
pigeon keygen
pigeon inspect pass.json
What this is not
Pigeon is a small primitive. It is not a platform, a policy engine, an identity provider, or a key custodian. It does not stop prompt injection. It bounds blast radius along the dimensions you put on the Pass, and only those.
- Protocol:
SPEC.md - Limits:
SECURITY.md - More scripts:
examples/(infrastructure, data, code, then payments)
examples/01_infrastructure.py
"""Agent may deploy to staging, never production."""
from pigeon import grant, verify
authority = grant(
subject="agent:deployer",
capabilities=["deploy"],
resources=["environment:staging"],
)
allowed = verify(authority, action="deploy", resource="environment:staging")
print("staging:", "allowed" if allowed.allowed else allowed.reason_code)
assert allowed.allowed
denied = verify(authority, action="deploy", resource="environment:production")
print("production:", denied.reason_code, denied.message)
assert not denied.allowed
assert denied.reason_code == "RESOURCE_NOT_ALLOWED"
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.