Arcus Docs

Introduction

Quickstart

Sign in, issue a key, allow one route, and send a governed dispatch — then watch Arcus refuse a request that tries to leak a credential. About five minutes end to end.

Before you start#

You need an Arcus account and somewhere for a dispatch to land. If you do not have a receiving endpoint yet, step 4 includes a nine-line one you can run locally.

Note

The examples below use https://your-arcus-host for the gateway. Replace it with the host your Arcus instance runs on. If you are running Arcus yourself, see Running Arcus.


1. Create your account#

Open the Arcus dashboard and sign in. Authentication is handled by Clerk, so email or a social provider both work.

Your account is your tenant. There is no separate workspace to create and no team to invite before you can do anything — keys, policies, logs and your audit chain all hang off your account directly. See Core concepts.

Every new account starts on the Free plan, which includes the full enforcement pipeline.


2. Issue an API key#

Go to API Keys and create one.

FieldWhat it does
LabelFor your own reference — intake-agent (staging).
Owner emailWho this key was issued to. Distinct from your account email, so a key handed to a contractor is attributable.
Agent ID (recommended)Binds the key to one agent identity. A dispatch from this key must declare from equal to this value, or it is rejected as spoofing.

The raw key is shown once. Arcus stores only its SHA-256 hash and a 15-character display prefix, so there is no "show key again" — copy it now or revoke and reissue.

text
ark_live_4f8c21a90b73e5d6c8a41f27b9e03d5a6c17f82be40d95c3
└──┬───┘ └─────────────────────────┬──────────────────────┘
 prefix         48 hex characters (24 random bytes)

Treat it like a production credential

This key authorizes dispatches on your account, under your policies and your audit chain. Put it in an environment variable or a secret manager — never in source control, and never in a file you might publish. Arcus's own DLP engine recognises the ark_live_ shape and will refuse a payload containing one.

Store it:

bash
export ARCUS_KEY="ark_live_xxxxxxxx"
export ARCUS_URL="https://your-arcus-host"

3. Allow one route#

Arcus is default-deny. A key with no matching policy can dispatch nowhere, which means a stolen key on a fresh account is worth nothing.

Go to Policies and create one:

FieldValue
Sender agentintake-agent
Target endpointhttp://localhost:4101/*
ActionALLOW
DLP enabledLeave on

* is the only wildcard, and it matches any run of characters. http://localhost:4101/* allows every path on that host and port; * alone in the target field allows any endpoint, which is worth reaching for only in development.

Start narrow

A policy of intake-agenthttps://api.example.com/webhook/triage is a better first policy than a wildcard, because widening it later is a deliberate, audited act. See Policy engine for how overlapping policies resolve.


4. Run a receiver#

Any HTTP server that accepts a POST and returns 2xx will do. This one has no dependencies:

python
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        print(json.dumps(json.loads(self.rfile.read(length) or b'{}'), indent=2))
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(b'{"status":"received"}')

print('listening on :4101')
HTTPServer(('127.0.0.1', 4101), Handler).serve_forever()
bash
python3 receiver.py

5. Send your first dispatch#

bash
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
  -H "Authorization: Bearer $ARCUS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": "http://localhost:4101/webhook/triage",
    "from": "intake-agent",
    "message": "Patient reports severe pain in the lower left molar.",
    "data": { "priority": "urgent", "caseId": "C-4471" }
  }'

Arcus responds as soon as the decision is made — not when your target replies:

json
{
  "ok": true,
  "transferLogId": "clx8m2p4k0001abcd",
  "queued": true
}

Your receiver prints the payload a moment later. Open Transfer Logs in the dashboard and the dispatch is there with status SUCCESS, the target URL, the payload size, the attempt count and the delivery time.


6. Watch it refuse something#

The point of a gateway is what it stops. Send the same request with a credential in the payload — exactly the mistake an agent makes when a secret is sitting in its context:

bash
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
  -H "Authorization: Bearer $ARCUS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": "http://localhost:4101/webhook/triage",
    "from": "intake-agent",
    "message": "Here is the database password: hunter2-prod-primary",
    "data": { "aws_access_key": "AKIAIOSFODNN7EXAMPLE" }
  }'
json
{
  "error": "Payload blocked by DLP",
  "status": "BLOCKED_BY_DLP",
  "transferLogId": "clx8m2p4k0002abcd",
  "violations": ["PASSWORD_FIELD", "AWS_ACCESS_KEY"],
  "matchCount": 2,
  "detailAvailableOn": "pro"
}

422, nothing delivered, and your receiver never saw it. The violations array names the categories on every plan; the exact field paths that matched require the dlp.configure capability — see Data loss prevention.

Two more worth trying:

bash
# Wrong sender for a bound key -> 401, recorded as a spoofing attempt
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
  -H "Authorization: Bearer $ARCUS_KEY" -H "Content-Type: application/json" \
  -d '{"target":"http://localhost:4101/x","from":"some-other-agent","message":"hi"}'

# Unallowed target -> 403, default-deny
curl -sS -X POST "$ARCUS_URL/v1/dispatch" \
  -H "Authorization: Bearer $ARCUS_KEY" -H "Content-Type: application/json" \
  -d '{"target":"https://evil.example.com/exfil","from":"intake-agent","message":"hi"}'

7. Verify the record#

Open Audit Trail in the dashboard. Every decision above is there in sequence — allowed, blocked by DLP, spoof rejected, blocked by policy — each with its actor, its summary and its hash.

Click Verify chain. Arcus recomputes every hash from the first event forward and reports whether the chain is intact. If a row had been edited or deleted, this is where it would show, with the sequence number of the first break.

That is the whole loop: govern the request, record the decision, prove the record.


Point your real agent at it#

The change to an existing agent is usually one function. Instead of calling the target:

python
import os
import requests

ARCUS_URL = os.environ["ARCUS_URL"] + "/v1/dispatch"
ARCUS_KEY = os.environ["ARCUS_KEY"]      # never hardcode this

def dispatch(target: str, message: str, data: dict | None = None):
    response = requests.post(
        ARCUS_URL,
        headers={
            "Authorization": f"Bearer {ARCUS_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "target": target,
            "from": "intake-agent",
            "message": message,
            "data": data or {},
        },
        timeout=15,
    )

    if response.status_code == 202:
        return response.json()

    # 401 identity, 403 policy, 422 DLP or guardrail, 429 quarantined.
    # The body always names the reason.
    raise RuntimeError(f"Arcus refused: {response.status_code} {response.text}")

Full field and status-code reference: Dispatch API.


Next steps#