Skip to main content

Agent policies

Implementation status — Phase 2 (deferred-but-planned). None of the surface described on this page is in Phase 1: no agent-policies/ directory recognition, no kind = agent token, no E040 lint code, no push-time policy enforcement. This page documents what those pieces will do when they land. Spec text is preserved as written so a future implementation can pick up against a stable target.

Agents that take actions on telemetry findings — disabling a flag in response to T007, rolling back a variant after detecting T001, narrowing a rollout — operate under explicit, declarative policies stored in the manifest repository. The policy file is the contract between the agent and the system: it bounds what the agent may do, and it is enforced at the exd-server upload boundary.

This is the telemetry analogue of access control — but where token-based access control is binary (write or no write), agent policies are conditional (write IF the cited finding meets the policy's triggers).


Why policies are declarative

Three properties fall out of representing agent permissions as version-controlled TOML files:

  1. Reviewable. Every change to what an agent may do goes through PR review like any flag change.
  2. Auditable. Git history shows when a policy was tightened, loosened, or revoked.
  3. Enforceable. exd-server can validate every agent push against the active policy without trusting the agent.

A non-declarative alternative (an admin UI on exd-server that mutates agent permissions) was considered and rejected for breaking the "manifest is the source of truth" invariant.


File location

manifest-repo/
agent-policies/
regression-watcher.toml
cleanup-bot.toml

The agent-policies/ directory is OPTIONAL. A flag namespace with no agent-policy files admits no agent-driven manifest pushes; tokens of kind = agent cannot push.


Policy file schema

# agent-policies/regression-watcher.toml
name = "regression-watcher"
version = 1
description = "Auto-rollback on detected regressions in checkout experiments."

token_id = "tok_01HXYZ..."

allowed_actions = ["disable_flag", "set_rollout_percentage_to_zero"]
allowed_namespaces = ["checkout", "experiments/*"]
forbidden_namespaces = ["billing", "auth"]

[[triggers]]
diagnostic = "T007"
required_thresholds = { drop_fraction = 0.5 }
min_sample_size = 500
window = "10m"

[[triggers]]
diagnostic = "T001"
required_thresholds = { significance_level = 0.001 }
min_sample_size = 1000

[notifications]
on_action = ["slack:#deploys"]
on_block = ["slack:#exd-alerts", "pagerduty:exd-oncall"]

Top-level fields

FieldRequiredTypeDescription
nameyesslugStable identifier. MUST equal the filename stem.
versionyesinteger ≥ 1Policy version. Increment on substantive change.
descriptionnostringFree-form.
token_idyesstringThe token identifier (NOT the secret) bound to this policy. The token MUST be of kind = agent.
allowed_actionsyesarray of action keysThe set of action keys this agent may perform. See § Actions. Non-empty.
allowed_namespacesyesarray of flag-namespace patternsGlob patterns for flag namespaces the agent may write. Non-empty.
forbidden_namespacesnoarray of flag-namespace patternsGlobs that mask allowed_namespaces. Defaults to empty.
triggersyesarray of tablesConditions under which the agent may push. Non-empty.
notificationsnotableSide-channel notifications on action and block.

[[triggers]] fields

FieldRequiredTypeDescription
diagnosticyesT-codeThe T-code whose detection authorizes a push.
required_thresholdsnoinline tableThreshold values that MUST have been in effect for the cited finding.
min_sample_sizenointegerMinimum record_count in the cited finding's provenance.
windownodurationMaximum age of the cited finding's executed_at relative to the push timestamp.

A push is authorized iff at least one trigger's conditions are satisfied by the cited finding.


Actions

The initial set of action keys:

Action keyManifest mutation
disable_flagSet enabled = false for one flag in one or all environments.
set_rollout_percentage_to_zeroSet the rollout percentage of a percentage-bucket segment to 0.
revert_to_previous_versionReplace the entire manifest with the immediately-previous manifest version.
set_variant_defaultChange [flag.environments.<env>].variant to a different declared variant.

Actions MUST be small and orthogonal. Adding a new action key is a spec revision; the implementation MUST validate that the diff in an agent push corresponds exactly to one of the policy's allowed_actions. A push containing changes that do not map to any action key is rejected.


Cited-finding requirement

Every agent push MUST carry a structured Git trailer in its commit message identifying the finding that authorizes it.

auto-action: disable_flag
agent: regression-watcher@v1.4.2
policy: regression-watcher@1
trigger: T007
finding-source: s3://acme-data/exd/checkout/2026-05-08/
finding-time-range: 2026-05-08T14:00:00Z..2026-05-08T14:30:00Z
finding-query: telemetry.summary@1
finding-query-version: 1
finding-executed-at: 2026-05-08T14:32:13Z
finding-record-count: 4280
finding-thresholds-source: queries/thresholds.toml@a1b2c3d
finding-key-result: error_rate_lift=0.052 p=0.0008
finding-reproduce: exd telemetry summary --flag checkout-redesign --since 30m --compare-to 30m

These trailers are produced from the provenance block of the JSON envelope (see provenance) — every field has a direct counterpart there.

The trailer block follows the standard Git trailer convention (Key: value, one per line, blank line above the block). Multi-value trailers are not used.


Server-side enforcement

exd-server validates every push that authenticates with an agent-kind token against the flag namespace's active policy file. Validation steps:

  1. Resolve the token to its policy via token_id. Reject if no policy exists.
  2. Check the target flag namespace against allowed_namespaces minus forbidden_namespaces. Reject otherwise.
  3. Compute the diff between the current and proposed manifest. Reject if any change cannot be expressed as one of the policy's allowed_actions.
  4. Read the commit-message trailers. Reject if the required keys are absent or malformed.
  5. Match the cited trigger against the policy's [[triggers]] array. Reject if no trigger admits the cited finding's diagnostic, required_thresholds, min_sample_size, and window.
  6. Run the standard manifest lint pipeline. Reject on any error-severity diagnostic.

Steps 1–5 run before the lint pipeline so that policy-rejected pushes don't consume lint resources.

The single new manifest-spec lint diagnostic introduced for this enforcement:

CodeSeverityTriggers when
E040errorA push with an agent-kind token has no commit-message trailer block, has malformed trailers, or cites a finding that does not satisfy any active trigger.

E040 is enforced at the server only — local exd lint does not see the token kind. The error envelope returned to the client is manifest_lint_failed with the E040 code and a human-readable description of which step failed.


Policy versioning and revocation

A policy is active iff it lives in the current manifest's agent-policies/ directory. Removing the file revokes the agent's ability to push immediately; no out-of-band token rotation is needed.

A policy version bump (incrementing the version field) does NOT revoke prior pushes — it only affects future authorizations. Findings produced under an older policy version remain reproducible because their provenance cites the manifest commit that defined the policy at execution time.


Notifications

The [notifications] table is informative for exd-server and acts as configuration for downstream notification systems. The reference server emits webhook events to configured endpoints when:

  • on_action — a push from this agent was accepted and persisted.
  • on_block — a push from this agent was rejected by the validation steps above.

Webhook payload format and channel-prefix syntax (slack:, pagerduty:, webhook:) are defined in the server-api spec. The telemetry reference is authoritative for the [notifications] field shape; the server reference is authoritative for delivery semantics.


End-to-end example

  1. The regression detector observes that flag checkout-redesign variant variant-b produced a 5.2% error-rate lift with p=0.0008 over 22 minutes. The detector calls exd telemetry summary --flag checkout-redesign --since 30m --compare-to 30m, which emits diagnostic T007.
  2. The detector clones the manifest repo, modifies flags/checkout-redesign.toml to flip [flag.environments.production].variant to "off", and constructs a commit whose trailer block carries the provenance of the T007 finding.
  3. The detector calls exd manifest push checkout <repo-path> with its agent token.
  4. exd-server resolves the token to agent-policies/regression-watcher.toml@v1, validates the action (disable_flag), the flag namespace (checkout matches allowed_namespaces), the trailer block (well-formed, finding cited), and the trigger (T007 matches [[triggers]] with drop_fraction = 0.5, min_sample_size = 500, window = 10m). All pass.
  5. The lint pipeline runs and passes.
  6. The push is persisted with manifest version bumped. SDKs poll, see the new version, refresh, and stop returning variant-b.
  7. exd-server emits the on_action webhook.

The full audit trail — who did what, on which evidence, under which policy — is in git, with no out-of-band state.


See also

  • diagnostics — the T-code whose firing authorizes an agent push.
  • provenance — the citation block agent commits embed in their trailers.
  • thresholds — the values [[triggers]].required_thresholds references.
  • tokenskind = agent (deferred-but-planned) joins the existing token kinds.
  • access-control — binary write authorization; this page describes the conditional layer on top.