Skip to main content

Pure-function policy matrix evaluator for AI coding agents (repo x capability x context -> deny/require_approval/auto_allow).

Project description

agent-policy

Pure-function policy matrix for AI coding agents. Maps (repo, capability, context) to one of three modes: deny / require_approval / auto_allow.

Status: 0.1.4 alpha. The public API is frozen for v0.1; examples and hook/wrapper recipes will grow in v0.2.

Why

AI coding agents (Claude Code, Codex, Aider, and friends) need a single place to answer one question, the same way, every time:

"The agent wants to do X in repo Y — should I let it?"

agent-policy is that single place. It is deliberately tiny:

  • One pure functionevaluate(policy, repo, capability, context).
  • No I/O, no logging, no global state. The evaluator does not touch disk, network, or clocks. It is safe to call from a hook, a test, or a long-running daemon.
  • Fail-closed defaults. A missing default_mode is require_approval, unknown fields in policy files are rejected, and hard guardrails cannot be overridden by repo policy.

It does not parse shell commands, manage state, or send messages. Those belong in the wrapper layer that calls evaluate.

Install

pip install yui-agent-policy

From a source checkout, install the package in editable mode so both the library and examples/check.py can resolve import agent_policy:

pip install -e .

Requires Python 3.11+ (uses stdlib tomllib). The only runtime dependency is pydantic >= 2.

Quick start

from agent_policy import evaluate, PolicyMatrix, RepoPolicy

policy = PolicyMatrix(
    default_mode="require_approval",
    repo_policy=[
        RepoPolicy(
            repo="acme/app",
            ownership_class="internal",
            capabilities={
                "read": "auto_allow",
                "commit": "auto_allow",
                "push": "auto_allow",
                "shell": "require_approval",
            },
        ),
    ],
)

decision = evaluate(
    policy,
    repo="acme/app",
    capability="commit",
    context={"ownership_class": "internal"},
)

print(decision.mode)         # "auto_allow"
print(decision.reason)       # "repo_policy"
print(decision.matched_repo) # "acme/app"

Load the same policy from a TOML file:

from agent_policy import evaluate, load_policy_file

policy = load_policy_file("policy.toml")
decision = evaluate(policy, repo="acme/app", capability="commit")

evaluate also accepts a plain dict in the same shape as PolicyMatrix, which is convenient for tests and one-off scripts.

Decision model

Every call returns a frozen PolicyDecision with three fields:

Field Type Meaning
mode "deny" | "require_approval" | "auto_allow" What the caller should do.
reason "hard_guardrail" | "repo_policy" | "default_mode" | ... Which rule produced the decision.
matched_repo str | None The repo string that matched, or None.

Decisions are evaluated in this order:

  1. Hard guardrails — cannot be overridden by repo policy.
    • push.force → always deny.
    • merge.pr → always require_approval.
    • External first_write_to_repo on a mutating capability → require_approval. Read is not blocked.
  2. Repo policy match — every [[repo_policy]] entry for the requested repo is scanned (optionally gated by ownership_class). The first entry that declares the capability wins. Splitting a repo's policy across multiple entries is supported.
  3. default_mode fallback — used when no repo policy declares the capability. Defaults to require_approval if unset.

HARD_GUARDRAILS is exported as a constant so tooling can assert against it without importing private symbols.

Policy file format

# policy.toml
default_mode = "require_approval"

[[repo_policy]]
repo = "acme/app"
ownership_class = "internal"

[repo_policy.capabilities]
read = "auto_allow"
commit = "auto_allow"
push = "auto_allow"

[[repo_policy]]
repo = "acme/app"                # same repo, extra constraint
[repo_policy.capabilities]
shell = "require_approval"

Unknown top-level fields or typos inside [[repo_policy]] fail loudly with a pydantic.ValidationError — there is no silent degradation.

Wrapper pattern

agent-policy deliberately does not know how to parse git push --force or a shell command line. The intended shape is:

           ┌────────────────────────┐
agent ───▶ │ wrapper (hook / CLI)   │ ──▶ agent-policy.evaluate()
           │  - normalize capability│         │
           │  - build context       │         ▼
           │  - act on decision     │   PolicyDecision
           └────────────────────────┘

The wrapper owns: parsing the agent's intent, mapping it to one of the MVP capabilities (read, write, commit, push, push.force, merge.pr, shell), and executing whatever side effect the decision implies (block, prompt for approval, log and allow).

A runnable minimal wrapper lives in examples/check.py.

Approval wrapper checklist

For require_approval decisions, keep the approval layer outside agent-policy but make the wrapper contract explicit. Production wrappers should:

  • Bind approval records to the exact capability, session, path, and command being executed. A command change after approval should fail closed.
  • Record the source decision event or audit hash in the approval record, then verify that the event still exists before running the approved command.
  • Treat approvals as single-use for side-effecting operations such as artifact.publish; reserve a local use marker before executing the command so retry races cannot reuse the same approval.
  • Keep bypass corpora, private logs, .env* files, and red-team transcripts outside tracked paths, and add an independent scanner such as yui-agent-guard to CI.

These checks belong in the wrapper/admission layer rather than the pure evaluator. The ai_resilience_policy.toml example shows the capability vocabulary; downstream repositories can combine it with their own approval record schema and CI gates.

Examples

See examples/. Runnable after installing the package (pip install yui-agent-policy, or pip install -e . from a source checkout):

  • policy.toml — a minimal fail-closed policy with two repos.
  • ai_resilience_policy.toml — a safety-oriented vocabulary example for publication, constitution, audit, secret-materialization, and scanner policy changes. These remain repo-policy capabilities rather than hard guardrails until downstream wrappers prove they are universal invariants.
  • check.py — a tiny CLI wrapper that maps PolicyDecision to JSON on stdout and a process exit code, suitable for PreToolUse hooks.
  • claude_code_hook.sh — a Claude Code PreToolUse hook that reads the hook payload from stdin, maps the tool to a capability, and shells out to check.py. Set AGENT_POLICY_FILE and AGENT_POLICY_REPO in the hook's environment, then point ~/.claude/settings.json at it.
  • codex_hook.sh — a Codex CLI PreToolUse hook (shell guardrail pilot). Codex hooks currently intercept Bash commands only — read, write, and edit operations are not covered. Maps git push --force to push.force, gh pr merge to merge.pr, and everything else to shell. Requires features.codex_hooks = true in your Codex config and a hooks.json in ~/.codex/ or <repo>/.codex/.
  • capability_map.py — stdlib-only helper that turns a raw Bash command into one of push.force / merge.pr / shell. Both hook wrappers shell out to it instead of doing substring matching, so quoted literals like printf '%s\n' 'git push --force' no longer produce a false push.force classification. See the file header for the exact algorithm (heredoc stripping → shlex tokenization → scan-anywhere → recursive bash -c / eval).

Codex CLI hook — known MVP limitations

The Codex CLI hook feature is marked "Under development" in upstream docs. Two gaps affect how agent-policy presents decisions through it, and they are worth knowing before you enable it:

  • require_approval degrades to block. Codex hook events accept only allow or deny — there is no permissionDecision: "ask" yet. examples/codex_hook.sh therefore exits 2 for both deny and require_approval, and the only UX signal distinguishing the two is the stderr line (DENY ... vs require_approval ...). Users must retry after manual approval rather than being prompted inline.
  • Bash-only scope. Codex hooks intercept shell commands and nothing else. Read, write, and edit tool calls are invisible — if you need capability gating on those, use the Claude Code hook.
  • Heuristic command parsing. capability_map.py is shlex-based, not a full shell. It handles quoted literals, heredocs, compound statements, and the common bash -c '...' / eval wrappers, but exotic forms such as git --git-dir=/path push --force, process substitution, or function definitions are not modeled. The fail-closed default is shell, which policy can still flag as require_approval or deny.

Releases

Tag-driven. Pushing a vX.Y.Z annotated tag triggers .github/workflows/release.yml, which first verifies that the tag matches [project].version in pyproject.toml, checks that the version is not already present on PyPI, then builds the sdist + wheel and publishes to PyPI via Trusted Publishing (OIDC). No maintainer-side credentials are required. Manual workflow_dispatch with publish=false is a build-only dry run; it skips the publish job. Manual publish=true must be run against a v* tag ref; running it from a branch fails before build.

License

MIT.

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

yui_agent_policy-0.1.4.tar.gz (31.2 kB view details)

Uploaded Source

Built Distribution

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

yui_agent_policy-0.1.4-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

File details

Details for the file yui_agent_policy-0.1.4.tar.gz.

File metadata

  • Download URL: yui_agent_policy-0.1.4.tar.gz
  • Upload date:
  • Size: 31.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for yui_agent_policy-0.1.4.tar.gz
Algorithm Hash digest
SHA256 f8c2438959df30c72c85621af7e79438e33ed712fa0e055191ab2da77e56c694
MD5 7281d49b03a85e5b0160ed6b220066ef
BLAKE2b-256 7ee3c99a06ff724e3be2e5aa0bbeae2cf19eefdfd13620ea15d49215b3904103

See more details on using hashes here.

Provenance

The following attestation bundles were made for yui_agent_policy-0.1.4.tar.gz:

Publisher: release.yml on yui-stingray/agent-policy

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

File details

Details for the file yui_agent_policy-0.1.4-py3-none-any.whl.

File metadata

File hashes

Hashes for yui_agent_policy-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 d5f0dfecfd77eb626420d7a6349fdfb3a79d75591deca0d8ce4f26858d3e0126
MD5 e6b88ed5bf17c7a225748b2981d11a9c
BLAKE2b-256 35e19cc568c810541ac91c1854de8bceddfe168f21cdf7eaccf6e1fe1c48c0f6

See more details on using hashes here.

Provenance

The following attestation bundles were made for yui_agent_policy-0.1.4-py3-none-any.whl:

Publisher: release.yml on yui-stingray/agent-policy

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