Enforcement
Data loss prevention
Gate 4 walks every outbound payload looking for credential-shaped values and sensitive field names. Seven categories, no third-party service, no model call — and a refusal before anything leaves your perimeter.
What it is for#
The most common way an AI agent leaks a secret is not malice. A credential is in its context — because it was in a document, a previous tool result, an error message, a config file it was asked to summarise — and the agent includes it in an outbound message because including relevant context is exactly what it was built to do.
DLP is the check that assumes this will happen and refuses the request when it does.
{
"error": "Payload blocked by DLP",
"status": "BLOCKED_BY_DLP",
"transferLogId": "clx8m2p4k0005abcd",
"violations": ["PASSWORD_FIELD", "AWS_ACCESS_KEY"],
"matchCount": 2,
"detailAvailableOn": "pro"
}422, recorded as dispatch.blocked_by_dlp, nothing delivered.
The seven categories#
| Category | What triggers it |
|---|---|
CREDIT_CARD | A 13–19 digit sequence that passes the Luhn checksum |
API_KEY | Provider-shaped keys — Stripe, GitHub, Slack, and Arcus's own ark_live_ |
BEARER_TOKEN | Bearer … headers and JWTs (eyJ… with three dot-separated segments) |
PRIVATE_KEY | PEM blocks — -----BEGIN … PRIVATE KEY----- |
AWS_ACCESS_KEY | AKIA or ASIA followed by 16 uppercase alphanumerics |
SECRET_FIELD | A field name matching secret-ish patterns — secret, token, credential, apiKey |
PASSWORD_FIELD | A field name matching password patterns — password, passwd, pwd |
Two different kinds of detection are at work, and both matter.
Value detectors look at what the data is. A GitHub token is recognisable whether the field is called token, note, or message:
{ "message": "here you go: ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8" }Field-name detectors look at what the data claims to be. A field called password is refused regardless of what is in it, because a field with that name should not be crossing your perimeter at all:
{ "data": { "db_password": "correct-horse-battery-staple" } }Together they cover the two realistic failure modes: a secret pasted into free text, and a secret passed through structurally.
The Arcus key is on the list#
ark_live_[a-f0-9]{16,} is an API_KEY match. An agent that tries to send its own Arcus credential through Arcus is refused. This is why the key format has a fixed prefix — it is designed to be recognisable, by Arcus and by any secret scanner you point at your repository.
Credit cards use Luhn, not just length#
A 16-digit order number, a tracking id and a timestamp concatenation are all 16 digits. Requiring the Luhn checksum removes the overwhelming majority of those false positives at the cost of missing a card number that was typed wrong — which is an acceptable trade for a check that runs on every request you send.
How the payload is walked#
The whole validated dispatch is scanned: message, every key and value inside data, and the target URL. Traversal is recursive through nested objects and arrays to a maximum depth of 12.
{
"target": "https://api.example.com/webhook",
"from": "intake-agent",
"message": "Escalating this case",
"data": {
"case": {
"notes": [
{ "author": "system", "text": "auth failed with sk_live_51H8xK2..." }
]
}
}
}That sk_live_… is four levels deep inside an array inside an object and is still found. The depth cap exists so a pathological payload cannot turn a scan into an unbounded walk; twelve levels is far past anything a legitimate dispatch carries.
The scan is dependency-free and in-process. No third-party DLP service, no network hop, no model inference. Adding DLP to your request path costs a payload walk, not a round trip — which is why the latency budget does not change when it is enabled.
Where it sits in the pipeline#
DLP is gate 4 — after policy, before guardrails.
- After policy, because a dispatch to a route you never authorized should be refused without paying to scan its body. Policy is an indexed read and a string match; DLP reads everything.
- Before guardrails, because a leaked credential is unconditional — no threshold, no severity, nothing to configure. A guardrail is specific and tunable. When both would fire, the unconditional one should be the reported reason.
Full reasoning: The enforcement pipeline.
What you see, per plan#
Every plan scans every payload. What differs is the detail in the response.
| Free | Pro / Max | |
|---|---|---|
| Payload scanned | Yes | Yes |
| Dispatch refused on a match | Yes | Yes |
violations — category names | Yes | Yes |
matchCount | Yes | Yes |
| Matched field paths | — | Yes |
| Matched values | Never | Never |
On Free, detailAvailableOn: "pro" appears in the response to name what is being withheld. With the dlp.configure capability you additionally get the paths that matched:
{
"error": "Payload blocked by DLP",
"status": "BLOCKED_BY_DLP",
"violations": ["API_KEY", "PASSWORD_FIELD"],
"matchCount": 2,
"matches": [
{ "category": "API_KEY", "path": "data.case.notes[0].text" },
{ "category": "PASSWORD_FIELD", "path": "data.db_password" }
]
}The matched value is never returned, on any plan
Not in the API response, not in the transfer log, not in the audit event, not in the dashboard. A DLP system that echoes the secret it caught has copied that secret into a second place — usually one with looser access control than the first. Arcus records the category and the path, which is everything you need to find and fix the leak at the source.
The path is the diagnostic. data.case.notes[0].text tells you the credential came in through a notes array — so the fix is upstream, in whatever populates those notes, not in the agent's prompt.
Turning it off for one route#
DLP is controlled per policy, via dlpEnabled on the winning policy for that dispatch.
{
"senderAgentId": "rotation-worker",
"targetEndpoint": "https://vault.internal.example.com/rotate",
"action": "ALLOW",
"dlpEnabled": false
}The legitimate case is 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.
Exempt the narrowest possible route
Because the flag comes from the winning policy, dlpEnabled: false on a broad pattern can quietly cover more than you intended if it outscores a narrower rule. Exempt one exact endpoint, and check the specificity of what you wrote — see Overlap resolution.
Disabling DLP disables exactly one gate on exactly those routes. Threat detection, identity binding, policy, guardrails and the audit chain are unaffected.
When DLP fires#
Look at the transfer log entry, which records the category and the path but never the value. The right response is almost always upstream of the agent:
| Path shape | Usual cause | Fix |
|---|---|---|
message | The credential was in the model's context and it wrote it into prose | Stop putting the secret in the context; pass a reference instead |
data.<field> where the field name is secret-ish | A config object was serialised wholesale into the payload | Send the specific fields the target needs |
data…[n].text inside a collected array | Upstream data — logs, notes, tickets — carries secrets | Redact at ingestion, not at dispatch |
target | A credential in a query string | Move it to a header the receiver expects, or to the body |
A DLP block is a finding, not a nuisance
Each one is a credential that would otherwise have left your perimeter. The count of dispatch.blocked_by_dlp events over a month is a genuinely useful number to show a security reviewer — it is evidence the control is live and doing work, and it is sealed in the audit chain rather than asserted.
Testing it#
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" }
}'AKIAIOSFODNN7EXAMPLE is AWS's published documentation example — it is not a live credential, and it matches the AWS_ACCESS_KEY shape exactly, which makes it the right thing to test with. The password in the message triggers PASSWORD_FIELD.
Expected: 422, violations: ["PASSWORD_FIELD", "AWS_ACCESS_KEY"], and your receiver never sees the request.
Limits, stated honestly#
DLP is pattern matching. It is very good at the thing it does and it is not a semantic understanding of your data.
It will catch credentials with a recognisable shape — provider-prefixed API keys, AWS access key ids, PEM blocks, JWTs, bearer headers, Luhn-valid card numbers — and any field whose name declares it holds a secret.
It will not catch a high-entropy string with no distinguishing format (a raw database password in a field called value), a secret that has been base64-encoded or otherwise transformed, or business-sensitive content that is not credential-shaped — a customer list, a medical record, an unreleased figure.
For the last category, parameter guardrails are the right tool: they let you write conditions about the meaning of a field rather than the shape of its bytes. DLP handles "this is a secret"; guardrails handle "this is more than this agent should be sending".