Skip to content

Agents and AI ​

Actuator is built for both humans and AI agents to operate the same surface. The REST API, the audit log, the evaluator, and the MCP server are peers — agents are not second-class consumers. This page consolidates the agent-relevant material into one orientation.

A quick map of what's below:

  • What surface do agents have? Scoped tokens with five capability levels, six action verbs, and a delegation chain that tops out at the human who minted the token — see Token model.
  • How do agents reason about a change before making it? A counterfactual preview endpoint that evaluates a hypothetical ruleset against a list of contexts — see Counterfactual preview.
  • How do agents propose changes safely? A two-phase propose / apply flow with version-drift detection — see Propose / apply.
  • How do agents connect? Either via REST or the first-party MCP server — see MCP server.

Token model ​

Every API token declares a scope along several dimensions:

DimensionValues
EnvironmentsSpecific env(s), all envs in a project, or all envs in the org
ResourcesSpecific keys, key prefixes (payments.*), or all
Actionsread, propose, write, toggle, promote, delete
Capability levelobserver, proposer, operator, maintainer, admin
Rate limitRequests per minute per token (defaults to your organization's ceiling; set explicitly to narrow a delegated key)
TTLAuto-expires (1 hour to 90 days; renewable)
Approval requiredWhether mutations need human approval before taking effect
IP allowlistOptional CIDR range restriction

The five capability levels are pre-baked scope templates that reduce misconfiguration:

  • Observer — read-only. Audit queries, flag/config reads, evaluation tracing. The default for agents whose job is to investigate, summarize, or report.
  • Proposer — read + create proposals. Cannot mutate directly; every change goes through the approval workflow. The safe default when an agent acts on behalf of a user with limited authority, or when an operator wants review on every agent action.
  • Operator — toggle existing flags, adjust rollout percentages, edit existing rules. Cannot create or delete flags, cannot change approval settings. The on-call assistant profile.
  • Maintainer — full CRUD on flags and configs. Cannot change RBAC, rotate tokens, or modify approval rules. Suitable for agents that manage flag lifecycle (creation, cleanup, scheduled changes).
  • Admin — everything. Intended for humans, not agents. Granting Admin to an agent is supported but requires an explicit override and generates a prominent warning.

A safe default agent token: read + propose on production, write + toggle on staging, 7-day TTL, 600 req/min.

Delegation ​

A user can mint short-lived tokens scoped at-or-below their own permissions and hand them to agents. Token minting is itself an audited action. Delegated tokens inherit a ceiling from the delegator — agents cannot escalate beyond their human's permissions, and revoking the human cascades-revoke every token they delegated. This is the default pattern for giving an engineer's personal agent temporary elevated access without a permanent configuration change.

The audit row for every agent-originated mutation carries the full chain: actor_token_id → delegator_user_id → (if approval-gated) approver_user_id.

Counterfactual preview ​

POST /api/v1/envs/:envId/evaluate/preview evaluates a hypothetical ruleset against a list of contexts and returns both the live and preview resolution side-by-side, so an agent can reason about blast radius before committing a change.

http
POST /api/v1/envs/{envId}/evaluate/preview
Content-Type: application/json

{
  "spotCheck": [
    { "userId": "u_42", "plan": "enterprise" },
    { "userId": "u_99", "plan": "free" }
  ],
  "ruleset": {
    "flags": [
      {
        "key": "ui.theme",
        "type": "string",
        "rules": [],
        "defaultValue": "midnight"
      }
    ]
  },
  "verboseReason": false
}

The response carries the env's current liveVersion plus, for each context, the live resolution and the preview resolution:

json
{
  "environmentId": "9f7a32b5-1234-4abc-9def-...",
  "liveVersion": 142,
  "spotCheck": [
    {
      "context": { "userId": "u_42", "plan": "enterprise" },
      "live":    { "ui.theme": { "value": "classic",  "defaultValue": "classic",  "reason": { "kind": "default" } } },
      "preview": { "ui.theme": { "value": "midnight", "defaultValue": "midnight", "reason": { "kind": "default" } } }
    },
    {
      "context": { "userId": "u_99", "plan": "free" },
      "live":    { "ui.theme": { "value": "classic",  "defaultValue": "classic",  "reason": { "kind": "default" } } },
      "preview": { "ui.theme": { "value": "midnight", "defaultValue": "midnight", "reason": { "kind": "default" } } }
    }
  ]
}

Notes:

  • The transient ruleset is subject to the same save-time validation as a write would be (cycle detection, regex compilation, JSON Schema, type compatibility) — invalid rulesets return 400 with the same errors POST /flags would have produced.
  • The preview is not stored anywhere server-side and does not bump any version.
  • A request is capped at 50 spot-check contexts. The endpoint is for spot-checking specific personas (the CEO, your test fixtures, named accounts), not for scanning a user base.
  • asOf and ruleset are mutually exclusive; preview is always against the supplied ruleset.

The dashboard's "Preview impact" panel on the flag detail page is a thin client over this endpoint.

Propose / apply ​

The propose / apply flow is the durable, auditable equivalent of "agent suggests, human approves." It builds on the preview endpoint above: the agent captures the blast radius at propose time, and the applier verifies the live ruleset hasn't drifted before letting the change land.

Lifecycle ​

pending (TTL: 1h default, 24h max)
    │
    ├──► applied   (apply handler committed the diff)
    ├──► cancelled (explicit reject by proposer or operator)
    └──► expired   (background sweeper after expiresAt)

A proposal is single-use: any second apply or cancel against a non-pending row returns 410 proposal_gone, and an apply against a cancelled row returns 410 too.

Propose ​

http
POST /api/v1/proposals
Content-Type: application/json

{
  "envId": "9e1f…",
  "kind": "set_default_value_flag",
  "resourceKey": "ui.theme",
  "diff": { "defaultValue": "midnight" },
  "spotCheck": [
    { "userId": "u_42", "plan": "enterprise" }
  ],
  "reason": "switch default theme; 0 of 1 sampled contexts flip",
  "expiresInSeconds": 3600
}

spotCheck is capped at 50 entries. expiresInSeconds is optional; default 3600 (1h), max 86400 (24h). resourceKey identifies the target primitive — the resource type is implied by kind and validated server-side.

Returns 201 Created with Location: /api/v1/proposals/{id} and a Proposal body that carries:

  • liveVersion — the env's version at propose time. Compared at apply time for drift detection.
  • blastRadius — the materialized output of running /evaluate/preview against spotCheck with the proposed ruleset. Each entry has {context, live, preview} envelopes.
  • expiresAt — absolute timestamp; the sweeper uses this.
  • status: "pending", plus the proposer attribution (proposerTokenId or proposerUserId).

Error responses:

  • 409 in_use for delete_segment of a segment referenced by another flag/config; fields lists referrers as <resourceType>.<resourceKey> filtered by the caller's scope.
  • 503 ruleset_not_cached if the env has no live blob yet (a freshly created env hits this until its first write).

Kinds (the verb vocabulary) ​

kind selects the mutation; each kind names a concrete verb the apply path switches on.

ResourceKinds
Flagset_default_value_flag, set_rules_flag, kill_flag, delete_flag
Configset_default_value_config, set_rules_config, delete_config
Segmentset_rules_segment, delete_segment

diff is kind-specific:

  • set_default_value_* → { "defaultValue": <typed> }
  • set_rules_* → { "rules": [<rule>, ...] } (segments also accept allowList?, denyList?)
  • kill_flag → {} (boolean flags only; the verb is "set defaultValue=false, rules=[]")
  • delete_* → {}

Get ​

http
GET /api/v1/proposals/{id}

Returns the same Proposal body propose returns, useful when the agent host's context window has been compacted and you need to re-read the blast radius before applying. Cross-tenant access collapses to 404.

Apply ​

http
POST /api/v1/proposals/{id}/apply

Inside a single transaction, the apply handler:

  1. Takes a row-level lock on the proposal, asserting status = 'pending'.
  2. Reads the env's current version.
  3. Compares it to the captured liveVersion.
    • Mismatch: returns 409 version_drift with body {liveVersion, proposedVersion}. The proposal stays terminal-eligible (still pending until the sweeper expires it); the agent is expected to re-propose against the new live state, which captures a fresh blastRadius.
    • Match: executes the diff via the canonical write path (audit + cache + version bump). The audit row carries reason="proposal:<id>"; the proposal back-links via appliedAuditId.
  4. Marks the proposal applied with resolvedAt, resolverTokenId or resolverUserId, appliedAuditId, and appliedVersion set to the new env version.

Returns 200 OK with:

json
{
  "proposalId":     "...",
  "status":         "applied",
  "appliedVersion": 143,
  "appliedAuditId": "...",
  "resolvedAt":     "2026-04-29T14:33:00Z"
}

Error responses:

  • 409 version_drift — env was written between propose and apply.
  • 409 in_use — delete_segment apply finds new referrers that landed between propose and apply.
  • 410 proposal_gone — proposal is already applied or past its expiresAt.

The version-drift check is the load-bearing safety property: two concurrent applies can't both succeed — the second sees the new env version and the comparison fails.

The propose action gates POST /proposals; the apply action gate matches the underlying CRUD verb (write for set/kill kinds, delete for delete kinds), so a Proposer-level token can stage any change but only an Operator+ token can apply.

Cancel ​

http
POST /api/v1/proposals/{id}/cancel
Content-Type: application/json

{ "note": "human reviewed; not shipping this change" }

Body is optional — {} cancels without a note. Reachable by:

  • the original proposer (regardless of action scope), so an agent can withdraw its own pending proposal; or
  • anyone holding the action the apply would have required (so an Operator+ token can reject any pending row in scope).

Returns the canceled Proposal row with status='cancelled', resolvedAt, the resolver attribution, and resolverNote (also copied to the proposal's reason if set).

Error responses:

  • 410 proposal_gone — proposal is already applied, cancelled, or past its expiresAt.

Expire ​

A background sweeper inside the server flips status='expired' for any pending row past its expiresAt. Expired proposals can't be applied; the agent re-proposes if the human takes too long.

Audit chain ​

Every mutation — agent or human, write or apply — is recorded with the full accountability chain:

ColumnMeaning
actor_typeuser | api_token | agent_token | system
actor_idToken id (or user id, for human actions)
delegator_user_idThe human who minted the token, if delegated
approver_user_idThe human who approved the apply, if gated
resource_type, resource_key, resource_idWhat changed
previous_value, new_value, diffThe change payload
reasonFree-text — e.g., "incident #4821 mitigation" or "proposal:4f2a"

The dashboard's audit page filters by actor type, actor, time range, and resource. Every column above is queryable via GET /api/v1/orgs/:slug/audit.

Proposal lifecycle is also audited ​

Every proposal transition writes its own audit row with resourceType=proposal and one of the following actions:

  • proposal.created — when POST /proposals succeeds.
  • proposal.applied — when POST /proposals/{id}/apply commits.
  • proposal.cancelled — when POST /proposals/{id}/cancel flips it.
  • proposal.expired — when the sweeper flips a TTL'd row.

This timeline is independent of the data-change rows. When a proposal applies, the underlying primitive's audit row carries reason="proposal:<id>" and the proposal-side row's appliedAuditId back-links — so two queries reconstruct the full story:

  • audit?resourceType=proposal&resourceId=<id> — the proposal's lifecycle (created → applied / cancelled / expired).
  • audit?resourceType=flag&resourceKey=<key> (or config / segment) — the data-change timeline; rows produced by an apply are tagged with reason="proposal:<id>".

MCP server ​

A first-party Model Context Protocol server, distributed as the @actuator/mcp package (npx @actuator/mcp). The shipped tool surface mirrors the REST endpoints — agents that prefer raw HTTP can call those directly with no loss of capability.

CategoryTools
Navigationlist_organizations, list_projects, list_environments, list_flags, list_configs, list_segments
Describedescribe_flag, describe_config, describe_segment
Evaluationevaluate_for_context, lookup_flags_for_target, get_ruleset_version
Auditaudit_query, describe_audit_event
Propose (one verb per kind)propose_set_default_value, propose_set_default_value_config, propose_set_rules_flag, propose_set_rules_config, propose_set_rules_segment, propose_kill_flag, propose_delete_flag, propose_delete_config, propose_delete_segment
Apply / cancel / inspectapply_proposal, cancel_proposal, describe_proposal
Direct createcreate_flag, create_config, create_segment
Metadata editsupdate_flag_metadata, update_config_metadata, update_segment_metadata
Append-rule helpersappend_flag_rule, append_config_rule, append_segment_rule

The propose tools are kind-specific so each one carries a typed schema for its diff — the agent host can validate the call locally before sending. Every propose tool returns a proposalId and the materialized blast radius; the canonical pattern is "propose → present blast radius to the human → call apply_proposal(proposalId) on approval."

create_* and the metadata/rule-append helpers exist alongside the propose surface for low-risk operations where round-tripping a human approval would be friction with no safety win — they call the underlying POST /flags / PATCH /flags/{key} endpoints directly under the calling token's write action gate. A token without write falls through to using the propose tools.

Every mutation tool respects the calling token's capability level and the env's approval-gate configuration. An Observer-level token calling a write tool receives a structured permission error pointing it at the matching propose_* tool instead.

See also ​