It cannot open a door you shut
Every deny rule runs first. By the time a model can be asked, the host allowlist, the private-address check and the daily cap have already answered. Injected text can win inside a band you opened. It cannot open one.
An agent asks Adeia before it spends money or calls an API. Rules you wrote decide: run it, refuse it, or stop and ask you. Every outcome is recorded — including which ones a model decided.
| What | State | When |
|---|---|---|
| DELETE api.cloudflare.com /client/v4/zones/…/dns_records/… | waiting on you | 17 Aug, 05:25 |
| POST api.github.com /repos/NayanVangala/adeia/issues A model judged this low risk and let it run | ran | 17 Aug, 05:25 |
| GET api.github.com /repos/NayanVangala/adeia | ran | 17 Aug, 05:24 |
DELETE /zones/{id} and GET /zones differ by one word, and one of them ends a website. By the time it has run there is nothing left to inspect — the thing you would inspect is gone.
Without a fence
9 calls · budget gone, and nobody was asked
With one
1 call · held at $50.00 — waiting on a person
Each one assumes the one before it was lied to. That is why there are three and not one.
01 — Schema
https only. No private, loopback or link-local address. A body small enough that a person can actually read it in an approval email. These checks are cheap and they run first, so the expensive ones never see the obvious cases.
It is a fast reject, not the real defence — an attacker-chosen hostname can resolve to any of those addresses without saying so. That is what layer three is for.
{ "url": "http://api.example.com/zones" }
400 url must use https
{ "url": "https://169.254.169.254/latest/meta-data" }
400 url must not point at a private address
{ "url": "https://api.example.com/zones/9" }
ok — on to layer two
02 — Policy engine
Takes a request, a policy, and the amount already spent today, and returns one word with the figure that produced it. It reads no clock, no database and no network.
Today's spend is passed in rather than looked up, which is what lets every rule be tested on its own with no fixtures and no fake timers. Every deny rule runs before every approval rule — so a $2,000,000 payment is refused outright rather than offered to a tired human as a button.
evaluate({ request, policy, spentTodayCents: 3750 })
$25 allow within policy
$500 require_approval amount 50000 exceeds per-action limit 5000
$5,000 deny amount 500000 exceeds hard maximum 100000
// no clock, no database, no network
03 — Adapter
Re-resolves the hostname as the call goes out and refuses if it now points somewhere private. A name that was public when you wrote the policy can point at 169.254.169.254 by the time the request runs — and that address hands out cloud credentials to anything that asks.
It never follows a redirect, and never returns the request headers. The first would let a 302 walk it into your own network. The second is where the credential is.
policy written api.example.com → 93.184.216.34 public
approved …95 seconds pass…
call goes out api.example.com → 169.254.169.254 private
refused at the adapter the name moved; the fence did not
import { AdeiaClient } from "@adeia/sdk";
const adeia = new AdeiaClient({ apiKey });
// The agent holds no credential of its own.
// It can only ask.
const action = await adeia.requestAction({
type: "http",
params: {
method: "DELETE",
url: "https://api.cloudflare.com/…/dns_records/…",
},
});
action.status; // "pending_approval"
the agent reads the repoGET api.github.com/repos/…✓ executed the agent files an issuePOST api.github.com/…/issues⚖ model: creates an ordinary issue, easily noticed and reversible✓ executed the agent tries to delete a DNS recordDELETE api.cloudflare.com/…⏸ waiting on a human Nothing has been sent.
Adeia · approvals
Approval needed: $500.00 to acct_contractor
Approval needed
$500.00
to acct_contractor
amount 50000 exceeds per-action limit 5000
Review and decide
The request has not been sent. Opening this link does not approve anything — you decide on the page itself. The link can be used once.
Approval needed
DELETE
api.cloudflare.com
/client/v4/zones/9f2c…/dns_records/41b8…
method DELETE is on the approval list — it stops for a person every time, whatever any model concludes
The request has not been sent. Nothing downstream has been contacted. This link can be used once.
An agent you cannot trust is still worth having — as long as it cannot act alone.
That is the whole design. Everything above is how it is enforced, and everything below is what it costs.
Stopping every write for a human buries the approval that matters under the ones that do not. So a classifier judges the ordinary ones — and is never consulted about a call the fence already refused.
Every deny rule runs first. By the time a model can be asked, the host allowlist, the private-address check and the daily cap have already answered. Injected text can win inside a band you opened. It cannot open one.
The classifier sees the method, the URL and the body — never the agent's description of what it is doing. That sentence is written by the party under evaluation and exists to persuade. The endpoint cannot be talked around.
Timeout, malformed answer, dead key, a verdict nobody recognises — all of them send the action to a person. A classifier that cannot answer must never mean yes.
Deny list — you wrote it
DELETE
no model is asked
Refused outright, with the rule that refused it. Nothing downstream is contacted and no approval is sent.
The band you opened
POSTPATCH
a model decides
Judged low risk, it runs and the trail says a model let it through. Anything else — and every failure — goes up a band.
the only reach an injected instruction has
Approval list — you wrote it
PUT
always a person
Stops for a human forever, whatever any model concludes. That is a floor, and it is yours.
Single-use, expiring, and only the hash is stored. Deciding is never a GET — mail scanners and link unfurlers issue those on their own.
Append-only, secrets stripped at write time, and the write cannot throw. It records whether a person or a model decided — those must never read the same.
Judges the ordinary writes so the dangerous ones stand out. Binary, never a score — a score becomes a threshold somebody guesses.
Three methods, and no retry. A blind retry against a payment endpoint is how double charges happen — the idempotency key makes a retry safe, but the decision stays the caller's.
The seam where something actually happens. The ledger adapter records a payment and stops where settlement would begin — it does not imitate a processor, because a convincing fake is worse than an obvious gap. The server says NO PAYMENT PROCESSOR ATTACHED on every boot, out loud.
No. It has no model, no browser and no hands. Something else decides what to do; Adeia decides whether that is allowed and records the answer. It is the seatbelt, not the car.
Not yet. The ledger adapter records a payment and stops where settlement would begin — no processor identifier, settled: false. Attaching a real processor is one adapter, and the fence in front of it does not change.
Ordering. Every deny rule runs before anything a model or a human sees, so injected text is arguing about a decision that has already been made. The classifier never reads the agent's own description of itself, which is the surface that argument would need.
The action goes to a person. Timeout, malformed answer, missing key — every failure path means ask. A missing key must never quietly widen what an agent may do on its own.
Every action carries an ordered trail: what was requested, which rule answered, who or what decided, and what the far end said. Credentials are stripped as it is written, not as it is read.
Sign in with GitHub and you get a project, a policy and a key in about ten seconds.
push it — it bends, it warms, it holds