What Ladder is

A human control plane for consequential WebMCP actions.

WebMCP gives agents the ability to act on websites. Ladder gives humans the ability to decide exactly what those actions are allowed to change.

Before an agent changes real data, Ladder runs the exact same tool against a clone of the page's state, records every write, and turns them into a proof the human can edit. The human approves or narrows it. Ladder then reruns the same tool against real state under a guard, and anything outside the approved proof rolls the entire commit back.

Why this matters

WebMCP settled how an agent acts on a page: a tool is registered, the agent calls it, the page does the work. That is the hard part and it is solved. What it did not settle is what happens when the agent is wrong. The operator sees a function call and a name. A call that is ninety per cent right can only be taken whole or thrown away whole.

Freight operations run on legacy systems built around human operators: screens, workflows, permissions, manual decisions. The next generation of these systems will be AI-native. Agents will search records, propose actions and execute operational workflows directly. That creates a new problem. How do humans and agents collaborate when an agent can change the operational system itself?

An agent may correctly identify 27 shipments that need a remedy, and the right answer is almost never "do all 27". One carries lithium-ion batteries and cannot go on a passenger flight at all. One has twelve hours of temperature-control endurance left and cannot wait until morning. One is ordinary freight whose customer will not notice. The operator needs to apply that judgment to the agent's proposal without taking the whole task back by hand.

As legacy operational software becomes AI-native, the interface between human judgment and agent autonomy becomes part of the operating system itself. Ladder is a pattern for building that boundary. The freight story below is the proof, not the identity.

What it does

Ladder executes the same WebMCP tool twice: once as a shadow transaction, once as the guarded commit.

The shadow run calls the tool's real execute() against a structuredClone of the page's state, behind a Proxy that records every write. Nothing real is touched. What the tool would do becomes a diff.

The person sees the blast radius: how many records, how much money, how many of the actions are irreversible, and a bar showing that share against the whole dataset. They untick what they do not want, and every figure moves as they do it. The button they press says "Apply 20 · Refer 4 to duty manager".

Then the same execute() runs again, against real state, through a Proxy that lets the approved writes through, silently skips the ones that were narrowed out, and throws a ScopeViolation if the tool goes anywhere the preview never showed. A violation rolls the entire commit back.

The developer writes one function, the way they already do. Ladder runs it twice.

Nothing disappears into "success"

One run, accounted for end to end:

Agent requested:                         27
Above the operator's authority, referred: 4
Left for the operator to decide:         23
Operator removed:                         3
Applied:                                 20

Every requested action is accounted for. applied plus every rejected count equals requested on every path the app can reach, including the ones where nothing lands. There is a test for it.

What people and agents can now do together

The person's edit reaches the agent as data it can act on.

When someone cuts 27 down to 20, the tool does not return "success". It returns what actually happened and why the rest did not:

{
  "status": "partially_applied",
  "requested": 27,
  "applied": 20,
  "rejected": [
    { "reason": "the operator removed these from the change", "count": 3,
      "ids": ["HAWB-70003", "HAWB-70005", "HAWB-70007"] },
    { "reason": "above the gateway operator's authority, referred to a duty manager", "count": 4,
      "ids": ["HAWB-70001", "HAWB-70002", "HAWB-70023", "HAWB-70025"] }
  ],
  "replan_required": true
}

Different refusals carry different reasons and the agent can tell them apart. A row the tool itself declined comes back with the rule that declined it. A record the person removed comes back as its own bucket. A record that changed while they were deciding says so. The agent is never left guessing which kind of no it received, so it can ask again about the four only instead of re-proposing all twenty-seven.

Autonomy works the same way. After a run of clean approvals Ladder drafts a standing rule with caps and an expiry, and a person ratifies it the way they would approve a pull request. A person can also set one up front. When a rule is ratified, Ladder calls unregisterTool() and re-registers the tool with a description that states the rule, so the agent's own toolset changes as trust is granted. When the rule expires, the tool reverts. Irreversible tools can never carry a rule at all.

The case it was built on

Thursday, 19:40. A flight from Frankfurt to Chicago is cancelled. Two consolidations were on it, so 42 shipments belonging to 31 different customers are now unbooked. Nobody assembled that list. The cancellation did.

There are ninety minutes to the first cutoff, and three ways out. Rebook on the same carrier tomorrow morning, which is free and costs eighteen hours. Put it on a competitor's freighter tonight, which recovers the time and costs money. Truck it to another airport and fly from there, which lands one day late instead of two.

The person responsible has to decide which shipments are worth which remedy, against consequences, in the ninety minutes they have.

How we built it

React, TypeScript and Vite. Two products ship from one engine to prove the engine is not the domain:

  • index.html. Air freight recovery. Four tools: search_shipments and get_shipment read, propose_remedy and notify_customers write.
  • edge.html. Edge infrastructure rollout. Four tools: list_pops and inspect_pop read, roll_config and page_oncall write.

Eight tools across two pages. Neither product's engine knows what a shipment or a POP is. The second product took a day. That is the evidence that the engine is an engine.

The core is about 900 lines. runShadow() forks state with structuredClone and records writes through a Proxy. runCommit() replays the same function against real state under a write guard that throws on anything the preview never showed. Reads carry readOnlyHint and skip the whole path. No fork, no prompt, no proof. 394 tests cover it.

What we learned

Chrome's own guidance for gating a consequential step is to not register the tool and let the user do it by hand. An action is either fully automatic or fully manual, with the decision made at tool-registration time, before the change is understood. Ladder is the in-between. The tool stays registered, the agent still calls it, and the human decides against consequences at the moment those consequences exist.

What's next

Persistence and a durable audit trail. Standing rules, history and the activity log are in memory today and a reload clears them. Per-field approval instead of per-record. Widening the recorder past two levels. And an honest answer for concurrent commits against one store, which is not supported today.

Implementation and limitations

agent.requestUserInteraction() does not exist yet. The spec defines this hook for pausing execute() to ask the user. Measured directly against Chrome 151's WebMCP testing build on 26 August 2026: execute receives only its first argument. There is no agent object, so there is no requestUserInteraction to call and no AbortSignal to listen on. Ladder feature-detects for the hook and renders its approval surface in the page. Both paths are written and both are spec-conformant; only one is exercised in the runtime a judge will open. We are not claiming to mount on the platform's own hook and we are not listing AbortSignal handling as delivered.

We also measured that a pending execute survives 96 seconds and returns its structured result intact, which is why the approval can take as long as a person actually needs.

The write recorder is two levels deep by design. Rather than let an unpreviewed write land quietly on real state, objects read below that depth are read-only views that throw on any write, with the full path in the message, and a commit that hits one rolls back whole and returns denied. The limit is real and we would rather widen it than live with it. It never fails silently.

The demo video's calls are scripted. The tools do run under a real agent: we drove the full loop in ChatGPT desktop's built-in browser and in Chrome with the WebMCP extension. The film uses a scripted caller anyway, because a recording needs the same input every take. So the calls in it go through the page's own registration, not the browser's transport, and the terminal pane says so in its own title bar. Everything after the call is the production path unchanged.

Ladder is a guard, not a sandbox. It governs writes and effects that pass through the tool context it provides. A tool that reaches around that context, by importing the real store or calling a server directly, is not governed by it. This is a review and transaction layer for cooperative tools, not containment for code you do not trust.

Nothing persists. There is no auth. Approval is per record, not per field. Tools must be deterministic. Irreversible actions are held and released, never previewed as a diff. Concurrent commits against one store are not supported.

Built With

Share this project:

Updates

Submission history