# Enforcement API

> Check a proposed tool call against policy with POST /v1/enforce, and poll a step-up hold with GET /v1/enforce/hold/{hold_token}.

Source: https://docs.atensecurity.com/docs/api-reference/enforcement

The SDKs call these endpoints for you before every tool execution. Call them
directly only when you are building a custom integration. Both use your unified
tenant API base URL and a runtime key with `execute` permission; see
[Authentication](https://docs.atensecurity.com/docs/api-reference#connection-and-authentication).

## Check a tool call

**POST** `/v1/enforce`

Evaluates a proposed tool call against your configured server policy, the
authenticated identity and the request context, and returns a decision before
the tool runs.

`approved_scope` is a list you supply, not an authorization grant, and the
requested `enforcement_mode` informs evaluation without guaranteeing an outcome.
Always act on the returned decision; never run the raw callback after a refusal
or an unresolved hold.

### Body parameters

- `agent_id` (string, required): Agent identifier.
- `tenant_id` (string, required): Your Thoth tenant identifier.
- `tool_name` (string, required): Name of the tool the agent wants to call.
- `session_id` (string, required): Current session identifier.
- `user_id` (string, required): User who initiated the agent action.
- `approved_scope` (string[], required): Tool names the agent is intended to use in this session.
- `session_tool_calls` (string[], required): Tool names already called in this session, in order.
- `enforcement_mode` (string, required): One of `observe`, `progressive`, `step_up` or `block`.
- `tool_args` (object): Proposed tool arguments, used for policy and redaction checks.
- `environment` (string): Policy environment tag. Defaults to `prod`.
- `session_intent` (string): Declared workflow intent for minimum-necessary checks.
- `purpose` (string): Declared purpose for purpose-aware filtering.
- `data_classification` (string): Declared data class used by policy controls.
- `endpoint_id` (string): Endpoint or workstation identifier for attribution.
- `fleet_id` (string): Fleet identifier for scoped governance.
- `enforcement_trace_id` (string): Correlation ID carried through decision evidence.
- `identity_binding` (object): Identity tuple for identity and freshness enforcement.
- `auth_context` (object): Canonical auth context for attribution.
- `delegation_context` (object): Delegation-chain context for confused-deputy protection.
- `model_name` (string): Model identifier for model-governance checks.
- `model_provider` (string): Model provider for allowlist checks.
- `token_count` (number): Planned token usage for cost and threshold controls.
- `estimated_cost_usd` (number): Planned spend for budget controls.
- `metadata` (object): Additional context propagated to decision evidence.

### Returns

An `EnforcementDecision`. Act on `decision`:

- **`ALLOW`**: run the tool. If `authorization_decision` is `MODIFY`, run it with
  `modified_tool_args` instead of the original arguments (for example, after
  outbound redaction).
- **`STEP_UP`**: do not run the tool yet. Poll the `hold_token` until the hold
  resolves. A policy deferral (`authorization_decision: "DEFER"`) is returned this way.
- **`BLOCK`**: do not run the tool. Surface `reason` and keep `violation_id` for audit.

- `decision` ("ALLOW" | "STEP_UP" | "BLOCK", required): The verdict to act on.
- `authorization_decision` ("ALLOW" | "DENY" | "MODIFY" | "STEP_UP" | "DEFER"): Normalized authorization outcome recorded in decision evidence.
- `reason` (string): Human-readable explanation.
- `decision_reason_code` (string): Stable machine-readable reason code.
- `modified_tool_args` (object): Arguments to use instead of the originals. Present with `MODIFY`.
- `modification_reason` (string): Why the arguments were changed. Present with `MODIFY`.
- `hold_token` (string): Token to poll. Present with `STEP_UP`.
- `step_up_timeout_seconds` (integer): How long the hold stays open. Present with `STEP_UP`.
- `violation_id` (string): Audit ID for the refusal. Present with `BLOCK`.
- `risk_score` (number): Risk score used in the decision.
- `latency_ms` (number): Enforcer evaluation time in milliseconds.

Evidence and explainability fields may also be present, including
`enforcement_trace_id`, `receipt`, `decision_evidence`, `score_components`,
`top_contributors`, `matched_rule_ids`, `matched_control_ids`,
`policy_references`, `model_signals` and policy-pack version metadata.

```bash title="Request"
curl -X POST "$THOTH_API_URL/v1/enforce" \
  -H "X-API-Key: $THOTH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "invoice-processor-v2",
    "tenant_id": "acme-corp",
    "tool_name": "submit_payment",
    "session_id": "550e8400-e29b-41d4-a716-446655440001",
    "user_id": "user-123",
    "approved_scope": ["search_docs", "read_invoice"],
    "session_tool_calls": ["search_docs"],
    "enforcement_mode": "progressive"
  }'
```

#### Responses

**ALLOW**

```json title="200 OK"
{
  "decision": "ALLOW",
  "decision_envelope_version": "1.0",
  "authorization_decision": "ALLOW",
  "reason": "tool is within approved scope",
  "decision_reason_code": "policy_allow",
  "risk_score": 0.12,
  "latency_ms": 18.4
}
```

**MODIFY**

```json title="200 OK"
{
  "decision": "ALLOW",
  "decision_envelope_version": "1.0",
  "authorization_decision": "MODIFY",
  "decision_reason_code": "outbound_dlp_redaction",
  "modification_reason": "Outbound DLP redacted 1 sensitive match(es) (credential)",
  "modified_tool_args": {
    "body": "Use key [REDACTED:credential] for the export"
  },
  "risk_score": 0.21,
  "latency_ms": 20.3
}
```

**STEP_UP**

```json title="200 OK"
{
  "decision": "STEP_UP",
  "decision_envelope_version": "1.0",
  "authorization_decision": "STEP_UP",
  "reason": "high-value payment requires human approval",
  "hold_token": "sfn-abc123",
  "step_up_timeout_seconds": 900,
  "risk_score": 0.74,
  "latency_ms": 19.8
}
```

**BLOCK**

```json title="200 OK"
{
  "decision": "BLOCK",
  "decision_envelope_version": "1.0",
  "authorization_decision": "DENY",
  "reason": "tool 'submit_payment' is not in approved scope",
  "decision_reason_code": "scope_violation",
  "violation_id": "550e8400-e29b-41d4-a716-446655440000",
  "risk_score": 0.91,
  "latency_ms": 22.1
}
```

Values are illustrative. Use tool names covered by your configured policy.

## Poll a step-up hold

**GET** `/v1/enforce/hold/{hold_token}`

Returns the state of a pending step-up hold. Poll until `resolved` is `true`
or your own timeout passes, then act on `resolution`. Treat anything other than
`ALLOW` as a refusal. The SDKs run this loop for you.

### Path parameters

- `hold_token` (string, required): The `hold_token` from a `STEP_UP` decision.

### Returns

- `token` (string): Hold token identifier.
- `tenant_id` (string): Tenant associated with the hold.
- `agent_id` (string): Agent that requested the call.
- `tool_name` (string): Tool held for approval.
- `session_id` (string): Session correlation ID.
- `expires_at` (string (ISO 8601)): When the hold expires.
- `resolved` (boolean): `false` while pending, `true` once resolved.
- `resolution` ("ALLOW" | "BLOCK" | null): Final decision once resolved.
- `resolved_by` (string | null): Approver identity, or `timeout`.
- `resolved_at` (string | null): When the hold was resolved.
- `reason` (string | null): Resolution reason, when available.
- `step_up_timeout_seconds` (integer | null): How long the hold stays open.
- `initial_receipt` (object): Decision receipt for the original `STEP_UP` decision.
- `terminal_receipt` (object): Decision receipt for the final outcome, once resolved. The Python SDK attaches it to decision evidence.

The response contains only these fields. An unknown hold token returns `404`; a
hold that belongs to a different caller returns `403`.

```bash title="Request"
curl "$THOTH_API_URL/v1/enforce/hold/sfn-abc123" \
  -H "X-API-Key: $THOTH_API_KEY"
```

#### Responses

**Pending**

```json title="200 OK"
{
  "token": "sfn-abc123",
  "tenant_id": "acme-corp",
  "agent_id": "invoice-processor-v2",
  "tool_name": "submit_payment",
  "session_id": "550e8400-e29b-41d4-a716-446655440001",
  "expires_at": "2026-03-13T10:15:00.000Z",
  "resolved": false,
  "resolution": null
}
```

**Approved**

```json title="200 OK"
{
  "token": "sfn-abc123",
  "tenant_id": "acme-corp",
  "agent_id": "invoice-processor-v2",
  "tool_name": "submit_payment",
  "resolved": true,
  "resolution": "ALLOW",
  "resolved_by": "jane.doe@acme.com",
  "resolved_at": "2026-03-13T10:03:22.000Z"
}
```

**Denied or timed out**

```json title="200 OK"
{
  "token": "sfn-abc123",
  "tenant_id": "acme-corp",
  "agent_id": "invoice-processor-v2",
  "tool_name": "submit_payment",
  "resolved": true,
  "resolution": "BLOCK",
  "resolved_by": "timeout",
  "resolved_at": "2026-03-13T10:15:00.000Z",
  "reason": "step-up auth timeout"
}
```

```python title="poll_hold.py"
import os
import time

import httpx


def wait_for_hold(hold_token: str, timeout_seconds: int = 900) -> str:
    base = os.environ["THOTH_API_URL"].rstrip("/")
    headers = {"X-API-Key": os.environ["THOTH_API_KEY"]}
    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        resp = httpx.get(f"{base}/v1/enforce/hold/{hold_token}", headers=headers, timeout=10)
        resp.raise_for_status()
        hold = resp.json()
        if hold.get("resolved") is True:
            return hold.get("resolution") or "BLOCK"
        time.sleep(5)
    return "BLOCK"  # treat an unresolved hold as a refusal
```
