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, nokind = agenttoken, noE040lint 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:
- Reviewable. Every change to what an agent may do goes through PR review like any flag change.
- Auditable. Git history shows when a policy was tightened, loosened, or revoked.
- Enforceable.
exd-servercan 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
| Field | Required | Type | Description |
|---|---|---|---|
name | yes | slug | Stable identifier. MUST equal the filename stem. |
version | yes | integer ≥ 1 | Policy version. Increment on substantive change. |
description | no | string | Free-form. |
token_id | yes | string | The token identifier (NOT the secret) bound to this policy. The token MUST be of kind = agent. |
allowed_actions | yes | array of action keys | The set of action keys this agent may perform. See § Actions. Non-empty. |
allowed_namespaces | yes | array of flag-namespace patterns | Glob patterns for flag namespaces the agent may write. Non-empty. |
forbidden_namespaces | no | array of flag-namespace patterns | Globs that mask allowed_namespaces. Defaults to empty. |
triggers | yes | array of tables | Conditions under which the agent may push. Non-empty. |
notifications | no | table | Side-channel notifications on action and block. |
[[triggers]] fields
| Field | Required | Type | Description |
|---|---|---|---|
diagnostic | yes | T-code | The T-code whose detection authorizes a push. |
required_thresholds | no | inline table | Threshold values that MUST have been in effect for the cited finding. |
min_sample_size | no | integer | Minimum record_count in the cited finding's provenance. |
window | no | duration | Maximum 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 key | Manifest mutation |
|---|---|
disable_flag | Set enabled = false for one flag in one or all environments. |
set_rollout_percentage_to_zero | Set the rollout percentage of a percentage-bucket segment to 0. |
revert_to_previous_version | Replace the entire manifest with the immediately-previous manifest version. |
set_variant_default | Change [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:
- Resolve the token to its policy via
token_id. Reject if no policy exists. - Check the target flag namespace against
allowed_namespacesminusforbidden_namespaces. Reject otherwise. - Compute the diff between the current and proposed manifest. Reject if any change cannot be expressed as one of the policy's
allowed_actions. - Read the commit-message trailers. Reject if the required keys are absent or malformed.
- Match the cited trigger against the policy's
[[triggers]]array. Reject if no trigger admits the cited finding'sdiagnostic,required_thresholds,min_sample_size, andwindow. - 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:
| Code | Severity | Triggers when |
|---|---|---|
E040 | error | A 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
- The regression detector observes that flag
checkout-redesignvariantvariant-bproduced a 5.2% error-rate lift with p=0.0008 over 22 minutes. The detector callsexd telemetry summary --flag checkout-redesign --since 30m --compare-to 30m, which emits diagnosticT007. - The detector clones the manifest repo, modifies
flags/checkout-redesign.tomlto flip[flag.environments.production].variantto"off", and constructs a commit whose trailer block carries the provenance of theT007finding. - The detector calls
exd manifest push checkout <repo-path>with its agent token. exd-serverresolves the token toagent-policies/regression-watcher.toml@v1, validates the action (disable_flag), the flag namespace (checkoutmatchesallowed_namespaces), the trailer block (well-formed, finding cited), and the trigger (T007matches[[triggers]]withdrop_fraction = 0.5,min_sample_size = 500,window = 10m). All pass.- The lint pipeline runs and passes.
- The push is persisted with manifest version bumped. SDKs poll, see the new version, refresh, and stop returning variant-b.
exd-serveremits theon_actionwebhook.
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-codewhose firing authorizes an agent push. - provenance — the citation block agent commits embed in their trailers.
- thresholds — the values
[[triggers]].required_thresholdsreferences. - tokens —
kind = agent(deferred-but-planned) joins the existing token kinds. - access-control — binary write authorization; this page describes the conditional layer on top.