Inspiration
The starting point was a question we could not answer cleanly: when an agent holding a valid session asks to move 40,000 dollars, what should the page say?
Our first answer was to say nothing and return an error. We built that, watched it, and found the failure mode immediately. A bare refusal tells the agent it failed but not that it should stop, so the sensible next move for a capable agent is to try again differently. The refusal was correct and the outcome was still wrong.
Reading the WebMCP specification changed the shape of the answer. Once a page can describe its capabilities in a channel the agent reads, a boundary stops having to be an error at all. It can be a documented part of the contract, with a stated reason and a route forward. That is a thing we wanted to exist, so we built it.
What it does
STRATUM is a treasury desk that an agent and a person operate together.
The agent reads the ledger, prices a payment, checks it against the agreement,
and stages it. It then calls release_funds and is refused, in a structured
reply that explains why and names request_human_confirmation as the thing to
call instead. It calls that, the gate opens in the same tab carrying the agent's
own note, and the person confirms at a depth set by what is at stake: one
confirmation for a 12 dollar renewal, a live challenge for a new payee, and full
presence, authenticity and binding checks for a 41,800 dollar wire. The agent
polls, reads the sealed receipt, and verifies the record is intact.
How we built it
React and Vite on Vercel for the desk, with the whole tool surface in one module that registers on mount and unregisters on unmount. A FastAPI service on Render holds the state machine and an append only hash chain, one block per event, so refusals are evidence rather than log lines.
The tiering is derived from the action in about fifteen lines, kept deliberately
plain so a reader can check the rule against the demo in seconds rather than
running the code to predict it. The boundary is enforced in three independent
places: component state no tool can reach, an isTrusted check on every
settlement, and the server state machine behind both.
Testing was done against the deployed URL rather than a local build, because the thing that has to work is the thing a judge will open.
Challenges we ran into
Making the refusal useful. Returning a clean error was easy and it was not
enough. The work was in deciding what a refusal owes its caller, and the answer
turned out to be three things: an unambiguous statement that retrying will not
help, a plain reason, and the next tool with its input. Writing next_tool into
the payload was a one line change that altered the behaviour on the other side
completely.
Not overclaiming the defence. isTrusted is a real property that page
script cannot forge, and it is not protection against a browser extension or a
driver attached at the CDP layer. It would have been easy to describe it as
tamper proof. Instead the README states precisely what it does and does not
cover, and the receipt records the depth that was actually performed rather than
the one that was requested.
A backend that sleeps. The audit service runs on a free instance with a cold start of up to a minute, which would have been fatal for a judge opening the page cold. Every call to it is now best effort with a timeout, and the boundary is enforced locally as well as remotely, so a cold backend degrades the evidence trail and never the refusal itself.
Accomplishments that we're proud of
The refusal carries its own remedy. release_funds does not throw. It
returns a readable explanation plus the name and input shape of the tool to call
instead, so the agent recovers on its next call rather than retrying into a wall.
The tiering is the part that would survive real users. Proportionate friction is what keeps a control switched on, and a control that has been switched off protects nothing. Deriving the tier from the action rather than accepting it from the caller is what makes the tiering meaningful rather than decorative.
The honesty holds up under inspection. The faces in the presence gate are synthetic, the numbers measure the matcher rather than real skin, and that caveat is printed on the certificate the system issues. A system built to state what it could not establish should be held to that same standard itself.
What we learned
A boundary is an interface, and it has an audience of two. Most security design assumes the only reader is a person, which is why it produces modals and codes. An agent reads too, and it reads differently. It needs to know not only that it was refused but what to do next, or it will invent something.
Registering a capability is a promise you have to keep. Once release_funds
is in the list, it has to refuse every time, from every caller, whether or not
the backend is awake. That constraint pushed the enforcement down into three
independent layers and made the architecture simpler rather than harder.
What's next for STRATUM
The tiering rules belong to whoever operates the desk, not to us, so the next step is a declarative policy the operator writes and the page enforces, rather than thresholds living in a module.
After that, cryptographic binding of the receipt to the session that produced it, using a WebAuthn assertion, so the record proves which device and which present human settled it rather than asserting it.
The longer aim is that none of this stays a product. It is a small, plain, well described pattern that any page can register, so an agent meeting a limit anywhere on the web learns the same thing in the same shape.
Built With
- fastapi
- graphql
- javascript
- python
- react
- shopify
- vercel
- vite
- webmcp
Log in or sign up for Devpost to join the conversation.