SDK & API
Governance as a call. Register an agent once, then ask about each step. No model calls on the governed path — a decision is arithmetic, so it costs microseconds.
Quickstart
Three calls. The first two happen once; the third happens per agent step.
# 1 · get a key (once per organisation) curl -s https://api.diacroma.com/v1/signup \ -d '{"email":"you@company.com","company":"Acme"}' # → {"api_key":"d4a_live_…","free_calls":1000} (shown once — store it) # 2 · register the agent (once per agent) curl -s https://api.diacroma.com/v1/agents \ -H "Authorization: Bearer $D4A_KEY" -d '{ "mission":"Resolve billing disputes within refund policy.", "tools":["lookup_invoice","issue_refund","escalate_to_human"], "signing_authority":"acme-deploy-key", "hard_constraints":[{"tool":"issue_refund","arg":"amount","op":"<=","value":200}] }' # → {"agent_id":"agt_…","anchor_hash":"…"} # 3 · govern each step (per action your agent proposes) curl -s https://api.diacroma.com/v1/agents/agt_…/step \ -H "Authorization: Bearer $D4A_KEY" \ -d '{"proposed_tool":"issue_refund","tool_args":{"amount":500},"output_kind":"action"}' # → {"action":"block_escalate","block":true, # "reason":"admissibility:hard_constraint:issue_refund.amount=500 !<= 200"}
block is true, don't run the tool —
return the reason to your agent so it can replan. Everything else is optional.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. Watch three agents do it →
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
Python SDK Self-hosted
If you build your own agents, this is the shortest path: the governor reads the same mission and tools your agent already has.
Running in your process means the source runs on your machines, so this is licensed per site at a flat annual fee rather than metered per call — internal use, no redistribution, audit rights. The hosted API above is the metered option and needs nothing installed.
# one line — the governor is created from the agent itself import d4a agent = Agent(instructions=MISSION, tools=[lookup_invoice, issue_refund, escalate]) gov = d4a.govern(agent, signing_authority="acme-deploy-key") # then, before each tool call: r = gov.observe(AgentStep(step_id=i, proposed_tool=name, tool_args=args, output_kind="action", proposed_text=text)) if r.action.value == "block_escalate": raise ToolDenied(r.reason) # never runs elif r.action.value == "warn_replan": hint = r.reason # feed back; let it re-plan
Agent factory — govern by default
Change your factory once and every new agent in the organisation is governed, with nothing to remember per agent.
def create_agent(mission, tools, authority): agent = Agent(instructions=mission, tools=tools) gov = d4a.from_definition(mission=mission, tools=tools, signing_authority=authority) return d4a.bind(agent, gov)
HTTP API
Language-agnostic. Any HTTP client works; the payload is small and the response is a verdict.
import requests D4A = "https://api.diacroma.com" H = {"Authorization": f"Bearer {KEY}"} def governed_call(agent_id, tool, args, text=None): r = requests.post(f"{D4A}/v1/agents/{agent_id}/step", headers=H, json={ "proposed_tool": tool, "tool_args": args, "proposed_text": text, "output_kind": "action"}).json() if r["block"]: raise PermissionError(r["reason"]) return run_tool(tool, args)
const D4A = "https://api.diacroma.com"; async function governedCall(agentId, tool, args, text) { const r = await fetch(`${D4A}/v1/agents/${agentId}/step`, { method: "POST", headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ proposed_tool: tool, tool_args: args, proposed_text: text, output_kind: "action" }) }).then(x => x.json()); if (r.block) throw new Error(r.reason); return runTool(tool, args); }
curl -s $D4A/v1/agents/$AGENT/step \
-H "Authorization: Bearer $D4A_KEY" \
-H "Content-Type: application/json" \
-d '{"proposed_tool":"issue_refund","tool_args":{"amount":500},
"output_kind":"action","proposed_text":"Processing your refund."}'MCP server Self-hosted
The universal path. Register the governance layer on the tool seam and every MCP-compatible agent is governed — no change to the agent.
In-process, like the SDK, and licensed the same way. If you want the MCP seam without an on-site licence, point the adapter at the hosted API instead.
# route the agent's tools THROUGH the governance layer
agent.mcp_servers = [ d4a.mcp(your_mcp_server, mission=MISSION) ]
Tool calls are inspected in flight; a denied call returns an MCP error the client surfaces to the model, which is what lets it replan rather than fail blindly.
"protected": false) and in
the dashboard. You are never told you are protected when you are only being watched.CLI
Exit codes make it a drop-in gate for pipelines and CI. The d4a package is a client for this API and nothing more — no scoring, no thresholds, no drift mathematics run on your machine.
pip install d4a # no dependencies; the client is an HTTP client d4a signup --email you@company.com d4a provision --mission "Resolve billing disputes within refund policy." \ --tools lookup_invoice,issue_refund,escalate_to_human \ --authority acme-deploy-key --cap issue_refund.amount:200 d4a check --agent agt_… --tool issue_refund --args '{"amount":500}' d4a usage d4a audit --agent agt_…
| exit | meaning |
|---|---|
| 0 | allow — proceed |
| 1 | warn — proceed, but replan toward the mission |
| 2 | block — do not run the action; escalate |
| 3 | quota exhausted (see 402) |
| 4 | the command or the connection was wrong — never a governance verdict, so 2 always means a real block |
Verify it end to end, on your machine
Sixty seconds, no model key, no sign-up anywhere and no network: you start the service on your laptop and talk to it. What follows is a real transcript — the same commands, the same output.
1 · Start the service
cd C:\Apps\D4A # or wherever you keep it
python3 cli.py serve --port 8123
With no database configured it uses SQLite in a file. For production you pass D4A_DB=postgres://… and the store changes, not the governance.
2 · Sign up and register an agent WITH its cap
curl -s localhost:8123/v1/signup -H 'Content-Type: application/json' \
-d '{"email":"tu@empresa.com"}'
# -> {"api_key":"d4a_live_…","free_calls":1000, …}
curl -s localhost:8123/v1/agents -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{
"mission": "Resolve billing disputes within refund policy.",
"tools": ["lookup_invoice","issue_refund"],
"signing_authority": "acme-deployment-key",
"acknowledge_no_semantic_sensor": true,
"hard_constraints": [{"tool":"issue_refund","arg":"amount","op":"<=","value":200}]
}'
# -> {"agent_id":"agt_2e309d72c4084bf2","anchor_hash":"1500254eb6f0ec1b…"}
The cap has to be declared. If you leave it out, that cap does not exist and nothing checks it — that is not an oversight, it is the house rule: what you do not declare is not governed.
3 · Four steps, and what each one answers
POST /v1/agents/agt_…/step
lookup_invoice -> ALLOW
issue_refund {"amount":150} -> ALLOW
issue_refund {"amount":500} -> BLOCK_ESCALATE
admissibility:hard_constraint:issue_refund.amount=500 !<= 200
drop_the_database -> BLOCK_ESCALATE
admissibility:tool_out_of_scope:drop_the_database
Both blocks come from admissibility, not from scoring: they are decided before any number is looked at, and no later score can re-admit them.
4 · The audit verifies itself
curl -s localhost:8123/v1/agents/agt_…/audit -H "Authorization: Bearer $KEY"
# -> "verify": {"ok": true, "first_broken": null, "detail": "verified 3 records"}
# "head": "27c524018969f4f5e018d2c4f9a53b7b…"
5 · The same over the CLI, with exit codes
d4a provision --mission "…" --tools lookup_invoice,issue_refund \
--authority acme-key --cap issue_refund.amount:200
d4a check --agent agt_… --tool issue_refund --args '{"amount":150}'
# ALLOW verdict=nominal cumulative=0.0000 admissible=True
# exit code = 0
d4a check --agent agt_… --tool issue_refund --args '{"amount":500}'
# BLOCK_ESCALATE admissible=False admissibility:hard_constraint…
# exit code = 2
Mind the --cap separator: it is a colon, issue_refund.amount:200. Written as amount<=200 the command is rejected with a message — the cap is never half-applied nor silently ignored.
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
Create an organisation and issue an API key. No auth. The key is shown once and stored only as a hash.
Register an agent; returns agent_id and anchor_hash.
List your registered agents.
Govern one step. This is the metered call.
Same, with agent_id in the body — convenient for thin clients.
The signed anchor as stored.
The hash-chained decision log plus a verification result.
Calls used, remaining, and amount due.
Liveness. No auth.
Step request fields
| field | type | notes |
|---|---|---|
| proposed_tool | string|null | Tool the agent wants to call. null when it is only speaking. |
| tool_args | object | Checked against hard_constraints. |
| proposed_text | string | What it intends to say. Enables the honesty check (claims of verification without a retrieval call). |
| output_kind | string | action · answer · ask · refuse · frontier |
| executed_tool | string | What actually ran — enables proposed-vs-executed detection. |
| considered_tools | string[] | Alternatives weighed, for the trade-off channel. |
| step_id | int | Optional; auto-increments per agent. |
Errors & quota
| code | meaning |
|---|---|
| 400 | Malformed request — the message names the field. |
| 401 | Missing or invalid API key. |
| 402 | Free tier exhausted. Not a failure — a typed refusal with reopening conditions. |
| 404 | Unknown agent, or an agent belonging to another organisation. |
| 500 | Our fault. Decide your posture — see below. |
// 402 — the free tier is a constructive refusal, not a wall { "error": "free_tier_exhausted", "mode": "REFUSE", "blocking_condition": "free tier of 1000 governed calls is exhausted", "reopening_conditions": [ "start a contract to continue — diacroma@veritglobal.com", "we raise your quota the same day; your agents and anchors stay as they are" ] }
Usage warnings appear in every response from 80% of the tier onward, so nobody discovers the limit mid-run.
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 exists once, in the response to your signup call.
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.
Self-hosting Self-hosted
One dependency — the Postgres driver, and only if you point it at Postgres; with a SQLite file path it is standard library only. The container is small and starts in under a second. Mount a volume for the ledger, or give it a Postgres DSN, and point a hostname at it.
docker build -t d4agent .
docker run -p 8088:8088 -v d4a-data:/data d4agent
# or with no container at all
python3 -m service.app --port 8088
Self-hosting is licensed at a flat annual fee per site, not metered per call. Once the service runs in your infrastructure the usage ledger is your database, so the terms are contractual rather than technical: internal use, no redistribution, audit rights. Air-gapped installs welcome. Everything metered per governed step runs on the hosted API.