Agent policy decides what coding agents may do on a developer's machine or in a sandbox: which commands they run, which files they read and write, where they connect, what they push and install, which hooks run, and which MCP tools and models they use. You write one policy in Govern. Govern compiles it for each agent engine and signs it; the endpoint agent (keystone-agent) enforces it locally, and the model and MCP gateways enforce the model and tool rules on the server.
The console service is Agent policy, with the pages Agent policy, Policy preview, Coding tool control, Sector templates, Hooks, Agent approvals and Agent decisions. Organisation routes are under /api/v1/orgs/{orgID}/agent-policy.
Permissions
| Permission | Allows |
|---|---|
agentpolicy:read | Read settings, rules, templates, the preview, hooks, the decision log and revocations |
agentpolicy:write | Change settings (including suspend), create, update and delete rules, apply templates, submit hooks, revoke people and clear revocations |
agentpolicy:approve | Decide runtime approval requests; approve, reject and revoke hooks |
The people who write rules need not be the people who approve actions or hooks. Deciding a runtime approval also needs eligibility in the shared approvals service (approvals:approve or membership of the request's approver group), so give approvers a role with both agentpolicy:approve and approvals:approve.
1. Configure signing (operator)
Govern signs every policy manifest with Ed25519, and the endpoint agent refuses a manifest whose signature does not verify. Without a key, the manifest routes answer 503 signing_unavailable: an unsigned policy is never served. The preview works without a key.
openssl genpkey -algorithm ed25519 -out /etc/keystone/agentpolicy-signing.pem
chmod 600 /etc/keystone/agentpolicy-signing.pem
export NI_AUTH_AGENTPOLICY_SIGNING_KEY_FILE=/etc/keystone/agentpolicy-signing.pem
export NI_AUTH_AGENTPOLICY_SIGNING_KEY_ID=ap-2026-10 # optional; derived from the key otherwise
| Variable | Purpose |
|---|---|
NI_AUTH_AGENTPOLICY_SIGNING_KEY_FILE or NI_AUTH_AGENTPOLICY_SIGNING_KEY_PEM | Active Ed25519 private key (PKCS#8 PEM). _PEM wins when both are set. |
NI_AUTH_AGENTPOLICY_SIGNING_KEY_ID | Key id; without it, ap- and 16 hex characters of the public key's SHA-256 |
NI_AUTH_AGENTPOLICY_SIGNING_KEY_NOT_BEFORE | RFC 3339 start of the key's validity |
NI_AUTH_AGENTPOLICY_RETIRED_KEYS_FILE | JSON key set of rotated-out public keys, each with not_after |
KEYSTONE_AGENTPOLICY_AGENT_COMMAND | The endpoint agent executable the compiled settings call (default keystone-agent) |
The public keys are served at GET /.well-known/neuralinverse-agentpolicy-keys.json. To rotate, move the old public key into the retired keys file with not_after set to the rotation time, set the new key with NOT_BEFORE at the same time, and restart. An invalid key or setting stops the server at startup.
2. Set defaults and delegation
For actions no rule matches, the organisation sets a default effect per kind. Only the organisation sets defaults, and it decides per kind whether teams (or teams and projects) may loosen a rule (delegation: none, teams or teams_and_projects). Without delegation, a team or project rule can only tighten.
| Kind | Default without a setting |
|---|---|
shell, file_write, network, git_push, package_install, mcp_tool | ask |
file_read, hook, model | allow |
git_force_push | deny |
The hook default is allow or deny, never ask, because hooks run unattended.
curl -sS -X PUT "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/settings" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"offline_grace_seconds": 14400, "approval_ttl_seconds": 900,
"kinds": {"shell": {"default": "ask", "delegation": "teams"},
"network": {"default": "deny", "delegation": "none"}}}'
| Setting | Default | Range | Meaning |
|---|---|---|---|
offline_grace_seconds | 86400 | 0 to 2592000 | How long a manifest stays valid without reaching Govern |
approval_ttl_seconds | 900 | 60 to 86400 | Lifetime of a pending approval request and of a once grant |
session_grant_max_seconds | 28800 | 300 to 604800 | Lifetime of a session grant |
decision_retention_days | 90 | 7 to 400 | How long the decision log keeps a decision |
suspended, suspended_reason | false | reason at most 500 characters | Revokes every manifest of the organisation |
Only the fields you send change. Every change that alters a manifest increments the organisation's revision; the endpoint agent fetches a fresh manifest every 300 seconds while online.
3. Write rules
A rule is a scope, a kind, a pattern and an effect (allow, ask or deny).
- Scope:
org,team(scope_idis a team) orproject(scope_idis a repository identifier such asgithub.com/acme/fw, normalised to lower case without a trailing.gitor/). - Locked (org and team rules): lower levels may not override the same target, even when the kind is delegated.
- Approval (ask rules):
user(default), the developer confirms in the tool;approvers, a named approver must grant the action in Govern.approverslists up to 20user:<uuid>orteam:<uuid>entries; an empty list means any member withagentpolicy:approveother than the requester.
Action kinds
| Kind | Pattern matches | Example |
|---|---|---|
shell | One command line, decided per simple command | npm run *, rm -rf /* |
file_read | A path an agent tool reads | **/.env, ~/.ssh/** |
file_write | A path created, edited or deleted | **/*.ld, /etc/** |
network | A destination host, *.domain or an IP address, never a URL | *.internal.example, registry.npmjs.org |
git_push | A push to a branch | main, release/* |
git_force_push | A force push to a branch | * |
package_install | Installing a package, as ecosystem:name | npm:*, pypi:requests |
hook | Running an organisation-approved hook, by name | format-* |
mcp_tool | An MCP tool call, as server/tool | jira/*, github/create_pull_request |
model | Using a model, by model id | claude-opus-* |
Patterns are at most 512 characters. Paths in a home directory are written ~/...; /home/<user>/... and /Users/<user>/... are refused. The :* suffix of older Claude Code rules is refused; write npm run *. A rule that names an MCP server or a model without a wildcard must name one that exists in the organisation's MCP catalog or model catalog.
Examples
# Organisation: never read .env files, and lock it.
curl -sS -X POST "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/rules" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"scope": "org", "kind": "file_read", "pattern": "**/.env", "effect": "deny", "locked": true,
"description": "Credentials stay out of agent context."}'
# Team: pushes to main need a named approver.
curl -sS -X POST "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/rules" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"scope": "team", "scope_id": "'$TEAM_ID'", "kind": "git_push", "pattern": "main",
"effect": "ask", "approval": "approvers", "approvers": ["team:'$LEADS_TEAM_ID'"]}'
# Project: allow npm scripts in one repository (needs shell delegated to teams_and_projects).
curl -sS -X POST "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/rules" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"scope": "project", "scope_id": "github.com/acme/fw", "kind": "shell",
"pattern": "npm run *", "effect": "allow"}'
Govern refuses (400) a team or project rule that could never take effect: one against a locked org rule, a looser effect on a kind not delegated to that level, or the same effect as the org rule. One target exists at most once per scope (409). To change a rule, send PUT .../rules/{ruleID} with the full rule and the version you read; a stale version answers 409.
How a decision is made
Every effective rule whose pattern matches counts, and the strictest effect wins (deny, then ask, then allow) regardless of how specific its pattern is: an allow git_push feature/* does not open main if deny git_push * exists. If no rule matches, the organisation default applies. The decision reports the rule that decided it.
Shell command lines are split into simple commands across ;, &&, ||, pipes, substitutions, sh -c, eval and similar, and the strictest command decides. Privilege wrappers and environment prefixes are stripped before deny and ask rules are checked, so deny dd * also catches sudo dd, /bin/dd and FOO=1 dd. A line that chains commands, substitutes output, redirects to a file, runs a nested script or uses sudo needs at least the developer's confirmation, even when every command in it is allowed. A line that cannot be parsed is refused.
Text rules over shell commands reduce what an agent can reach; they are not an isolation boundary. A program a rule allows can do anything it is able to do.
4. Apply a sector template
Templates add a starting set of rules, each with its rationale in description. Applying one adds the rules whose target is not already present at the scope and, at organisation scope, sets defaults for kinds you have not configured. It never removes or loosens anything, and applying it twice changes nothing. Template rules that ask named approvers have none named; edit them to name yours.
curl -sS -X POST "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/templates/medical/apply" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"scope": "org"}'
| Template | Written for |
|---|---|
firmware | Embedded C and C++ on microcontrollers: linker scripts, startup code and device programming stay under human control |
hdl-fpga | HDL projects on FPGA toolchains: programming a device, writing bitstreams and changing timing and pin constraints need a person |
plc-ot | PLC and industrial control projects: agents never download logic to a controller or reach plant networks; safety logic needs a named approver |
automotive | Vehicle software: changes to safety-relevant components and calibration data need a named approver |
avionics | Airborne software: requirements, trace data and qualified-tool configuration are controlled items that need a named approver |
medical | Medical device software: agent reads of patient data are denied; changes to released and controlled items need a named approver |
finance | Financial services code: payment, ledger and infrastructure changes need a named approver; agents never read cardholder data or production credentials |
mainframe | Mainframe development: agents may edit COBOL, PL/I and JCL in the repository, but submitting jobs and changing host datasets need a person |
The answer lists rules it skipped and why (skipped) and defaults it left because you set them (defaults_skipped). Templates are a starting point; review every rule against your own process before you rely on it.
5. Preview before you roll out
curl -sS "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/preview?user_id=$USER_ID&project=github.com/acme/fw&engine=codex" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN"
The preview (console Policy preview) shows exactly what one person gets in one project on one engine, without signing. user_id defaults to you, engine to claude-code.
| Part | Shows |
|---|---|
resolved.rules | The effective rules with their source |
resolved.ineffective | Stored rules that do not apply to this person and project, with the reason |
compiled.files | The files the endpoint agent installs for the engine |
compiled.gaps | Every rule the engine cannot express exactly: unsupported (the engine does not enforce it as written), approximated (the engine enforces something stricter) or uncovered (no hook of the tool sees the action) |
Gaps are enforced by the endpoint agent through the enforcement hook, but only where the endpoint agent runs. Check the gaps for each engine before you rely on a rule; Third-party AI coding tools lists what each tool covers.
6. Review hooks
Only hooks your organisation approved run, and only through the endpoint agent, which checks the script's SHA-256 against the approved digest before running it. The compiled settings stop the engines from running any other hook. Approved hooks are delivered to Claude Code and Codex, not to the vendor tools that only run Govern's enforcement hook.
curl -sS -X POST "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/hooks" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d @- <<'EOF'
{"name": "format-on-edit", "event": "PostToolUse", "matcher": "Edit|Write",
"engines": ["claude-code", "codex"], "timeout_seconds": 30,
"script": "#!/bin/sh\nexec make fmt\n"}
EOF
- Events:
PreToolUse,PostToolUse,UserPromptSubmit,SessionStart,SessionEnd,Stop,SubagentStop,PreCompactandNotification(Codex does not runNotification). - The script is at most 64 KiB and starts with a
#!line; the timeout is 1 to 600 seconds. - A submitted hook is
pending. A member withagentpolicy:approveother than the submitter approves or rejects it withPOST .../hooks/{hookID}/reviewand{"decision": "approve", "comment": "..."}(orreject). Submit hooks as a person: a hook submitted by an API key not bound to a person cannot be reviewed. POST .../hooks/{hookID}/revokestops an approved hook. Ahookrule can deny an approved hook for a team or project.
7. Revoke a person or suspend the organisation
curl -sS -X PUT "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/revocations/$USER_ID" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"reason": "Laptop reported stolen"}'
A revoked person's manifests deny every action on every engine and project. DELETE .../revocations/{userID} lets their agents act again. To stop every agent of the organisation, set {"suspended": true, "suspended_reason": "..."} in the settings. A revocation or suspension reaches an online agent with its next manifest fetch (within 300 seconds); an offline agent keeps its last manifest until it expires, so keep offline_grace_seconds short where fast revocation matters.
Delivery to machines
- The endpoint agent fetches the compiled policy for its engine as an Ed25519-signed manifest, verifies it, and refuses a manifest older than the one it holds, so a replayed manifest cannot restore a looser policy.
- Offline, it uses the last verified manifest until it expires. After that, every action is denied until a fresh manifest verifies.
- A revoked manifest denies every action, online or offline.
- Every action goes through
keystone-agent policy enforce, which answers before the engine's own hook timeout and denies on any internal error.
Decision log
The endpoint agent reports every decision: time, person, project, engine, session, kind, effect, outcome (allowed, denied, asked, approved, rejected or error), the deciding rule, the enforcer and the approval. Command text, paths and hosts are never stored; the target is kept only as a SHA-256 hash.
curl -sS "$KEYSTONE_URL/api/v1/orgs/$ORG_ID/agent-policy/decisions?outcome=denied&from=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $ORG_ADMIN_TOKEN"
Filter by user_id, project, engine, kind, effect, outcome, from and to. The console page is Agent decisions.
Policy change control
- Every change is audited in its own transaction:
agentpolicy.settings.updated,agentpolicy.rule.created,.updated,.deleted,agentpolicy.template.applied,agentpolicy.hook.submitted,.approved,.rejected,.revoked,agentpolicy.approval.requested,.approved,.denied,.consumed,.expired,agentpolicy.revocation.setand.cleared. If the event cannot be written, the change fails. Audit metadata holds identifiers, kinds and effects, never scripts or approval targets. - Separate permissions for writing rules and approving actions or hooks.
- Preview every change per engine before it reaches people.
- Optimistic concurrency on rules: an update must carry the version it read.
API
| Route | Permission | Purpose |
|---|---|---|
GET, PUT /settings | read, write | Grace, lifetimes, retention, suspend, per-kind default and delegation |
GET, POST /rules; GET, PUT, DELETE /rules/{id} | read, write | Rules |
GET /templates, GET /templates/{id}, POST /templates/{id}/apply | read, write | Sector templates |
GET /preview?engine=&user_id=&project= | read | Effective policy and compiled output |
GET, POST /hooks, GET /hooks/{id} | read, write | Hooks |
POST /hooks/{id}/review, /revoke | approve | Review and revoke hooks |
GET /approvals, GET /approvals/{id}, POST /approvals/{id}/decision | approve | Approvals; see Approvals |
GET /decisions | read | Decision log |
GET /coding-tools | read | Verified control level of each coding tool |
GET /revocations, PUT, DELETE /revocations/{userID} | read, write | Per-person revocation |
Paths are relative to /api/v1/orgs/{orgID}/agent-policy. Lists take limit (1 to 200, default 50) and cursor. Errors are {"error": "<message>", "code": "<code>"}. On the hosted service, ask rules with named approvers and custom hooks depend on the plan (403 not_entitled); a self-hosted installation includes everything.