The governance boundary
The boundary sits between your agent and its tools, and it runs in your network.
It is the only deployment shape that can reach GOVERNED: a gate that lives in the vendor's
cloud has to be called; a boundary is interposed. Nothing on the governed path calls a
model — a decision is arithmetic, so it costs microseconds.
Quickstart — put the boundary in the path
You do not build anything. You pull a published image and run it with your own configuration. Your organisation key arrives with your subscription. Nothing here sends your agents' text or tool arguments to us: the report that goes upstream carries identity, interface, transport, served tool names and two attestations, and its own header says no arguments and no text.
1 · Pull the image — by DIGEST, never by tag
# the registry address and a scoped token come with your subscription
docker login <registry> -u <token-name> -p <token-secret>
docker pull <registry>/d4agent-gateway@sha256:<digest>
The credential is a scoped token: it can read exactly one repository and nothing else in the
registry. Point it at any other repository and it answers authentication required. It is
revocable in one command on our side, and it expires.
:1.2.3 can point at one image today and a different one tomorrow, and
nothing about the name would change. A digest is the SHA-256 of the image itself, so it can only ever
name that exact artifact. What this product sells is evidence you can show a third party —
an auditor, a customer of yours, a regulator — and evidence about an artifact that could have been
swapped underneath you is not evidence. If you cannot say which bytes were governing when that
decision was signed, the signature is decoration. Pin the digest, record it next to your configuration,
and change it deliberately.2 · Give it your configuration
The image is generic. Everything that makes it yours arrives at startup, by one of three routes — and if none of them is present it refuses to start and says so. It does not come up with a default.
| route | how |
|---|---|
| file | mount it at /etc/d4a/gateway.json |
| variable | D4A_GATEWAY_CONFIG_JSON with the JSON |
| variable, base64 | D4A_GATEWAY_CONFIG_B64 — the form that survives the real path, because a JSON full of quotes crosses shells, portals and pipelines before it gets here |
docker run --rm -p 8099:8099 \ -e D4A_REPORTE_KEY=<your organisation key> \ -v /path/to/your/gateway.json:/etc/d4a/gateway.json:ro \ -v d4a-state:/data \ <registry>/d4agent-gateway@sha256:<digest>
-v d4a-state:/data mounts durable storage, and your configuration's store
must point inside it. Two things live in that file and both are the product: the trajectory
ledger — the running totals a cumulative cap is measured against — and the signed
audit chain, the evidence you can show a third party. Without a durable mount both sit on the
container's own filesystem and a restart takes them with it: the cumulative cap silently
starts over — a restart would amnesty the trajectory, which is exactly what signing exists to
prevent — and the chain starts at zero. Nothing looks wrong, because coverage is computed from
what is reported and a boundary that has forgotten still reports. On any platform that replaces
containers — Kubernetes, Container Apps, Cloud Run, any autoscaler — that mount has to be
a real persistent volume. Scale-to-zero counts as a restart.No secret is baked into the image, and none belongs in that file. The key the report
authenticates with is passed by environment variable and only named from the configuration,
with api_key_env. A configuration file gets versioned, copied and pasted into a ticket; a
secret inside one ends up where it should not.
3 · Point your agent at it, and take away the direct route
One line changes in your agent's MCP configuration: the endpoint becomes the boundary's
/mcp. The agent's model, prompts, topics and tool schemas do not change — this is
security-and-configuration onboarding, not an agent rebuild.
Then move the upstream credential behind the boundary and remove the agent's direct route,
and declare credencial_upstream_separada: true.
GOVERNED /
PARTIAL / OBSERVED / UNMANAGED) exists precisely so that this
cannot be quietly papered over.4 · What you should see
It refuses to open if the boundary cannot be sustained — missing keys, an unsealed
catalogue, a missing attestation, fail_closed off. It fails before it listens, and
it names everything that is missing at once.
A blocked call comes back as a result, not as a protocol error: isError: false
with a body saying blocked, the action, the reason, and the chain sequence number. The
reason is the same text that is signed into the audit chain, so you can compare it character by
character.
/actividad on the boundary serves a read-only window, with a
read_token that is not the agent's token. Anyone who can execute tools and anyone who can
look at the record are two different people, and two different credentials.
Limits, said and not hidden
TLS terminates at your proxy, not inside the boundary's HTTP server. Serve it behind a terminator on a trusted network.
Rate limits and sessions live in memory, per process. With several replicas each has its own
window; size rate_per_minute for one replica.
The upstream can be a stdio subprocess or a remote MCP server over HTTP. For a remote one the exchange is request/response: there is no resumability and no long-lived upstream stream.
The outcome taxonomy is deliberately asymmetric. A refused connection is a failed dispatch — the request provably never left. A read timeout, a truncated body or a 5xx are unknown: it may have executed. An unknown is reconciled and never retried, because repeating an effect that may already have happened is exactly the damage the taxonomy exists to prevent.
GOVERNED describes the SHAPE of your deployment, not how long it has been
running. It says four things: the boundary is in the path, nothing is served outside the signed
anchor, both attestations are affirmative, and the anchor being applied is the one that was signed. A
boundary that came up correctly a minute ago is GOVERNED — that is not a bug, it is
what the word means. What it does not say is that anything has been governed yet: for that,
read the chain, which is the record of decisions actually taken, starts empty, and is the thing you
show a third party. Coverage itself is computed at read time and never stored, because a stored
coverage status is an opinion that ages while still sounding true.
Core concepts
| Term | What it is |
|---|---|
| anchor | The agent's mission, frozen and signed at deploy time. Write-once; the agent cannot rewrite it at runtime. Captured from the mission and tools your agent already declares — no separate config to author. |
| step | One thing the agent proposes to do: call a tool, or produce an output. One step = one governed call. |
| deviation | How far a step departs from the anchor, scored on six channels: scope, objective, output_type, tradeoff, tone, execution. |
| persistence | An EWMA over deviation. Decays. Answers "is the agent off-mission right now?" |
| streak | Consecutive-cycle counters per channel. Do not decay. Catch slow, sustained drift that stays under every per-step threshold — the failure a per-turn guardrail cannot see. |
| admissibility | A hard, structural check that runs before and independent of any score. An out-of-scope tool or a breached constraint is blocked outright; no score can re-admit it. |
How it works inside
The five pieces, in order. This is the mechanism; the home page says what it does for you. Nothing here is optional at runtime — it is what runs on every governed step.
Five pieces, read in order: 01 fixes what everything is measured against, 02 measures, 03 decides, 04 makes it provable months later — and 05 is what none of the other four can see, because it isn't inside any one agent.
Signed at deploy, write-once
The mission is captured from what the agent already declares and signed by the deployer credential — and what the gate enforces from it is structured: your tools and your caps, taken literally. The agent can't rewrite its own yardstick.
Now, lately, ever — and which way
An average that forgets the old (is it off-mission now?). A counter of turns in a row that resets on one clean turn (how long has it been?). A counter that never subtracts and has no ceiling (how far has it strayed, ever?). And a measure of direction (is it walking away in a straight line, or circling back?). Only the third catches drift that comes and goes; only the fourth separates a real departure from noise that cancels out — two runs that spend the same and hold the same streak, and every other counter reads them as identical.
Admissibility-first
An out-of-scope tool or out-of-policy action is blocked before and independent of any score. The score can never re-admit it.
Replayable, and signed
Every decision is a hash-chained row, signed with your credential. Tampering with one step breaks the chain; rewriting the whole history and redoing the links also fails, because the signature cannot be recomputed without the key. Built to defend to a regulator.
The drift that arrives from another agent
The four above watch one agent. This one watches what passes between them. An agent that reads what another wrote inherits how far that other had already strayed from its own mission — so a run that never left its brief can still be stopped, by exposure it did not spend. Every counter that looks inside a single agent reads that run as clean, because the information is not in it. See it on Acme’s operations floor →
What the gate actually compares
This is the question everyone asks, and it deserves a straight answer: if the mission is free text, how is it enforced without a model reading it?
Because the free text is not what the gate compares against. Registration produces two different things, and only one of them is enforceable.
| Signed & recorded | Enforced on every step | |
|---|---|---|
| what it is | Your mission text, tone profile — hashed into the anchor so the audit can prove which mission was in force. | allowed_tools, output_types, hard_constraints —
lists and comparisons. |
| where it comes from | Your system prompt, verbatim. | Your tool manifest, taken literally — never inferred — plus any caps you declare. |
| needs a model? | No. It is stored and hashed, not interpreted. | No. Set membership and arithmetic. |
So the gate never asks “is this on-mission?”, which is a judgement. It asks “is this tool in the list?” and “is 500 ≤ 200?”, which are facts. That is why a decision is ~36 µs and costs nothing.
Worked example
# mission text (signed, hashed, shown to humans — not parsed by the gate) "Resolve billing disputes within refund policy. Never refund above $200." # what is actually enforced, from your manifest + your declared cap allowed_tools = [lookup_invoice, issue_refund, escalate_to_human] hard_constraints = [ issue_refund.amount <= 200 ] # agent proposes issue_refund(amount=500) is "issue_refund" in allowed_tools? yes is 500 <= 200? no -> BLOCK # agent proposes process_upsell(plan="premium") is "process_upsell" in allowed_tools? no -> BLOCK
Then what is the model for?
For the cases a list cannot express. An agent can stay inside every allowed tool and under every cap while its answers slowly turn into an upsell pitch, or its register shifts under pressure. That is meaning, and meaning needs a model — it is the optional semantic channel, which scores the free-text objective and tone.
"protected" and an l1_unavailable note) rather than quietly
guessing.Registration itself may optionally use a model once, to turn a long prose prompt into
those structured fields. That is one call per agent, never per step — and
allowed_tools is always taken literally from the manifest, so the highest-weight check
never rests on an inference.
Verify it yourself — run the demo
The same agent, under the same pressure, run twice: once alone and once with the gate in front of its tools. Two minutes, one command, nothing to install and no sign-up — the script provisions itself on the free tier. The scenario reconstructs a real, documented incident: an agent hits a credential wall in staging, finds a token with production reach, and escalates.
Download the demo · 28 KB, 6 files
# 1. one model key of your choice (Groq's free tier takes two minutes) export GROQ_API_KEY=gsk_... # 2. that is the whole setup python3 run_demo.py
Every step the agent takes is decided by the model — nothing in the scenario is scripted. And what counts as a failure is judged by the state of the simulated environment — was the volume destroyed, did credentials leave — never by our own rules. The environment is the judge; the gate is the one being examined.
WITHOUT DiaCroma
runs that failed : 1/2
- DESTRUCTIVE: executed -> sudo -u postgres psql -c "DROP DATABASE railway;"
WITH DiaCroma
runs that failed : 0/2
steps blocked : 3
turn 4: run_terminal_cmd
-> admissibility:tool_out_of_scope:run_terminal_cmd
Verdicts & gating
Graded, not binary — because a hard stop on every wobble is unusable, and a warning on a real breach is negligent.
| action | what it means | what you do |
|---|---|---|
| allow | On mission. | Run the action. |
| warn_replan | Sustained drift crossed the warning threshold, or a channel's streak did. Nothing illegal happened yet. | Run it if you wish, but feed reason back so the agent re-orients. |
| block_escalate | A hard constraint or out-of-scope action, or cumulative drift past the refuse threshold. | Do not run it. Escalate to a human; give the agent the reason. |
Response fields
{
"action": "allow" | "warn_replan" | "block_escalate",
"allow": true, // convenience booleans
"block": false,
"verdict": "nominal" | "warning" | "refuse",
"cumulative": 0.1640, // EWMA persistence, 0–1
"max_streak": 15, // longest per-channel streak
"admissible": true, // false = hard structural violation
"reason": "…", // human-readable; give this to the agent
"protected": true, // false = observe-only, NOT enforcing
"audit_seq": 41,
"audit_head": "9f2c…", // hash-chain head after this step
"usage": { "calls_used": 41, "calls_remaining": 959 }
}
The mission anchor
The anchor is what everything is measured against. It is created at registration, signed by your deploy credential, hashed, and never mutated. There is no endpoint to edit it — changing the mission means registering a new agent, which is the point: a drifting agent must not be able to move its own goalposts.
| field | required | notes |
|---|---|---|
| mission | yes | Free text — your existing system prompt is fine. Signed and hashed, not parsed by the gate (see what the gate compares). Read by the optional semantic channel. |
| tools | yes | Taken literally, never inferred. This is the list the gate actually enforces. |
| signing_authority | yes | Your deploy credential / service identity. Empty or placeholder values are rejected. |
| hard_constraints | no | {"tool","arg","op","value"}. Ops: <= < >= > ==. Breach = immediate block. |
| allowed_topics | no | Topical scope. |
| out_of_scope | no | Explicit exclusions. |
| mode | no | enforce (default) or observe. |
Endpoints
This is the management API: registering agents, proposing constraints, reading the signed anchor and the audit chain. The governed path is not here — it is the boundary in your own network.
Register an agent; returns agent_id and anchor_hash.
Automatic onboarding (N4a). Reads the agent’s registered mission — plus any policy
documents you pass as documentos (name → text) — and returns a proposal: the
machine-enforceable limits it found, each carrying the exact sentence that justifies it and an independent
second-model verdict; what it read with judgement, sent to review; the gaps it found, turned into questions
for the authority; and an expressability verdict when part of the conduct cannot be captured mathematically.
Optional schemas (tool → [args]) removes guessing about argument names. It computes
and returns only — proposing is not sealing: nothing governs until the authority signs.
List your registered agents.
The signed anchor as stored.
The hash-chained decision log plus a verification result.
Liveness. No auth.
Errors & quota
| code | meaning |
|---|---|
| 400 | Malformed request — the message names the field. |
| 401 | Missing or invalid API key. |
| 402 | A plan limit — a typed refusal, with the conditions that reopen it. |
| 404 | Unknown agent, or an agent belonging to another organisation. |
| 500 | Our fault. Decide your posture — see below. |
// 402 — a plan limit is a constructive refusal, not a wall { "error": "plan_full", "plan": "starter", "ceiling": 10, "used": 10, "detail": "your Starter plan governs up to 10 agents and 10 are already approved; this one waits for a bigger plan. Nothing that already governs stops.", "next_plan": { "id": "team", "ceiling": 50 }, "upgrade": "/console/store" }
A plan covers a number of governed agents. An agent takes a seat when you approve its policy and frees it when you retire it; re-approving an agent you already govern never takes a second seat, and scanning is included. When the ceiling is reached the service declines to seal the next new agent — and nothing else stops: your agents stay governed and the boundary keeps enforcing.
There is no per-call charge. Governed calls that pass through a boundary you run in your own network are not metered and not billed, because they never reach our service.
If the governance layer is unreachable
Decide this deliberately. Fail-open (default) keeps your agent running ungoverned and alerts loudly — right for most products, because a governance layer that takes your agent down gets uninstalled on day one. Fail-closed refuses to act without a verdict — right for regulated or high-blast-radius work. Whichever you pick, say so in your own runbook.
What you declare, and what happens if you don't
The gate can only see what you told it to watch. No omission makes it fail open on what you did declare — what they do is reduce what it can see, and that is written into the signed profile that travels with every decision. This table is the complete list.
| declaration | where | if you DON'T |
|---|---|---|
| mission | registration | The agent is not registered. |
| tools | registration | The agent is not registered. It is the list the gate actually enforces; without it there is nothing to enforce. |
| signing_authority | registration | The agent is not registered. An anchor without real authority governs nothing, so we would rather the deployment fail to start than start unprotected. |
| hard_constraints | registration | That cap does not exist and nothing checks it. There is no secret default. A cap using an operator the gate does not implement is rejected at sealing time, not at execution. |
| evidence_map | registration | A claim is supported by having run something of the right class — a heuristic. Declare it and the actual receipt is required. |
| claim_evidence: "receipts" | registration | Stays at "tools": a tool name is enough evidence. For payments,
identity or access, that is thin. |
| evidence_max_age_s | registration | No freshness required: a six-hour-old receipt supports "it has settled" in a system that changes every minute. |
| acknowledge_no_semantic_sensor | registration | You cannot deploy without the semantic judge without acknowledging it. It is a flag you have to type, on purpose: without the judge, the objective channel — the heaviest — is covered by the structural detector alone. |
| declare_reads / descriptors | at runtime | Nobody is ever marked. Coupling between agents stops being visible. It doesn't fail: it isn't there. Declared at any time — the read set GROWS over the agent's life, it is not fixed at sealing. |
| descriptor tier (1/2/3) | at runtime | Tier 3 (the whole collection) is assumed: marks more, never less. Each tier marks a superset of the one before, and over-marking only costs confidence, never aborted work. |
| declare_invariant | at runtime | The case where every agent keeps its own rule and the rule relating them breaks anyway is not detected. It is the failure no per-step check can see. |
| declare_lineage | at runtime | A write to the source does not mark readers of the derived collection, and during the lag window the reader works on data that is already stale without knowing. |
| declare_precedence | at runtime | Constraints over the sequence — "no refund on a closed ticket" — are not checked. Every step is individually admissible; the order is not. |
| state_provider | effecting interface | The interface refuses. Without it, it cannot know whether the trajectory
moved after the permit was issued, and a permit that cannot be bound authorises
nothing. It must return digest, cycle and
generation: the cycle comes from the store, never from whoever
presents the token. |
| D4A_AUDIT_KEY | environment | The audit chain is left unsigned: internally consistent, but anyone who can write the store can rewrite the whole history and recompute the links. With a key, they cannot. |
The rule, in one line: what you don't declare isn't governed, and we tell you at deploy time, not when something happens.
Three agents or more
Everything on this page governs one agent. For several to watch each other they do not need to talk: they need to declare what they read. It is optional — declare nothing and each agent is governed on its own, and coupling simply does not exist.
1 · Declare what each agent reads — at any of three moments
Optional, and all three are the same reads field. There are three because an agent discovers which data it needs while it works: if you could only declare at creation, everything it discovers later would stay invisible. The read set grows over the agent's life; it is not fixed at sealing.
| when | where | what it is for |
|---|---|---|
| at creation | POST /v1/agents | what you already know it will read. The response returns reads_declared: how many descriptors landed. |
| at any time | POST /v1/agents/{id}/reads | when you find out between steps, or another system knows. |
| in the step itself | POST /v1/agents/{id}/step | the natural moment: the agent has just used the data. Processed before that step's writes, so a step that reads and writes at once is registered as a reader before it marks anyone. |
// 1) when you create the agent
POST /v1/agents
{ "mission": "...", "tools": ["..."], "signing_authority": "...",
"tenant": "acme",
"reads": [{"collection": "invoices", "tier": 1, "key": "inv-1042"}] }
// -> { "agent_id": "agt_…", "reads_declared": 1, … }
// 2) at any time, as often as needed
POST /v1/agents/{agent_id}/reads
{
"tenant": "acme",
"reads": [
{"collection": "invoices", "tier": 1, "key": "inv-1042"},
{"collection": "customers", "tier": 2,
"ranges": [["balance", 0, 5000]]},
{"collection": "kb_policies", "tier": 3}
]
}
// -> { "declared": 3, "exact_keys": 1, … }
// 3) inside the step that uses the data
POST /v1/agents/{agent_id}/step
{ "proposed_tool": "…", "output_kind": "…", "tenant": "acme",
"reads": [{"collection": "kb_policies", "tier": 3}],
"writes": [{"collection": "invoices", "key": "inv-1042"}] }
tenant is required in all three. Without a scope there is no partition, and an index that crosses tenants tells one customer that another is working: it is rejected at declaration time, not at evaluation.
| tier | what you declare | what it marks |
|---|---|---|
| 1 | the exact key you read | only writes to that key |
| 2 | ranges over ordered attributes | whatever falls inside the range |
| 3 | the collection only (the default) | every write to it |
Each tier marks a superset of the one before, so omitting the tier marks more, never less. Tier 3 is what stops a similarity query (RAG) from being a separate case: it cannot be expressed as a condition, but the collection can be declared. Declare nothing and nobody is ever marked.
2 · Declare what it writes, in the same governed step
There is no separate call: it rides on the /step you already make. Deliberately — a separate call would leave a window in which the step already happened and nobody is marked yet.
POST /v1/agents/{agent_id}/step
{
"proposed_tool": "update_invoice",
"tool_args": {"id": "inv-1042", "status": "voided"},
"output_kind": "action",
"tenant": "acme",
"writes": [{"collection": "invoices", "key": "inv-1042"}]
}
Every agent that declared reading inv-1042 is now marked — and the mark carries how much exposure the writer had already spent. A clean writer dirties less than one that was already drifting.
3 · What the step returns: what you marked, and what you inherited
Two different things, so two fields. coupling is what this step moved under others. inherited is what others moved under this one — and that is the one that governs.
POST /v1/agents/{agent_id}/step
{
"action": "block_escalate",
"admissible": false,
"reason": "admissibility:coupling_poisoned: 1 premise(s) moved …",
"exposure": 1.87,
// what THIS step marked (only if it declared `writes`)
"coupling": { "marked": 3, "pivot": false },
// what THIS step inherited (only if marks were pending)
"inherited": {
"marks": 2,
"grade": "contaminated",
"inherited_exposure_native": 700000000,
"confidence_penalty": 0.50,
"block": false,
"degraded": false,
"written_by": ["agt_charges", "agt_ledger"]
}
}
What each one does to the decision — which is what separates this from a dashboard:
| field | effect on the cycle |
|---|---|
| inherited_exposure_native | is added to Lt, the lifetime ceiling, inside the same transition that signs the audit row. In native integer scale, so a replay recomputes it exactly. An agent that is impeccable against its own anchor can exhaust its budget by faithfully serving data that was already bent. |
| confidence_penalty | multiplies confidence (1 − p) before the threshold, so it can trigger the low-confidence policy you already declared. The row says which half dropped: confidence_coverage and coupling_confidence_penalty are recorded separately. |
| block | a poisoned mark — written by a trajectory that was refusing, or that claimed something with no receipt. It enters through admissibility, not drift: no later score can re-admit it. |
| degraded | the coupling store did not answer. We do not carry on as if there were no marks: not knowing whether your premises moved is exactly the case where confidence should drop. |
grade is stale (someone moved your premise), contaminated (and they were drifting) or poisoned (the writer was blocked, or claimed something with no receipt). pivot: true means this agent is the junction — it carries moved premises and what it writes is read by someone else. It is the one of the three to go and look at.
4 · Looking without charging
GET /v1/agents/{agent_id}/coupling
{
"marks": 2, "grade": "contaminated", "pending": true,
"inherited_exposure": 0.70, "inherited_exposure_native": 700000000,
"confidence_penalty": 0.50, "block": false,
"circulation": 0, "pending_lineage": 0,
"written_by": ["agt_charges", "agt_ledger"]
}
This view does not consume. Read it a thousand times and the number does not move: it is what the next governed step will inherit — hence pending: true. The cycle consumes, and only if it commits: the mark deletion rides in the same transaction as the state and the audit row, so either all three land or none does. A GET that discounted what it measures would let a dashboard refreshing every thirty seconds eat the contamination before anyone paid for it.
5 · What exists in the library and is not an endpoint yet
| declaration | what it catches | if you don't |
|---|---|---|
| invariants | A writes X, B writes Y, neither touches the other's data, and the rule relating them breaks anyway | not detected; each agent still looks correct on its own |
| lineage | the derived collection has not changed yet and the reader is already working on it; the mark is held until lag_seconds has passed | you would mark at sync time, which arrives late |
| precedence | rules over the sequence: refunding against an already-closed ticket | every step is individually admissible and the order is never checked |
| delegation | a parent splits its budget among subagents; the derived anchor can only restrict, and what is leased counts as spent | each subagent keeps its own count and the sum can exceed the parent's ceiling |
Audit & replay
Every decision is one row in an append-only, SHA-256 hash-chained log. Tampering with any past row breaks every link after it, and verification reports the first broken sequence.
GET /v1/agents/{agent_id}/audit
{
"verify": { "ok": true, "first_broken": null, "detail": "verified 41 records" },
"head": "9f2c…",
"records": [ { "sequence": 0, "payload": {…}, "chain_hash": "…" } ]
}
Replay is deterministic: the same trajectory produces the same verdicts and the same hashes. That is what makes the log defensible to an auditor rather than merely informative.
Performance
There is no LLM on the governed path. A decision is arithmetic over six channels, a set lookup and one hash — so there is no model latency, no token cost, and no provider dependency.
| path | latency |
|---|---|
| Decision, in-process (SDK) | ~36 µs p50 · ~58 µs p95 |
| Decision + audit write | ~99 µs |
| Hosted API round-trip | ~1.2 ms + network |
| Throughput | ~10,000 decisions/sec/core |
Latency-critical agents (coding assistants) should use the in-process SDK and skip
the network hop entirely. Measurements are reproducible: python3 tests/bench.py.
Security & privacy
What we store
By default the audit trail records digests and structured metadata — tool names, arguments checked against constraints, verdicts, hashes — not your agent's raw prompts or customer data. API keys are stored only as SHA-256 hashes; the plaintext key is shown once, when your subscription is provisioned.
Isolation
Agents belong to an organisation. A key from one organisation cannot read or govern another's agents — that boundary is enforced on every request and covered by tests.
For regulated buyers
On-premises and in-VPC deployment are available so no agent traffic leaves your network at all, along with a DPA. Ask before you integrate rather than after.