Thoth SDK
sdk v0.1.6 / proxy v0.2.7

Enforcement API

Document POST /v1/enforce for policy enforcement decisions and GET /v1/enforce/hold/{hold_token} for polling step-up approval status.

POST /v1/enforce

Check enforcement policy for a proposed tool call. Returns an EnforcementDecision indicating whether the tool should be allowed, blocked, or held for human approval.

The SDKs call this endpoint automatically before every tool execution. Call it directly only if you are building a custom integration.

Request

POST /v1/enforce
Authorization: Bearer thoth_live_your_key_here
Content-Type: application/json
{
  "agent_id": "invoice-processor-v2",
  "tenant_id": "acme-corp",
  "tool_name": "submit_payment",
  "session_id": "session-uuid",
  "user_id": "user-123",
  "approved_scope": ["search_docs", "read_invoice"],
  "enforcement_mode": "progressive",
  "session_tool_calls": ["search_docs", "read_invoice", "search_docs"]
}

Request body

FieldTypeRequiredDescription
agent_idstringYesAgent identifier
tenant_idstringYesYour Thoth tenant identifier
tool_namestringYesName of the tool the agent wants to call
session_idstring (UUID)YesCurrent session identifier
user_idstringYesUser who initiated the agent action
approved_scopestring[]YesAuthorized tool names for this session
enforcement_modestringYesEnforcement mode: observe | progressive | step_up | block
session_tool_callsstring[]YesTool names already called in this session (in order)

Response

HTTP/1.1 200 OK
Content-Type: application/json

ALLOW decision

{
  "decision": "ALLOW",
  "reason": "tool is within approved scope",
  "risk_score": 0.12,
  "latency_ms": 18.4
}

BLOCK decision

{
  "decision": "BLOCK",
  "reason": "tool 'submit_payment' is not in approved scope",
  "violation_id": "vio_abc123def456",
  "risk_score": 0.91,
  "latency_ms": 22.1
}

STEP_UP decision

{
  "decision": "STEP_UP",
  "reason": "high-value payment requires human approval",
  "hold_token": "hold_xyz789",
  "risk_score": 0.74,
  "latency_ms": 19.8
}

EnforcementDecision schema

FieldTypePresent whenDescription
decision"ALLOW" | "BLOCK" | "STEP_UP"AlwaysThe enforcement decision
reasonstringAlwaysHuman-readable explanation of the decision
violation_idstringBLOCK onlyAudit ID — correlates with enforcement and evidence records
hold_tokenstringSTEP_UP onlyOpaque token for polling approval status
risk_scorenumberAlwaysAnomaly risk score (0.0–1.0)
latency_msnumberAlwaysEnforcer evaluation time in milliseconds

Decision semantics

ALLOW — proceed with tool execution. Emit a TOOL_CALL_POST event after the tool returns.

BLOCK — do not execute the tool. Raise PolicyViolationError / ThothPolicyViolation with the violation_id for the caller to log and handle gracefully.

STEP_UP — pause execution and poll GET /v1/enforce/hold/{hold_token} with the hold_token until the approver responds or the timeout is reached.

How the enforcer evaluates calls

The enforcer evaluates several signals to produce a decision:

  1. Scope check — Is tool_name in approved_scope? If not, this is an out-of-scope call.
  2. Mode — In observe mode, always ALLOW. In block mode, always BLOCK out-of-scope calls.
  3. Progressive scoring — In progressive mode, the enforcer weights:
    • Ratio of out-of-scope calls in session_tool_calls
    • Frequency of repeated blocked tools (probing behavior)
    • Unusual tool sequences for this agent_id (anomaly model)
  4. Step-up triggers — Tools tagged as high-risk in policy configuration always trigger STEP_UP regardless of scope.

Example (curl)

curl -X POST https://api.atensecurity.com/v1/enforce \
  -H "Authorization: Bearer $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"],
    "enforcement_mode": "progressive",
    "session_tool_calls": ["search_docs"]
  }'

GET /v1/enforce/hold/{hold_token}

Poll the status of a pending step-up approval. Call this repeatedly until the status is approved or denied (or until your timeout is reached).

Request

GET /v1/enforce/hold/hold_xyz789
Authorization: Bearer thoth_live_your_key_here

The hold token is passed as a path parameter — there is no request body.

Response

HTTP/1.1 200 OK
Content-Type: application/json

Pending

{
  "status": "pending",
  "hold_token": "hold_xyz789",
  "tool_name": "submit_payment",
  "agent_id": "invoice-processor-v2",
  "created_at": "2026-03-13T10:00:00.000Z",
  "expires_at": "2026-03-13T10:15:00.000Z"
}

Approved

{
  "status": "approved",
  "hold_token": "hold_xyz789",
  "tool_name": "submit_payment",
  "agent_id": "invoice-processor-v2",
  "approved_by": "jane.doe@acme.com",
  "approved_at": "2026-03-13T10:03:22.000Z"
}

Denied

{
  "status": "denied",
  "hold_token": "hold_xyz789",
  "tool_name": "submit_payment",
  "agent_id": "invoice-processor-v2",
  "denied_by": "jane.doe@acme.com",
  "denied_at": "2026-03-13T10:04:15.000Z",
  "reason": "Invoice amount exceeds pre-approved limit"
}

Expired

{
  "status": "expired",
  "hold_token": "hold_xyz789",
  "tool_name": "submit_payment",
  "expires_at": "2026-03-13T10:15:00.000Z"
}

StepUpStatus schema

FieldTypeDescription
status"pending" | "approved" | "denied" | "expired"Current approval status
hold_tokenstringThe hold token you polled with
tool_namestringTool name pending approval
agent_idstringAgent that requested the tool call
created_atstring (ISO 8601)When the step-up request was created
expires_atstring (ISO 8601)When the request will auto-expire
approved_bystringApprover email (present when status === "approved")
approved_atstring (ISO 8601)Approval timestamp
denied_bystringDenier email (present when status === "denied")
denied_atstring (ISO 8601)Denial timestamp
reasonstringDenial reason (present when status === "denied")

Polling pattern

import time, httpx
 
def poll_step_up(hold_token: str, api_key: str, timeout_seconds: int = 900) -> str:
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        resp = httpx.get(
            f"https://api.atensecurity.com/v1/enforce/hold/{hold_token}",
            headers={"Authorization": f"Bearer {api_key}"},
        )
        resp.raise_for_status()
        data = resp.json()
        if data["status"] == "approved":
            return "approved"
        if data["status"] in ("denied", "expired"):
            return data["status"]
        time.sleep(5)  # poll every 5 seconds
    return "timeout"

The SDK handles this polling loop automatically — you only need to call it directly if building a custom integration.