ES

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.

no dependenciesHTTP · SDK · MCP · CLI ~36 µs per decision1,000 calls free

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"}
That's the whole integration. If block is true, don't run the tool — return the reason to your agent so it can replan. Everything else is optional.

Core concepts

TermWhat it is
anchorThe 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.
stepOne thing the agent proposes to do: call a tool, or produce an output. One step = one governed call.
deviationHow far a step departs from the anchor, scored on six channels: scope, objective, output_type, tradeoff, tone, execution.
persistenceAn EWMA over deviation. Decays. Answers "is the agent off-mission right now?"
streakConsecutive-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.
admissibilityA 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.

01 · ANCHOR

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.

02 · FOUR MONITORS

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.

03 · HARD GATE

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.

04 · AUDIT

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.

05 · COUPLING

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 & recordedEnforced 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.

Two layers, and the difference matters commercially. The structural layer is deterministic, model-free, microseconds, and it is what blocks. The semantic layer is optional, cached, kept off the hot path, and it advises — it can raise a signal the structural layer missed, but it can never lower one. If the model is unreachable the API tells you so ("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
If your agent does not fail, the demo says so, and then says what that is worth. Zero failures in n runs is not a zero rate; it is an upper bound of 3/n at 95 % confidence. The tool prints that bound and multiplies it by your monthly volume, because “we have never seen it happen” and “up to 300 incidents a month” can be the very same measurement. Capable models often hold the line — you still cannot rule out the tail by watching.

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.

Where a platform won't let you block, the layer degrades to observe-only and marks itself as such — in the API response ("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_…
exitmeaning
0allow — proceed
1warn — proceed, but replan toward the mission
2block — do not run the action; escalate
3quota exhausted (see 402)
4the 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.

actionwhat it meanswhat you do
allowOn mission.Run the action.
warn_replanSustained 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_escalateA 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.

fieldrequirednotes
missionyesFree 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.
toolsyesTaken literally, never inferred. This is the list the gate actually enforces.
signing_authorityyesYour deploy credential / service identity. Empty or placeholder values are rejected.
hard_constraintsno{"tool","arg","op","value"}. Ops: <= < >= > ==. Breach = immediate block.
allowed_topicsnoTopical scope.
out_of_scopenoExplicit exclusions.
modenoenforce (default) or observe.
Trust boundary, stated plainly. If someone can rewrite your agent's system prompt without authorisation, they can rewrite its mission — but at that point they own the agent anyway. We inherit exactly your deployment pipeline's trust boundary: no weaker than your platform, and no ceremony that adds no real security. What is structural: the agent at runtime cannot rewrite its anchor or bypass the gate.

Endpoints

POST/v1/signup

Create an organisation and issue an API key. No auth. The key is shown once and stored only as a hash.

POST/v1/agents

Register an agent; returns agent_id and anchor_hash.

GET/v1/agents

List your registered agents.

POST/v1/agents/{agent_id}/step

Govern one step. This is the metered call.

POST/v1/govern

Same, with agent_id in the body — convenient for thin clients.

GET/v1/agents/{agent_id}/anchor

The signed anchor as stored.

GET/v1/agents/{agent_id}/audit

The hash-chained decision log plus a verification result.

GET/v1/usage

Calls used, remaining, and amount due.

GET/healthz

Liveness. No auth.

Step request fields

fieldtypenotes
proposed_toolstring|nullTool the agent wants to call. null when it is only speaking.
tool_argsobjectChecked against hard_constraints.
proposed_textstringWhat it intends to say. Enables the honesty check (claims of verification without a retrieval call).
output_kindstringaction · answer · ask · refuse · frontier
executed_toolstringWhat actually ran — enables proposed-vs-executed detection.
considered_toolsstring[]Alternatives weighed, for the trade-off channel.
step_idintOptional; auto-increments per agent.

Errors & quota

codemeaning
400Malformed request — the message names the field.
401Missing or invalid API key.
402Free tier exhausted. Not a failure — a typed refusal with reopening conditions.
404Unknown agent, or an agent belonging to another organisation.
500Our 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.

declarationwhereif you DON'T
missionregistration The agent is not registered.
toolsregistration The agent is not registered. It is the list the gate actually enforces; without it there is nothing to enforce.
signing_authorityregistration 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_constraintsregistration 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_mapregistration 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_sregistration No freshness required: a six-hour-old receipt supports "it has settled" in a system that changes every minute.
acknowledge_no_semantic_sensorregistration 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 / descriptorsat 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_invariantat 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_lineageat 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_precedenceat runtime Constraints over the sequence — "no refund on a closed ticket" — are not checked. Every step is individually admissible; the order is not.
state_providereffecting 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_KEYenvironment 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.

whenwherewhat it is for
at creationPOST /v1/agents what you already know it will read. The response returns reads_declared: how many descriptors landed.
at any timePOST /v1/agents/{id}/reads when you find out between steps, or another system knows.
in the step itselfPOST /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.

tierwhat you declarewhat it marks
1the exact key you read only writes to that key
2ranges over ordered attributes whatever falls inside the range
3the 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:

fieldeffect 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.

An honesty note. None of this infers relationships. If two agents read the same data and neither declares it, there is no coupling to see — and we tell you at deploy time, not when something happens.

5 · What exists in the library and is not an endpoint yet

Not in the hosted API yet. Declared invariants, cross-system lineage, operation precedence and delegated budgets are built and tested in the library, and are not exposed as HTTP routes. They are used today by self-hosting the SDK. We would rather say that than document a route that returns 404.
declarationwhat it catchesif 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.

pathlatency
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.

Next

See the live drift demo →
Pricing →
Get an API key →