Agent Control Room API

Ask this policy engine from your own systems. An application asks before an agent acts; the engine answers from an ordered rule set, records the answer, and hands back a short-lived decision bound to the exact payload the step is about to touch.

Quick start

Every call goes to a single endpoint and names an operation. Replace <tenant> with your workspace address and the key with an API key created in Workspace settings > API access.

Endpoint

POST https://<tenant>.agent-cr.com/api/public/v1/gateway

Authenticate with the x-api-key header. The key identifies the workspace, so you never pass a workspace id — and a key can never reach another workspace's rules or decisions. A key only answers on its own workspace address.

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "decideAction",
    "parameters": {
      "agent": "library-curator",
      "action": "catalogue.read",
      "payloadHash": "<64 hex chars>"
    }
  }'

Request and response format

Requests are JSON: an operation name and a parameters object. Every response carries statusCode (matching the HTTP status — failures are never returned as 200), the operation, a requestId you can quote in support requests, and one payload key — or an error object with a stable code.

{
  "statusCode": 200,
  "operation": "decideAction",
  "requestId": "8f3c1e2a-…",
  "decision": { "decisionId": "…", "effect": "grant", "expiresAt": "…" }
}

Request ids and safe replay

Send your own request id in an Idempotency-Key header — up to 200 characters. It is echoed back as requestId and in the Idempotency-Key response header, and written into the audit trail, so a call you made and a call you are asking us about are the same line. Without the header we mint a request id for you.

For decideAction the key does more than name the call: the successful answer is stored under it for 24 hours and returned again, unchanged, if the same key arrives with the same request. Only a settled answer is stored: a pending answer — a person was asked — is never kept, so the identical call sent again after someone has approved claims the grant rather than replaying the wait.

The match is over the entire validated decideAction body — agent, action, resourceRef, payloadHash, context and actionDescription — serialized canonically. The recommended way to derive the key is a hash of the entire request body, context included: the same step then always produces the same key, retries are free, and if a fact value ever legitimately changes, the changed body simply yields a new key and a fresh evaluation instead of a refusal. One trade-off to know about: a body-derived key is not a stable step identifier across legitimate fact changes — the same logical step appears under a new key — so if you trace steps across systems, keep your own step id in your logs.

Freeze context for the step before you send the first decideAction call, and reuse that frozen context for every retry and the final claim; a timestamp or any value that changes between calls changes the fingerprint and the call is refused with idempotency_key_mismatch. That is how a claimed grant survives a lost connection — you get the same signed decision back instead of consuming a second approval. A replayed answer carries replayed: true and an Idempotency-Replayed: true header.

Reusing one key for a different request is refused with idempotency_key_mismatch, so a key must change whenever the payload does. Retrying without a key is still safe while a step waits: a repeat decideAction call with the same agent, action, resourceRef and payloadHash returns the same approvalId rather than opening a second request.

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: step-8891-abstract-v3" \
  -H "Content-Type: application/json" \
  -d '{ "operation": "decideAction", "parameters": { "agent": "library-curator", "action": "catalogue.read", "resourceRef": "entry_8891", "payloadHash": "<64 hex chars>" } }'

Waiting for a person

An approval has exactly five statuses, and they mean the same thing on every screen in Agent Control Room and in every operation that reports one: pending (nobody has answered yet), granted (a person approved and the grant is still claimable), refused (a person said no), expired (nobody answered in time, or the approval lapsed unclaimed) and consumed (the grant was claimed and spent). There is no separate vocabulary for administrators and callers.

A pending decideAction is asynchronous. Store the approvalId it returns and poll getApprovalStatus with it — never decideAction — while the answer is pending. Poll every five seconds, back off to every fifteen after two minutes, pause while the screen is hidden, and run exactly one poller per waiting step. When the status turns granted, issue the identical decideAction call once: that call claims the approval and returns the signed, single-use grant.

Stop on refused, expired or consumed — each terminal answer carries its own recovery step, described under getApprovalStatus. An approval that nobody claims lapses after fifteen minutes and the step stays blocked. Do not change the payload — prompt, model, version or resource — while you wait: a changed payload is a different request and the approval will not apply to it.

1. decideAction            → effect: "pending", approvalId: "7c1e…"
2. getApprovalStatus → status: "pending"    … keep polling
3. getApprovalStatus → status: "granted"    … claimable
4. decideAction (identical) → effect: "grant", signature bound to payloadHash
5. getApprovalStatus → status: "consumed"   … stop

Context contracts: rules for callers

An action may carry a context contract — the facts a workspace expects the caller to supply about the step. Contracts keep the integration predictable only if every caller follows the same handful of rules:

  1. Always send a context object in every decideAction call — an empty object {} when the action expects nothing. One shape for every call, no exceptions.
  2. Compute each fact once, when the step is created, and freeze it. Never place timestamps, request ids or random values in context: the body must be identical on every retry and on the final claim.
  3. Discover expectations with describePolicyEngine. Each action publishes the facts its contract declares — field, kind, whether it is required, and any agreed values. Send declared facts before the workspace switches enforcement on: extra fields are always accepted and ignored, so sending early is safe. Send first, enforce second.
  4. Derive the Idempotency-Key from a hash of the entire request body, context included. Changed facts then produce a new key and a fresh evaluation rather than idempotency_key_mismatch.
  5. Treat context_validation_failed as not decidable yet: log the fields it names, add them, and ask again. Never retry blindly, and never act without a grant.
  6. Undecidable is not a refusal. A validation failure produces no grant and no approval entry, and it does not count against the agent — it means the question was incomplete.
  7. A complete context does not guarantee permission. It only means the request can be judged; the rules still make the decision.
  8. Where a fact declares provenance, send it as { "value": …, "assertedBy": "records-service", "observedAt": "2026-09-20T10:00:00Z" } instead of a bare value. assertedBy must be one of the asserters the contract names, observedAt is ISO-8601 UTC, and both must come from the service that actually observed the fact — never from the agent asking for permission. A missing or unlisted asserter is reported as the issue code provenance; an observation older than the contract's maxAgeSeconds as stale. Both are undecidable, not refusals. Provenance metadata does not change the identity of a waiting approval: only the value does.
  9. A rule may cap how often it grants. When that cap is reached, decideAction answers with effect reject and error rate_limit_exceeded, and the rationale names the limit and the time the next slot opens. Wait until that time and ask again; never retry in a loop. If the step was already approved by a person, the approval is not spent — claim it once a slot is free. Limits are counted per rule: grants from other rules for the same agent and action do not use them up, and renaming a rule's id starts its counter again from zero.
  10. Workspace circuit breakers apply to every grant, local or from a remote engine. When one stops a step, decideAction answers with effect reject and one of circuit_open, chain_velocity_exceeded, chain_fanout_exceeded or fleet_velocity_exceeded, and the rationale names when the circuit closes. A chain refusal holds only that delegation chain; a workspace refusal holds every agent. Stop the chain, wait until the stated time and ask again; never retry in a loop.
  11. Approvals are tied to the contract revision and the context they were granted on. When the contract or the facts change, expect a fresh evaluation — and claim with the identical frozen body.

Standard context facts

Scanner verdicts and agent-to-agent hand-offs have published field names, so every gateway, scanner and orchestrator sends them the same way. Send them in context, flat and literal. Agent Control Room never scans content or tracks chains itself: it reads what the trusted system reports. Each fact must come from the system named — never from the agent asking. A missing fact never matches a rule condition, so a rule relying on it does not grant. Send each with provenance:

"context": {
  "sec.dlp_verdict": {
    "value": "clean",
    "assertedBy": "dlp-scanner",
    "observedAt": "2026-10-09T10:00:00Z"
  },
  "delegation.hop_count": {
    "value": 1,
    "assertedBy": "orchestrator",
    "observedAt": "2026-10-09T10:00:00Z"
  }
}

Security scanner

sec.dlp_verdictlistclean, flagged, blocked, not_scannedYour DLP scanner or the gateway's PII guardrail (for example an API or MCP gateway).
sec.pii_detectedlisttrue, falseYour DLP scanner or the gateway's PII guardrail.
sec.pii_categories_bandlistnone, contact, financial, special_categoryYour DLP scanner.
sec.secrets_detectedlisttrue, falseYour secrets scanner, run before the ask.
sec.prompt_injection_scorenumber0 to 1Your prompt-injection classifier or the gateway's guardrail.
authz.external_verdictlistallow, deny, not_evaluatedYour authorisation engine (for example OPA or Cedar), called by your application or gateway.

Agent delegation

delegation.initiator_agenttextyour own agent keysThe orchestrator or A2A gateway that carried the hand-off — never the agent itself.
delegation.via_agenttextyour own agent keysThe orchestrator or A2A gateway that carried the hand-off — never the agent itself.
delegation.hop_countwhole number0 and aboveThe orchestrator or A2A gateway that carried the hand-off — never the agent itself.
delegation.human_originlisttrue, falseThe orchestrator or A2A gateway that carried the hand-off — never the agent itself.

Checking a grant offline

Fetch the keys once with getGrantVerificationKey and cache them. Then check each grant locally before acting. A local check cannot see a revocation; call verifyDecision when that matters.

// Node 18+
import { createPublicKey, verify } from "node:crypto";
const key = keys.find((k) => k.keyId === decision.keyId);
if (!key) throw new Error("unknown keyId: refuse");
const l = decision.lineage; // null for a root grant
const message = ["acr-grant-v2", workspaceId, decision.keyId,
  decision.decisionId, myPayloadHash, decision.expiresAt,
  l?.parentDecisionId ?? "", l?.rootDecisionId ?? decision.decisionId,
  String(l?.depth ?? 0)].join("|");
const ok = verify(null, Buffer.from(message),
  createPublicKey({ key: key.jwk, format: "jwk" }),
  Buffer.from(decision.publicSignature, "base64url"));
if (!ok || Date.parse(decision.expiresAt) <= Date.now()) refuse();
# Python 3, pip install cryptography
import base64, time
from datetime import datetime
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
b64 = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
key = next(k for k in keys if k["keyId"] == d["keyId"])
l = d.get("lineage") or {}  # empty for a root grant
message = "|".join(["acr-grant-v2", workspace_id, d["keyId"],
    d["decisionId"], my_payload_hash, d["expiresAt"],
    l.get("parentDecisionId", ""), l.get("rootDecisionId", d["decisionId"]),
    str(l.get("depth", 0))]).encode()
Ed25519PublicKey.from_public_bytes(b64(key["publicKey"])).verify(
    b64(d["publicSignature"]), message)  # raises if invalid
expires = datetime.fromisoformat(d["expiresAt"].replace("Z", "+00:00"))
assert expires.timestamp() > time.time()

A gateway's round trip

1. decideToolCall      { agent, toolName, toolArguments, context }
   → effect grant: keep decisionId, payloadHash, publicSignature
   → effect pending: return 202 with approvalId; poll getApprovalStatus
   → effect reject: block the tool call
2. Check the grant locally (see "Checking a grant offline")
   → invalid or expired: block the tool call
3. Forward the tool call to the MCP server
4. reportExecutionOutcome { decisionId, payloadHash, outcome: "executed" }
   (or "aborted" / "downstream_error" with a short detailCode)
Ready-made recipes: Plug-ins →

Optional Complyee Policy Library [CPL] policy review

A workspace administrator pairs and tests Complyee Policy Library, then configures each action as Off, Advisory (Path B), or Gate (Path C). This is the implemented ACR–CPL request and assessment contract, not a generic protocol for interchangeable policy-library endpoints. Advisory records a reading with the decision and shows it to approvers without changing effect, signature or expiry. A Gate runs only after local rules would Allow and before a counted grant slot or signature is issued. A supporting assessment leaves Allow in place; a Refuse or existing approval hold can never be upgraded. The Gate is inert in remote-engine mode.

For a Gate, an administrator chooses a fallback of Ask a person first or Refuse when an assessment is unavailable or is not supporting. A specific requires_approval assessment always holds the step for a person, even if Refuse is selected. A timeout, unreachable Policy Library service, ambiguous or conflicting reading never becomes a grant. A gate hold or refusal is indexed with reasonCode library_gate_held or library_gate_refused, respectively; the immediate decideAction decision carries libraryGate for that reason. The caller handles pending via the usual getApprovalStatus and identical decideAction claim flow; no separate polling protocol is needed.

A consulted request sends CPL the agent and action keys, a decision correlation identifier, a bounded plain-language actionDescription (up to 2,000 characters, falling back to the declared action description), and up to 40 context facts. Extra fields can be forwarded too; send only facts suitable for CPL. It does not send raw documents, prompts, generated content, the payload or payloadHash. Context and descriptions must not contain secrets or sensitive content. The CPL reading is recorded beside the decision, not used as a rule fact or an approval key. A matching recent reading can be reused for the same step. While approval is open, identical retries reuse its reading and the eventual human-approved grant inherits it; the claim does not re-consult CPL.

Gate outcomes for a locally allowed action

Rule outcome: Allow
Library assessment: supports           → Allow (gate applied: false)
Library assessment: requires_approval  → Ask a person first (library_gate_held)
Other outcome or no reading, fallback Ask a person first
                                        → Ask a person first (library_gate_held)
Other outcome or no reading, fallback Refuse
                                        → Refuse (library_gate_refused)
Rule outcome: Refuse or Ask a person first
                                        → unchanged; library cannot grant

Decision evidence and export

The decision index returned by listPolicyDecisions and getDecision may include advisory: an assessed reading (status, outcome, summary, citations, assessmentId, assessedAt and corpusFingerprint) or a not_consulted status with reason, detail and time. A Gate reading also carries gate: { applied, from, to }, recording whether it tightened the local outcome. inheritedFrom identifies a reading carried into the eventual grant. The decision index includes reasonCode and the payloadHash where recorded; the full evidence record remains in workspace storage. These fields describe what was recorded, not independent verification of caller-supplied facts.

In Decisions, a Gate supported badge is reserved for a local rule-backed grant with a recorded supporting assessment. Workspace administrators can download Export recent decisions (CSV): a spreadsheet-safe snapshot of the most recent 200 indexed decisions, excluding intermediate claim requests. It includes the authorization path, policy and rule, reason, payload fingerprint, assessment ID, corpus fingerprint and citations, timestamps and evidence-record path. It is not a signed CSV, a complete historical export or the separate administrative audit stream. Approval-backed grants retain their own human-approval attribution.

Operations

Twenty-five operations cover the current API surface, in three families: describing the engine, asking it for a decision, and governing the rules it answers from. Call listOperations with any key to list them all — each entry is marked callable: true or false depending on that key's scopes, so an integration can discover what exists and what to ask its administrator for.

Who calls what: the application whose agents are governed calls decideAction, polls getApprovalStatus when a step is held for a person, then verifies and acts. A reporting or CMDB integration calls the describing and audit operations. A governance system — not an agent — calls putRuleSet, decideApproval and revokeDecision. Agents and the data they act on belong to the applications this engine decides for; the engine holds rules, decisions and their evidence.

listOperations

Required scope: Always allowed

Lists every operation this API serves. Each entry carries `callable: true` when the calling key's scopes allow it, so an integration can discover the full surface and see which operations to ask its administrator to grant.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listOperations",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "listOperations",
  "requestId": "8f3c1e2a-…",
  "operations": [
    {
      "operation": "decideAction",
      "parameters": "agent (string, required), action (string, required), payloadHash (string, required)",
      "payloadKey": "decision",
      "description": "Asks whether an agent may take this action on this payload.",
      "scope": "policy.decide",
      "callable": true
    },
    {
      "operation": "putRuleSet",
      "parameters": "rules (array, required), note (string, optional)",
      "payloadKey": "ruleSet",
      "description": "Replaces this workspace's policy rule set.",
      "scope": "policy.admin",
      "callable": false
    }
  ]
}

Common errors: invalid_api_key · network_not_allowed · rate_limited

describeSystem

Required scope: Always allowed

Returns a machine-readable record of this AI system as deployed for this workspace: purpose and excluded uses, decision method, data categories, retention, access controls, human-oversight surfaces, the dependency list under components, and sub-processors. Built for registering this engine as a configuration item in a CMDB or AI system registry, and for the system description an AI Act deployer or a NIST AI RMF GOVERN review is expected to hold. recordVersion is bumped whenever the shape of the record changes, so an integration can pin to it; recordHash changes only when the content changes, so a poll can skip a no-op sync.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "describeSystem",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "describeSystem",
  "requestId": "1b7d94c0-…",
  "system": {
    "recordVersion": 9,
    "recordHash": "9f2c…",
    "generatedAt": "2026-08-31T19:00:00.000Z",
    "system": { "name": "Complyee Agent Control Room", "vendor": "Complyee AB", "kind": "Deterministic policy decision point for agent actions." },
    "workspace": { "id": "…", "slug": "acme", "name": "Acme", "address": "https://acme.agent-cr.com" },
    "purpose": { "intendedUse": "…", "excludedUses": ["…"], "automatedDecisionMaking": "…" },
    "decisionPoint": { "outcomes": ["allow", "refuse", "ask a person first"], "languageModelInDecisionPath": false, "policyLibrary": "…" },
    "data": { "retention": { "auditEvents": "365 days", "decisionRecords": "…" } },
    "accessControls": { "singleSignOn": "Required", "networkRestrictions": { "enabled": true, "ranges": 2 } },
    "humanOversight": ["…"],
    "components": [{ "kind": "storage", "name": "acme-audit", "provider": "Google Cloud Storage (Complyee)", "location": "EU", "role": "Holds the workspace's audit records." }],
    "deployerRegistry": { "provided": true, "externalSystemId": "CI-004217", "owner": { "name": "…", "email": "…" } },
    "subProcessors": [{ "name": "Google Cloud EMEA Limited", "location": "EU or US, fixed per workspace." }]
  }
}

Common errors: invalid_api_key · network_not_allowed · rate_limited

listSystemChanges

Required scope: audit.read

Returns the configuration changes that matter to a registry, newest first: session limits, API keys and network rules, role changes, agent changes, and edits to the deployer's own governance fields. Each entry carries a plain-language summary of what changed, so a CMDB can record why the record hash moved. Derived from the workspace audit stream in your own storage bucket.

Parameters

NameTypeDescription
fromstringrequiredStart date, YYYY-MM-DD.
tostringrequiredEnd date, YYYY-MM-DD.
limitnumberoptionalMaximum number of changes to return, 1–500. Defaults to 100.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listSystemChanges",
    "parameters": { "from": "2026-08-01", "to": "2026-08-31", "limit": 50 }
  }'

Example response

{
  "statusCode": 200,
  "operation": "listSystemChanges",
  "requestId": "6c0a1d92-…",
  "changes": [
    {
      "at": "2026-08-24T09:12:44.000Z",
      "category": "configuration",
      "action": "settings.updated",
      "summary": "idle timeout: 480 → 240 minutes",
      "fields": ["idle timeout"],
      "actor": "admin@acme.com",
      "outcome": "success"
    },
    {
      "at": "2026-08-19T14:03:01.000Z",
      "category": "registry",
      "action": "system_registry.updated",
      "summary": "Deployer-supplied governance fields were updated.",
      "fields": ["external_system_id", "owner_name"],
      "actor": "admin@acme.com",
      "outcome": "success"
    }
  ],
  "truncated": false
}

Common errors: scope_denied · invalid_parameters · invalid_api_key · rate_limited

getPostureSnapshot

Required scope: Always allowed

Returns the workspace security checklist as structured data: identity, network restrictions, sessions, people, data handling and retention, each row with its current value, a status of good, review or not-configured, and the explanation shown in the product. Read live from configuration at the moment of the call, with the timestamp it was read.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "getPostureSnapshot",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "getPostureSnapshot",
  "requestId": "9a41c7e0-…",
  "posture": {
    "tenantSlug": "acme",
    "tenantName": "Acme",
    "generatedAt": "2026-09-02T08:14:22.000Z",
    "summary": { "good": 11, "review": 2, "notConfigured": 3 },
    "groups": [
      {
        "id": "identity",
        "title": "Identity",
        "rows": [
          { "id": "sso", "label": "Single sign-on", "value": "Required", "status": "good", "note": "…" }
        ]
      }
    ]
  }
}

Common errors: invalid_api_key · network_not_allowed · rate_limited

getSystemDossier

Required scope: audit.read

Returns the dated system dossier for a period: a plain-language cover, the full system record, the deployer's governance fields, the posture checklist as read at that moment, the configuration changes in the period, and a manifest counting the audit records held for it. Intended for archiving evidence on a quarterly or annual cadence, not for routine polling — use describeSystem and recordHash for that. The audit records themselves are not included; they stay in your own storage bucket.

Parameters

NameTypeDescription
fromstringrequiredStart date, YYYY-MM-DD.
tostringrequiredEnd date, YYYY-MM-DD.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "getSystemDossier",
    "parameters": { "from": "2026-07-01", "to": "2026-09-30" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "getSystemDossier",
  "requestId": "b7d1f004-…",
  "dossier": {
    "dossierVersion": 2,
    "generatedAt": "2026-10-01T06:00:11.000Z",
    "period": { "from": "2026-07-01", "to": "2026-09-30" },
    "cover": { "title": "System dossier", "workspace": "Acme", "preparedBy": "API key \"Archive job\"", "about": "…", "contents": ["…"], "limitations": "…" },
    "systemRecord": { "recordVersion": 9, "recordHash": "9f2c…", "…": "…" },
    "posture": { "summary": { "good": 11, "review": 2, "notConfigured": 3 }, "groups": ["…"] },
    "changes": { "entries": [{ "at": "2026-08-24T09:12:44.000Z", "summary": "idle timeout: 480 → 240 minutes" }], "truncated": false },
    "auditManifest": {
      "bucketConfigured": true,
      "streams": [
        { "stream": "admin", "events": 214, "earliest": "2026-07-01T…", "latest": "2026-09-30T…", "byAction": [{ "action": "policy.rules_changed", "count": 12 }] }
      ]
    }
  }
}

Common errors: scope_denied · invalid_parameters · invalid_api_key · rate_limited

listAuditEvents

Required scope: audit.read

Reads administration and security audit events for this workspace from its own storage bucket — who changed rules, settled approvals, managed agents, members and keys, and when. Grant this scope only to systems allowed to see that.

Parameters

NameTypeDescription
fromstring (YYYY-MM-DD)requiredStart of the date range, inclusive.
tostring (YYYY-MM-DD)requiredEnd of the date range, inclusive.
limitnumber (1–2000, default 500)optionalMaximum events to return; truncated is true when more exist.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listAuditEvents",
    "parameters": {
      "from": "2026-08-01",
      "to": "2026-08-30",
      "limit": 100
    }
  }'

Example response

{
  "statusCode": 200,
  "operation": "listAuditEvents",
  "requestId": "8f3c1e2a-…",
  "events": [
    {
      "at": "2026-08-29T14:03:11.000Z",
      "action": "policy.rules_changed",
      "outcome": "success",
      "actor": "jane@example.com"
    }
  ],
  "truncated": false,
  "bucketConfigured": true
}

Common errors: invalid_parameters · scope_denied · rate_limited

listPolicyDecisions

Required scope: policy.read

Lists indexed decisions for this workspace, newest first. The index includes agent, action, resource reference, effect, rule set version, engine, reasonCode, payloadHash, policy/rule when matched, optional advisory policy-library reading and the path of the full record in workspace storage. advisory.gate shows whether a local Gate actually tightened the rule outcome; a supporting Gate can leave a rule grant unchanged. Older rows may lack newer evidence fields. The index is not the administrative audit stream and does not expose request context or full rationales.

Parameters

NameTypeDescription
limitnumberoptional1–200, default 25.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listPolicyDecisions",
    "parameters": { "limit": 10 }
  }'

Example response

{
  "statusCode": 200,
  "operation": "listPolicyDecisions",
  "requestId": "7d21b0aa-…",
  "decisions": [
    {
      "decisionId": "1f0e…",
      "agentKey": "library-curator",
      "action": "catalogue.read",
      "resourceRef": "entry_8891",
      "effect": "grant",
       "reasonCode": "rule_granted",
       "ruleId": "curator-read",
       "advisory": {
         "status": "assessed", "outcome": "supports",
         "assessmentId": "assessment-…", "assessedAt": "2026-09-04T08:00:02.000Z",
         "corpusFingerprint": "corpus-…", "citations": ["Policy section …"],
         "gate": { "applied": false, "from": "grant", "to": "grant" }
       },
      "policyVersion": "builtin:builtin-1",
      "engineMode": "local",
      "engineEndpoint": null,
      "issuedAt": "2026-09-04T08:00:03.000Z",
      "expiresAt": "2026-09-04T08:05:03.000Z",
      "recordPath": "policy/decisions/2026/09/04/2026-09-04T08:00:03.000Z-9ab21c4d.json"
    }
  ]
}

Common errors: invalid_api_key · scope_denied · rate_limited

getDecision

Required scope: policy.read

Returns one indexed decision by id, including its reasonCode, matched policy/rule, payloadHash and optional advisory reading where recorded. An inheritedFrom field on that reading links a human-approved claim to the assessment seen when the step was held. Follow recordPath for the full evidence in workspace storage; this index is not the complete rationale. A policy-engine operation.

Parameters

NameTypeDescription
decisionIduuidrequiredThe decision id returned by decideAction.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "getDecision",
    "parameters": { "decisionId": "1f0e5c2b-…" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "getDecision",
  "requestId": "0be71c33-…",
  "decision": {
    "decisionId": "1f0e5c2b-…",
    "agentKey": "library-curator",
    "action": "catalogue.read",
    "resourceRef": "entry_8891",
    "effect": "grant",
    "policyVersion": "builtin:builtin-1",
    "engineMode": "local",
    "issuedAt": "2026-09-04T08:00:01.000Z",
    "expiresAt": "2026-09-04T08:05:01.000Z",
    "recordPath": "policy/decisions/2026/09/04/…json"
  }
}

Common errors: invalid_api_key · scope_denied · not_found · rate_limited

listRules

Required scope: policy.read

Returns the rule set in force: its version, its default effect and its ordered rules. Evaluation is first-match-wins, so reading the list top to bottom tells you exactly what an agent may do. A workspace may replace the built-in set by writing policy/rules.json into its own bucket; a file that cannot be read refuses everything rather than falling back.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listRules",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "listRules",
  "requestId": "5cd0f7a2-…",
  "rules": {
    "version": "builtin-1",
    "source": "builtin",
    "defaultEffect": "reject",
    "valid": true,
    "rules": [
      { "id": "curator-read-catalogue", "effect": "grant",
        "conditions": [
          { "type": "agent_is", "values": ["library-curator"] },
          { "type": "action_is", "values": ["catalogue.read"] }
        ] }
    ]
  }
}

Common errors: invalid_api_key · scope_denied · rate_limited

registerAgent

Required scope: policy.admin

Registers an agent in this workspace, or updates one already registered. Call it from your deployment pipeline on every release: it is idempotent, so the same agent is never duplicated and an administrator's edits are not reset. Only a registered, active agent can be granted anything — an unknown agent that calls decideAction is recorded as pending and refused until somebody approves it. Sending allowedActions that this workspace has not declared is refused as invalid_parameters, with nothing saved.

Parameters

NameTypeDescription
agentstringrequiredThe key the agent sends when it asks for a decision. Lower case, 2-64 characters.
displayNamestringoptionalHuman-readable name shown in the control room.
descriptionstringoptionalWhat the agent does, in one or two sentences.
ownerNamestringoptionalPerson or team accountable for the agent.
ownerEmailstringoptionalContact address for that owner.
allowedActionsstring[]optionalActions this agent may even ask for, enforced at the registry before any policy rule runs. Every name must be an action this workspace has declared; an undeclared name fails the whole call with invalid_parameters, naming the offending entries, and nothing is saved. Omit the field or send null to leave an existing agent's list exactly as it is (a new agent then starts with no restriction). Send an empty array to allow every declared action; an empty list is always accepted, including before the workspace has declared anything.
statusstringoptionalpending, active or disabled. Defaults to active on first registration.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "registerAgent",
    "parameters": {
      "agent": "library-curator",
      "displayName": "Library curator",
      "ownerEmail": "ai-platform@example.com",
      "allowedActions": ["catalogue.read", "catalogue.write"]
    }
  }'

Example response

{
  "statusCode": 200,
  "operation": "registerAgent",
  "requestId": "9f1c2b4e-…",
  "agents": [
    { "agentKey": "library-curator", "status": "active", "origin": "api",
      "allowedActions": ["catalogue.read", "catalogue.write"] }
  ]
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · rate_limited

listAgents

Required scope: policy.read

The agent registry of this workspace: key, name, standing (active, disabled or waiting to be approved), owner, the actions each agent may ask for, how the record came to exist, and when it was last seen asking. Intended for a CMDB or AI system registry to poll and reconcile against its own inventory. This is an enforcement record, not an inventory: it carries no models, versions, datasets or risk assessments.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listAgents",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "listAgents",
  "requestId": "3ab77d10-…",
  "agents": [
    { "agentKey": "library-curator", "displayName": "Library curator",
      "status": "active", "origin": "registered",
      "allowedActions": [], "requestedActions": ["catalogue.read"],
      "lastSeenAt": "2026-09-08T09:12:44Z", "lastEffect": "grant" }
  ]
}

Common errors: invalid_api_key · scope_denied · rate_limited

describeAgent

Required scope: policy.read

One agent's registry record in full, including its recent runs. Returns null if this workspace has never seen an agent by that name.

Parameters

NameTypeDescription
agentstringrequiredThe agent key.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "describeAgent",
    "parameters": { "agent": "library-curator" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "describeAgent",
  "requestId": "c81e0f5b-…",
  "agent": { "agentKey": "library-curator", "status": "active", "runs": [] }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · rate_limited

decideAction

Required scope: policy.decide

Asks whether an agent may perform one exact action on one exact payload. The answer is a grant or a reject with a rationale, rule set version and, for a grant, a short-lived signature bound to payloadHash. An unregistered agent is refused with agent_not_registered and recorded as pending in the registry — register agents before they run.

A third answer, pending, means a rule or a restrictive policy-library Gate asked for a person: the step is held and an approvalId is returned. A Gate hold has library_gate_held; a Gate refusal has library_gate_refused. Neither issues a usable grant. The Gate can only tighten a local Allow. Poll getApprovalStatus while the step waits; when the person has approved, call decideAction once more with the identical frozen request to claim a signed, single-use grant. The claim inherits the reading that was recorded when the step was held.

Repeating decideAction while the request is still open is safe — it returns the same approvalId rather than opening another — but it is not a status check. If a granted approval is not claimed in fifteen minutes, the claim window expires and the step stays blocked. A changed payload starts a new request. The restrictive local Gate does not run in remote-engine mode.

Parameters

NameTypeDescription
agentstringrequiredAgent key asking for permission.
actionstringrequiredcatalogue.read, catalogue.write or customer.export.
resourceRefstringoptionalWhat it wants to act on — a record id, a queue name, a path.
payloadHashstringrequiredSHA-256 hex over the exact payload that will be acted on.
contextobjectoptionalNon-sensitive context the rules may use. Send an object on every call — an empty one when the action expects nothing. Read optional actions[].context from describePolicyEngine for the facts an action expects. Supply literal top-level keys (for example resource.classification), not nested objects, and freeze the values for the step: identical on every retry and on the claim. A published contract does not change decisions; an enforced contract is checked before any rule runs, and a request missing a required fact is returned as context_validation_failed — neither allowed nor refused, and nobody is asked. The decision then carries error, contractRevision and contextIssues[] (field and code: missing, type, value, range, provenance or stale). A fact that declares provenance is sent as { value, assertedBy, observedAt } rather than a bare value. Extra fields are still accepted.
actionDescriptionstringoptionalA plain-language description of this one step, 1–2000 characters. Used only when the action is marked for Complyee consultation and the workspace has paired Complyee; otherwise ignored. When omitted, the action's description from the workspace vocabulary is used. This is the one natural-language field sent to Complyee — keep it free of personal data and document content. Advisory readings do not change the rule outcome. A Gate on a locally allowed step can hold (library_gate_held) or refuse (library_gate_refused) it; a reading of requires_approval always holds even if the fallback was Refuse. Part of the Idempotency-Key match, so freeze it for retries and the claim.
parentDecisionIdstringoptionalAttenuated delegation. The decisionId of the grant this step is handed on from, when one agent delegates work to another. ACR checks the parent against its own decision record before any rule runs — it must exist in this workspace, be a grant, be unexpired and unrevoked (as must every grant above it), stay within the workspace chain-depth limit (default 5) and not repeat the same agent, action and payload earlier in the chain — otherwise the step is refused with reasonCode parent_grant_invalid, parent_grant_revoked, parent_grant_expired, delegation_depth_exceeded or delegation_cycle. ACR then derives lineage facts rules can compare (lineage.depth, lineage.parent_agent, lineage.parent_action, lineage.parent_policy, lineage.root_agent, lineage.root_decision_id); any lineage.* fact the caller sends is discarded. After the rules answer, the step is refused with delegation_not_permitted when the parent's rule sets delegable: false or a delegableTo list that does not include this action, and with delegation_amplified when a numeric fact is larger than the parent recorded. A child grant never outlives its parent, the parent joins the approval key, the answer carries lineage { parentDecisionId, rootDecisionId, depth }, and verifyDecision reports a delegated grant invalid once any grant above it is revoked. Omit it for a step that is not delegated (depth 0).

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "decideAction",
    "parameters": {
      "agent": "library-curator",
      "action": "catalogue.read",
      "resourceRef": "entry_8891",
      "payloadHash": "9f2c1b…",
      "actionDescription": "Read the entry for the exhibition checklist.",
      "context": {
        "resource.classification": "internal",
        "recipient.class": "internal"
      }
    }
  }'

Example response

{
  "statusCode": 200,
  "operation": "decideAction",
  "requestId": "b5f24c81-…",
  "decision": {
    "decisionId": "1f0e5c2b-…",
    "effect": "grant",
    "rationale": "The library curator is allowed to read catalogue entries of its own workspace.",
    "policyVersion": "builtin:builtin-1",
    "issuedAt": "2026-09-04T08:00:01.000Z",
    "expiresAt": "2026-09-04T08:05:01.000Z",
    "signature": "6c1a…",
    "publicSignature": "Zx3v…",
    "keyId": "a3f19c0d7be24e51",
    "engine": { "mode": "local", "endpoint": null }
  }
}

// Held for a person — poll getApprovalStatus, then repeat the identical
// decideAction body (not just the same payloadHash) to claim an approval:
{
  "statusCode": 200,
  "operation": "decideAction",
  "requestId": "b5f24c82-…",
  "decision": {
    "decisionId": "1f0e5c2c-…",
    "effect": "pending",
    "approvalId": "7c1e…",
    "rationale": "This step needs a person in the workspace to approve it.",
    "policyVersion": "workspace-3",
    "issuedAt": "2026-09-04T08:00:01.000Z",
    "engine": { "mode": "local", "endpoint": null }
  }
}

// The rules allowed this step, but the paired policy library's Gate held it for
// a person. The reading that caused the hold is recorded with the decision and
// reused when the approval is claimed — see listPolicyDecisions.
{
  "statusCode": 200,
  "operation": "decideAction",
  "requestId": "b5f24c84-…",
  "decision": {
    "decisionId": "1f0e5c2e-…",
    "effect": "pending",
    "approvalId": "9d4b…",
    "rationale": "The rules allowed this step, but the policy library gate held it for a person because its reading was "requires_approval".",
    "policyVersion": "workspace-3",
    "issuedAt": "2026-09-04T08:00:01.000Z",
    "signature": "",
    "engine": { "mode": "local", "endpoint": null },
    "libraryGate": "library_gate_held"
  }
}

// The same step with the Gate's fallback set to refuse — refused outright. No
// approval is opened and nothing can be claimed:
{
  "statusCode": 200,
  "operation": "decideAction",
  "requestId": "b5f24c85-…",
  "decision": {
    "decisionId": "1f0e5c2f-…",
    "effect": "reject",
    "rationale": "The rules allowed this step, but the policy library gate refused it because its reading was "prohibits".",
    "policyVersion": "workspace-3",
    "issuedAt": "2026-09-04T08:00:01.000Z",
    "signature": "",
    "engine": { "mode": "local", "endpoint": null },
    "libraryGate": "library_gate_refused"
  }
}

// Neither Gate outcome carries a usable signature; in a real workspace the
// rule's own rationale comes first and the gate sentence is appended to it.
// A held step is claimed with the identical decideAction body once approved.

// Enforced context contract, a required fact missing — undecidable, not refused.
// Nothing was granted and nobody was asked: add the fact and call decideAction again.
{
  "statusCode": 200,
  "operation": "decideAction",
  "requestId": "b5f24c83-…",
  "decision": {
    "decisionId": "1f0e5c2d-…",
    "effect": "reject",
    "error": "context_validation_failed",
    "contractRevision": "r3",
    "contextIssues": [
      { "field": "resource.classification", "code": "missing" },
      { "field": "recipient.class", "code": "value" }
    ],
    "rationale": "context_validation_failed: the request for catalogue.read did not carry the facts this workspace requires, so it could not be decided.",
    "policyVersion": "workspace-3",
    "issuedAt": "2026-09-04T08:00:01.000Z",
    "engine": { "mode": "local", "endpoint": null }
  }
}

Common errors: invalid_api_key · invalid_parameters · scope_denied · rate_limited · idempotency_key_mismatch

describePolicyEngine

Required scope: policy.read

Each action may additionally publish context: { mode: published | enforced, revision, facts: [{ field, kind, required, values?, label?, meaning?, source?, min?, max?, minInclusive?, maxInclusive?, integer?, unit?, acceptNumericText?, asserters?, provenanceRequired?, maxAgeSeconds? }] }. Kinds are enum (string values), text, boolean and number. A number fact expects a JSON number; its optional limits, whole-number setting and unit describe which numbers are valid information, and a value outside them is reported with the issue code range. A number sent as text is rejected unless the fact sets acceptNumericText. A fact with provenanceRequired expects { value, assertedBy, observedAt } instead of a bare value; assertedBy must be one of the named asserters and observedAt no older than maxAgeSeconds, otherwise the issue codes provenance and stale are returned and the request is undecidable. Required is advisory while the contract's mode is published. When a workspace switches an action's contract to enforced, required facts are checked before any rule runs and a request that omits one, sends the wrong type or an unlisted value is returned as context_validation_failed with contractRevision and contextIssues[] — undecidable rather than refused. Optional facts are validated only when supplied. Catalogue values remain strings without coercion. Extra context fields are permitted.

Revisions change on contract edits; absent context means no published expectations. Remote mode returns the remote engine’s description, not a local enforcement guarantee: when an action is enforced locally and the workspace uses an external engine, decideAction fails closed with context_enforcement_unsupported instead of returning a decision without the check.

The policy engine describes itself: identity, mode, rule set version and source, default effect, and every action it answers for with the payload fields each decision is bound to. The action list is the workspace's own declared vocabulary — key, label, optional description and what the payload hash binds — maintained by its administrators; an action absent from it matches no rule and falls to the default effect, which is a refusal. The current describePolicyEngine response does not include an action's policy-library consultation mode or Gate fallback. Read those in Workspace Policies; do not infer Off, Advisory or Gate from the action description.

The optional 43-action catalogue is not returned wholesale: only imported or manually declared workspace actions appear. Imports never overwrite local definitions or install rules; existing broad grants or a grant default can nevertheless cover a newly imported action.

Map provider operations to exact consequence-based keys (for example, an outbound email to email.send rather than generic api.mutate). Suggested conditions use literal top-level context keys: {"recipient.class":"internal"} or {"record.legal_hold":false}, not nested objects. The corresponding Request field equals one of condition uses recipient.class = internal or record.legal_hold = false. A Request number compares condition tests a numeric context field against a threshold (below, at most, above, at least, between) and is the place for permission thresholds; a contract's limits only say whether the number is valid at all. Missing facts fail that equality check, not necessarily the entire ordered rule set.

Trusted application code must populate facts, freeze context for retries, and bind safety-relevant intent into payloadHash. Descriptive binding notes do not cause ACR to inspect content, scan files, enforce budgets or configure connectors. This is the contract an external engine must implement to be swappable. A policy-engine operation — when the workspace uses an external engine, that engine's own answer is returned.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "describePolicyEngine",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "describePolicyEngine",
  "requestId": "9c4e2af1-…",
  "engine": {
    "engine": "acr-builtin",
    "mode": "local",
    "ruleSetVersion": "2026-09-01",
    "ruleSetSource": "workspace",
    "defaultEffect": "reject",
    "ruleCount": 4,
    "ruleSetValid": true,
    "selfDescribed": true,
    "actions": [
      {
        "action": "catalogue.write",
        "label": "Update the catalogue",
        "description": "Writing a new or changed catalogue entry.",
        "boundPayload": "the hash of the exact entry to be written"
      }
    ]
  }
}

Common errors: invalid_api_key · scope_denied · rate_limited

explainDecision

Required scope: policy.read

Returns why one decision came out the way it did: the effect, the rule set version, the engine that answered and where the full rationale record is stored. A policy-engine operation — it moves with the engine if the engine is relocated.

Parameters

NameTypeDescription
decisionIduuidrequiredThe decision to explain.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "explainDecision",
    "parameters": { "decisionId": "3d47…" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "explainDecision",
  "requestId": "cc31be04-…",
  "explanation": {
    "decisionId": "3d47…",
    "effect": "grant",
    "action": "catalogue.read",
    "policyVersion": "2026-09-01",
    "engineMode": "local",
    "recordPath": "policy/decisions/2026/09/3d47….json",
    "rationaleLocation": "The full rationale is in this workspace's own storage at the path shown."
  }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · not_found · rate_limited

decideToolCall

Required scope: policy.decide

The same decision as decideAction, asked in the shape an MCP gateway or proxy already holds for a tools/call. Send the tool name and its arguments; ACR computes the payload fingerprint itself and runs exactly the same rules, Complyee Policy Library reading, approvals and signing as decideAction, so the outcome is identical.

Fingerprint (sha256-canonical-json-v1): SHA-256, lowercase hex, over the canonical JSON of { "tool": toolName, "arguments": toolArguments } — object keys sorted, no whitespace, arrays in order. A gateway recomputes it over the call it is about to forward and checks it equals decision.payloadHash before checking the grant signature, so a grant for one tool or argument set cannot be reused for another.

The answer adds an mcp block: allowExecution is true only for a signed grant; holdToken is the approvalId when a person must decide first — return 202 to the agent, poll getApprovalStatus, then repeat the identical call to claim. Tool arguments are used only to compute the fingerprint and are never stored or forwarded.

Parameters

NameTypeDescription
agentstringrequiredAgent key asking for permission.
toolNamestringrequiredThe MCP tool name from tools/call. Used as the action when action is omitted.
toolArgumentsobjectrequiredThe tool's arguments exactly as they will be forwarded. At most 64 KB as canonical JSON.
actionstringoptionalThe workspace action to judge, when it differs from the tool name.
resourceRefstringoptionalWhat the tool acts on — a record id, account or path.
contextobjectoptionalSame as decideAction: frozen, literal top-level facts the rules may use.
actionDescriptionstringoptionalSame as decideAction: the one plain-language field sent to the Complyee Policy Library when the action is marked for consultation.
parentDecisionIdstringoptionalSame as decideAction: the grant this tool call is delegated from. Checked, attenuated and recorded identically.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "decideToolCall",
    "parameters": {
      "agent": "library-curator",
      "toolName": "catalogue.read",
      "toolArguments": { "entryId": "entry_8891" },
      "resourceRef": "entry_8891",
      "context": { "resource.classification": "internal" }
    }
  }'

Example response

{
  "statusCode": 200,
  "operation": "decideToolCall",
  "requestId": "5b0e41d9-…",
  "decision": {
    "decisionId": "3d47…",
    "effect": "grant",
    "action": "catalogue.read",
    "payloadHash": "e3a1…",
    "expiresAt": "2026-10-09T18:21:00.000Z",
    "signature": "b81f…",
    "publicSignature": "q9Zk…",
    "keyId": "acr-ed25519-1"
  },
  "mcp": { "allowExecution": true, "payloadHashAlgorithm": "sha256-canonical-json-v1" }
}

Common errors: invalid_api_key · invalid_parameters · scope_denied · rate_limited · idempotency_key_mismatch

reportExecutionOutcome

Required scope: policy.decide

Closes the loop from permission to execution. After acting on a grant, the enforcement point reports what happened: executed, aborted, or downstream_error. The report is evidence only — it never changes the decision, never reopens an approval and never grants anything. Only a grant can be reported on, and payloadHash must match the hash the grant is bound to. Each decision takes one outcome: an identical repeat returns the stored record (replayed: true), a different outcome is refused with conflict. An executed report after the grant expired or was revoked is still recorded, with flags late or revokedAtReport — the report is a fact, the flag is the finding. No payload or result content is sent; detailCode is a short machine code only. The outcome then appears on getDecision and listPolicyDecisions as an execution block.

Parameters

NameTypeDescription
decisionIduuidrequiredThe grant that was acted on.
outcomestringrequiredexecuted | aborted | downstream_error.
payloadHashstringrequiredSHA-256, 64 hex characters, of the payload that was acted on; must match the grant.
occurredAtstringoptionalISO 8601 time the step ran or stopped. Defaults to the time of the report.
detailCodestringoptionalShort machine code (letters, digits, . _ : -, up to 64), e.g. core_banking_timeout. Never free text.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "reportExecutionOutcome",
    "parameters": { "decisionId": "3d47…", "outcome": "executed", "payloadHash": "9f2c…", "occurredAt": "2026-10-09T18:24:11Z" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "reportExecutionOutcome",
  "requestId": "c19a4e07-…",
  "execution": {
    "decisionId": "3d47…",
    "outcome": "executed",
    "reportedAt": "2026-10-09T18:24:12Z",
    "occurredAt": "2026-10-09T18:24:11Z",
    "detailCode": null,
    "flags": {},
    "replayed": false
  }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · not_found · not_a_grant · payload_mismatch · conflict · rate_limited

verifyDecision

Required scope: policy.read

Checks whether a decision is still a live grant for exactly this payload: right id, right payload hash, not expired, and a grant rather than a refusal. A policy-engine operation — an enforcement point running in the calling application calls this before acting on a decision it was handed.

Parameters

NameTypeDescription
decisionIduuidrequiredThe decision to check.
payloadHashstringrequiredSHA-256, 64 hex characters, of the exact payload about to be acted on.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "verifyDecision",
    "parameters": { "decisionId": "3d47…", "payloadHash": "9f2c…" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "verifyDecision",
  "requestId": "aa71f0c2-…",
  "verification": {
    "valid": false,
    "reason": "Decision does not match this payload.",
    "decision": { "decisionId": "3d47…", "effect": "grant", "action": "catalogue.read" }
  }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · rate_limited

getDecisionGraph

Required scope: audit.read

Returns the whole delegation tree a decision belongs to — a verifiable execution graph built only from ACR's own decision index. Every decision sharing the same root is listed with its agent, action, effect, reason code, depth, payload fingerprint and reported execution outcome, joined by parent → child edges, with counts of granted, refused, held, revoked, executed and failed steps and of open leaves (granted, no outcome reported yet). Metadata only: no prompts, payloads or agent messages exist in ACR to include. Up to 500 nodes; truncated is true beyond that. Workspace administrators can also download the same graph as a signed file (acr-decision-graph-…json) carrying the verification keys and an Ed25519 fileSignature over acr-graph-v1|<canonical JSON of the file without fileSignature>. An audit operation.

Parameters

NameTypeDescription
decisionIduuidrequiredAny decision in the chain; the graph is resolved from its root.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "getDecisionGraph",
    "parameters": { "decisionId": "7c1e…" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "getDecisionGraph",
  "requestId": "e2b1…",
  "graph": {
    "rootDecisionId": "3d47…",
    "requestedDecisionId": "7c1e…",
    "nodes": [
      { "decisionId": "3d47…", "parentDecisionId": null, "agentKey": "orchestrator", "action": "prepare_payment", "effect": "grant", "depth": 0, "status": "granted_executed", "execution": { "outcome": "executed", "reportedAt": "2026-10-16T09:01:02Z" } },
      { "decisionId": "7c1e…", "parentDecisionId": "3d47…", "agentKey": "payments-agent", "action": "transfer_funds", "effect": "pending", "depth": 1, "status": "held", "execution": null }
    ],
    "edges": [{ "from": "3d47…", "to": "7c1e…" }],
    "stats": { "nodes": 2, "maxDepth": 1, "granted": 1, "refused": 0, "held": 1, "revoked": 0, "executed": 1, "failed": 0, "openLeaves": 0 },
    "truncated": false,
    "generatedAt": "2026-10-16T09:05:00Z"
  }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · not_found · rate_limited

getGrantVerificationKey

Required scope: policy.read

Returns the public keys that sign grants, so any enforcement point — an API or MCP gateway, an agent orchestrator, or the backend that performs the work — can check a grant offline, without a call back to this engine. Every grant carries publicSignature (Ed25519, base64url) and keyId beside the existing signature. To check one: find the key with the matching keyId, rebuild the message acr-grant-v2|workspaceId|keyId|decisionId|payloadHash|expiresAt|parentDecisionId|rootDecisionId|delegationDepth using the payload hash you computed yourself and the grant's lineage block (for a root grant: empty parent, root = decisionId, depth 0 — so a delegated grant cannot pass as a root grant), verify the signature, and refuse if expiresAt has passed. Refusals and held steps are never signed. During a key rotation both the current and previous key are listed, so grants already issued stay checkable until they expire. A local check cannot see a revocation; call verifyDecision when a grant revoked before expiry must be caught. A policy-engine operation.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "getGrantVerificationKey",
    "parameters": {}
  }'

Example response

{
  "statusCode": 200,
  "operation": "getGrantVerificationKey",
  "requestId": "b41e09d2-…",
  "verificationKeys": {
    "workspaceId": "5b0c…",
    "messageFormat": "acr-grant-v2|workspaceId|keyId|decisionId|payloadHash|expiresAt|parentDecisionId|rootDecisionId|delegationDepth",
    "signatureEncoding": "base64url",
    "configured": true,
    "keys": [
      {
        "keyId": "a3f19c0d7be24e51",
        "algorithm": "Ed25519",
        "status": "current",
        "publicKey": "q8Hk…",
        "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "q8Hk…", "kid": "a3f19c0d7be24e51", "use": "sig", "alg": "EdDSA" }
      }
    ]
  }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · rate_limited

listApprovals

Required scope: policy.read

Lists the steps waiting for a person, and how earlier requests were answered. A rule with the effect require_approval does not refuse and does not grant: it holds the step and records a request naming the agent, the action, the resource and the fingerprint of the exact payload — never the payload itself. Statuses use the same five words as getApprovalStatus — pending, granted, refused, expired, consumed — so one record reads the same on every screen and in every operation. A policy-engine operation.

Parameters

NameTypeDescription
statusstringoptionalpending (default), granted, refused, expired or all. The older words approved and rejected are accepted synonyms for granted and refused.
limitnumberoptional1-200, default 50.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "listApprovals",
    "parameters": { "status": "pending" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "listApprovals",
  "requestId": "aa71f0c2-…",
  "approvals": [
    {
      "id": "7c1e…",
      "agentKey": "library-curator",
      "action": "catalogue.write",
      "resourceRef": "entry:41b9…",
      "payloadHash": "9f2c…",
      "status": "pending",
      "requestedAt": "2026-09-05T08:00:01.000Z",
      "expiresAt": "2026-09-06T08:00:01.000Z"
    }
  ]
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · rate_limited

getApprovalStatus

Required scope: policy.read

Asks after one waiting step by the approvalId a pending decide returned. Read-only: it mints nothing and changes nothing, so it is the operation to poll while a person is deciding.

The answer is pending (nobody has answered yet), granted (approved and still claimable — re-issue the identical decideAction call once to receive the signed grant), refused (a person said no), expired (nobody answered in time, or the approval lapsed unclaimed) or consumed (the grant was already minted and spent). It also names the policy and rule that held the step and the approver's reason once there is one. expiresAt is always present: while pending it is the seven-day answer deadline, and once granted it is the fifteen-minute claim deadline, so a caller can always display the time remaining.

Every terminal answer also carries its own next step. refused: the step is blocked by a person's decision — do not retry the same request; take the step back to your own queue and send a fresh decideAction only if the circumstances (or the policy) have genuinely changed.

expired: nobody answered in time or the grant lapsed unclaimed — the step stays blocked; re-issue the identical decideAction call, unchanged payload and context, to open a fresh request rather than reusing this approvalId.

consumed: the grant was already minted and spent — consumedAt and resultingDecisionId show when and under which decision; check getAgentRun to see what that grant covered before concluding the step never ran, and never send the step again on the strength of this answer.

A not_found error means the approvalId is unknown in this workspace — compare the id you stored and the workspace key you sent; a not_found alone is never a signal to re-submit the step. A policy-engine operation.

Parameters

NameTypeDescription
approvalIduuidrequiredThe id the pending decide returned.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "getApprovalStatus",
    "parameters": { "approvalId": "7c1e…" }
  }'

Example response

{
  "statusCode": 200,
  "operation": "getApprovalStatus",
  "requestId": "aa71f0c2-…",
  "approval": {
    "approvalId": "7c1e…",
    "status": "granted",
    "claimable": true,
    "agent": "library-curator",
    "action": "catalogue.write",
    "resourceRef": "entry:41b9…",
    "payloadHash": "9f2c…",
    "policy": "Catalogue abstraction",
    "ruleId": "abstract-write",
    "requestedAt": "2026-09-05T08:00:01.000Z",
    "expiresAt": "2026-09-05T08:15:01.000Z",
    "decidedAt": "2026-09-05T08:00:44.000Z",
    "decisionReason": "Reviewed by the data office.",
    "resultingDecisionId": null,
    "consumedAt": null
  }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · not_found · rate_limited

decideApproval

Required scope: policy.admin

Answers one waiting step. An approval is not itself a grant: it lets the agent's next attempt at exactly that payload receive one short-lived, single-use decision. A different payload needs a new request, and an approval can never bless work already attempted. A policy-engine operation.

Parameters

NameTypeDescription
approvalIduuidrequiredThe waiting request.
approvebooleanrequiredtrue to approve, false to refuse.
reasonstringoptionalRecorded in the audit trail.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "decideApproval",
    "parameters": { "approvalId": "7c1e…", "approve": true, "reason": "Reviewed by the data office." }
  }'

Example response

{
  "statusCode": 200,
  "operation": "decideApproval",
  "requestId": "aa71f0c2-…",
  "approval": { "id": "7c1e…", "status": "granted", "expiresAt": "2026-09-05T08:15:01.000Z" }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · not_found · rate_limited

revokeDecision

Required scope: policy.admin

Takes a grant back before it is used. The enforcement point refuses any step presenting a revoked decision, even inside its validity window. A policy-engine operation.

Parameters

NameTypeDescription
decisionIduuidrequiredThe grant to withdraw.
reasonstringoptionalRecorded in the audit trail.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "revokeDecision",
    "parameters": { "decisionId": "3d47…", "reason": "Issued during an incident." }
  }'

Example response

{
  "statusCode": 200,
  "operation": "revokeDecision",
  "requestId": "aa71f0c2-…",
  "decision": { "decisionId": "3d47…", "effect": "grant", "revokedAt": "2026-09-05T08:20:00.000Z" }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · not_found · rate_limited

putRuleSet

Required scope: policy.admin

Replaces the rule set the policy engine applies for this workspace. Leaving actions out keeps the workspace's action vocabulary and its context contracts exactly as they are — replacing rules can never erase them by accident. Send the full actions list only to replace the vocabulary, contracts included; removal is then a deliberate, complete list. Context contracts accept mode published or enforced, up to 50 unique literal fields, enum/text/boolean kinds, and up to 100 unique enum values. The server assigns contract revisions; an enforced contract must mark at least one fact required, and switching an action to enforced changes decision behaviour for that action immediately. Rules are ordered and the first match wins; each has an effect of grant, reject or require_approval. The set is validated against the agents and actions that actually exist before it is stored in the workspace's own bucket — a rule about something that does not exist is a governance illusion, not a control. A policy-engine operation.

Parameters

NameTypeDescription
versionstringrequiredYour name for this rule set, quoted in every rationale.
defaultEffectstringrequiredgrant or reject, applied when no rule matches.
rulesarrayrequiredOrdered rules of { id, policy?, conditions?, obligations?, effect, reason? }. A rule matches when every one of its conditions holds; no conditions means every request.
actionsarrayoptionalThe complete action vocabulary of { key, label?, description?, boundPayload?, retired?, context? }. Omit it to keep the existing vocabulary and contracts unchanged; send it to replace the vocabulary in full.

Example request

curl -X POST https://<tenant>.agent-cr.com/api/public/v1/gateway \
  -H "content-type: application/json" \
  -H "x-api-key: cmp_live_xxxxxxxxxxxxxxxx" \
  -d '{
    "operation": "putRuleSet",
    "parameters": { "version": "workspace-3", "defaultEffect": "reject", "rules": [ { "id": "curator-read", "effect": "grant", "conditions": [ { "type": "agent_is", "values": ["library-curator"] }, { "type": "action_is", "values": ["catalogue.read"] } ] }, { "id": "curator-write-needs-approval", "effect": "require_approval", "conditions": [ { "type": "action_is", "values": ["catalogue.write"] } ] } ] }
  }'

Example response

{
  "statusCode": 200,
  "operation": "putRuleSet",
  "requestId": "aa71f0c2-…",
  "engine": { "mode": "local", "ruleSetVersion": "workspace-3", "ruleCount": 2, "defaultEffect": "reject" }
}

Common errors: invalid_api_key · scope_denied · invalid_parameters · rate_limited

Registering this engine in your CMDB or AI registry

The engine describes itself. describeSystem returns one machine-readable record of this system as deployed for your workspace: what it is for, what it processes, how long records are kept, who can reach it, which human-oversight surfaces exist, and which sub-processors are involved. It is written to be stored as a configuration item — we report configuration, we do not assess it and we make no claim that your organisation is compliant.

The framing is the deployer's, not ours: EU AI Act Article 26 expects the organisation operating an AI system to know its purpose, its oversight arrangements, its logging and who is accountable for it, and NIST AI RMF GOVERN expects documented ownership, intended use, risk tiering, third parties and change control. Those facts should come from the system itself rather than from a spreadsheet that ages.

The polling pattern

Poll on a schedule — nightly is enough. Every record carries recordHash, a stable hash over the record with the timestamp removed, so an unchanged system is a cheap no-op. When the hash moves, pull listSystemChanges for the narrative of what changed and write both to the configuration item.

state = { recordHash, lastSyncedAt }      # stored on your CI

record = engine.describeSystem()

if record.recordHash != state.recordHash:
    ci.update(map(record))                       # see the field mapping below
    changes = engine.listSystemChanges(          # needs the audit.read scope
        from = state.lastSyncedAt.date(),
        to   = today())
    for change in changes:
        ci.addChangeEntry(change.at, change.category, change.summary, change.actor)
    state.recordHash = record.recordHash

state.lastSyncedAt = now()

Field mapping

A workable default mapping onto a typical CI.

system.name, system.vendorCI name and manufacturer / vendor
workspace.slug, workspace.addressInstance name and URL
purpose.intendedUse, purpose.excludedUsesIntended use and use restrictions
components[]Dependency / relationship records (region, storage, interface, sub-processors)
data.retentionRetention and logging attributes
accessControlsAccess control attributes (SSO, network, sessions, API)
recordVersion, recordHash, generatedAtSchema version, change detection and last-verified timestamp

There is no push or webhook: polling is what CMDB integrations do, and it means we never hold your endpoints or secrets. describeSystem and getPostureSnapshot need no scope beyond a valid key; listSystemChanges and getSystemDossier need audit.read.

Evidence for auditors: the system dossier

A registry records what the system is today. An auditor also asks what it was over a period and what changed. getSystemDossier answers that in one file: a plain-language cover, the full system record, the security checklist as it read at that moment, the configuration changes in the period, and a manifest counting the audit records held for it — by stream and by action. The audit records themselves are not copied; they stay in your own storage bucket, and the manifest tells an auditor what exists there.

Archive it quarterly or annually rather than nightly; describeSystem with recordHash is the cheap daily check. Workspace administrators can download the same file from the Security page without an integration. getPostureSnapshot returns just the checklist, if you want it on its own dashboard. Nothing in the dossier asserts that your organisation is compliant — it states what the system is, how it was configured and what evidence exists.

Agent governance

Agents run in the applications you operate; this engine decides what they may do. An agent never decides what it is allowed to do itself. Before it acts, the application asks this engine. Local rules evaluate in order; if configured, an external policy-library reading may be recorded as advice or used only to tighten an Allow. Every answer is recorded with its reason. When a rule or Gate asks for a person, the step is held and the calling application must ask again to claim the approval once it is given.

The interface is deliberately small. decideAction, getDecision, verifyDecision, explainDecision, listPolicyDecisions, listRules and describePolicyEngine are what a governed application calls; putRuleSet, listApprovals, decideApproval and revokeDecision are what a governance system calls. Running agents and performing the governed work stay in the applications this engine serves. The same request and response shapes apply whichever workspace you point at, so integrators write against one contract. An engine that cannot be reached blocks the waiting step rather than letting it through.

Decisions are bound to a payload

A decision carries a payloadHash, an expiry and a signature over both. The code that performs the side effect re-hashes what it is about to act on and refuses if the hash or the expiry does not match, so a grant for one payload can never be replayed for another. Decision records — including the rationale and the request context — are written to the workspace's own storage under policy/decisions/; the database keeps only a non-sensitive index.

Nothing happens without a decision

Enforcement is not advisory. Every privileged step an agent takes runs through one guard: it asks the policy engine, verifies that the answer is a grant, is unexpired and is signed over the hash of exactly what the step is about to touch, records the step, and only then performs the work. A refusal, an expiry, a payload that does not match, a revoked decision, a step still waiting for a named person to approve it, a disabled agent, a paused workspace, a stop request or an engine that cannot answer all end the same way — the step is blocked and logged, never performed. Grants are single-use and short-lived, so one cannot be carried to the next step. Use getAgentRun to see, step by step, which decision allowed what.

Writing your own rules

A workspace can replace the built-in rule set by writing policy/rules.json into its own bucket. Rules are evaluated in order and the first match wins; if none match, defaultEffect applies. A file that cannot be parsed refuses every request rather than silently reverting to the built-in set.

{
  "version": "acme-2026-09",
  "defaultEffect": "reject",
  "rules": [
    { "id": "curator-read", "effect": "grant",
      "conditions": [
        { "type": "agent_is", "values": ["library-curator"] },
        { "type": "action_is", "values": ["catalogue.read"] }
      ],
      "reason": "The curator may read catalogue entries of this workspace." },
    { "id": "no-hr-documents", "effect": "reject",
      "conditions": [{ "type": "resource_starts_with", "values": ["hr/"] }],
      "reason": "HR material is out of scope for agents." }
  ]
}

Authentication and scopes

Each API key carries a set of scopes that decide which operations it may call. listOperations is always allowed and returns every operation with a callable flag, so an integration can discover both its own permissions and the full surface. Give each key only what its integration needs, and revoke keys that are no longer used.

Available scopes

  • audit.read — Read the audit log — listAuditEvents, listSystemChanges, getSystemDossier. Returns audit events, the configuration change feed and the dated system dossier, including who asked what and when. Grant only to systems allowed to see this.
  • policy.decide — Ask the policy engine and report back — decideAction, decideToolCall, reportExecutionOutcome. Lets a calling system ask this engine whether an action is allowed. Grant only to the applications whose agents must be decided for, never to reporting integrations.
  • policy.read — Read decisions, rules, approvals and agents — listPolicyDecisions, getDecision, listRules, listApprovals, getApprovalStatus, verifyDecision, getGrantVerificationKey, explainDecision, describePolicyEngine, listAgents, describeAgent. Returns the decisions the engine made, the active rule set, the approval queue and the agent registry. A waiting caller also needs this to poll getApprovalStatus. Rationales stay in your own storage; this returns the index only.
  • policy.admin — Govern the policy engine — putRuleSet, decideApproval, revokeDecision, registerAgent. Changes what agents are allowed to do: the rule set, approvals of waiting steps, revocation of grants and registration of agents. The strongest scope there is — grant it only to a governance system you control.

Network restrictions

If the workspace has an IP allow-list, API calls must come from an approved network. A key can also carry its own list of approved IP ranges — that is the normal way to authorise a server-to-server integration in a network-restricted workspace. A network-scoped key answers only from those networks, regardless of where it is presented from.

Error codes

Failures return a non-200 status with an error.code you can branch on. invalid_api_key, api_disabled, wrong_entry_point, network_not_allowed and rate_limited can answer any operation, so the per-operation lists below name only what is particular to that call. context_validation_failed is not one of these: it is a 200 answer from decideAction without a usable grant, not an HTTP error. library_gate_held and library_gate_refused are recorded decision reasons, not HTTP errors:

CodeHTTPMeaning
invalid_body400The request body is not valid JSON, or does not match { operation, parameters }.
invalid_parameters400A parameter failed validation; the message names the parameter.
invalid_api_key401The x-api-key header is missing or the key is unknown or revoked.
api_disabled403API access is not enabled for this workspace.
network_not_allowed403The calling network is not on the workspace or key allow-list.
scope_denied403The key does not hold the scope this operation requires.
unknown_operation404No such operation; call listOperations to see what the key may use.
not_found404The named resource (e.g. a decision or approval id) does not exist in this workspace.
wrong_entry_point403The key was used on another address; call it on the workspace's own subdomain.
not_a_grant422reportExecutionOutcome named a refusal or held step; only a grant can be executed.
payload_mismatch422reportExecutionOutcome sent a payloadHash that differs from the one the grant is bound to.
conflict409A different execution outcome was already reported for this decision; each decision takes one.
idempotency_key_mismatch409This Idempotency-Key was already used for a different request body; use a new key when the request changes.
rate_limited429The key's per-minute limit is exceeded; respect Retry-After.
circuit_open200Not an HTTP error: a 200 decideAction answer with effect reject, because a circuit breaker for this delegation chain or for the whole workspace is open. The rationale names when it closes — stop the chain and wait.
chain_velocity_exceeded200Not an HTTP error: a 200 reject because this delegation chain reached the workspace limit on grants per chain; the chain's circuit is now open for the hold period.
chain_fanout_exceeded200Not an HTTP error: a 200 reject because the parent grant already handed out as many delegated steps as the workspace allows; the chain's circuit is now open for the hold period.
fleet_velocity_exceeded200Not an HTTP error: a 200 reject because the workspace reached its limit on grants across all agents; the workspace circuit is now open for the hold period.
rate_limit_exceeded200Not an HTTP error: a 200 decideAction answer with effect reject, because the answering rule's limit on how often it may grant is reached. The rationale names the limit and when the next slot opens — wait for that time and ask again.
internal_error500Something failed on our side; quote the requestId when contacting support.

Rate limits

Each key has a per-minute request limit shown in Workspace settings. Responses carry an X-RateLimit-Limit header; when the limit is exceeded the API answers 429 with error.code rate_limited and a Retry-After: 60 header. Back off and retry rather than hammering the endpoint.

Shared responsibility & trust boundary

A workspace API key with policy.decide permission identifies a trusted policy enforcement point: your server-side application that asks before an agent acts and enforces the result. Agent Control Room authenticates the key, checks its scope and network restrictions, validates the declared context shape, evaluates the rules and records the decision. It does not inspect the content represented by payloadHash or independently verify the truth of context facts supplied by your application.

The assertedBy and observedAt fields are caller-supplied metadata, not cryptographic proof of origin or observation time. An enforced contract can reject a missing or unlisted asserter and an old timestamp; it cannot tell whether the caller's claim is honest. Anyone who gains control of a policy.decide key can submit false facts that match an allow rule and obtain a grant. Audit evidence makes this reviewable, not impossible.

Keep policy.decide keys only in trusted backend enforcement services, never in agents or browsers. Derive policy facts from authoritative systems rather than agent claims, use least-privilege scopes and network restrictions, rotate or revoke exposed keys, and verify each signed grant against the exact payload before execution. If a rule requires independently verified source assertions, caller-supplied provenance alone is insufficient; add a separate verification integration before relying on that fact for permission. When a workspace pairs Complyee Policy Library, a consulted action sends the action and agent keys, up to 40 context facts (including extra fields if supplied), a bounded plain-language action description (up to 2,000 characters) and consultation metadata — never documents, prompts, payloads or payloadHash. Its reading is recorded and shown to approvers and can never grant anything. On actions a workspace marks as gated, a reading other than "supports" — or no reading at all — holds a step the rules allowed for a person or refuses it; the gate only ever tightens a decision.

Using the API safely

  • Call the API from your server or backend service. Never embed a key in browser JavaScript or mobile apps — an exposed key can be used to ask for decisions in your workspace.
  • If you need to call this engine from a frontend, route the request through your own backend so the key stays server-side.
  • Browser-based API tools (e.g. Postman Web) may be blocked by our edge protection. Test with desktop Postman, curl, or a server script instead.
  • Store keys somewhere safe when generated — we keep only a hash and cannot show a key again.

API access is enabled per workspace. To have it switched on, contact us at hello@complyee.ai.