@mandate-dev/proxy
Enables agents to operate on GitHub repositories under task-scoped mandates, including reading repository contents, writing files, and creating pull requests, while enforcing branch, path, and destination restrictions and blocking unauthorized actions such as merges via scoped GitHub App tokens and an MCP proxy.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@mandate-dev/proxyfix issue #42 in acme/api and open a PR against main"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
LeastPilot
LeastPilot gives an AI coding agent only the access its current task needs, and enforces that while the agent runs.
Tell it the task, say "Fix issue #42 and open a PR", and it writes down exactly what that task is allowed to touch: which repository, which branches and files, which actions, where data may go, and when it all expires. That written-down permission slip is called a mandate. A plain rule-based checker (no AI involved) proves the mandate doesn't go beyond what the person asking and the organization already allow. Then LeastPilot hands the agent a GitHub token limited to that one repository, puts a proxy between the agent and GitHub that checks every call against the mandate, and records everything it decided.
Status: 0.2.0 is on npm
It's on npm as
@leastpilot/cli, published from CI with provenance that traces each package to the commit it was built from. The whole loop runs from one command: write a mandate, check it, get it approved, mint a scoped token, run the agent under it in a locked-down container, and keep a signed log.It's been measured on six synthetic tasks, not the fifty the plan called for, and it misses three of its targets: task completion, how long writing a mandate takes, and how much it cuts a token's access when the token was already limited to one repository. See Benchmark, and the benchmark report, which lists its own shortfalls before any number.
The project was called Mandate until 2026-10-12. The product, the
leastpilotcommand, the@leastpilot/*packages and theLEASTPILOT_*variables are new names. The thing it writes is still called a mandate.
Why this exists
Coding agents usually run with far more access than any one task needs. Today's ways of limiting them each fall short:
Broad credentials. Agents tend to run on a developer's personal token or a wide GitHub App install. An agent fixing one issue can often merge pull requests, edit CI workflows, read secrets or touch other repositories.
Policies written once. Tools like GitHub's MCP toolsets or GitHub Agentic Workflows let you write limits per agent or per workflow. Those are either too loose for most tasks or too tight for some.
Asking a model to judge. Some products ask a language model, during the run, whether an action fits the task. A model can be talked out of its answer, and its decisions are hard to audit.
LeastPilot writes limits per task, checks them with ordinary code, and enforces them with controls GitHub already has (scoped App tokens) plus a proxy in front of GitHub's MCP server.
It does not try to detect prompt injection. It assumes an agent can be tricked, and limits what a tricked agent can do.
Related MCP server: telekey-mcp
How it works
Here's the mandate for "Fix issue #42 and open a PR":
mandate: fix-issue-42
task: "Fix issue #42 and open a PR"
requestedBy: user:alice # the person asking, never the agent
expiresInMinutes: 60
ceiling: org-policy@v12 # the org policy it must fit inside
grants:
- action: repo.read
enforcedBy: token
resources: ["acme/api"]
- action: contents.write
enforcedBy: proxy
resources: ["acme/api"]
branches: ["agent/42-*"]
paths: ["src/**"]
denyPaths: [".github/workflows/**", "**.env**"]
- action: pull_request.create
enforcedBy: proxy
resources: ["acme/api"]
branches: ["agent/42-*"]
base: main
max: 1
destinations:
allow: ["github.com/acme/api"]Anything not granted is denied. So this agent can read acme/api, write
under src/ on its own agent/42-* branch (never a CI workflow or a .env
file), and open one pull request into main. It can't merge, can't touch
another repository, and can't send repository data anywhere but that
repository.
A task goes through these steps:
Write. A model reads the task and proposes a mandate. It's treated as untrusted: its output is a proposal, and only the validator can turn a proposal into something the rest of the system will accept. You can also write a mandate by hand.
Validate. Plain code checks the proposal against two limits: the organization's ceiling (a policy file the operator writes, in Cedar) and the requester's real permission on the repository, read from GitHub. It also rejects risky shapes outright, like writing to the default branch or touching workflows and secrets. If a grant is too wide, the error names the ceiling rule it broke and gives an example it would have allowed.
Approve. The requester reads a permission diff of what's being granted. If you ask for it (
--approval), nothing is minted until they approve that exact mandate by posting/leastpilot approve <digest>on the issue.Mint. LeastPilot creates a GitHub App token limited to the one repository and the permissions the grants need, expiring within the hour.
Run. The agent's only route to GitHub is
leastpilot serve, an MCP server that hides the tools the mandate doesn't grant and checks every call's repository, branch, base, file paths, destinations and count. A refused call names the rule it hit.Widen. If the mandate turns out too narrow, a refused call can be turned into a request for more scope. That request goes back through the validator and the requester, never the agent.
Expire and log. The token is revoked at the end, and every call and decision is recorded, optionally in a signed log.
Two things about the checks matter more than they look:
The validator checks whole patterns, not examples. A grant like
branches: ["agent/42-*"]stands for every branch name that matches it. Checking a few sample names would prove nothing about the rest, so the validator turns each pattern into a small state machine and proves one pattern's matches are a subset of the other's. That's why a refusal can hand you a specific counterexample.A GitHub token can't limit branches or files. An App token can be limited to a set of repositories and permissions, and nothing finer. So every grant records what actually enforces it (
token,proxyorsandbox), and docs/enforced-where.md lists what each layer covers and what gets around it.
System design
Two halves: everything that happens before the agent starts decides what it may do, and everything during the run holds it to that.
flowchart TB
subgraph limits["What the validator checks against"]
direction LR
ceiling[("Org ceiling<br/>a Cedar policy")]
level[("Requester's permission<br/>read from GitHub")]
end
subgraph before["Before the run: decide what the agent may do"]
direction LR
task["Task text"] --> writer["Writer<br/>a model, untrusted"]
writer -- "proposal" --> validator{"Validator<br/>plain code"}
validator -- "validated<br/>mandate" --> approve["Requester<br/>approves"]
end
subgraph compiled["What the compiler produces"]
direction LR
token["GitHub App token<br/>one repo, needed permissions"]
rules["Proxy rules<br/>branches, paths, counts"]
egress["Egress allowlist<br/>for the sandbox"]
token ~~~ rules ~~~ egress
end
limits --> before
before --> compiledThe line that matters is the validator. Everything above it is untrusted: task text, issue bodies, the model's proposal, an agent's plan. Everything below it only accepts a validated mandate, and the only code that can create one is the validator. That's enforced by the type system and by a lint rule, not by convention.
Here's a run in the sandbox. The agent's container holds no GitHub credential at all, and its only way out to the internet is an allowlist:
flowchart TB
subgraph box["Sandbox container: no GitHub credential"]
direction LR
agent["Agent<br/>e.g. Claude Code"] -- "MCP" --> relay["Relay"]
agent -- "everything else" --> squid["Egress proxy<br/>allowlist only"]
end
subgraph host["Your machine"]
direction LR
key[("App private key")] --> serve["leastpilot serve<br/>holds the token,<br/>checks every call"]
serve --> log[("Signed<br/>audit log")]
serve -- "allowed calls only" --> mcp["github-mcp-server<br/>pinned by digest"]
end
box -- "relay, with a secret handshake" --> host
host --> github["GitHub API"]
box -. "the mandate's destinations and the model's API;<br/>GitHub only through serve" .-> internet["Internet"]Why the sandbox matters: the proxy only sees MCP calls. An agent with a shell
could run git push or curl directly, or find a token somewhere on disk,
and never touch the proxy. In the sandbox the agent works in a clean clone of
your repository (no .git/config, no ignored .env), gets only the
environment variables you name, and can only reach hosts on the allowlist.
iptables rejects everything else, over IPv4 and IPv6, and if those rules
can't be loaded the container exits instead of starting the agent. Without --sandbox, the
agent runs as your user and could read the App's private key, so destination
limits are advisory. docs/threat-model.md §3.1 goes
through this.
The code is split into small packages, with the validator in the middle:
Package | What it does |
| 46 GitHub operations. For each: the REST route, the MCP tool, the App permission it needs, and how risky it is. Just data. |
| The mandate format, hashing, and the "validated" type only the validator can create. |
| The subset proofs, the Cedar ceiling, and the lints for risky mandates. |
| The model that proposes a mandate. Untrusted by design. |
| Mints and revokes the scoped token, and turns a mandate into proxy rules, the egress allowlist and the gh-aw export. |
| The MCP proxy: hides ungranted tools, checks every call, records decisions, and writes the signed audit log. |
| Replays a recorded run against a mandate and reports what it over- or under-granted. |
| The |
| Private. The benchmark. |
More detail on how they fit together: docs/architecture.md.
Install
You'll need Node 22.12 or later:
npm install -g @leastpilot/cliWhere it runs unattended, install an exact version into a project and check the registry's signatures and provenance there:
npm install @leastpilot/cli@0.2.0 && npm audit signaturesRunning an agent in the sandbox also needs Docker Desktop on macOS.
The examples below use the sample ceilings in this repository's fixtures/,
so run them from a clone:
git clone https://github.com/pkravella/mandate && cd mandateTo build from source instead, run pnpm install && pnpm build in the clone
and use node packages/cli/dist/index.js wherever the examples say
leastpilot.
Try it
Check a mandate, offline and free. Save the example above as
mandate.yaml, then:
leastpilot validate mandate.yaml \
--ceiling fixtures/ceilings/org-policy-v12.cedar \
--schema fixtures/ceilings/schema.cedarschema \
--ceiling-destinations fixtures/ceilings/org-policy-v12.destinations \
--as alice --level pushIt prints a permission diff and exits 0 if the mandate fits. If it doesn't, you get exit 1 and something like this:
Mandate rejected.
[not-contained] in grant #0 (contents.write) grant contents.write is wider
than ceiling clause allow-agent-branch-writes: it would permit "feature/42-"
ceiling clause: allow-agent-branch-writes
counterexample: feature/42-
Nothing was minted. Narrow the mandate, or request a ceiling change.--level push asserts Alice's permission, and the output says so every
time. To check it for real, use --repo acme/api instead, which reads it from
GitHub using your App's credentials (LEASTPILOT_APP_ID,
LEASTPILOT_INSTALLATION_ID, LEASTPILOT_APP_KEY_PATH). Only that version
counts as a record of who was allowed to do what. There's no default, so you
always pick one.
Run an agent under a mandate. leastpilot run checks the mandate, writes
an MCP config pointing at leastpilot serve, and starts whatever agent you
give it. {mcpConfig} is replaced with that config's path:
leastpilot run \
--mandate mandate.yaml \
--ceiling fixtures/ceilings/org-policy-v12.cedar \
--schema fixtures/ceilings/schema.cedarschema \
--ceiling-destinations fixtures/ceilings/org-policy-v12.destinations \
--as alice --repo acme/api --trace run.jsonl --diff \
-- claude --mcp-config '{mcpConfig}' -p "Fix issue #42 and open a PR"LeastPilot doesn't drive any particular agent. Every MCP client already knows how to start a server from a config, so being that server is the whole integration.
Run it in the sandbox. Build the base image, then an image with your agent
in it, then add --sandbox:
leastpilot sandbox build
docker build -t mandate-agent-claude packages/cli/sandbox/examples/claude-code
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
leastpilot run --mandate mandate.yaml \
--ceiling fixtures/ceilings/sandbox-v1.cedar \
--schema fixtures/ceilings/schema.cedarschema \
--ceiling-destinations fixtures/ceilings/sandbox-v1.destinations \
--as pkravella --repo pkravella/mandate-sandbox \
--sandbox --image mandate-agent-claude --agent-egress api.anthropic.com \
--pass-env ANTHROPIC_API_KEY --pass-env CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC \
-- claude -p "read src/retry.js and say what it does" \
--mcp-config '{mcpConfig}' --strict-mcp-config --allowedTools mcp__github--agent-egress opens the agent's own model API, and --pass-env passes its
key by name. Both are your choice and are printed in the output; a mandate can
never ask for them. Ctrl-C stops the container, then revokes the token, then
removes the workspace. This was verified live with Claude Code: the allowed
read worked, a read of another repository was refused, and the only traffic
out was to the model's API.
The quickstart walks through all of this step by step.
The rest of the loop
Approvals. --approval owner/repo#42 on run or serve refuses to mint
a token until the requester has posted /leastpilot approve <digest> on that
issue or pull request. The digest covers the mandate, the ceiling it was
checked against, and who asked, so a mandate edited after review needs a new
approval. Only the requester's own, unedited comment counts.
Asking for a mandate in a comment. leastpilot review writes a mandate
for a task and posts the permission diff on the issue, with the exact line
that approves it. Because a model call costs money and anyone can comment, it
checks the requester's permission, an hourly limit per repository and the
task's length before it calls the model.
examples/workflows/leastpilot-review.yml
wires this up so /leastpilot <task> posts a review and the requester's
approval runs the agent.
Widening. Run serve or run with --pauses <dir> and every refused
call is saved there, along with the command to ask for more scope.
leastpilot widen applies that request and re-checks the whole mandate from
scratch. Some refusals can't be widened on purpose: a denied path, an expired
mandate, an operation that wasn't granted at all, or a disallowed destination.
A one-click "allow .github/workflows/**" would make the deny list pointless.
Audit log. --audit-log <path> writes a signed, append-only record of the
session: the mandate, every call sent to GitHub, every decision, and a final
seal. Each line is signed with its own Ed25519 key (leastpilot audit keygen)
and chained to the one before. leastpilot audit verify checks it with only
the public key. The signature only means something against the agent when the
agent runs in the sandbox, because otherwise it could read the key.
docs/audit-log.md has the details.
Exporting to GitHub Agentic Workflows. leastpilot export gh-aw turns a
checked mandate into a gh-aw workflow,
where the agent runs read-only and gh-aw's own job does the writing within the
mandate's limits. It refuses anything gh-aw can't hold exactly, rather than
quietly loosening it. docs/gh-aw.md has the mapping.
The GitHub Action. There's an Action and an
example workflow. It installs the
published CLI from a committed lockfile
(action-install/) with npm ci, so every
dependency is pinned by hash, and checks the registry's signatures and
provenance before it runs anything. That lockfile is updated in a pull request
after each release, so the Action at a release's own commit still runs the
previous CLI: pin the Action to a commit after that update, and check which
version action-install/package.json names. The workflow sets
permissions: {} and the Action refuses to run if it finds a GITHUB_TOKEN,
since a second credential is access no mandate limits.
It takes the agent command one argument per line, so no workflow input is ever
run through a shell.
Benchmark
Six synthetic tasks against a test repository, each run under its generated mandate. One run per task per mode, so the numbers are noisy. The full report is generated from committed results, and a test re-renders it to make sure they still match.
Metric | Result | Target |
Tasks completed | 83.3% (5 of 6) | at least 90% |
Access cut at the token, vs. a token for 1 repository | 32.3% | at least 80% |
Access cut at the proxy, vs. a token for 1 repository | 87.1% | at least 80% |
Access cut at the token, vs. a developer's token (63 repositories) | 98.9% | at least 80% |
Access cut at the proxy, vs. a developer's token (63 repositories) | 99.8% | at least 80% |
Granted but never needed | 0.0% | under 10% |
Runs paused for something in scope, tools hidden | 0.0%, but see below | under 5% |
Runs paused for something in scope, every tool offered | 66.7% (4 of 6, 3 of which still finished) | under 5% |
Time to write a mandate | ~10.2 s at effort | under 10 s |
Grants outside the ceiling, including 42 attack cases | 0 | 0 |
How to read these:
The 63-repository numbers flatter it. Nearly all of that cut comes from limiting the token to one repository, which any scoped token gets without LeastPilot. Per repository, LeastPilot takes a task from 31 operations down to 21 at the token and 4 at the proxy.
Zero pauses doesn't mean the mandates were the right size. The proxy hides tools the mandate doesn't grant, so the agent never tries them and nothing gets refused. Replay of unconstrained runs predicted seven calls the mandates were missing, yet the proxy refused nothing. So the benchmark also runs each mandate with every tool visible. That turned up five refusals. Four were redundant reads of an issue the agent already had. The fifth was real: the triage task's mandate didn't grant commenting, so the agent couldn't report what it found, and that task failed.
Writing a mandate costs about $0.03 and takes two model calls. Effort
mediumgave the same mandates ashighon all six tasks, and on the five tasks written at both settings everymediumwrite was faster, a median of 10.4 seconds against 11.7 (10.2 across all six), somediumis the default. It still misses the ten-second target (writer-effort-results.json).
The attack suite is separate from the benchmark: 42 cases where injected issue text, hostile tool descriptions, over-asking agent plans or tricky arguments try to widen what the agent can do. Each case assumes the model writing the mandate was fully fooled and proposed exactly what the attacker wanted. The question is only whether the validator and the proxy refused it anyway. They did in every case.
Commands
Command | What it does |
| Check a mandate against the ceiling and the requester's permission, and print the permission diff |
| Check a mandate, then start an agent whose only route to GitHub is the enforcing proxy ( |
| The enforcing MCP server for one mandate, which is what an agent's MCP config starts |
| Write a mandate for a task and post its permission diff on an issue or pull request |
| Recover the mandate an approval digest names, from the review comment |
| Apply a request for more scope and re-check the whole mandate |
| Turn a checked mandate into a GitHub Agentic Workflows workflow |
| Make an audit-log signing key, and check a log |
| Build the sandbox's base image from the source in the package |
What it doesn't do
Detect prompt injection. It limits what a tricked agent can do instead.
Protect you from another credential. If the agent can find a personal token, a logged-in
gh, or an SSH key, it doesn't need the proxy. That's what the sandbox is for.Ask a model whether an action is OK. A model proposes; plain code decides.
Be an identity provider, a secrets vault, or another MCP gateway.
Work outside GitHub, for now.
Known gaps
Stated here so nobody has to discover them:
No auto-approval. Every mandate needs a person to approve it. There's no library of task templates yet for an org to mark as safe to auto-approve.
Requests from automation accounts are refused. GitHub only tells an App what another App may do, so LeastPilot has no way to check an
app:requester's access.One export backend out of four. gh-aw works. Docker MCP Gateway, AWS AgentCore (Cedar) and OPA exports don't exist yet.
Thirteen catalog operations have no MCP tool, so the proxy never sees them. Most are classed as forbidden and rejected by the validator;
branch.deleteis the one to watch.The sandbox is macOS with Docker Desktop only. The Linux path has never been run, so
--sandboxrefuses there, including on GitHub's runners.maxcounts per session, not per task. A new session starts from zero. With--sandboxonly you can start one; without it, the agent could.The audit log has no outside witness. A log with no seal was cut off, and nothing in it can say how.
Six tasks, not fifty. A fifty-task run would cost about $14 plus recording reference runs, and the project's budget was $10.
The full list of what breaks it is in docs/threat-model.md §3.
Prior work
Project | What it does | What LeastPilot adds |
Model-written capability leases, a checker that only narrows, MCP and eBPF enforcement | The closest design. LeastPilot is a maintained tool with GitHub's semantics and replay measurement | |
A policy language and a model-written policy per task | An org-level ceiling, expiry, and GitHub-native enforcement | |
Per-task policy from trusted context, checked deterministically | Released code | |
Plain English to Cedar, formally checked, enforced at AgentCore Gateway | Policy per task rather than per agent, and it works outside AWS | |
A read-only agent that writes through limited safe outputs | Limits per task instead of per workflow; LeastPilot can export to it | |
Deterministic data-flow rules | Derives the rules from the task instead of writing them by hand |
The npm scope is @leastpilot. @mandate and mandate-dev belong to
unrelated projects, which is part of why the name changed.
Security
The threat model says what holds, what's out of scope
and what breaks it. Short version for operators: run agents with --sandbox,
use --repo rather than --level, require an approval, give the job no
GITHUB_TOKEN, and never paste issue text into the agent's prompt. Pass the
issue number and let the agent fetch it through its granted tools.
To report a security problem in LeastPilot itself, see SECURITY.md.
Documentation
Check a mandate, then run an agent under it | |
Every field, and what gets rejected | |
The org policy file and the subset of Cedar it uses | |
Which control enforces what, and what gets around each one | |
What holds, what's out of scope, what breaks it | |
Turning it on, checking it, and what the signature proves | |
What maps to GitHub Agentic Workflows and what's refused | |
How the packages fit together, and why | |
The full measurements, generated from the results | |
How the packages get to npm |
Development
pnpm install
pnpm typecheck # builds, then typechecks the tests
pnpm lint # includes the rules that guard the validator boundary
pnpm test
pnpm buildAbout 1,450 tests run offline with no credentials, and that's what CI runs.
Use the root pnpm build: the packages have no build script of their own, so
pnpm --filter <pkg> build does nothing and still exits 0.
Fifty-two more tests are switched off by default, each behind its own variable, because they spend money, use real credentials, write to a real repository, or need Docker. CI never runs them and needs no secrets.
Variable | What it runs | Cost |
| The writer against the real model, real token minting, and the whole loop end to end | ~$0.03 to ~$0.30 |
| Builds the sandbox and checks its egress, the relay, and | free, needs Docker |
| Seeds the benchmark tasks into the test repository: writes to its default branch and opens issues | free |
| Re-records the reference runs, overwriting the committed ones | ~$3 |
| The benchmark sweep and the report | ~$1.80 for six tasks |
| Rewrites each task's mandate at several effort settings and compares | ~$0.03 a write |
The ones that use credentials also need ANTHROPIC_API_KEY,
LEASTPILOT_APP_ID, LEASTPILOT_INSTALLATION_ID, LEASTPILOT_APP_KEY_PATH
and LEASTPILOT_TEST_REPO. See CONTRIBUTING.md, in
particular the one rule that isn't negotiable.
License
Apache-2.0. See NOTICE for attribution.
This server cannot be deployed
Maintenance
Related MCP Connectors
Find, vet, and run MCP tools through a secure audited gateway with prompt-injection risk scoring
Identity, authorization, audit trails, and revocable permissions for AI agents accessing MCP tools.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables AI agents to safely call MCP tools through a security gateway that enforces per-role authorization and least-privilege tool scoping.1-
- AlicenseNot gradedqualityBmaintenanceEnables MCP agents to be issued limited-access capability passports that can only be narrowed when delegated, enforcing least privilege and preventing permissions creep across tool calls and sub-agents.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP clients to govern downstream tool servers by enforcing default-deny authority and attestation on every tool call, with live monitoring, approval, and mid-session revocation.3Apache 2.0

MetaMynd MCP Gatewayofficial
AlicenseNot gradedqualityAmaintenanceEnables MCP-based AI agents to route tool calls through governance without code changes, checking each call against a mandate and allowing, refusing, or holding for approval while keeping tool API keys out of the AI app.192 npmMIT