Skip to main content

Compound-path and cross-release analysis of what an AI agent is permitted to do

Project description

AgentMandate

What is your AI agent actually allowed to do?

PyPI Python 3.10+ CI Coverage: 100% License: Apache-2.0

A Python library and CLI that reads a short description of your agent's tools, called a manifest, and finds the limits it can slip past by combining actions that are each permitted on their own. It also tells you when a release widened what the agent can reach, before that release ships.

Policy engines decide one call at a time. This looks at the whole tool graph offline, so it catches the gaps that only appear across a sequence.

What you run Question it answers
Cedar, OPA, AgentCore Policy, AgentWard May this agent make this call, right now?
AgentMandate What can it reach by combining permitted calls, and did this release widen that?
AgentVerity Were the reviewed decision routes exercised repeatably?
Your tests and runtime traces Did the tools execute and the declared controls hold?

Imagine a payment-dispute agent that can open a case and issue a human-approved refund. Each refund is capped at 500 GBP per case, and the whole run is capped at 500 GBP. Release 2 adds one read-only tool: search_cases.

That tool spends nothing. It does, however, let the agent get hold of more cases, and the 500 GBP cap is measured per case. Two separately valid refunds become possible, so reachable extraction doubles to 1,000 GBP while every individual call still looks permitted.

A read-only case search makes two approved refunds reachable and breaches the run limit

AgentMandate builds the tool graph and answers the questions a per-tool check cannot:

  • What legal sequence breaks a limit? mandate reach returns the path, not a risk score.
  • What did this release make possible? mandate diff compares effective authority, not configuration text.
  • Does runtime evidence match the declaration? mandate verify fails closed when a required control field is absent.
  • What must the tests exercise? mandate obligations turns reachable authority into reviewable test obligations.

Alpha. Apache-2.0.

Try it

pip install "agentmandate[yaml]"

The repository includes that payment-dispute agent. In release 1, refunds are capped at 500 GBP per case, every refund requires human approval, the whole run is capped at 500 GBP, and nothing spends through a service account. It passes:

$ mandate lint examples/dispute-resolver.yaml
no single-manifest findings

$ mandate reach examples/dispute-resolver.yaml
no reachable breach within depth 8. 3 tool(s) reachable, most extractable 500 GBP

Release 2 adds one read-only tool so the agent can find existing cases instead of always opening a new one. A conventional review sees no write effect:

  - name: search_cases
    effect: read
    produces: case
    unbounded: true
$ mandate lint examples/dispute-resolver-v2.yaml
no single-manifest findings

$ mandate reach examples/dispute-resolver-v2.yaml
BREACH  cumulative value 1000 GBP exceeds limit 500 GBP
  1. open_case(case#1)
  2. search_cases(case#2)
  3. issue_refund(case#1, 500 GBP)
  4. issue_refund(case#2, 500 GBP)

The lint is still clean because no single tool is wrong. The refund ceiling is measured against one case, and the new tool lets the agent obtain fresh cases, so the per-case ceiling no longer bounds the whole run. In CI:

$ mandate diff examples/dispute-resolver.yaml examples/dispute-resolver-v2.yaml
authority diff  v1 -> v2
  + tool: gained search_cases
  + extractable value: 500 -> 2000 GBP
  + reachable breach: gained cumulative_value

verdict: WIDENING
a widening change needs named review before release

Exit code 1. The configuration change was read-only. The effective authority was not.

Why a config diff is not an authority diff

A pull request shows what somebody typed. It does not show what the agent can now do, because reachability composes and text does not. Adding a read tool, relaxing an enum in a schema, or removing one precondition can each open a path that did not exist, and none of them look like a permission change in review.

That is the same reason git diff never replaced type checking. The question is not what changed, it is what the change makes possible.

Starting from an existing agent

You do not have to write the first manifest by hand:

$ mandate scan examples/mcp-tools.json --agent dispute-resolver > mandate.yaml

The skeleton is loadable straight away, and every judgement the catalogue could not supply is marked:

  - name: issue_refund
    # REVIEW: effect guessed from the name. read | write | irreversible
    effect: irreversible
    # REVIEW: does this spend the caller's authority or a service account?
    principal: caller
    requires: [case]
    # REVIEW: amount looks like a value argument. A ceiling needs scope_key too.
    # value_arg: amount
    # scope_key: case
    # ceiling: { amount: 0, currency: GBP }
    requires_approval: true

Unrecognised verbs are proposed as irreversible, because under-calling an effect is the more expensive mistake.

The manifest

Reachability needs three facts per tool that an ordinary tool schema does not carry: the effect class, which argument spends value, and which scope the ceiling is measured against.

version: 1
agent: dispute-resolver
identity: spiffe://bank/agents/dispute-resolver

limits:
  total: { amount: 500, currency: GBP }
  depth: 8

tools:
  - name: open_case
    effect: read              # read | write | irreversible
    produces: case            # mints a binding of scope "case"

  - name: issue_refund
    effect: irreversible
    principal: caller         # caller | service
    requires: [case]
    value_arg: amount
    scope_key: case           # the ceiling is per case
    ceiling: { amount: 500, currency: GBP }
    requires_approval: true

Asking for full preconditions and postconditions would be more expressive and would not get written. This is the minimum that makes compound analysis possible.

A ceiling is the maximum cumulative value one tool may spend against one binding of its scope_key. unbounded: true marks a tool that can be called repeatedly to mint fresh bindings, which is what turns a per-scope ceiling into no ceiling at all.

Commands

Command What it does
mandate scan Derives a manifest skeleton from an MCP tools/list catalogue, with a REVIEW marker on every guess
mandate lint Single-manifest control checks: separation of duties, ungated irreversible effects, service-account principals, ceilings scoped to nothing
mandate reach Bounded search for a legal call sequence that breaches a limit, reported as a counterexample
mandate diff Effective-authority comparison of two manifests, including limits, preconditions, approvals, effects, and scope minting. --record emits a change record
mandate verify Replays recorded tool calls against the manifest and fails closed when evidence required by a declared control is missing
mandate obligations Derives reviewable test obligations from reachable authority, and renders reviewed ones as an AgentVerity decision suite

Every analysis command takes --json and exits non-zero on a finding, so they drop into CI unchanged. scan writes a manifest to standard output and is a one-off, not a gate.

Exit code Meaning
0 Clean
1 A finding: lint error, reachable breach, widening diff, or a non-conformant replay
2 Usage error or a malformed manifest

In a pull request, the useful gate is diff against the manifest on the default branch, so a change that widens authority stops and gets a named reviewer:

- name: Authority diff
  run: |
    git show origin/main:mandate.yaml > /tmp/released.yaml
    mandate diff /tmp/released.yaml mandate.yaml

verify is what keeps the rest honest. A manifest nobody checks is a wish, and the declaration drifts from the implementation the moment someone ships a connector change. For a spending tool, each trace record must carry the scope, value, currency, approval state, and executing principal. Missing or malformed control evidence does not pass as an empty value.

Where it fits, and what already exists

This is analysis, not enforcement. It runs in CI against a manifest, it does not sit in the request path.

Tool What it does Relationship
Policy in Amazon Bedrock AgentCore Evaluates all applicable Cedar policies for each gateway tool invocation, with default-deny, forbid-wins, and analysis that flags always-allow and always-deny policies Enforces each invocation. Its documented analysis is policy-level, not a model of a sequence of permitted calls
AgentWard Runtime proxy enforcing policy per call, diffs two policy files Enforces. Diffs declared text rather than reachable authority
AgentShield Scans agent configuration and MCP servers, drift gate over findings Scans. Drift is over finding counts, not permission direction
AgentGuard Attribute-based access control for tool calls Enforces
OPA, Cedar Decide one authorisation at a time Enforces

Use those to enforce. AgentMandate is the offline half: it analyses sequences of individually permitted calls and compares effective authority across releases.

If you already run AgentCore Policy, the gap is specific. The policy engine answers "may this principal invoke this tool now" by evaluating all applicable policies, and its documented analysis catches policy-level problems such as an unconditional allow. It does not model whether four separately permitted calls compose into a 1,000 GBP breach or whether a release widened what the agent can reach. AgentMandate is vendor-neutral and runs in CI before deployment, so it complements the gateway rather than duplicating it.

The closest prior art in a neighbouring domain is IAM Access Analyzer, which derives reachable access from policy by automated reasoning rather than waiting for a log event. This is that idea pointed at agent tool graphs.

The lint command deliberately overlaps the scanners above. A tool that reported only compound findings would need one of them running alongside it to be usable at all.

Scope

What this does not do, on purpose:

  • No enforcement. No proxy, no runtime interception, no blocking.
  • No data-flow reachability. Finding that a read tool feeds an exfiltration path needs taint labels the manifest does not carry. Cumulative value and scope minting are what the current model supports honestly.
  • No model behaviour. Whether the agent would take a path is a different question from whether it may. This measures permission.
  • No inference of the fields that matter. mandate scan reads an MCP catalogue and writes the skeleton, but it cannot know whether an effect is reversible or what a ceiling is measured against. It guesses conservatively and marks every guess REVIEW. Extract then annotate, never extract and trust.

Search is bounded by limits.depth. No breach at depth 8 is not proof that none exists at depth 20, and the report says when it truncated.

Documentation

Development

python -m pip install -e ".[dev]"
python -m pytest -q
python -m pytest -q --cov=agentmandate --cov-fail-under=100
ruff check .

main is protected. Every change lands through a pull request with CI green.

Status

Alpha, version 0.3.0. The authority model is the part most likely to change, because it has not yet been pointed at enough real tool graphs to know where it is too coarse. Issues describing a graph it models badly are the most useful thing you can file.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agentmandate-0.3.1.tar.gz (64.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agentmandate-0.3.1-py3-none-any.whl (38.3 kB view details)

Uploaded Python 3

File details

Details for the file agentmandate-0.3.1.tar.gz.

File metadata

  • Download URL: agentmandate-0.3.1.tar.gz
  • Upload date:
  • Size: 64.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for agentmandate-0.3.1.tar.gz
Algorithm Hash digest
SHA256 30f3af2c85534f84e75cde28cd2aefa72b50a80c1a887bae5ff5cc6718e129c6
MD5 e8131df67a4eab749c48ed81ba69d344
BLAKE2b-256 7217397ef929e47e1f528af774e5e5cb987b9756a339c80ac6b5ae5cb4da7d97

See more details on using hashes here.

Provenance

The following attestation bundles were made for agentmandate-0.3.1.tar.gz:

Publisher: release.yml on mrwersa/agentmandate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agentmandate-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: agentmandate-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 38.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for agentmandate-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e4aef326bc0060ea28d665dbacdae8e82226bb74c0f0242ef2b56873dfdb56b5
MD5 5fdabf35780a680a6b25db6fe8602ee7
BLAKE2b-256 8cd7ce69316d34a4c3e14bd1d0475fbc2bf34728824cc1c97f565d1a62e7f96f

See more details on using hashes here.

Provenance

The following attestation bundles were made for agentmandate-0.3.1-py3-none-any.whl:

Publisher: release.yml on mrwersa/agentmandate

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page