Arcus Docs

Enforcement

Policy engine

Policies answer one question — may this agent reach this endpoint at all? Default-deny, * as the only wildcard, and a specificity score that makes overlapping rules resolve predictably.

Default-deny#

A dispatch is forwarded only if a policy explicitly allows it. No matching policy is a denial, not a fallthrough.

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

403, recorded as dispatch.blocked_by_policy, nothing delivered.

The consequence worth internalising: a stolen key on a fresh account is worth nothing. It authenticates, and then it can reach precisely zero endpoints. Every route an attacker could use is a route you deliberately opened, which also means every route is on a list you can audit.

policyId: null distinguishes the two denial reasons — null means nothing matched, a populated id means a policy matched and its action was BLOCK.

Anatomy of a policy#

FieldTypeMeaning
senderAgentIdstring, 1–200Which agent this applies to. * matches any.
targetEndpointstring, 1–500Which destination. * matches any run of characters.
actionALLOW / BLOCK / REQUIRE_APPROVALWhat to do on a match.
enabledbooleanDisabled policies are skipped entirely.
dlpEnabledbooleanWhether gate 4 scans payloads on this route. Default on.
bash
curl -sS -X POST "$ARCUS_URL/v1/admin/policies" \
  -H "Authorization: Bearer <clerk-session-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "senderAgentId": "intake-agent",
    "targetEndpoint": "https://api.internal.example.com/webhook/*",
    "action": "ALLOW",
    "dlpEnabled": true
  }'

Wildcards#

* is the only wildcard. It is not a glob and not a regex: it matches any run of characters, including /.

PatternMatchesDoes not match
https://api.example.com/webhook/triageExactly that URLAnything else
https://api.example.com/*Every path on that hosthttps://api.example.com with no trailing slash; a different host
https://api.example.com/webhook/*/webhook/triage, /webhook/a/b/c/admin/x
*Any target at all
*.example.com/*Any subdomain path

There is deliberately no regex support. A policy language you can misread is a policy language that will eventually allow something you did not intend, and a target pattern is exactly the place where "I thought that anchored" becomes an exfiltration route.

`*` in the target field allows everything

{"senderAgentId": "my-agent", "targetEndpoint": "*", "action": "ALLOW"} gives that agent the open internet. It is useful for a first five minutes of development and should not survive into production. Narrow it before you deploy.

Overlap resolution#

When several enabled policies match, Arcus picks the most specific one by score:

ContributionPoints
senderAgentId is an exact match, not *+100
targetEndpoint is not *+50
Plus the length of the literal (non-wildcard) characters in the target+1 each

Highest score wins. The ranking is deliberate: an exact sender outranks any target pattern (sender identity is the stronger statement), a literal target outranks a wildcard, and among two literal patterns the longer — more specific — one wins.

Worked example#

Four policies exist:

#SenderTargetActionScore
A**BLOCK0
Bintake-agent*ALLOW100
Cintake-agenthttps://api.example.com/*ALLOW100 + 50 + 26 = 176
Dintake-agenthttps://api.example.com/admin/*BLOCK100 + 50 + 32 = 182

A dispatch from intake-agent:

TargetWinnerResult
https://api.example.com/webhook/triageCAllowed
https://api.example.com/admin/usersD — longer literalBlocked
https://other.example.net/xBAllowed
From billing-agent to anywhereABlocked

The pattern this enables is the useful one: allow broadly, then carve out exceptions with longer, more specific BLOCK policies. D beats C without you having to think about ordering, because specificity does the work.

There is no explicit priority field

Ordering is derived from the rules themselves. This is a design choice — a manual priority number drifts out of sync with intent the moment someone inserts a rule in the middle, whereas specificity is recomputed from what the rule actually says.

The three actions#

ALLOW#

Proceed to DLP and guardrails. Surviving those, the dispatch is queued and delivered.

BLOCK#

Refuse with 403 BLOCKED_BY_POLICY and the matching policyId. The payload is never scanned and never delivered.

Use an explicit BLOCK when you want the reason recorded as a deliberate rule rather than as an absence. policyId in the response and in your audit chain then names the exact rule, which is a much better answer to "why was this refused" than "nothing allowed it".

REQUIRE_APPROVAL#

The dispatch is held, not refused:

json
{
  "ok": true,
  "status": "PENDING_APPROVAL",
  "transferLogId": "clx8m2p4k0004abcd",
  "queued": false
}

202, queued: false. The payload is retained and appears in your approval queue. Nothing is delivered until a human approves it, and if nobody does, nothing ever is.

Note what still runs: a held dispatch has already passed threat detection and identity binding. DLP and guardrails are evaluated too, so a REQUIRE_APPROVAL route carrying a credential is refused with 422 rather than queued for someone to approve by mistake.

See Approval workflow for the review and release path.

Per-route DLP#

Each policy carries dlpEnabled, taken from the winning policy for that dispatch. This is how a route is legitimately exempted from payload scanning.

The honest use case: a route whose entire purpose is moving credential-shaped material — a secret-rotation callback, an internal vault sync. DLP would refuse it correctly and constantly.

json
{
  "senderAgentId": "rotation-worker",
  "targetEndpoint": "https://vault.internal.example.com/rotate",
  "action": "ALLOW",
  "dlpEnabled": false
}

Exempt the narrowest possible route

dlpEnabled: false on a policy whose target is * disables DLP for that agent entirely. Because the flag comes from the winning policy, a broad exemption can also quietly win over a narrower protected rule if it happens to score higher. Exempt one exact endpoint, and check the specificity of what you wrote.

Turning DLP off does not turn off guardrails, threat detection, identity binding or the audit chain. It disables exactly one gate on exactly the routes that policy wins.

Start closed, open one route. Your first policy should be one sender to one endpoint. The default-deny floor is already the safest configuration you will ever have; every policy you write moves away from it, so each should be deliberate.

One agent, one purpose. billing-agent reaching only payment endpoints and support-agent reaching only ticketing endpoints means a compromise of one is contained to that blast radius. Split by capability, not by convenience.

Environment-specific hosts. https://api.staging.example.com/* and https://api.example.com/* as separate policies — so a staging agent that picks up a production URL is refused rather than obeyed.

A deliberate BLOCK for known-bad shapes. An explicit rule against */admin/* reads better in an audit than the absence of an allow, and it survives someone later widening an allow rule.

json
[
  { "senderAgentId": "intake-agent",  "targetEndpoint": "https://api.example.com/webhook/*", "action": "ALLOW" },
  { "senderAgentId": "intake-agent",  "targetEndpoint": "https://api.example.com/admin/*",   "action": "BLOCK" },
  { "senderAgentId": "billing-agent", "targetEndpoint": "https://payments.example.com/charge", "action": "REQUIRE_APPROVAL" },
  { "senderAgentId": "*",             "targetEndpoint": "*",                                  "action": "BLOCK" }
]

That last catch-all is redundant — default-deny already refuses — but it makes the intent explicit in the policy list, and it makes every denial name a rule instead of an absence.

Managing policies#

ActionEndpoint
List/v1/admin/policies
Create/v1/admin/policies
Delete/v1/admin/policies/:id

Creating appends policy.created to your audit chain; deleting appends policy.deleted. Both carry the actor. A change to your enforcement rules is itself a governance event, sealed in the same chain as the decisions it affects — so "the policy was different at the time" is a verifiable claim rather than an assertion.

Custom policies require the policy.custom capability (Pro and above). On Free the default-deny floor and all five gates still apply — what Free lacks is the ability to author fine-grained rules of your own. See Pricing & plans.

Testing a policy set#

The cheapest way to check a policy does what you think is to send the dispatch you expect to be refused, and read the reason:

bash
# Should be blocked — a route you did not open
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ARCUS_URL/v1/dispatch" \
  -H "Authorization: Bearer $ARCUS_KEY" -H "Content-Type: application/json" \
  -d '{"target":"https://api.example.com/admin/users","from":"intake-agent","message":"probe"}'
# 403

# Should be allowed
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ARCUS_URL/v1/dispatch" \
  -H "Authorization: Bearer $ARCUS_KEY" -H "Content-Type: application/json" \
  -d '{"target":"https://api.example.com/webhook/triage","from":"intake-agent","message":"probe"}'
# 202

Both attempts are recorded, which is the point: your audit chain ends up containing the proof that the denial path works, not just the success path.