Arcus Docs

Enforcement

The enforcement pipeline

Five checks run on every dispatch, in a fixed order, in one pass. This page explains what each one refuses, what it returns, and why the order is what it is.

The order#

1

Threat detection — 429 THREAT_DETECTED

Is this agent flooding, or already quarantined?

2

Identity binding — 401

Is the sender who the key says it is?

3

Policy — 403 BLOCKED_BY_POLICY

May this sender reach this endpoint at all?

4

DLP — 422 BLOCKED_BY_DLP

Does the payload carry something that must not leave?

5

Parameter guardrails — 422 BLOCKED_BY_PARAMETER

Is this specific instruction safe to carry out?

Before gate 1, the bearer key is authenticated: hashed, looked up, checked for revocation. A bad key never reaches the pipeline.

Why this order#

The ordering is a cost-and-dependency argument, not a preference.

Cheapest and most fundamental first. A flooding agent should cost one Redis read, not a policy evaluation followed by a full payload walk. Putting threat detection last would mean an abusive agent consumes the most expensive path on every request it makes — which is exactly what an attacker would want.

Identity before anything that reasons about identity. Policy, guardrails, rate limits and the audit actor all reference the sender. If a spoofed from were allowed to reach the policy engine, it would be matched against someone else's rules. So the sender is settled before it is trusted.

Route before content. Policy answers a question about the destination, which is cheap: an indexed read and a string match. DLP and guardrails read the whole payload. A request to a route you never authorized is refused without paying to scan its body.

DLP before guardrails. A leaked credential is unconditional — no threshold, no severity, no configuration required. A guardrail is specific and tunable. When both would fire, the unconditional one should be the reported reason.

Each gate in detail#

Gate 1 — Threat detection#

A per-(tenant, agent) counter in Redis. Over 60 dispatches in 60 seconds the agent is quarantined for 300 seconds.

json
{
  "error": "Rate spike: 61 dispatches in 60s (limit 60) — quarantined for 300s",
  "status": "THREAT_DETECTED",
  "retryAfter": 300
}

429, with a Retry-After header. The counter is keyed on the key's bound agent when one exists, rather than the claimed from — otherwise an agent could evade its own quarantine by changing one string. Fails open: if Redis is unreachable the request proceeds, because rate limiting is a throttle and not an authorization boundary. Full details: Threat detection.

Gate 2 — Identity binding#

If the key has an agentId, the dispatch's from must equal it.

json
{ "error": "Agent identity mismatch" }

401, recorded as dispatch.spoof_rejected. An unbound key may declare any from, which is convenient for a single-agent prototype and is why binding is recommended the moment you have more than one agent. Details: Authentication & keys.

Gate 3 — Policy#

Your policies are matched on (sender, target). The most specific match wins; no match is a denial.

json
{
  "error": "No policy allows intake-agent -> https://evil.example.com/exfil",
  "status": "BLOCKED_BY_POLICY",
  "transferLogId": "clx...",
  "policyId": null
}

403. An explicit BLOCK policy returns the same status with the matching policyId populated. A REQUIRE_APPROVAL policy does not refuse — it diverts to the hold branch and returns 202 PENDING_APPROVAL. The winning policy also carries dlpEnabled, which is how a single route can legitimately be exempted from scanning. Details: Policy engine.

Gate 4 — DLP#

The payload is walked to a depth of 12, checking field names against sensitive-key patterns and field values against credential detectors.

json
{
  "error": "Payload blocked by DLP",
  "status": "BLOCKED_BY_DLP",
  "transferLogId": "clx...",
  "violations": ["API_KEY", "PASSWORD_FIELD"],
  "matchCount": 2,
  "detailAvailableOn": "pro"
}

422. Categories are always returned; the matching paths require the dlp.configure capability. Runs entirely in process — no third-party scanning service, no model call. Details: Data loss prevention.

Gate 5 — Parameter guardrails#

Every enabled rule for your tenant whose sender pattern matches is evaluated against the payload, and all matches are collected. The strongest action among them decides the outcome: one block outranks any number of holds; a hold outranks a flag.

json
{
  "error": "Blocked by parameter guardrail",
  "status": "BLOCKED_BY_PARAMETER",
  "transferLogId": "clx...",
  "severity": "critical",
  "findings": [
    {
      "ruleId": "clx9...",
      "label": "No destructive commands",
      "field": "data.command",
      "path": "data.command",
      "condition": "matches_destructive",
      "expected": "critical",
      "observed": "DROP TABLE patients",
      "severity": "critical",
      "reason": "data.command matches DROP TABLE / DATABASE / SCHEMA (critical)"
    }
  ]
}

422 for block; 202 PENDING_APPROVAL for hold_for_approval; and for flag_only the dispatch is delivered with flagged: true and the findings attached. Details: Parameter guardrails.

Every outcome, with its response#

OutcomeHTTPstatusDelivered?
Allowed202— (ok: true, queued: true)Yes
Allowed but flagged202— (flagged: true)Yes
Quarantined429THREAT_DETECTEDNo
Identity mismatch401No
No policy / blocked403BLOCKED_BY_POLICYNo
DLP violation422BLOCKED_BY_DLPNo
Guardrail block422BLOCKED_BY_PARAMETERNo
Held for approval202PENDING_APPROVAL (queued: false)Not yet
Invalid request body400No
Bad or revoked key401No
Queue unavailable202recorded FAILEDNo

`202` does not always mean delivered

Three different things return 202: an accepted dispatch (queued: true), a held dispatch (queued: false, status: PENDING_APPROVAL), and an accepted-but-unqueueable dispatch when Redis is down. Always read queued and status, not just the HTTP code.

Every decision is recorded#

There is no path through the pipeline that decides without writing. Each terminal outcome writes a TransferLog row and appends a sealed audit event in a single database transaction, so there is no state in which a decision was made but not recorded.

Gate outcomeAudit event type
Alloweddispatch.allowed
Quarantineddispatch.blocked_by_threat
Identity mismatchdispatch.spoof_rejected
Policy denialdispatch.blocked_by_policy
DLP violationdispatch.blocked_by_dlp
Guardrail blockdispatch.blocked_by_parameter
Guardrail holddispatch.held_for_parameter
Guardrail flag (delivered)dispatch.flagged_by_parameter
Policy-driven holddispatch.held_for_approval
Delivery outcomedelivery.succeeded / delivery.failed

The audit actor for gateway traffic is the key prefix, not the claimed from. The sender name is caller-supplied and may be the very thing under suspicion; the key that authenticated the request is not.

What runs on which plan#

All five gates run on every request on every plan, including Free. Tiers change what you can configure and how much you can see:

FreeProMax
All five gates enforcedYesYesYes
Custom policies (policy.custom)YesYes
DLP match paths (dlp.configure)Categories onlyYesYes
Guardrail rules enabled2UnlimitedUnlimited
Statistical baselines (guardrails.baseline)YesYes
Chain verification (audit.verify)YesYes
Anomaly surface (anomaly)Yes

See Pricing & plans for the complete matrix.