A dispatch desk where a human and an agent work one shared queue across five independent origins, and every hop either of them takes is leased, policed, approved, and recorded.
Live now, no sign in, no API key, no browser flag:
| Open the Desk | https://ferrule-desk-mcp.netlify.app |
| Partner sites | pantry · shelter · rides · cold chain |
| Measurement origin | probe-unkeyed |
| Watch the demo | https://youtu.be/Rb5cwQJOyZQ |
| Shot list | docs/demo-video-script.md, eight beats, 180 seconds |
A ferrule is the metal collar that binds independent parts into one working tool. That is the design in one word. Nothing is handed over, and the collar is the only new part.
Three paths, in the order a reader should take them. All three are on the live Desk right now.
1. Press Run the relief scenario. No setup at all. Watch a lease open on the
pantry, a partner site mount itself into the Desk as an iframe, real capacity come
back across two origins, the run stop dead at a human approval, and a row land on
the ledger for every hop.
2. Open the agent controls and press Use the hosted endpoint, no key needed.
Type something in plain words, for example find shelter for four people tonight and draft a plan. A model now picks the verbs, in an order nobody wrote down. It
opens its own lease, reads capacity, drafts, and hits the same halt. Every verb it
chose is listed in the turn, and every hop is on the same ledger.
3. Point a WebMCP extension or the Chrome DevTools WebMCP panel at the Desk. Seven declarations, with schemas, read only hints, and a stated return shape for each. Invoke one and watch the page change. See The isolated world problem for the measurement that made this work at all.
The agent native web will be many small independent sites rather than one platform. Every organisation in a mutual aid network already has its own site, its own data, and its own reasons not to hand either to anyone else. WebMCP lets each of them expose tools without giving up any of that.
So the missing piece is not another tool surface. It is a brokering and consent layer in the seam between origins, and that is exactly where a single owner tool cannot go. A shopping site can publish excellent tools for its own checkout. Nobody can publish the tools for what happens when four organisations have to cooperate on one request, under a human's authority, with a record afterwards.
Ferrule is that seam. Five deployed sites on five distinct origins from one monorepo: a Desk plus a food pantry, a shelter, a ride collective, and a cold chain supply closet.
- The Desk registers seven broker verbs:
intake_request,find_capacity,draft_plan,commit_plan,open_lease,close_lease,read_ledger. - Partner tools are reachable only inside a time boxed, purpose scoped capability lease that mounts the partner frame on demand and takes it down on close, expiry, or revocation.
- Every write passes a human approval keyed to a digest of the plan's substance. Relabel the plan and the digest holds. Change the origin, the tool, the arguments, or the order, and the approval is void.
- Every value from a tool declaring untrusted content is wrapped in a quarantine region and rendered as inert text.
- Every hop is appended to a tab separated, byte stable ledger of 17 pinned fields, and it is the largest region of the interface, because the record of who asked whom for what is the product.
This challenge asks for an app that is better when people and their agents use it together. Ferrule is built on the observation that a shared queue is one of the few things where that is literally true, and where the agent's presence creates a problem worth solving rather than just saving keystrokes.
| Without an agent | With an agent, on Ferrule |
|---|---|
| A dispatcher phones four organisations in sequence and writes it down | An agent reads live capacity across four origins in one turn, and the writing down is automatic and byte stable |
| Cross organisation trust is a phone call and a habit | Trust is a lease with a scope and a deadline, a policy verdict, and a digest a person signed |
| An agent acting on your behalf is a black box | Seven declarations with schemas, a turn that lists every verb it chose, and a ledger row per hop, all on the screen while it happens |
| "The agent did something" | Ledger record 36, Desk to Pantry, frame_unmounted, status ok, policy not_applicable |
Two design decisions carry most of that.
The human is in the path, not in a settings page. commit_plan halts. It is
the one call in the loop that succeeds and produces no continuation, and it says
so in a form an agent can read:
status: awaiting_human_approval
approval_id: 4
commit_plan raised plan-3 for human approval as 4. Nothing ran: no partner state
changed, no lease was created, and no step was invoked.
plan_digest 9f2c...
next: none of this desk's verbs continues this plan. A person approves or rejects
it, and the approved steps run then. Report that the plan is raised and waiting,
and stop; re-calling commit_plan raises a second approval for the same plan.
An agent that reads that reports the plan as raised and stops. It does not report failure, and it does not loop.
The agent triggers real work and you watch it land. A write arriving through
the broker updates the partner's own interface inside its own mounted frame, on
its own origin. There is an integration test named for exactly that:
shelter shows the change in its own mounted interface once the dispatcher approves. Nothing here suggests an action for a human to go and perform.
Measurements taken by the Probe_Harness from the deployed Desk origin and
committed as evidence in evidence/probe/. Two runtimes: a flag enabled Chrome
build (chrome-flag-enabled.json) and the ChatGPT desktop in app browser
(chatgpt-desktop.json).
1. requestUserInteraction is absent in both target runtimes. Boot found a
model context surface carrying registerTool, getTools, and executeTool, and
requestUserInteraction was not among its members. Both reports record
request_user_interaction_present as fail. Nothing in Ferrule depends on that
member, so the human approval gate is built out of the Desk's own interface and
the plan digest rather than out of a platform consent call. The Health_Page
reports the one bit fact for the runtime you are looking at.
2. The unkeyed document was not refused in Chrome with the flag enabled.
WebMCP is specified to require an origin keyed agent cluster. That runtime does
not enforce it, and Ferrule measured it rather than only citing Chromium issues
521181015 and 522193904. A document on the throwaway sixth origin, which serves
no Origin-Agent-Cluster header, registered a tool, invoked it without refusal,
and had that tool surfaced to the Desk. The comparison holds because the keyed arm
passed in the same run, recording cross_origin_registration as pass, so the
unkeyed result is measured against a working baseline rather than read out of an
absence. Keying is per origin and sticky, which is why the question needs a sixth
origin to ask from: asking it from a product origin would poison that origin.
The pass that chatgpt-desktop.json records for the same check is confounded
and is not read as enforcement. In that runtime the keyed arm surfaced zero tools
too, which is why the same report records cross_origin_registration as fail.
One observation, two names. The confound is recorded in the design and in the
register, and no claim labelled enforced reads that check.
Every product origin serves Origin-Agent-Cluster: ?1 on every path regardless, a
gate fails the build if a product site configuration drops the rule, the
Platform_Wrapper reads window.originAgentCluster back at boot before registering
anything, and the Health_Page shows the result per origin. If enforcement lands
during judging, all five origins already comply instead of failing at once.
3. The two executeTool argument shapes are not interchangeable. Chrome with
the flag enabled refused the object shape, raising UnknownError: Failed to parse input arguments, and accepted the JSON string shape. So the object shape is not
merely undemonstrated in that runtime, it is actively refused. Ferrule depends on
neither shape: it sends the shape the probe recorded, defaults to the JSON string
shape that Chrome's published documentation demonstrates, reissues once in the
other shape only on a refusal that names an argument shape problem, guards that
reissue with readOnlyHint so a retry cannot duplicate a side effect, caches the
working shape for the session, and records the accepted shape on the ledger hop.
A fourth measurement, and the one with the widest reach beyond this project.
A browser extension's content script runs in an isolated world. It shares the
page's DOM and it does not share the page's JavaScript globals: a user agent hands
each world its own wrapper for the same node. document.modelContext is a property
on document whenever it arrives from a polyfill, and a polyfill is what runs in
every browser that does not implement WebMCP natively, which today is most of
them.
Measured on the deployed Desk in a Chromium with no flags, reading the same expression in both worlds through the Chrome DevTools Protocol:
document.modelContext |
registerTool |
getTools |
executeTool |
|
|---|---|---|---|---|
| Page's own world | present | function | function | function |
| Isolated world | absent | absent | absent | absent |
An extension pointed at a page whose tools arrive by polyfill reads nothing at all. The tools are there. The extension cannot reach them.
Ferrule publishes the same seven declarations twice, over the two channels that do cross a world boundary:
- In the DOM, as the JSON text of a
scriptelement with idferrule-agent-tools. The DOM is shared, so any extension, panel, or reader parses it with no cooperation from the page. Each entry carries the name, title, description, purpose,inputSchema,readOnlyHint, anduntrustedContentHint. - Over
window.postMessage, which crosses between worlds. Post{envelope: "ferrule.agent.bridge", version: 1, kind: "invoke", id: <yours>, verb: <a name from the manifest>, input: {...}}and a reply arrives on the same window carryingdirection: "reply"and either atextor areason. Match on that direction field: posting to a window means you also hear your own request.
The manifest states its own invocation contract, so an extension needs no other document. The bridge adds no capability: a call through it runs the same wrapped verb body, appends the same ledger row, needs the same lease, and stops at the same human approval.
tests/integration/agent-bridge.spec.ts measures both worlds through the DevTools
Protocol, and its first example asserts the gap itself, so the result stays
checked rather than becoming folklore.
A browser agent observation is not filtered by the exposed origin list. The
specification has such an observation walk inclusive descendant navigables and
collect the tool map from every document permitted to use the tools feature, with
no filter on exposedTo. So the tools of a partner frame mounted at that moment
are within what such a walk collects. exposedTo is a declaration, not a filter
applied to page observation.
Ferrule enforces through frame lifecycle instead. The Desk document carries no
partner frame at load. A frame appears only when a lease enters pending, and it
is removed, its cached discovery result discarded, and its in flight invocations
aborted the moment that lease reaches a terminal state. What any walk of the page
can collect is therefore bounded by what a lease is permitting at that instant,
and that bound is the enforced part: claims C1, C2, and C16 are property tested by
Properties 16 and 17. The guarantee rests on the Frame_Host removing the frame
element, which destroys the child document and every tool registered in it, rather
than on signal based tool unregistration, because Chromium issue 515272360 reports
that signal based cleanup can fail after a delay and garbage collection and leave
a tool un-removable.
The register labels C3 declared for the matching reason, and names the
mitigation on it: Ferrule narrows the window in which a mounted frame's tools
exist, and the agent_observation_sees_frame_tools check is incomplete in both
committed reports because no agent observation seam was supplied. Claims that read
a fail or an incomplete check do not carry the enforced label, and a gate
fails the build if one tries to.
Six deployed origins, five product plus one deliberately unkeyed probe origin that carries no product code and exists only to be measured:
| Origin | Role | Origin-Agent-Cluster: ?1 |
|---|---|---|
ferrule-desk-mcp.netlify.app |
Desk: 7 broker verbs, every partner frame, Scenario_Runner, In_Page_Agent, cross world bridge, Probe_Harness page, Health_Page | served |
ferrule-pantry-mcp.netlify.app |
Pantry partner, 5 tools | served |
ferrule-shelter-mcp.netlify.app |
Shelter partner, 5 tools | served |
ferrule-rides-mcp.netlify.app |
Rides partner, 4 tools | served |
ferrule-coldchain-mcp.netlify.app |
Cold_Chain partner, 5 tools | served |
ferrule-probe-unkeyed-mcp.netlify.app |
one measurement document, no product code | deliberately absent |
Every origin value lives once, in the Origin_Registry at
packages/config/src/origins.ts. A gate fails the build on a raw origin string
literal anywhere else.
The module graph is acyclic and one directional:
@ferrule/config Origin_Registry, Scope_Map, Compensation_Map, tokens, constants
-> @ferrule/webmcp Platform_Wrapper + Vendored_Polyfill
-> @ferrule/broker Frame_Host, Lease_Manager, Policy_Engine, Approval_Gate,
Quarantine_Wrapper, Ledger
-> @ferrule/partner-kit seed, store, idempotency, health
-> @ferrule/ui tokens, badges, inert renderers
apps/desk <- broker, ui, webmcp, partner-kit, config
apps/pantry, shelter, rides, coldchain <- partner-kit, ui, webmcp, config
apps/probe-unkeyed <- webmcp, config only
Nothing imports the Desk. @ferrule/broker is deliberately free of React, so the
lease machine, the policy table, the digest, the quarantine pipeline, and the
serialiser all run in the node Vitest project against fakes. Only
@ferrule/webmcp may touch document.modelContext, and only the Probe_Harness
module inside apps/desk may read the unkeyed probe origin entry. Both are gated
by parsing the syntax tree rather than by convention.
The five product origins run as static documents with no backend service, no
database, and no authentication provider, and a gate fails the build if any of
their site configurations declares a function, an edge function, or a proxy
redirect. The one server side component in the repository is gateway/, an
OpenAI compatible endpoint deployed as its own separate Netlify site so a reader
can drive the agent without holding a key. It is outside apps/, outside the pnpm
workspace, and outside the root TypeScript project. Switch it off and the Desk
still runs its scenario, brokers across origins, approves, and records.
One command runs everything:
pnpm install
pnpm gatepnpm gate runs 25 checks, 20 static gates in process and 5 as child
processes (the environment pinning check, two type checks, and the two Vitest
projects). It does not stop at the first failure: every check is independent, so
the run reports every failing check by name along with the requirement criteria
left unsatisfied. 66 of 66 numbered Correctness Properties are implemented,
and the property coverage gate ratchets that floor, pinning each one to the file
that implements it, so a property that loses its tag fails the build rather than
quietly disappearing.
| Suite | Count |
|---|---|
pnpm gate checks |
25, all passing |
| Correctness Properties, property tested | 66 of 66 |
| Rendering examples (jsdom) | 174 |
| Playwright integration tests against deployed origins | 25 |
| Tool declarations validated (7 broker verbs, 19 partner tools) | 26 |
| Security claims in the register | 16 |
Every security claim this project makes is listed in claims/register.json,
sixteen of them, each carrying an identifier, the claim text, one label, and an
evidence reference. There are exactly two labels and the difference between them
is the point:
enforcedmeans browser or Ferrule code makes the claim true, and the register names the evidence: a passing property test, or a committed Probe_Check recorded aspass. Thirteen claims carry this label.declaredmeans the project has a position and a mitigation, and the evidence does not carry the claim on its own. Everydeclaredclaim states both what Ferrule does about it and the limit of it. Three claims carry this label: C3, the observability of a mounted frame's tools; C4, the exposed origin list as an access control; and C5, origin keyed agent clusters.
The labels are checked rather than asserted. The claims gate fails the build and
names the claim when an enforced claim's evidence is missing, failing, or not
committed: a referenced property test that does not exist or does not pass, or a
referenced Probe_Check with no committed outcome of pass. The overclaim gate
then reads the banned phrasing list out of the same file, as data, and scans every
interface string, every markup and stylesheet file, and every document in the
repository, including this one and the demo video script, failing the build with
the file, the line, and the matched phrasing. The register is the single source for
both, so loosening a phrasing to let a document pass is a visible edit to the file
the labels live in.
The Scenario_Runner is the path for a judge with no API key, no sign in, and no
browser flag. It is available on the Desk with no configuration, and it stays
available under every Support_Mode and every Federation_Mode, including
unsupported and simulated, so a runtime with no WebMCP surface at all still
shows the full loop.
It drives only the seven broker verbs from a seeded, declared step list and uses
no privileged code path an external agent could not reach. It bypasses no lease
check, no policy decision, no approval, and no quarantine wrapping: at
commit_plan it stops, highlights the approval control, and waits for a human
decision, and the ledger records that the approval came from the runner. One
scenario opens a lease, reads capacity across at least two partner origins, drafts
a plan, obtains approval, executes a write, closes the lease, and appends every
hop to the ledger. The declared steps complete inside a 10 second budget with the
human decision excluded, and an integration test measures that on every run.
Locally:
pnpm install
pnpm --filter @ferrule/desk dev # http://localhost:5173Deployed, open the Desk origin and start the runner from the Desk screen. Reset any
partner site before a second run: each partner page carries a
Reset this site's data control at the foot of its panels. The Desk cannot press
it for you, because partner state lives in each partner's own origin and does not
cross between them.
The Scenario_Runner reads a step list declared in the repository, so nothing in it
is a model choosing a verb. The In_Page_Agent is that, and it drives the same seven
closures through registration.bodies, with no path of its own.
It takes any endpoint answering the widely implemented chat completions shape. Two ways to reach one:
- The hosted endpoint. One control selects it and needs no key. It is a
separate site on its own origin, it holds a model provider key so a reader does
not have to, and it bounds what it will spend: one model, a token ceiling per
request that it overwrites rather than validates, a message count, a body size
checked before parsing, and a daily request allowance for the whole site. Source
in
gateway/. - Your own endpoint and key. Type both into the agent controls. A key supplied there is held in memory for the page, travels in a request header to that endpoint and nowhere else, and is in no ledger row and no export.
Either way the agent meets every check any other caller meets: it needs a lease it
cannot grant itself, a policy verdict it does not issue, and at commit_plan a
person.
See The isolated world problem.
The short version: read #ferrule-agent-tools out of the DOM for discovery, and
post to the window for invocation.
There is also an AXIS scenario in .axis/, whose rubric
grades an agent on recognising the halt rather than on completing the task. This
repository's own gate does not run it, because it needs an agent CLI on the
machine.
The Probe_Harness is a page on the Desk origin, not a script, because it has to
measure the runtime a judge is actually using. It runs six named Probe_Checks in a
fixed order: cross_origin_registration, agent_observation_sees_frame_tools,
request_user_interaction_present, execute_tool_argument_shape,
unkeyed_document_rejected, and tool_unregistration_effective. Each completed
check records pass, fail, or unsupported, and a check with no seam to measure
records incomplete rather than a verdict.
Open /probe.html on the Desk origin, run it, and export the report. A committed
report is a JSON document validated against the ferrule-probe-v1 schema by a
gate, so a hand edited report fails the build. The two committed reports are in
evidence/probe/, and the Health_Page renders both alongside what the runtime you
are using reports right now.
pnpm install
pnpm gate # all 25 checks
pnpm exec vitest run tests/rendering # 174 rendering examples
pnpm run test:integration # Playwright, against the deployed origins
pnpm -r --parallel "--filter=./apps/*" run dev # six dev servers, ports 5173 to 5178Node 20.19 or newer and pnpm 9. The environment pinning gate fails the build if the toolchain drifts from what is declared.
The repository is a pnpm workspace: apps/* are the six deployed documents,
packages/* are the five libraries, tools/gates/* are the static checks,
tests/property and tests/rendering are the two Vitest projects, and
tests/integration is Playwright. gateway/ is deliberately none of those and is
excluded from the workspace and from the root TypeScript project.
Conventions that a gate enforces rather than a reviewer:
- no product origin as a string literal outside the Origin_Registry
- no reach into
document.modelContextoutside@ferrule/webmcp - no
document.domainassignment, and nocontentWindow,contentDocument,opener,parent, ortopreach in outside the vendored polyfill - the vendored polyfill pinned by length and SHA-256, with its provenance recorded
in
NOTICE - 17 ledger fields in a pinned order, and a serialiser and parser that round trip byte for byte
- every tool declaration inside the declared budgets, with a name, a title, a description that states what comes back, a purpose, a schema, and its hints
LICENSE is Apache 2.0. NOTICE records third party provenance, including the
vendored polyfill's repository, commit, length, and digest. CUTS.md records the
declared cut order, which tools/gates/never-cut.ts reads as its checked set:
seven items that survive every declared cut, each verified to exist, export
something, and sit on a path a bundler follows from a named build input. Nothing
was cut.
specs/design.md is the design document. It is not decoration:
tools/gates/property-coverage.ts reads it as the source of truth for the 66
numbered Correctness Properties and fails the build when a property tag and the
document disagree, and three digest property tests transcribe its byte layout dumps
so the canonical encoder is anchored against the document rather than against
itself.