> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Reference

> Complete REST API reference for the action-governance endpoints.

Action-governance endpoints are mounted under `/allow/*`, with versioned read endpoints under `/v1/allow/*`. The base URL is `https://api.visiqlabs.com`.

## Authentication

All endpoints require a Bearer credential: `Authorization: Bearer <key>`. Requests without a valid credential receive `401 Unauthorized`.

Two credential audiences exist:

* **Harness keys** — the operational credential your SDK or harness runs with. Either a `vq_prod_...` / `vq_test_...` key minted in the dashboard under **Settings → Harness Keys**, or the `allow_...` key returned once by `POST /allow/agents`. Harness keys are full-power on the operational endpoints (evaluate, bundle, telemetry, execution events, decision polling, agent self-registration, the mode stream) and **route-confined**: calling any management endpoint with one returns `403 {"error": "harness_key_not_permitted"}`.
* **Management keys** — general automation credentials governed by explicit permission grants. They can call every endpoint on this page. Management keys are **launching soon**: they are visible in the dashboard under **Settings → API Keys**, but creating one is not yet enabled. Until then, drive the management endpoints from the dashboard, which authenticates with your session.

Scoped keys are additionally checked against their granted scopes. Evaluation endpoints require `rules:evaluate` (or the legacy `allow:write`); read endpoints require `rules:read` (or the legacy `allow:read`); `full_access` satisfies everything. A key without the required scope receives `403 {"error": "insufficient_scope"}` listing the required and granted scopes.

### Rate limiting

Every API-key request passes a per-key sliding-window rate limit (default 600 requests per 60 seconds). Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers; exceeding the window returns `429` with a `Retry-After` header and body `{"error": "rate_limited", "detail": "API key rate limit exceeded.", "retryAfter": <seconds>}`.

### List responses

Every list endpoint on this page returns the same envelope:

```json theme={null}
{
  "data": [ ... ],
  "total": 128,
  "page": 1,
  "pageSize": 50
}
```

Pagination is controlled by `page` (default `1`) and `limit` (default `50`, max `100`) query parameters.

***

## Evaluation

The endpoints your SDK or harness calls at runtime. All of them accept a harness key.

***

### POST /allow/evaluate

Evaluate an action against your rule set and return an authorization decision. The SDK evaluates most actions locally from the cached rule bundle — this endpoint handles human-approval flows, uncovered-scenario fallback, and remote evaluation.

**Scope:** `rules:evaluate` or `allow:write`

**Request body:**

```json theme={null}
{
  "agent_id": "billing-agent",
  "target_app": "stripe.com",
  "action": "POST /v1/charges",
  "context": {
    "amount": 5000,
    "currency": "usd"
  }
}
```

| Field        | Type     | Required | Description                                                                   |
| ------------ | -------- | -------- | ----------------------------------------------------------------------------- |
| `agent_id`   | `string` | Yes      | Identifier of the agent making the request (1–255 chars)                      |
| `target_app` | `string` | Yes      | Hostname or app identifier being called (1–255 chars)                         |
| `action`     | `string` | Yes      | The action string — typically HTTP method + path or a tool name (1–255 chars) |
| `context`    | `object` | No       | Key-value context for rule matching. Default `{}`                             |
| `telemetry`  | `object` | No       | Opaque client telemetry, attached to the decision's signed record envelope    |

<Note>
  An `agent_id` the platform has never seen is **auto-provisioned in monitor mode** (observe-only) and the evaluation proceeds — there is no 404 for unregistered agents. The new agent appears on the dashboard Agents page, ready to be promoted to enforce.
</Note>

**Response:**

```json theme={null}
{
  "decision_id": "550e8400-e29b-41d4-a716-446655440000",
  "decision": "permit",
  "reason": "Matched rule: Allow Stripe reads",
  "rule_code": "R-1042",
  "enforced": true,
  "agent_mode": "enforce",
  "plane": "action",
  "operation": "write",
  "is_retrieval": false
}
```

| Field                 | Type                                                | Description                                                                                                                                                                                 |
| --------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decision_id`         | `string` (UUID)                                     | Unique ID for this decision — used for approval polling, execution events, and audit                                                                                                        |
| `decision`            | `permit` \| `deny` \| `approval_required` \| `mask` | The authorization outcome                                                                                                                                                                   |
| `reason`              | `string`                                            | Human-readable explanation                                                                                                                                                                  |
| `rule_code`           | `string` \| `null`                                  | The matched rule's `R-####` code, or a synthetic default-policy code (`D-READ-ALLOW`, `D-WRITE-DENY`, `D-DELETE-ASK`, …) when a no-coverage default decided                                 |
| `enforced`            | `boolean`                                           | `true` when the agent runs in enforce mode. `false` means the decision is observational ("would have blocked")                                                                              |
| `agent_mode`          | `enforce` \| `monitor` \| `off`                     | The agent's server-authoritative mode at decision time                                                                                                                                      |
| `plane`               | `string` \| `null`                                  | Classification of the action's schema (`action`, `retrieval`, …); `null` until the schema is mapped                                                                                         |
| `operation`           | `string` \| `null`                                  | Canonical operation verb from the schema mapping, when available                                                                                                                            |
| `is_retrieval`        | `boolean`                                           | Whether the event classifies as a retrieval                                                                                                                                                 |
| `arg_redaction_rules` | `array`                                             | Present on a `mask` decision — see below                                                                                                                                                    |
| `hitl_fallback`       | `deny` \| `mask`                                    | Present on an `approval_required` decision when a matched rule routed the action to approval; absent on gates raised by the no-coverage `ask` default (treat absence as `deny`) — see below |

**The `mask` decision** is allow-with-transform: the action proceeds, but the harness must first redact the named arguments. The response carries the directives to apply:

```json theme={null}
{
  "decision_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "decision": "mask",
  "reason": "Card numbers are masked for this agent",
  "rule_code": "R-1077",
  "enforced": true,
  "agent_mode": "enforce",
  "plane": "action",
  "operation": null,
  "is_retrieval": false,
  "arg_redaction_rules": [
    { "field": "card_number", "mode": "partial", "keepLast": 4, "maskChar": "*" }
  ]
}
```

Each redaction directive may include `field`, `pattern`, `replacement`, `mode` (`full` | `partial` | `email` | `custom`), `keepFirst`, `keepLast`, `maskChar`, and `keepPattern`. A `mask` decision whose rule resolves to no usable directives is downgraded to `deny` server-side — the platform never returns a mask that masks nothing.

**The `approval_required` decision** holds the action while a human approves or rejects it (see [Human-in-the-Loop](/rules/action/hitl)). The approval window is capped at **120 seconds** — an agent never blocks longer than 2 minutes. `hitl_fallback` tells the harness what to do if no human responds in time: `deny` (the default, fail-closed) or `mask` (proceed with the accompanying `arg_redaction_rules` applied).

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `429 Too Many Requests`, `500 Internal Server Error`

***

### GET /allow/rules/bundle

Fetch the compiled rule bundle for local evaluation. The SDK caches this bundle and revalidates it in the background, so decisions on the hot path never wait on the network.

**Query parameters:**

| Parameter  | Required | Description                                                                 |
| ---------- | -------- | --------------------------------------------------------------------------- |
| `agent_id` | Yes      | The agent the bundle is compiled for — the bundle carries that agent's mode |

<Note>
  `agent_id` is required — calling this endpoint without it returns `400 Invalid query parameters`. A first bundle pull for an unknown `agent_id` auto-provisions the agent in monitor mode, exactly like `POST /allow/evaluate`.
</Note>

**Request headers:**

| Header          | Description                                                                                     |
| --------------- | ----------------------------------------------------------------------------------------------- |
| `If-None-Match` | ETag from a previous response. The server returns `304 Not Modified` if the bundle is unchanged |

**Response:**

```json theme={null}
{
  "version": "a3b4c5d6e7f8...",
  "agent_mode": "enforce",
  "rules": [
    {
      "id": "rule-uuid",
      "rule_code": "R-1042",
      "name": "Allow Stripe reads",
      "description": "Permit read-only Stripe API calls",
      "effect": "allow",
      "rego_source": "package allow\n\ndefault allow = false\n...",
      "resource_type": "stripe.com",
      "target_app": "stripe.com",
      "action_pattern": "GET *",
      "conditions": [
        { "field": "resource_type", "operator": "equals", "value": "stripe.com" },
        { "field": "action", "operator": "glob", "value": "GET *" }
      ],
      "priority": 10
    }
  ],
  "no_coverage": {
    "no_coverage_defaults": { "read": "approve", "write": "approve", "delete": "approve", "admin": "approve" },
    "autopilot_enabled": false,
    "enduser_hitl_enabled": true,
    "hitl_timeout_seconds": 120
  }
}
```

| Field         | Description                                                                                                                                                                                                                              |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`     | SHA-256 hash of the serialized payload. Changes whenever a rule, the agent's mode, or the no-coverage settings change                                                                                                                    |
| `agent_mode`  | The agent's current mode, so the SDK branches on enforce / monitor / off without an extra round-trip                                                                                                                                     |
| `rules`       | Enabled action rules, sorted by priority descending. The SDK evaluates `rego_source` directly (with `target_app` / `action_pattern` as pre-filters) — the identical match loop the server runs. `conditions[]` is deprecated back-compat |
| `no_coverage` | Your organization's no-coverage policy, so the SDK resolves **uncovered** actions locally too — no round-trip for anything                                                                                                               |

**Response headers:**

| Header          | Value                                       |
| --------------- | ------------------------------------------- |
| `ETag`          | `"<sha256-version>"` (quoted, per RFC 7232) |
| `Cache-Control` | `private, max-age=60`                       |

**Status codes:** `200 OK`, `304 Not Modified`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### GET /v1/allow/decisions/:id

Poll the status of a decision by its ID. The SDK polls this every 2 seconds while a human approval is pending, until the decision resolves or the 120-second window lapses.

**Scope:** `rules:read` or `allow:read`

**Path parameter:** `:id` — UUID of the decision (from a `POST /allow/evaluate` response)

**Response:**

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "vendor_id": "vendor-uuid",
  "agent_id": "billing-agent",
  "target_app": "stripe.com",
  "action": "POST /v1/charges",
  "context": { "amount": 5000 },
  "decision": "approval_required",
  "reason": "High-value charge requires approval",
  "rule_id": "rule-uuid",
  "hitl_result": "approved",
  "hitl_responded_at": "2026-07-03T10:35:00Z",
  "hitl_responded_by": "reviewer@example.com",
  "created_at": "2026-07-03T10:30:00Z",
  "execution_result": null,
  "execution_completed_at": null,
  "execution_details": null,
  "hitl": {
    "id": "hitl-uuid",
    "status": "approved",
    "category": "enduser",
    "expires_at": "2026-07-03T10:32:00Z",
    "responded_at": "2026-07-03T10:31:12Z",
    "responded_by": "reviewer@example.com",
    "created_at": "2026-07-03T10:30:00Z"
  }
}
```

The `hitl` field is `null` for decisions that did not require approval.

**Status codes:** `200 OK`, `400 Bad Request` (invalid UUID), `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### POST /allow/telemetry

Submit a batch of locally-evaluated decisions. The SDK calls this after local bundle evaluation so every decision — local or remote — lands in the audit trail, and uncovered scenarios surface for review.

**Request body:**

```json theme={null}
{
  "decisions": [
    {
      "decision_id": "c1a7e2f0-3b4d-4c5e-8f90-123456789abc",
      "agent_id": "billing-agent",
      "target_app": "stripe.com",
      "action": "GET /v1/customers",
      "decision": "permit",
      "reason": "Matched rule: Allow Stripe reads",
      "evaluated_at": "2026-07-03T10:30:00.000Z",
      "rule_id": "rule-uuid",
      "rule_code": "R-1042",
      "enforced": true,
      "agent_mode": "enforce",
      "evaluation_mode": "local",
      "latency_ms": 1
    }
  ]
}
```

1–100 decisions per request. Required per-item fields: `decision_id` (UUID, client-generated), `agent_id`, `target_app`, `action`, `decision` (`permit` | `deny` | `approval_required` | `mask`), `reason`, and `evaluated_at` (ISO 8601). Optional: `rule_id`, `rule_code`, `enforced`, `agent_mode`, `raw_event`, `evaluation_mode` (`local` | `remote`), `latency_ms`, and `context` (max 32 KB serialized). Decisions without a `rule_id` are uncovered scenarios and create engineer-category review items.

Items are validated **individually**: valid items are ingested, malformed ones are rejected per-item and reported back — one bad item never discards the rest of the batch. Only a batch where every item is invalid returns `400`.

**Response (202):**

```json theme={null}
{
  "received": 5,
  "uncovered": 1,
  "new_schemas": 0,
  "rejected": [
    { "index": 3, "issues": [ { "path": ["evaluated_at"], "message": "Invalid datetime" } ] }
  ]
}
```

**Status codes:** `202 Accepted`, `400 Bad Request` (envelope invalid or all items rejected), `401 Unauthorized`, `500 Internal Server Error`

***

### POST /allow/execution-events

Record the post-decision execution outcome for a permitted action, closing the feedback loop on the audit trail. Idempotent: a second submission for the same `decision_id` returns `409 Conflict`.

**Request body:**

```json theme={null}
{
  "decision_id": "550e8400-e29b-41d4-a716-446655440000",
  "result": "success",
  "details": "Stripe charge created (ch_1234)"
}
```

| Field         | Type                              | Required | Description                                                                    |
| ------------- | --------------------------------- | -------- | ------------------------------------------------------------------------------ |
| `decision_id` | `string` (UUID)                   | Yes      | ID of the decision (from `POST /allow/evaluate` or client-generated telemetry) |
| `result`      | `success` \| `failure` \| `error` | Yes      | Execution outcome                                                              |
| `details`     | `string`                          | No       | Free-text details (max 2000 chars)                                             |

**Response:** `{ "updated": true }`

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found` (decision not found), `409 Conflict` (already recorded), `500 Internal Server Error`

***

### POST /allow/agents/register

Registration handshake: the SDK calls this once at startup (fire-and-forget) to report the agent's captured environment. Idempotent — it provisions the agent if this arrives before the first evaluate call.

**Scope:** `rules:evaluate` or `allow:write` (harness keys); management keys additionally need the agent-management permission

**Request body:**

```json theme={null}
{
  "agent_id": "billing-agent",
  "os": "linux",
  "hostname": "worker-7",
  "ip": "10.0.4.12",
  "username": "svc-agents",
  "kind": "sdk"
}
```

All fields except `agent_id` are optional. `kind` is `sdk` (framework harness, the default) or `cli_harness` (CLI harness). When `ip` is omitted, the server records the request's source IP. Owner and approval-routing fields are deliberately **not** accepted here — those are set by humans via the dashboard or the agent management API.

**Response:** `{ "ok": true, "agent_id": "billing-agent" }`

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### GET /allow/agents/me/stream

Server-Sent Events stream of mode changes for one agent, so the SDK flips enforcement instantly when you toggle a mode in the dashboard — no waiting for the next bundle poll.

**Query parameters:** `agent_id` (required)

**Events:**

* `mode_snapshot` — sent immediately on connect: `{ "agent_mode": "enforce" | "monitor" | "off" }`
* `mode_changed` — sent whenever the agent's mode changes: same payload
* `ping` — heartbeat every 25 seconds to keep idle proxies from closing the connection

**Status codes:** `200 OK` (stream), `400 Bad Request`, `401 Unauthorized`, `404 Not Found` (unknown agent), `500 Internal Server Error`

***

## Agents

Manage the agent registry. These are management endpoints — a harness key receives `403 harness_key_not_permitted` here.

***

### GET /allow/agents

List registered agents, newest first.

**Query parameters:** `page` (default `1`), `limit` (default `50`, max `100`)

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "agent-row-uuid",
      "agent_id": "billing-agent",
      "name": "Billing Agent",
      "description": "Handles subscription billing operations",
      "mode": "enforce",
      "owner_email": "owner@example.com",
      "hitl_pathway": "slack",
      "trust_tier": "tier2",
      "categories": ["transactional", "internal"],
      "business_function": "finance_accounting",
      "business_function_source": "ai",
      "no_coverage": null,
      "created_at": "2026-07-03T10:00:00Z",
      "updated_at": "2026-07-03T10:00:00Z"
    }
  ],
  "total": 3,
  "page": 1,
  "pageSize": 50
}
```

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### POST /allow/agents

Register a new agent. Returns the plaintext API key **exactly once** — store it securely; it cannot be retrieved again.

**Request body:**

```json theme={null}
{
  "agent_id": "billing-agent",
  "name": "Billing Agent",
  "description": "Handles subscription billing operations",
  "mode": "monitor",
  "owner_email": "owner@example.com",
  "hitl_pathway": "slack",
  "trust_tier": "tier2",
  "categories": ["transactional", "internal"]
}
```

| Field          | Type                            | Required | Description                                                                                                                                                                                                                                                                                                      |
| -------------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`     | `string`                        | Yes      | Logical identifier used in SDK config (max 255 chars)                                                                                                                                                                                                                                                            |
| `name`         | `string`                        | Yes      | Display name (max 255 chars)                                                                                                                                                                                                                                                                                     |
| `description`  | `string`                        | No       | Description (max 1000 chars)                                                                                                                                                                                                                                                                                     |
| `mode`         | `enforce` \| `monitor` \| `off` | No       | Evaluation mode. Default `monitor` (observe-only)                                                                                                                                                                                                                                                                |
| `api_key`      | `string`                        | No       | Custom API key (16–256 chars). Auto-generated (`allow_` + 64 hex chars) if omitted                                                                                                                                                                                                                               |
| `owner_email`  | `string`                        | No       | The agent's human owner — receives approval requests for this agent                                                                                                                                                                                                                                              |
| `agent_type`   | `string`                        | No       | The framework the agent runs on — a supported framework identifier such as `langchain`, `openclaw`, `llamaindex`, `autogen`, `crewai`, `vercel_ai`, `mastra`, `voltagent`, `openai_agents`, or `unknown`                                                                                                         |
| `hitl_pathway` | `slack` \| `teams` \| `email`   | No       | Channel the owner is messaged through for approvals                                                                                                                                                                                                                                                              |
| `trust_tier`   | `tier1` \| `tier2` \| `tier3`   | No       | Ordinal trust level (`tier1` = highest trust). Rules condition on it as `input.agent.trust_tier`                                                                                                                                                                                                                 |
| `categories`   | `string[]`                      | No       | Controlled multi-label vocabulary: capability labels (`read_only`, `transactional`, `data_processor`, `external_comms`, `code_exec`, `privileged`) and exposure labels (`internal`, `customer_facing`, `experimental`, `third_party`)                                                                            |
| `no_coverage`  | `open` \| `closed` \| `null`    | No       | Per-agent fail-safe when **no rule matches** an action (enforce mode). `open` permits every uncovered operation class; `closed` denies them all — an explicit lockdown that also takes precedence over autopilot; `null`/omitted inherits the org-level no-match defaults. Takes precedence over the org setting |

**Response (201):** the agent object (as in the list response) plus `api_key`:

```json theme={null}
{
  "id": "agent-row-uuid",
  "agent_id": "billing-agent",
  "name": "Billing Agent",
  "description": "Handles subscription billing operations",
  "mode": "monitor",
  "owner_email": "owner@example.com",
  "hitl_pathway": "slack",
  "trust_tier": "tier2",
  "categories": ["transactional", "internal"],
  "business_function": null,
  "no_coverage": null,
  "created_at": "2026-07-03T10:00:00Z",
  "updated_at": "2026-07-03T10:00:00Z",
  "api_key": "allow_a1b2c3d4e5f6..."
}
```

<Warning>
  The `api_key` field is only present in this response. Subsequent reads omit it entirely.
</Warning>

**Status codes:** `201 Created`, `400 Bad Request`, `401 Unauthorized`, `409 Conflict` (`agent_id` already registered), `500 Internal Server Error`

***

### GET /allow/agents/:id

Get a single agent by its row UUID. The response includes the machine-reported environment (`agent_os`, `agent_hostname`, `agent_ip`, `agent_username`) captured via `POST /allow/agents/register`, and never includes key material.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### PUT /allow/agents/:id

Update an agent. Only provided fields change; at least one field is required.

**Request body:** any subset of `name`, `description`, `mode`, `owner_email` (nullable), `hitl_pathway` (nullable), `trust_tier` (nullable), `categories`, `no_coverage` (`open` | `closed`, or `null` to clear — see the POST field table), and `business_function` — one of the twelve business-function identifiers (`finance_accounting`, `hr`, `engineering`, `it_security`, `legal`, `privacy_compliance`, `sales`, `marketing`, `operations`, `customer_support`, `procurement`, `generic`), or `null` to clear.

```json theme={null}
{
  "mode": "enforce",
  "trust_tier": "tier1"
}
```

Setting `name` or `business_function` pins it as human-owned — the platform's AI naming and classification never overwrite a value you set. A `mode` change is broadcast immediately to any live `GET /allow/agents/me/stream` subscriber.

**Response:** the updated agent object.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### POST /allow/agents/:id/regenerate-naming

Re-run AI naming for one agent now, from its observed and normalized action schemas. An explicit opt-in: it hands name ownership back to the AI even if the name was previously user-set. A naming failure returns `200` with `status: "skipped"` — it never errors the agent.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### DELETE /allow/agents/:id

Delete an agent from the registry.

**Response:** `{ "deleted": true }`

<Note>
  Deleting an agent does not revoke API keys. Revoke the agent's key separately under **Settings → Harness Keys**.
</Note>

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

## Rules

Rule management. The recommended authoring path is the dashboard's rule editor (natural-language compile, visual condition builder, and a Simulate panel that checks a new rule against your recent traffic — rules projected to inhibit more than 5% of it are blocked). These endpoints are the programmatic equivalent.

***

### GET /allow/rules

List action rules, sorted by priority descending — the same order the engine evaluates them in (first match wins).

**Query parameters:** `page` (default `1`), `limit` (default `50`, max `100`)

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "rule-uuid",
      "name": "Allow Stripe reads",
      "description": "Permit read-only Stripe API calls",
      "natural_language": "Allow my billing agent to read from Stripe",
      "priority": 10,
      "enabled": true,
      "created_at": "2026-07-03T10:00:00Z",
      "updated_at": "2026-07-03T10:00:00Z"
    }
  ],
  "total": 12,
  "page": 1,
  "pageSize": 50
}
```

The list omits `rego_source`; fetch a single rule to read the policy source.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### POST /allow/rules

Create a rule.

**Request body:**

```json theme={null}
{
  "name": "Allow Stripe reads",
  "description": "Permit read-only Stripe API calls",
  "rego_source": "package allow\n\ndefault allow = false\n\nallow { input.action == \"GET /v1/customers\" }",
  "natural_language": "Allow my billing agent to read from Stripe",
  "priority": 10,
  "enabled": true
}
```

| Field              | Type      | Required | Description                                                            |
| ------------------ | --------- | -------- | ---------------------------------------------------------------------- |
| `name`             | `string`  | Yes      | Rule name, unique per organization (max 255 chars)                     |
| `description`      | `string`  | No       | Description (max 1000 chars)                                           |
| `rego_source`      | `string`  | Yes      | Policy source (see note below)                                         |
| `natural_language` | `string`  | No       | The plain-English intent the policy was compiled from (max 2000 chars) |
| `priority`         | `number`  | No       | Evaluation priority — higher runs first. Default `0`                   |
| `enabled`          | `boolean` | No       | Whether the rule is active. Default `true`                             |

<Note>
  `rego_source` holds the rule's policy source in the platform's Rego-subset condition language (equality, inequality, set membership, `startswith`/`endswith`/`contains`, regex, counts, and numeric comparisons). The effect (permit, deny, approval, mask) is derived from the decisions in the policy body. Prefer `POST /allow/rules/compile` — the compiler drafts and validates the policy against the live evaluation engine.
</Note>

**Response (201):** the created rule object, including `rego_source`.

**Status codes:** `201 Created`, `400 Bad Request`, `401 Unauthorized`, `409 Conflict` (a rule with this name already exists), `500 Internal Server Error`

***

### POST /allow/rules/compile

Compile a natural-language description into a rule. The compiler reads your existing rules for context, drafts the policy source, and validates it against the live evaluation engine.

**Rate limit:** 10 compile requests per minute per organization — exceeding it returns `429`.

**Query parameters:** `stream=true` — stream the compile over Server-Sent Events instead of a single JSON response (also triggered by `Accept: text/event-stream`).

**Request body:**

```json theme={null}
{
  "prompt": "Allow my billing agent to read from Stripe but block all write operations"
}
```

| Field     | Type     | Required | Description                                                  |
| --------- | -------- | -------- | ------------------------------------------------------------ |
| `prompt`  | `string` | Yes      | Plain-English rule description (max 4000 chars)              |
| `nodeRef` | `string` | No       | Rule-graph node reference for editor context (max 255 chars) |

**Response:**

```json theme={null}
{
  "rego_source": "package allow\n\ndefault allow = false\n...",
  "natural_language": "This rule permits read operations against stripe.com for the billing agent and denies writes...",
  "name": "Stripe Read-Only Access",
  "description": "Permits Stripe reads, denies Stripe writes",
  "suggested_priority": 50
}
```

**Streaming response** (`?stream=true`): a `text/event-stream` of JSON events —

```
data: {"type":"text","content":"Drafting the policy..."}

data: {"type":"done","result":{"rego_source":"...","natural_language":"...","name":"...","description":"...","suggested_priority":50}}
```

On failure the stream emits `{"type":"error","message":"..."}` and closes.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `422 Unprocessable Entity` (the compiler could not produce a valid policy — refine the prompt), `429 Too Many Requests`, `500 Internal Server Error`

***

### GET /allow/rules/:id

Get a single rule by UUID, including `rego_source`.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### PUT /allow/rules/:id

Update a rule. Same fields as `POST /allow/rules`, all optional; at least one is required. Only provided fields change.

**Response:** the updated rule object.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### DELETE /allow/rules/:id

Delete a rule permanently.

**Response:** `{ "deleted": true }`

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

## Human-in-the-Loop Queue

Review items land here in two categories: **`enduser`** items are live approval gates (a rule routed the action to a human — the agent is waiting), while **`engineer`** and the other categories are coverage-gap reviews (the action was already decided by a no-coverage default; the item asks whether a rule should exist). The two categories take different response bodies.

These are management endpoints — approvals normally arrive as Slack, Teams, or email prompts, or through the dashboard's Escalations page. A harness key receives `403 harness_key_not_permitted` here.

***

### GET /allow/hitl/queue

List queue items. Defaults to `status=pending`, ordered oldest-first (FIFO for approvers); non-pending queries return newest-first.

**Query parameters:**

| Parameter       | Description                                                                                                     |
| --------------- | --------------------------------------------------------------------------------------------------------------- |
| `page`, `limit` | Pagination (defaults `1` / `50`, max `100`)                                                                     |
| `status`        | `pending` (default) \| `approved` \| `rejected` \| `timeout` \| `expired` \| `dismissed` \| `resolved` \| `all` |
| `category`      | `enduser` \| `engineer` \| `finance` \| `security` \| `compliance` \| `other` \| `all`                          |

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "hitl-uuid",
      "decision_id": "decision-uuid",
      "agent_id": "billing-agent",
      "target_app": "stripe.com",
      "action": "POST /v1/charges",
      "context": { "amount": 5000 },
      "category": "enduser",
      "status": "pending",
      "ai_recommended_rule": null,
      "notified_via": ["slack"],
      "expires_at": "2026-07-03T10:32:00Z",
      "responded_at": null,
      "responded_by": null,
      "created_at": "2026-07-03T10:30:00Z"
    }
  ],
  "total": 2,
  "page": 1,
  "pageSize": 50
}
```

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### POST /allow/hitl/queue

Create an approval item directly, for a client that resolved a retrieval `escalate` decision locally and needs to queue the human gate without re-evaluating. Writes the decision record and the queue item together — never one without the other.

**Request body:**

```json theme={null}
{
  "agent_id": "billing-agent",
  "action": "read_customer_pii",
  "context": { "record_count": 200 },
  "reason": "Human approval required by retrieval escalate policy",
  "category": "enduser",
  "timeout_seconds": 120
}
```

| Field             | Type     | Required | Description                                                                 |
| ----------------- | -------- | -------- | --------------------------------------------------------------------------- |
| `agent_id`        | `string` | Yes      | Must be a registered agent (unknown → `404`)                                |
| `action`          | `string` | Yes      | The action awaiting approval (max 255 chars)                                |
| `target_app`      | `string` | No       | Default `"openclaw"`                                                        |
| `context`         | `object` | No       | Default `{}`                                                                |
| `reason`          | `string` | No       | Why approval is required (max 1000 chars)                                   |
| `category`        | `string` | No       | One of the categories above. Default `engineer`                             |
| `rule_code`       | `string` | No       | The rule code that routed this action to approval, recorded on the decision |
| `timeout_seconds` | `number` | No       | 10–120. Default `120`                                                       |

**Response (201):** the created queue item.

**Status codes:** `201 Created`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found` (unknown agent), `500 Internal Server Error`

***

### POST /allow/hitl/queue/:id

Respond to a pending item. The request body depends on the item's category.

**`enduser` items** (live approval gates) — approve or reject:

```json theme={null}
{
  "decision": "approved",
  "responded_by": "reviewer@example.com"
}
```

| Field          | Type                     | Required | Description                                  |
| -------------- | ------------------------ | -------- | -------------------------------------------- |
| `decision`     | `approved` \| `rejected` | Yes      | The human's decision                         |
| `responded_by` | `string`                 | Yes      | Who responded (email or name, max 255 chars) |

**All other categories** (coverage-gap reviews) — dismiss, or link the rule you created to cover the gap:

```json theme={null}
{
  "action": "create_rule",
  "responded_by": "reviewer@example.com",
  "linked_rule_id": "rule-uuid"
}
```

| Field            | Type                       | Required          | Description                                 |
| ---------------- | -------------------------- | ----------------- | ------------------------------------------- |
| `action`         | `dismiss` \| `create_rule` | Yes               | Dismiss the gap or record the covering rule |
| `responded_by`   | `string`                   | Yes               | Who responded                               |
| `linked_rule_id` | `string` (UUID)            | For `create_rule` | The rule that now covers this scenario      |

Sending the wrong body shape for the item's category returns `400`.

**Response:**

```json theme={null}
{
  "id": "hitl-uuid",
  "decision_id": "decision-uuid",
  "status": "approved",
  "responded_at": "2026-07-03T10:31:12Z",
  "responded_by": "reviewer@example.com"
}
```

Every response also emits a signed record envelope, so the human decision itself is part of the verifiable audit trail.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `409 Conflict` (item already resolved — the current status is returned), `500 Internal Server Error`

***

## Audit Log

***

### GET /v1/allow/audit-log

Query the decision audit log. Every decision — local and remote, permit and deny — is recorded, including monitor-mode observations.

<Note>
  Tamper evidence comes from the signed record pipeline, not from the query API: each decision emits a record envelope that receives an asynchronous Ed25519 receipt, is Merkle-batched under a KMS-signed root with an RFC 3161 timestamp, and lands in a hash-chained checkpoint log. Verify any decision server-side via `GET /record/envelopes/:id/verify`.
</Note>

**Scope:** `rules:read` or `allow:read` (management keys / dashboard session — harness keys cannot query the log)

**Query parameters:**

| Parameter     | Type                                                | Description                                |
| ------------- | --------------------------------------------------- | ------------------------------------------ |
| `agent_id`    | `string`                                            | Filter by agent logical ID                 |
| `target_app`  | `string`                                            | Filter by target application               |
| `decision`    | `permit` \| `deny` \| `approval_required` \| `mask` | Filter by decision outcome                 |
| `hitl_result` | `approved` \| `rejected` \| `timeout`               | Filter by approval resolution              |
| `start_date`  | ISO 8601 datetime                                   | Lower bound on `created_at`                |
| `end_date`    | ISO 8601 datetime                                   | Upper bound on `created_at`                |
| `page`        | `number`                                            | Page index (default `1`)                   |
| `limit`       | `number`                                            | Records per page (default `50`, max `100`) |

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "decision-uuid",
      "agent_id": "billing-agent",
      "target_app": "stripe.com",
      "action": "POST /v1/charges",
      "context": { "amount": 5000 },
      "decision": "permit",
      "reason": "Matched rule: Allow Stripe reads",
      "rule_id": "rule-uuid",
      "hitl_result": null,
      "hitl_responded_at": null,
      "hitl_responded_by": null,
      "created_at": "2026-07-03T10:30:00Z",
      "execution_result": "success",
      "execution_completed_at": "2026-07-03T10:30:02Z",
      "execution_details": "Stripe charge created (ch_1234)"
    }
  ],
  "total": 843,
  "page": 1,
  "pageSize": 50
}
```

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `429 Too Many Requests`, `500 Internal Server Error`

***

## Settings

***

### GET /allow/settings

Get your organization's action-governance settings. If none exist yet, a defaults row is created and returned (`201`) — this endpoint always returns a valid settings object.

**Response:**

```json theme={null}
{
  "id": "settings-uuid",
  "no_coverage_defaults": { "read": "approve", "write": "approve", "delete": "approve", "admin": "approve" },
  "autopilot_enabled": false,
  "hitl_timeout_seconds": 120,
  "notification_channels": [],
  "enduser_hitl_enabled": true,
  "recall_retain_masked_original": false,
  "auto_disable_high_interference": true,
  "recall_floor_disabled_detectors": [],
  "recall_floor_enabled_optin": [],
  "created_at": "2026-07-03T10:00:00Z",
  "updated_at": "2026-07-03T10:00:00Z"
}
```

| Field                                                                                            | Type      | Description                                                                                                                                                                                                                                      |
| ------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `no_coverage_defaults`                                                                           | `object`  | Per-operation-type default when **no rule matches** in enforce mode: `read`, `write`, `delete`, `admin`, each `approve` \| `deny` \| `ask`. Defaults to all-`approve` (no default disruption — uncovered actions proceed until you tighten them) |
| `autopilot_enabled`                                                                              | `boolean` | AI auto-drafts a rule for uncovered scenarios and permits the request while it awaits review                                                                                                                                                     |
| `hitl_timeout_seconds`                                                                           | `number`  | Approval wait window, 30–120 seconds. Default `120` — the hard ceiling; unapproved requests fall back per `hitl_fallback`                                                                                                                        |
| `notification_channels`                                                                          | `array`   | Organization-level approval notification channels (`{type, config}` entries). Configure channels under **Integration → Connectors**                                                                                                              |
| `enduser_hitl_enabled`                                                                           | `boolean` | When `false`, `ask` no-coverage defaults resolve without holding the agent (recorded as bypassed)                                                                                                                                                |
| `recall_retain_masked_original`, `recall_floor_disabled_detectors`, `recall_floor_enabled_optin` | —         | Retrieval-governance settings sharing this row; see the retrieval-governance docs                                                                                                                                                                |
| `auto_disable_high_interference`                                                                 | `boolean` | Automatically disable a rule that inhibits more than 5% of recent evaluations. Default on                                                                                                                                                        |

**Status codes:** `200 OK`, `201 Created` (first access, defaults created), `401 Unauthorized`, `500 Internal Server Error`

***

### PUT /allow/settings

Update settings. Validated field ranges are as documented above — in particular `hitl_timeout_seconds` must be 30–120 (a higher value is rejected with `400`, and the evaluation path clamps to 120 regardless).

<Warning>
  This endpoint **replaces** the settings row: fields you omit are reset to their defaults, and `no_coverage_defaults` is stored exactly as sent. Read the current settings first and send the complete object back with your changes.
</Warning>

**Request body:**

```json theme={null}
{
  "no_coverage_defaults": { "read": "approve", "write": "ask", "delete": "deny", "admin": "deny" },
  "autopilot_enabled": false,
  "hitl_timeout_seconds": 120,
  "notification_channels": [],
  "enduser_hitl_enabled": true,
  "auto_disable_high_interference": true
}
```

**Response:** the updated settings object.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`
