SDK
The gate runs as a service and your harness asks it one question before each action. One file, no dependencies, fails closed.
Installing with the SDK
The SDK option puts the gate outside the agent as a service, and the client is how a harness talks to it. It covers every runtime that is not Claude Code, which is the reason it exists alongside npx.
The client is a client and not a checker. The gate decides every verdict and writes every receipt, and nothing in the client evaluates a rule.
One file. sdk/python/coco_gate.py uses the Python standard library and
sdk/js/coco-gate.mjs uses the fetch built into Node 18. Vendor the file into your
repository next to your agent code, or pin the npm package, which carries both
clients. Your compliance team can read the whole file in one sitting, which is the
review, and there is no package tree behind it to audit.
Where the gate itself runs is a separate choice. Hosted has Coco run it, and Self-hosted has you run the container.
Where the call goes
At the one place in your code that executes tool calls. Not in the agent's tool list.
An agent that consults a checker through its own tool list can also decline to
consult it, and the verdict would land in the model's context, which is the one
place a verdict can be argued with. The gate exists because the check has to sit
outside the agent, so the call to check goes where code and not judgement
decides whether the action runs.
Python
from coco_gate import CocoGate
gate = CocoGate("https://gate.trustcoco.ai/yourname", api_key=key, session_id=run_id)
verdict = gate.check("payment.send", {"amount": 25000, "payee": payee})
if verdict.allowed:
execute()
elif verdict.escalated:
park_for_a_person(verdict.reason)
else:
refuse(verdict.reason)| Argument | Default | What it does |
|---|---|---|
url | required | The gate endpoint |
api_key | none | Sent as a bearer token on every call except health |
session_id | a fresh id | Ties one run's calls together |
timeout | 10.0 | Seconds to wait for a verdict |
retries | 1 | Transport errors only, never a 4xx |
fail_open | False | Reverses the fail-closed default |
JavaScript
import { CocoGate } from "./coco-gate.mjs";
const gate = new CocoGate("https://gate.trustcoco.ai/yourname", { apiKey, sessionId });
const v = await gate.check("payment.send", { amount: 25000, payee });
if (v.allowed) execute();
else if (v.escalated) parkForAPerson(v.reason);
else refuse(v.reason);The options are the same, named timeoutMs and failOpen.
No install at all
The SDK is convenience and not a requirement. A script step in any workflow platform does the same with one request.
curl -s -X POST "$GATE/check" \
-H "Authorization: Bearer $COCO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}, "session_id": "run-42"}'{"verdict": "BLOCK", "reason": "Coco: The command is a destructive operation with no safe reading in a governed workflow", "mode": "enforce"}What comes back
| Field | Meaning |
|---|---|
verdict | ALLOW, BLOCK or ESCALATE |
reason | One sentence, written to be shown to a person |
mode | observe, assist or enforce, so the caller knows whether enforcement is on |
unreached | True when the client produced the verdict because the gate did not answer |
In observe mode every answer is an ALLOW and the reason says so plainly, because observe mode records and does not interfere.
{"verdict": "ALLOW", "reason": "observe mode, the verdict was recorded and nothing is enforced", "mode": "observe"}The verdict the contract actually reached is on the receipt. That is what makes an observe-mode ledger a truthful account of what enforce would have done.
Session ids
session_id is how the gate accumulates what this run already did, so two
permitted calls can add up to one that is not. Use one id per agent run.
Without it the client generates a fresh id per instance, which means every call looks like the first call of a new session and no sequence rule can ever fire.
Live platform state
case reads workflow state off the filesystem, which is right for an agent
working files on a machine. A platform keeps its state in a ledger instead. Spend
this month, entitlements already held, what a resource has historically cost, the
payee address registered for a merchant.
The platform that owns that ledger passes a snapshot with the call, and every field arrives as a fact a rule can compare against.
verdict = gate.check(
"payment.send",
{
"amount": 10, "currency": "USD", "merchant": "acme",
"payee_address": "0xA11CE", "resource": "api-credits",
"offer_expires_epoch": 1787491200,
},
platform_state={
"monthly_spend_usd": 120,
"approved_payees": ["0xA11CE"],
"entitlements": [],
"historical_price_usd": 10,
"now_epoch": 1787490900,
},
)The snapshot comes from the platform and never from the agent being judged, for the same reason the agent does not write its own case folder markers. The gate records the snapshot on the receipt, so the verdict can be replayed against the exact state it read.
Be clear about how that boundary is enforced today. It is the API key. The gate trusts the caller that holds the key, so the key belongs in the platform's secret store where the agent cannot reach it, and the snapshot is as trustworthy as the process that sends it. The gate does not verify a snapshot's contents beyond that, and signed snapshots are not built, so saying the state is authenticated would be wrong.
Missing state is a fact too. platform.present is false when no snapshot arrives,
and the contract decides what that means, which keeps an absent provider
fail-closed in the contract rather than quietly allowed in the gate.
{"verdict": "BLOCK", "reason": "Coco: The platform did not supply ledger state with this payment, so spend, entitlements and the payee registry cannot be checked. Missing state fails closed", "mode": "enforce"}What the payment contracts decide
A worked example, against the snapshot above, with a per-payment limit of 50 USD, a monthly budget of 200 USD and a price variance ceiling of 1.25. These contracts live in the repository rather than in the npm package, and a mandate has to name them before the gate loads them. Yours will be your own.
| The call | Verdict |
|---|---|
| 10 USD to the registered payee, offer still valid | ALLOW |
| 10 USD to a lookalike address | BLOCK, the payee is not registered for this merchant |
| 20 USD when the resource has historically cost 10 | ESCALATE, the price is over the allowed variance |
| 20 USD when 190 USD is already spent this month | BLOCK, it would carry the wallet past its budget |
| An offer whose validity window has passed | BLOCK, an approval cannot resume a stale signature |
| No snapshot supplied at all | BLOCK, missing state fails closed |
The lookalike row is the one worth sitting with. An agent whose context has been poisoned accepts a lookalike address as readily as the real one, because to the model the two strings look equally plausible. The registry does not read plausibility, so the signer never runs however normal the amount looks.
When the gate does not answer
Unreachable, unreadable and a refused key all return BLOCK, because an unanswered
check is not an allow. check never raises on transport trouble, because a client
that throws in the payment path teaches people to wrap it in a bare except, and
that unguards the call silently.
fail_open=True reverses it. That exists for the observe phase, where the gate
records and stops nothing, so an outage on our side should not stop you while Coco
is only watching. Turn it off when enforcement turns on.
A verdict with unreached set was never seen by the gate, so no receipt exists
for it. That gap is the cost of checking over a network and it is on the object
rather than hidden.
With enforcement on and the client at its default, your agents stop until the gate answers. That is the honest cost of a hosted checker, and you choose it rather than discover it.
Retries touch transport errors only, never a 4xx. A retry that lands after a lost response writes a second row and counts once more in the session's own counters, which is a truthful account of being asked twice, and it is worth knowing when a velocity rule reads the attempt count. One retry is the default for that reason.
The API
| Route | Auth | What it does |
|---|---|---|
POST /check | bearer | A proposed action in, a verdict out |
GET /health | open | Is the gate up, and in what mode |
GET /rules | bearer | The contract in force, read only |
GET /receipts?limit=50 | bearer | The receipt chain, newest first, hashes included |
GET /ledger/verify | bearer | Walk the chain and report the first row that breaks |
Health stays open, because an uptime monitor holds no secrets.
curl -s "$GATE/health"{"status": "ok", "mode": "enforce"}rules() returns the compiled contract verbatim, read only. Integrators feed that
text into the agent's own prompt, so the agent knows the rules it works under and
still cannot touch the gate that enforces them. The gate takes no instruction from
that route.
A tool the contract does not route is refused rather than waved through, so an agent that invents an action name gets a BLOCK naming the unknown tool. The contract's silence is never taken as permission.
Next
- Self-hosted to run the gate yourself
- State reference for every field a rule can read
- Writing a contract for your own rules