Tool

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.

Works with mcp

91
Spark score
out of 100
Updated 2 months ago
Source checked Oct 14, 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 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:

  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)

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.