Skip to main content
Glama
pkravella

@mandate-dev/proxy

by pkravella

Mandate

Task-scoped authority for AI agents.

Mandate gives each agent task its own authority and enforces it, before and during execution, with one artifact. From a request like "Fix issue #42 and open a PR" it writes a mandate: the resources, actions, argument limits, allowed data destinations and expiry that the task needs. A deterministic validator proves the mandate grants nothing beyond what the user and the organization already allow. Mandate then compiles it into controls GitHub and MCP gateways already enforce, and watches the run against it.

Status: v0.1 plus Phases 5 and 6 of the post-MVP plan. The loop runs end to end from one command, and is measured — but completion, write latency and the token-layer authority cut against one repository miss their targets, and the corpus is six tasks rather than fifty. A model writes a mandate, the validator proves it against a Cedar ceiling and a destination allowlist, a repo-scoped GitHub App token is minted, and mandate run launches an agent whose GitHub tools all go through the enforced proxy. With --sandbox the agent runs in a container that holds no GitHub credential at all and reaches only the mandate's destinations; without it, any credential the agent can find is its own (see the threat model §3.1). Every decision is recorded as an action graph, and a replay evaluator scores mandates against traces of unconstrained runs.

What is measured and what is missed: the benchmark report, which states its own shortfalls before any metric and is rendered from the committed results it reports. What is not built: below.

What a mandate looks like

mandate: fix-issue-42
task: "Fix issue #42 and open a PR"
requestedBy: user:alice        # an authorized source, never the agent
expiresInMinutes: 60
ceiling: org-policy@v12        # proved to be a subset of this
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.

Related MCP server: ContextBridge

What works today

pnpm install && pnpm build

node packages/cli/dist/index.js 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 --repo acme/api --repositories 20

--repo reads alice's permission on acme/api from GitHub, which is what makes the output an authorisation record. It needs MANDATE_APP_ID, MANDATE_INSTALLATION_ID and MANDATE_APP_KEY_PATH; the token it mints carries metadata: read and nothing else, and is revoked straight after the lookup.

Without credentials, assert the level instead — and the output says, every time, that it is an assertion:

node packages/cli/dist/index.js 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 push

There is no default. --level used to default to push, so every run quietly asserted push authority with nothing in the output saying so.

Exit 0 prints the permission diff, 1 prints the rejection with the ceiling clause and a counterexample, 2 means the input could not be read or the command was mistyped. Every result names the ceiling it was checked against by its file and the sha256 of its contents; a mandate's own ceiling: field is shown only as the claim it is.

A paused call can be re-reviewed with more scope. mandate widen takes the pause record the proxy emitted and re-validates the whole mandate from scratch, so the agent can never grant itself anything:

node packages/cli/dist/index.js widen mandate.yaml pause.json \
  --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

Not every denial is widenable, and the refusal list is the contract: a deny path, an expired mandate, an ungranted operation, a disallowed destination and an internal error are all refused with the reason instead. A one-click "add .github/workflows/** to your paths" would make the deny list decorative.

A refusal looks 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.

Which of the two you used is printed with the result, because "verified against GitHub" and "asserted by whoever ran the command" are different claims and output that does not distinguish them reads like an authorisation record without being one.

Running an agent under a mandate

mandate serve is an MCP server that enforces one mandate. mandate run validates, writes a config pointing at it, and launches the command you give:

node packages/cli/dist/index.js 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"

{mcpConfig} becomes the config path, and MANDATE_MCP_CONFIG is set in the agent's environment, so an agent that takes a flag and one that reads the environment both work.

Mandate does not drive a particular agent. Every MCP client already knows how to start a server from a config, so being that server is the whole integration — which is also what makes vendor neutrality true rather than aspirational. The cost of that choice: Mandate bounds the authority it mints, and cannot bound an agent holding a credential of its own. That is the sandbox's job, and docs/threat-model.md §3.1 says so.

Nothing is minted and no agent is launched until the mandate validates. The config the agent reads carries no credential: mandate serve mints its own token in its own process. But without the sandbox the agent spawns that process itself, with the operator's environment — which names the App's private key, readable by the agent's own user. That is what --sandbox is for.

In the sandbox

docker build -t mandate-sandbox sandbox
docker build -t mandate-agent-claude sandbox/examples/claude-code
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

node packages/cli/dist/index.js 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

mandate run starts mandate serve itself, on the host, and runs the agent in a container built from an allowlist: a depth-1 clone of HEAD (HEAD's tree and no other object — no history, stash or host hook, no ignored .env; a dirty tree is refused), egress to the mandate's destinations except GitHub's own hosts — the agent reaches GitHub only through serve — plus the hosts named with --agent-egress, and only the environment variables named with --pass-env, refusing anything GitHub-named outright. squid never matches a destination by reverse DNS and refuses IP addresses, and the agent runs with no way to regain privileges. Ctrl-C or a kill stops docker, then serve (which revokes), then removes the workspace. The agent reaches serve through a relay with a secret handshake. Verified live with Claude Code: the allowed read succeeded, a read of another repository was refused at repo.read.resources, and the only egress was two tunnels to the model's API. macOS with Docker Desktop only; --sandbox refuses elsewhere, which includes the Action's Linux runners.

An audit log

--audit-log <path> on run or serve writes a signed, append-only record of the session as it happens: the mandate, every call sent to GitHub, every decision, and a seal saying whether the session served or never opened. Each line is signed with an Ed25519 key of its own (MANDATE_AUDIT_KEY_PATH, made by mandate audit keygen), never the App's, and chained to the line before it. mandate audit verify <log> --public-key <pub> checks it with the public half alone and names any call that was sent and never answered. With --sandbox the key never reaches the agent; without it, the agent could read the key and rewrite the log. docs/audit-log.md has the format and what the signature does and does not prove.

Requiring an approval

--approval owner/repo#number on serve or run refuses to mint until the mandate's requester has approved it in a comment on that issue or pull request, on a line of its own:

/mandate approve 3f9c…

The refusal prints the exact line to post. The digest covers the mandate's content, the ceiling it was proved against and its requester, so a mandate edited after review, or validated against another ceiling, needs a new approval. Only the requester's own comment counts, and never an edited one, since a repository admin can edit anyone's comment. GitHub dates comments to the second, so an edit made within the second the comment was posted is not detected. An approval stands until its comment is deleted: it covers every session of that mandate, ceiling and requester, not one run. Each session's approval goes into the audit log. Reading it needs the App's issues: read and pull_requests: read: a pull request's comments are refused to an issues: read token (probed).

Asking for a mandate in a comment

mandate review writes a mandate for a requester's task and posts its permission diff on the issue or pull request, with the exact line that approves it:

node packages/cli/dist/index.js review --task-file task.txt --on acme/api#42 \
  --as alice --repo acme/api \
  --ceiling fixtures/ceilings/org-policy-v12.cedar \
  --schema  fixtures/ceilings/schema.cedarschema \
  --ceiling-destinations fixtures/ceilings/org-policy-v12.destinations --post

The model call costs money and the request is a comment, so everything that can refuse without the model goes first:

  • The requester's level. It is read from GitHub, and --level is refused. It must be at least --min-level (default push). The validator still bounds the mandate by that level afterwards.

  • A rate limit. At most --max-per-hour (default 10) mandates per repository per hour. Only the App's own review comments are counted, since anyone can type the marker it leaves. Rejections and failed writes count too, since they were paid for. The count is read, then the write happens, so two reviews running at once can both pass it: run them one at a time per repository (a workflow concurrency group does that). Deleting the App's comments resets the count.

  • The task's length. A mandate holds at most 4000 characters of task, and a longer one is refused before it is sent to the model.

The task text sits inside code fences it cannot close, so a comment cannot write Markdown under the App's name. A rejected proposal, or a writer that produced nothing, is posted too, with nothing to approve. Without --post, the comment is printed instead.

Exporting to gh-aw

mandate export gh-aw validates a mandate and prints it as the frontmatter of a GitHub Agentic Workflows workflow (gh-aw v0.89.21). The agent runs read-only, and gh-aw's own job writes the pull request or comments within the mandate's limits. It refuses anything gh-aw cannot hold exactly rather than widen it: branch patterns (gh-aw normalises a branch name after checking it, so agent/42-!! matches agent/42-* and is pushed as agent/42), reads limited by path, more than one repository, a mandate that writes nothing. It also switches off every gh-aw default that writes something on its own. gh-aw sets these limits when the workflow is written, so the result is a template for a kind of task, not one task's mandate. A conformance test runs gh-aw's own matcher code against Mandate's. docs/gh-aw.md has the mapping and every refusal.

In a repository, examples/workflows/mandate-review.yml runs this on comments:

  1. /mandate <task> from someone with at least push posts the review.

  2. /mandate approve <digest> from the same person runs the agent under that exact mandate. review/action.yml posts the review, and the main Action's approved-digest and approval inputs run it.

  3. Reviews run one at a time per repository, so the hourly limit holds.

There is also an Action and an example workflow. Both Actions build Mandate from source on every run, because the CLI is not published yet. That adds about a minute to a run, and it is why uses: pkravella/mandate@<sha> works from another repository. pnpm itself is pinned by hash, which corepack checks. The workflow sets permissions: {} and the Action refuses to run if it finds a GITHUB_TOKEN, because a second credential is authority no mandate bounds.

The Action takes the agent command one argument per line:

agent: |
  claude
  --mcp-config
  {mcpConfig}
  -p
  Fix issue 42 and open a pull request.

That shape is deliberate. A single string would have to be word-split by the shell, and a value reaching a shell from a workflow input is the classic injection route — the first version of this Action interpolated every input into its script and was a remote shell for anyone who could set one. One element per line means spaces inside a line are kept and nothing in a line is ever interpreted. It is enforced by a test that reads action.yml and fails on an interpolation inside a run: block.

Note what that does not protect: an issue title placed in the prompt is still attacker-controlled text going to the model. Mandate's answer to that is the mandate — an injected agent is bounded by what was granted — but the two are different problems. docs/threat-model.md §5 says what to do about it.

Documentation

Quickstart

Validate a mandate, run an agent under it

Mandate format

Every field, and what the lints refuse

Writing a ceiling

The constrained Cedar profile, as a contract

Enforced where

Which control bounds what, and what defeats each

Threat model

What holds, what is out of scope, what breaks it

Audit log

The signed session log: turning it on, checking it, its format, what it proves

gh-aw export

A mandate as a GitHub Agentic Workflows workflow: what maps, what is refused, what enforces it

Architecture

The package graph and why it is shaped that way

Benchmark report

Measured completion, authority cut, over-grant, and the denials an under-grant produces, rendered from its results

Three things worth knowing up front

Enforcement never rests on a model's judgment. A model proposes a mandate. A deterministic validator decides whether it may be granted, and the proof is a language-inclusion check over finite automata, not a policy engine answering point queries about representative requests. A grant is a pattern, so it denotes a set — and a point query proves nothing about the rest of that set. When a mandate is refused, the refusal names the ceiling clause it exceeded and a concrete counterexample it would have permitted.

No single control enforces a whole mandate, and the gaps are documented. A GitHub App token cannot limit by branch or path. Every grant therefore records which control enforces it, and docs/enforced-where.md states for each layer what it bounds, what it does not, and what defeats it.

Mandate does not detect prompt injection. It limits what an injected agent can do. That is a deliberate non-goal, along with being an identity provider, a credential vault, or another MCP gateway.

Build status

The v0.1 plan ran in four phases; the post-MVP plan continues from Phase 5.

Phase

Scope

State

1

Contract and ceiling: operation catalog, mandate schema, containment engine, Cedar ceiling, validator, lints

exit criterion met — 21/21 seeded over-grants rejected

2

Write and enforce: mandate writer, scoped GitHub App tokens, MCP proxy, argument enforcement, permission diff

exit criterion met — see below

3

Runtime and measurement: action graph, destination rules, egress sandbox, widen flow, replay evaluator, benchmark

built; exit criterion not met as written — it asks for 50 tasks and the corpus has 6

4

Adversarial testing and launch: the attack corpus, mandate run, packaging, docs

exit criterion met — zero ceiling breaches across 34 adversarial cases

5

Make the trust story true: the agent in the sandbox, destination extraction, requester and ceiling identity, the token clock, max

exit criterion met — every security property the docs assert is exercised by a test, threat-model §3 is shorter, and no module is tested but unreachable

6

Make the numbers mean something: the authority cut against a real baseline, under-granting made observable, write latency

exit criterion met — every figure in the report is measured on the corpus it names or labelled, and a test re-renders the report from its committed results; the derived 50-task corpus is not started (budget)

Phase 5 closed the distance between what the docs claimed and what the shipped path did, and most of what it fixed was found by probing rather than planned: an agent could leave the sandbox over IPv6; destination extraction read https://github.com@evil.example.com/ as github.com, missed HTTPS:// and ../ traversal, and never saw the one destination field the real server has (a fork's organization); the CLI printed a ceiling name chosen by the mandate's writer; a mandate could name any requester; and max: 1 on issue.create let three issues through when updates were also granted. It also ran a real agent in the sandbox for the first time, which found that no agent image could be built on the base image at all.

Phase 6 made the published numbers checkable. The first sweep had kept neither its mandates nor its results, and called a one-repository figure an org-wide baseline; a sweep now commits results.json beside the report, and a test re-renders the report from it. The authority cut is reported against one repository and against the 63 a developer's own token reaches. Each mandate is run twice — with tools filtered, as the product runs, and with every tool offered and refused on call — which is what finally made an under-grant show up as a denial. Grading had been crediting an earlier sweep's pull request and borrowing the mandate's own token; a task's acceptance command, which runs the agent's own code, ran on the host with the App key readable, and now runs in a container with neither. Two fresh reviewers then checked the phase and found no Critical issue.

Writing a mandate costs about $0.03 and misses the PRD's under-ten-second target. Phase 6 measured the lever: six tasks written again at effort medium gave mandates identical to high's, facet by facet. On the five tasks written at both settings, every medium write was faster -- a median of 10.4 seconds against 11.7 (10.2 across all six) -- so medium is now the default. Lowering only the second call changed three mandates in five. The remaining cost is two sequential model calls (writer-effort-results.json).

Phase 2's exit criterion was "an agent fixes a real issue end to end under a mandate, and an attempted merge is blocked." Run against a throwaway repository seeded with a retry() that swallowed its last error: the agent read the file, branched to agent/issue-1-rethrow-last-error, replaced the swallow with throw lastError, added a regression test asserting the last error specifically, and opened a pull request. Six turns, $0.23. The merge attempt came back refused with clause: mandate.grants. Ten proxy decisions, one denial, and that denial was the merge.

Phase 4 added the attack corpus and the commands. The adversarial suite was 34 cases (42 since Phase 5 added eight destination cases) across four families — injected issue text, hostile MCP tool declarations, over-asking agent plans, argument-level escapes — each paired with the invariant it must not break. Every case assumes a fully compromised writer that proposed exactly what the attack asked for, because Mandate does not detect prompt injection: the question is only whether the ceiling held anyway. It did, and the suite found three holes in the process, all closed.

mandate run was verified live. An agent importing nothing from this repository was handed an MCP config, spawned what it named, and was offered exactly get_file_contents, list_branches and search_code — three tools from a single repo.read grant, with no write tool and no merge. Calling merge_pull_request by name came back refused with clause: mandate.grants. The requester's level was read from GitHub rather than asserted, so that run is an authorisation record.

That second check is a negative control, and it exists because of what Phase 3 measured: the proxy filters tools/list, so an ungranted tool is never offered and therefore never attempted. Across six mandated benchmark runs there were zero denials while replay predicted seven under-granted calls. A clean run can mean the mandate was right or that nothing ever tested it, and the two look identical from the outside — so the suite calls the forbidden tool by name rather than waiting for a model to try.

Phase 3 added the runtime half and measured it; Phase 6 re-ran the sweep with each mandate in both tool-visibility modes. The full benchmark report is committed; the headline is:

Metric

Result

v0.1 target

Task completion (either mode)

83.3%

at least 90%

Authority cut, token layer, against 1 repository

32.3%

at least 80%

Authority cut, proxy layer, against 1 repository

87.1%

at least 80%

Authority cut, token layer, against 63 repositories

98.9%

at least 80%

Authority cut, proxy layer, against 63 repositories

99.8%

at least 80%

Over-grant

0.0%

under 10%

False pauses, tools filtered

0.0% — nothing was attempted, so nothing paused

under 5%

False pauses, every tool offered

66.7% — 4 of 6 runs, 3 of which still completed

under 5%

Write latency

13,010 ms at effort high; ~10.2 s at medium, now the default

under 10 s

Two caveats the report states at length and this table cannot: the corpus is 6 tasks, not the 50 Phase 3's exit criterion asks for, and those tasks are synthetic, so the completion figure is optimistic.

The authority cut is reported against two baselines and neither is the figure. One repository is the conservative one. Sixty-three is what the developer's own token reaches, which is the PRD's comparison, and nearly all of that cut is repository scoping that any token limited to one repository gets without Mandate. What Mandate adds per repository is 31 operations to 21 at the token and to 4 at the proxy.

The finding worth reading the report for. With tools filtered, as the product runs, the proxy issued zero denials across six runs while replay predicted seven under-granted calls. Filtering tools/list means a tool no grant reaches is never offered, so it is never attempted — R10's promise that every pause names the clause it hit is true and vacuous where there is no pause.

So the benchmark also runs each mandate with every tool offered and refused on call. That produced five denials, and they split two ways. Four were issue_read, three of them on tasks that finished anyway: the agent already had the issue text, so most of the "seven predicted" were redundant calls, not mandates too narrow. The fifth is the real one: the triage agent tried add_issue_comment, was refused, and could not report what it found. None of the five could be fixed by a widen, which may not add an operation, so "widen requests" stays at zero by design and the useful count is denials that need a new mandate. One run per mode per task; the report says how noisy that is.

Packages

The dependency graph is a DAG with the validator as its waist. Everything upstream of it is untrusted; everything downstream takes only a ValidatedMandate, whose constructor the validator owns.

Package

Responsibility

@mandate-dev/catalog

46 GitHub operations, each tying together Mandate's action atom, the REST route, the github-mcp-server tool name, and the GitHub App permission a token needs. Pure data.

@mandate-dev/schema

The mandate contract, canonical hashing, the ceiling digest, and the ValidatedMandate brand.

@mandate-dev/validator

Glob-set containment by DFA inclusion, the constrained Cedar ceiling profile, the two-layer subset proof, and the R4 lints.

@mandate-dev/writer

The one model-driven component, and untrusted. Covers from the catalog, prunes, and emits a ProposedMandate that only validate() accepts. Runs both calls at effort medium, by measurement.

@mandate-dev/compiler

Mints a repository-scoped GitHub App token and revokes it, and compiles the mandate into proxy rules — capped to end before the token does — plus the token-versus-proxy enforcement report and the gh-aw export.

@mandate-dev/proxy

An MCP server facing the agent and an MCP client facing github-mcp-server. Filters tools/list (or, in the benchmark's measurement mode only, offers every tool and refuses on call), checks every call's repository, branch, base, paths, destinations and count, records every decision as an action graph, and turns a denial into a clause and a reviewable widen request. Writes and verifies the signed audit log.

@mandate-dev/replay

Scores a mandate by replaying a recorded action graph through the production argument enforcer, reporting over-grant, under-grant, and which operations a task needed that the mandate lacked.

@mandate-dev/cli

mandate validate, widen, serve, run (with --sandbox: the workspace clone, the container launch and the relay listener) audit keygen/verify and export gh-aw, the permission diff, the rejection report, and the enforced session the CLI and the benchmark share.

@mandate-dev/bench

Private. The benchmark corpus loader, the seeder, the harness (each mandate run with tools filtered and with every tool offered; acceptance in a locked-down container), the writer-effort sweep, and the report renderer.

The egress sandbox is the layer that actually holds. An MCP proxy sees MCP traffic and nothing else, so an agent with a shell, a git push to a remote it configures itself, or a curl never transits it. sandbox/ is a container whose only route out is a CONNECT proxy allowlist compiled from the mandate's destinations, with iptables rejecting everything else from the agent's uid, over IPv4 and IPv6. mandate run --sandbox runs the agent in it, with no GitHub credential inside — the token stays with mandate serve on the host — and a clone of HEAD for a working copy. It verifies itself, including negative controls that widen the allowlist and strip the firewall rules to confirm the checks can fail, and it exits rather than starting the agent if it cannot install the rules. macOS with Docker Desktop for now.

Running the proxy needs the right upstream toolsets. github-mcp-server exposes 46 tools by default and 91 with GITHUB_TOOLSETS=all. Six catalog operations name tools in the non-default sets, so granting actions.read against a default server reaches nothing — and because the proxy only offers tools the upstream advertises, the agent never sees it and the task fails with nothing explaining why. TOOLSET_REQUIRED in the catalog names the toolset each one needs, and unreachableOperations() reports the gap for a running server.

The npm scope is @mandate-dev because @mandate is already taken; the CLI binary is still mandate. The project name is provisional pending a trademark check.

Not built in v0.1

Stated here rather than left to be discovered.

R5 has no auto-approval. There is no template library (R13) for an auto-approval policy to name. The review flow itself, examples/workflows/mandate-review.yml, has run end to end in a private test repository:

  1. A /mandate comment produced a review.

  2. The App's own reply re-triggered the workflow, and that run was skipped.

  3. The requester's approval ran the agent under that exact mandate.

No app: requesters. A mandate requested by an automation account is refused: GitHub reveals what another App may do only to that App, so Mandate has no source for an app principal's authority, and borrowing the operator's would attribute the request to the automation while a person's level bounded it. requestedBy must name the user checked with --as.

One of R12's four backends. mandate export gh-aw is built (see below). The Docker MCP Gateway, Cedar/AgentCore and OPA exports are not. Shipping four half-implemented backends would be four things that look supported and are not.

The audit log has no outside witness, and the Action does not write one. A log with no seal was killed or truncated, and nothing in the file can tell which. Without --sandbox the agent can read the audit key, so the signature proves nothing against it, and that includes the Action. See docs/audit-log.md.

Thirteen catalog operations no MCP tool reaches, so the proxy is not in the way of them at all. Most are classed forbidden and the lints reject them, which is the real mitigation. branch.delete is the sharp one.

The corpus is six synthetic tasks, not fifty derived ones. A 50-task sweep in both modes is now estimated at about $14 — the agent loop caches its prompt — plus recording ground truth for each new task, against a whole budget of $10 that Phase 6's measurements nearly used up. The corpus schema records synthetic as a provenance that cannot imply attribution it lacks, and loadCorpus reports the shortfall as a number so nothing can present six as fifty.

The sandbox is macOS and Docker Desktop only. On Linux the relay needs host-gateway and serve would have to bind the bridge address; that path has never been run, so --sandbox refuses there rather than claim it.

max limits a session, not a task. A new session starts the count at zero — with --sandbox only the operator can start one, without it the agent can, by restarting its MCP server — and where operations share a tool and a call cannot say which it is, their limits combine. docs/mandate-format.md has the exact rule.

A zero false-pause count is not evidence. The proxy filters tools/list, so an ungranted tool is never offered and therefore never attempted — measured, zero denials across six mandated runs while replay predicted seven under-granted calls. The benchmark's measurement mode, which offers every tool and refuses on call, surfaced the real one: the triage agent's refused add_issue_comment. docs/threat-model.md §3.3 explains why this matters more than it sounds.

Development

pnpm install
pnpm typecheck   # solution build, then typecheck the test files
pnpm lint        # includes the trust-boundary rules
pnpm test
pnpm build

Requires Node 22.12 or newer. 1,080 tests run offline with no credentials, and that is what CI runs. Rebuild with the root pnpm build: the packages have no build script of their own, so pnpm --filter <pkg> build exits 0 having done nothing.

Fifty-two more are gated, each behind its own variable, because they spend money, mint real credentials, write to a real repository, or need Docker. CI never runs any of them and needs no secrets.

Gate

What it does

Cost

MANDATE_LIVE=1

The writer against the live model, token minting against a real GitHub App, and the whole loop end to end

~$0.03 to ~$0.30

MANDATE_SANDBOX=1

Builds the sandbox and verifies its egress, the relay to mandate serve, and mandate run --sandbox end to end (the last two on macOS), and the benchmark's acceptance container, each with negative controls

free, needs Docker

MANDATE_SEED=1

Seeds the benchmark corpus into the bench repository — writes to its default branch and opens issues

free

MANDATE_GROUND_TRUTH=1

Re-records the benchmark's ground-truth traces, overwriting the committed ones

~$3

MANDATE_BENCH=1

The mandated sweep and the report: each task's mandate run with tools filtered and with every tool offered

~$1.80 for six tasks

MANDATE_WRITER_SWEEP=1

Writes each task's mandate again at several effort settings and compares it with the published one

~$0.03 a write

The credential-backed gates also need ANTHROPIC_API_KEY, MANDATE_APP_ID, MANDATE_INSTALLATION_ID, MANDATE_APP_KEY_PATH and MANDATE_TEST_REPO. See CONTRIBUTING.md — in particular the one rule that is not negotiable.

License

Apache-2.0. See LICENSE and NOTICE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    A policy-enforcing MCP gateway that intercepts all tool calls to downstream MCP servers, applying allow/deny/ask rules with human approval and audit logging for safe access to dangerous tools.
    5 npm
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    A security-first MCP gateway that enables AI assistants to safely inspect and interact with GitHub repositories through a controlled, auditable tool layer with policy enforcement and human approval for mutations.
    27
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables 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.
    1
    MIT