AgentPolicy reference
diffhold.dev/v1alpha1, frozen on 2026-11-04. The schema below is stable. Later v1alpha1 releases only add optional fields (see Compatibility).
An AgentPolicy binds one or more agents to a ServiceAccount and a list of rules. Diffhold loads policies from its own namespace (--policy-namespace, default: Diffhold's namespace) or from files (--policies). Both sources are validated the same way.
apiVersion: diffhold.dev/v1alpha1
kind: AgentPolicy
metadata:
name: sre-bot
namespace: diffhold-system
spec:
subjects:
- agent: sre-bot
credentials:
serviceAccount: agents/agent-sre-bot
grantTTL: 30s
limits:
maxObjectsPerCall: 3
ratePerMinute: 120
audit:
redact:
- kinds: [Deployment]
paths: ["spec.template.spec.containers[*].env"]
rules:
- name: destructive
verbs: [delete]
resources: [{kinds: [Deployment], namespaces: [team-a]}]
approval:
approvers: [group:team-a-oncall]
count: 1
timeout: 15m
defaults:
mode: enforce
spec.subjects[] (required, at least one)
| Field | Type | Meaning |
agent | string, required | The agent identity sent by the gateway as processor metadata agent, for example the JWT sub. Headers are not trusted unless --insecure-agent-header is set. An agent may appear in only one policy. |
spec.credentials (required)
| Field | Type | Default | Meaning |
serviceAccount | <namespace>/<name>, required | | The ServiceAccount that per-call tokens are minted for. The namespace must be in --grant-namespaces, and only one policy may use a given ServiceAccount (otherwise status ServiceAccountNotAllowed or Conflict). |
grantTTL | duration | 30s | How long a per-call token and its grant stay valid. |
singleUse | bool | true | The token is accepted for one admission request. Set to false only for tools that make several API calls per tool call. |
readTokenTTL | duration | 10m | Reads (get, list, watch, logs) share one token per agent instead of a grant per call. It cannot write: it has no grant, so the backstop denies every write made with it. 0s keeps a token per read; otherwise 10m to 1h. |
spec.rules[] (required, at least one)
Rules are evaluated in order. The first rule whose verbs and resources match an object, and that does not exclude it, decides. If its conditions are false, the call is denied. Every object in a call must match a rule.
| Field | Type | Meaning |
name | string, required, unique | Shown in audit records and denial reasons. |
verbs | list, required | get, list, watch, logs, apply, scale, delete, exec or *. |
resources[] | list, required | Selectors with kinds and namespaces. An omitted list, or *, matches everything. |
exclude[] | list | Selectors that this rule does not cover. Evaluation moves on to the next rule. |
conditions[] | list of CEL | Each must return bool. Variables: object (the proposed object, or name, namespace and replicas for scale), verb, kind, namespace, name. Conditions run twice: before the dry-run on the submitted object, and after it with object set to the predicted object (for apply) and blast filled in. Both must pass. blast has replicasBefore, replicasAfter, rollout, podsAffected (pods terminated or replaced), podsAffectedPercent and pdbViolations (matching PodDisruptionBudgets with fewer allowed disruptions than podsAffected); it is all zeros before the dry-run and for kinds that run no pods. oldObject is the live object read during the dry-run, or null when it does not exist yet, for example oldObject == null || object.spec.replicas >= oldObject.spec.replicas / 2. Conditions that use it run only after the dry-run, so they apply to writes; a call they would govern is denied when dry-run is off. |
approval | object | Human approval is required (see below). |
spec.rules[].approval
| Field | Type | Default | Meaning |
approvers | list, required | | user:<name> or group:<name>, as the API server authenticates the approver's token (TokenReview, audience diffhold). |
count | int ≥ 1 | 1 | Distinct people who must approve. The agent's own ServiceAccount and ServiceAccounts in agent namespaces never count. |
timeout | duration | --approval-ttl (1h) | How long the request waits for approval. When several rules apply, the shortest timeout wins. |
An approval is single-use and bound to the exact change. If the dry-run result differs on retry, including values hidden by redaction, approval is requested again.
spec.limits
| Field | Type | Meaning |
maxObjectsPerCall | int ≥ 1 | Calls touching more objects are denied. |
ratePerMinute | int ≥ 1 | Tool calls per agent per minute, as a token bucket with a burst of the same size. Excess calls get RESOURCE_EXHAUSTED. The limit is enforced per replica, so N replicas allow up to N times the rate. |
spec.audit
| Field | Type | Meaning |
redact[].kinds | list | Kinds the rule applies to. Empty or * means every kind. |
redact[].paths | list, required | Field paths whose values are replaced by <redacted> in diffs shown to the agent and approvers, in the audit log, and in AuthZEN responses. Syntax: a.b, a[*].b, a["key.with.dots"]. Use [*] for list elements. A path also covers everything below it. |
Secret data and stringData are always redacted. Changed keys stay visible, but their values do not.
spec.defaults
| Field | Values | Default | Meaning |
mode | enforce, audit | enforce | audit records would-be denials and allows the call. |
dryRun | required, off | required | off skips the server-side dry-run for scale and delete. apply always dry-runs, because its token is bound to the predicted object. |
unmappedTools | deny | deny | Reserved for future values. Tools without an adapter are denied. |
status
| Field | Meaning |
observedGeneration | Generation the status refers to. |
agents | Comma-separated agents bound by this policy. |
conditions[] | Ready with reason Loaded, Invalid, CompileError, Conflict or ServiceAccountNotAllowed. |
Compatibility
For the rest of v1alpha1:
- Allowed: new optional fields, whose absence keeps today's behaviour. New enum values are allowed only where the default behaviour is unchanged.
- Not allowed: removing, renaming or retyping a field; making a field required; tightening validation; or changing the meaning of an existing value or default.
- Breaking changes need
v1alpha2 with a conversion path.
Enforcement: api/v1alpha1/freeze_test.go in the repository fails on any schema change (testdata/fields.golden). It also checks that the Go types, CRD schema and enum values match, and validate_test.go checks that file policies are held to the same constraints as the CRD.