Diffhold pre-alpha

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)

FieldTypeMeaning
agentstring, requiredThe 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)

FieldTypeDefaultMeaning
serviceAccount<namespace>/<name>, requiredThe 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).
grantTTLduration30sHow long a per-call token and its grant stay valid.
singleUsebooltrueThe token is accepted for one admission request. Set to false only for tools that make several API calls per tool call.
readTokenTTLduration10mReads (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.

FieldTypeMeaning
namestring, required, uniqueShown in audit records and denial reasons.
verbslist, requiredget, list, watch, logs, apply, scale, delete, exec or *.
resources[]list, requiredSelectors with kinds and namespaces. An omitted list, or *, matches everything.
exclude[]listSelectors that this rule does not cover. Evaluation moves on to the next rule.
conditions[]list of CELEach 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.
approvalobjectHuman approval is required (see below).

spec.rules[].approval

FieldTypeDefaultMeaning
approverslist, requireduser:<name> or group:<name>, as the API server authenticates the approver's token (TokenReview, audience diffhold).
countint ≥ 11Distinct people who must approve. The agent's own ServiceAccount and ServiceAccounts in agent namespaces never count.
timeoutduration--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

FieldTypeMeaning
maxObjectsPerCallint ≥ 1Calls touching more objects are denied.
ratePerMinuteint ≥ 1Tool 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

FieldTypeMeaning
redact[].kindslistKinds the rule applies to. Empty or * means every kind.
redact[].pathslist, requiredField 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

FieldValuesDefaultMeaning
modeenforce, auditenforceaudit records would-be denials and allows the call.
dryRunrequired, offrequiredoff skips the server-side dry-run for scale and delete. apply always dry-runs, because its token is bound to the predicted object.
unmappedToolsdenydenyReserved for future values. Tools without an adapter are denied.

status

FieldMeaning
observedGenerationGeneration the status refers to.
agentsComma-separated agents bound by this policy.
conditions[]Ready with reason Loaded, Invalid, CompileError, Conflict or ServiceAccountNotAllowed.

Compatibility

For the rest of v1alpha1:

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.