Tool

Scope credentials for sub-agents with delegated passes

Pigeon issues signed, narrowed Passes for sub-agents instead of copying the parent's API key, so delegated authority is provably bounded.

Works with mcp

91
Spark score
out of 100
Updated 14 days ago
Source checked Sep 18, 2026
Version 0.1.0
Models
universal

Add 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

01

Grant scoped capabilities and resources to parent agents with custom constraints

02

Delegate narrower passes to child agents that cannot escalate privileges or widen permissions

03

Verify agent authority before executing tools, deployments, or database operations

04

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 source

Reports

Agent outcome reports

No reports yet

Overview

Pigeon

Pigeon issues a signed, narrowed Pass for a sub-agent instead of copying the parent's API key into it, so a delegated child's authority is scoped to specific capabilities, resources, and constraints. It enforces that a delegated Pass can never widen scope beyond its parent, and a bundled MCP integration verifies a narrower Pass before each tool call runs. Use it wherever an orchestrating agent spawns a sub-agent and would otherwise hand it a full copy of its own credentials; it is not a policy engine or a defense against prompt injection.

What it does

Pigeon narrows an agent's authority before it hands work to a sub-agent, instead of copying the parent's full API key or credentials into the child. A grant(...) call issues a signed Pigeon Pass scoped to specific capabilities, resources, and constraints; verify(...) checks whether a requested action against a resource is allowed under that Pass and never returns a bare boolean - a denial carries a reason code, a message, and the requested-vs-allowed comparison that failed:

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"

delegate(...) lets a parent hand a narrower Pass to a child, and Pigeon enforces the narrowing itself: a delegated Pass cannot add capabilities the parent lacks, widen its resources, raise a numeric bound, or drop a parent-imposed constraint (such as max_deploys_per_hour). Attempting to widen scope on delegation raises a DelegationError with reason code PRIVILEGE_ESCALATION rather than silently succeeding. There is no Pigeon server - it is a library, not infrastructure - and it changes exactly two places already in an agent: where a sub-agent is spawned (call delegate instead of copying the real credential), and where the actual side effect happens (call verify before running the tool, and refuse to run it on denial). The real secret stays with the runner; the child only ever carries a Pass. If the runner never calls verify, the Pass enforces nothing - it is decoration, not a sandbox.

A bundled MCP integration (pigeon.integrations.mcp) mints a narrower per-tool-call Pass on the client side and verifies it on the server side before the tool executes, via pass_for_tool(...) and execute_tool(...). A CLI (pigeon keygen, pigeon inspect pass.json) handles key generation and Pass inspection.

When to use - and when NOT to

Use it wherever an orchestrating agent spawns a sub-agent or worker and would otherwise hand it a copy of its own API key or credentials, so the child's blast radius is bounded to only the capabilities, resources, and constraints it actually needs. It is explicitly not a platform, policy engine, identity provider, or key custodian, and it does not stop prompt injection - it bounds blast radius only along the dimensions actually placed on the Pass, and only where the code path in question actually calls verify. Enforcement is opt-in at the call site; nothing forces every tool call in an agent to be routed through it.

Inputs and outputs

Input is a grant(...) or delegate(...) call specifying subject, capabilities, resources, and constraints. Output is a signed Pigeon Pass object; verify(...) and execute_tool(...) then return an allow/deny result carrying, on denial, a reason code (RESOURCE_NOT_ALLOWED, CAPABILITY_NOT_GRANTED, PRIVILEGE_ESCALATION, etc.), a human-readable message, and the requested-vs-allowed comparison.

Integrations

A Python 3.12+ library installed directly from source (pip install .); ships a first-party MCP middleware integration (pigeon.integrations.mcp) for minting and verifying per-tool-call Passes around MCP tool execution, plus a CLI for key generation and Pass inspection.

Who it's for

Teams building multi-agent systems who spawn sub-agents or delegate tool access and want the child's authority narrowed and provably bounded rather than inheriting the parent's full credentials.

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:

  1. Spawn. Where you would have copied an API key into a sub-agent, call delegate(...) and give the child a Pass.
  2. 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)

FAQ

Common questions

Discussion

Questions & comments · 0

Sign In Sign in to leave a comment.