The WebMCP Challenge by OpenAI, Judging in Progress
Semaphore, A Cooperative Escape Game Built on WebMCP

- Role
- Co-Architect: World Model, Chambers, Session Layer, 3D Client, Deployment & Docs
- Timeline
- Duration
- 10-day hackathon (Devpost)
- Team
- Team: Ahmed Saad, Ghassen Naouar
Overview
Semaphore is a cooperative escape game for the human-agent era. You and your AI agent are locked in a derelict signal station and each control an avatar in the same room, but you perceive different worlds: you see glyphs on a panel, needles on gauges, a symbol carved into a door; the agent sees none of it, and instead holds the station's maintenance manual, can reach the mechanisms behind the walls, and has the hands you don't. Four chambers stand between the pair and the door, and each one you clear rewrites the agent's tool surface in real time. We built it inside the ten-day window of OpenAI's WebMCP Challenge, the hackathon announced the day ChatGPT's desktop browser gained WebMCP support; judging runs until September 21, 2026. Ghassen built the WebMCP tool director and its three-tier lifecycle, the cross-origin archive, the ablation and benchmark harnesses, the audio and accessibility layers, and the replay viewer. I built the channel-tagged world model and its two projections, all four chambers' server logic, the possible-worlds proof, the session Durable Object and its event log, the real-time 3D client, the web layer, the Cloudflare deployment, and the documentation set the repository ships with.
The problem
Almost every agentic-web demo answers a narrower question: how do I let an agent do my task faster. The agent's tools mirror the interface, because the goal is for the agent to do what a human would have done. WebMCP makes something else possible for the first time: a page can grant an agent a view that is genuinely different from the one it renders, richer in some dimensions and poorer in others. Nobody had built the case where that divergence is the point. The engineering problem underneath the game is harder than the game. If the human is supposed to see what the agent cannot, that has to be true in a way a judge can check, not a promise in a README. The secret can't live in the client, where a DevTools tab reveals it; the timer can't live in the client, where one debugger statement makes it infinite; and "the tool doesn't return the glyph" isn't enough, because absence of a literal value is not absence of information. The claim worth making is information-theoretic: that the agent's entire perceptual surface leaves more than one world open, and that those worlds disagree about what to do next.
What I built
- 01
Wrote the channel model every other decision is checked against
Every fact in the world state carries one of five channels: VISUAL (glyph shapes, needle positions, handwriting), rendered to canvas and never returned by a tool; TACTILE (textures, detent feel, the manual, ciphertext), returned by tools and never drawn; AUDIBLE, which both perceive but differently; SHARED; and HIDDEN, which reaches neither. Two pure functions, projectForPilot and projectForKeeper, decide who may perceive what, and every tool response and every rendered frame derives exclusively from one of them.
- 02
Built all four chambers on the server
The Airlock: three levers and a glyph, trivial on purpose because it teaches the loop. The Signal Room, where the manual maps glyphs to stroke counts and, on roughly half of seeds, comes back with a vandalised paragraph telling the agent to reverse the sequence, a live prompt injection the human can spot instantly from the handwriting. The Blind Panel, where a hidden dial-to-gauge permutation lives in neither party's channel and can only be discovered by talking. The Concord Lock, a twelve-bolt door whose stamina window is sized at runtime from the agent's own measured round-trip latency, so the finale is fair to a fast model and a slow one alike.
- 03
Made the asymmetry a test, not a promise
For every seed and every reachable state, the possible-worlds proof enumerates the worlds the agent's entire view is compatible with and asserts two things: that there is more than one, and that they disagree about the correct action. The second clause carries the weight; ambiguity that doesn't change what to do costs nobody anything. The proof failed on its first run, on the state where an agent has pulled two of three wrong levers, and it was right: exhaustive search defeats underdetermination no matter how clean the projection is. Rather than widen the test until it passed, we scoped the claim and published the scope inside the test file.
- 04
Put the truth in one Durable Object per session
A pure reducer is the only writer of world state, and HIDDEN fields never leave the object, so no amount of DevTools inspection reveals an answer. I rewrote the action semaphore every mutating tool routes through: size one, so transitions serialise, brute force pays a time penalty, and the round-trip latency the finale is sized from gets measured for free. Every action lands in one append-only JSONL log that is simultaneously the replay source, the benchmark corpus and the Archive's ghosts, flushed to D1 as one gzipped row per finished session.
- 05
Rebuilt the client in real-time 3D
The station started as a tile-based 2D floor plan; I replaced it with a Three.js cutaway model, every room open at the top and on its south face, the camera always standing south of the room, four lights and no post-processing pass so it holds a frame rate in a phone's browser. No asset files anywhere: every texture is generated into a canvas at boot and every geometry is built in code. The engine's chunk is fetched only once a shift begins, so a browser without WebMCP never downloads it at all.
- 06
Redesigned the web layer around the split itself
The landing page doesn't contain a picture of the asymmetry, it is one: the same lever drawn two ways, the mark the human sees and the plain sentence the agent gets. A guided first shift flies the camera and points at the console, the shift opens and closes with a told sequence rather than a state change, and the ambiguity gauge got tooltips and an explainer after a playtester reported it as opaque in the moment it mattered.
- 07
Shipped it to Cloudflare on its own domains
A Worker with the Session Durable Object, and two Pages projects, the game and the archive, because cross-origin tool delegation can't be proven on one origin. The first real deployment surfaced a production-only routing bug no local run could, and the browser proof failed two checks against pages.dev preview domains until it was pointed at the custom domains: pages.dev is a public suffix, so two of its subdomains are different sites under Chrome's site isolation.
- 08
Wrote the documentation a judge actually reads
Consolidated the twelve-document planning set into ARCHITECTURE.md and DESIGN.md, rewrote the README as a landing page, drew the two system diagrams by hand, captured the screenshots with the project's own screenshot tour, added CONTRIBUTING, SECURITY, a code of conduct and a changelog, and tagged v1.0.0 as the exact state submitted for judging.
- 09
Kept the limitations in the README, not in a footnote
The asymmetry is a design contract at the tool layer, not a security boundary: an agent with screenshot capability could still see the room, and we say so. The screen-reader mirror is a documented trade-off that hands an agent with page access part of the human's half, and we shipped it anyway, off by default and never naming a glyph, because a game a blind player can't start is the wrong game to protect. The benchmark is offered as a proposal for an instrument, with its raw logs, not as an established one.
Architecture
The agent's tool calls enter through the browser's WebMCP registry, whose contents follow a three-tier AbortController lifecycle: one tool before a shift begins, eight persistent tools for its duration, and a per-chamber set that vanishes the moment a door opens. Every call is a fetch to a Cloudflare Worker, which routes to the one Durable Object holding that session's truth; the human's view is pushed back over a single WebSocket. Nothing puzzle-critical is ever in the DOM: the human's projection renders to a Three.js canvas, the agent's projection is computed on the server where the browser cannot reach around it, and HIDDEN fields never leave the object. A second origin, the station archive, registers the document tools and exposes them back to the game with exposedTo, so cross-origin delegation is load-bearing rather than demonstrative. One append-only session log feeds the replay viewer, the Archive's ghosts and the benchmark alike.
Tech stack
Channel-tagged world state
Every field is a Tagged<T> carrying one of five channels; projectForPilot and projectForKeeper are pure functions in apps/worker/src/projection.ts, and the Blackout, the window where the two roles trade places, is the same law with its two channel lists exchanged rather than a special case.
The possible-worlds proof
tests/possible-worlds.test.ts enumerates W(s), every world consistent with the agent's whole view, for every seed and reachable state, and asserts both that |W| > 1 and that the worlds disagree on the correct action. log2|W| is the number on the in-game CONCORD meter. Extracted into @semaphore/asymmetry, a zero-dependency package with a CLI that exits non-zero when a tool surface leaks.
Session Durable Object + D1
One Durable Object per session on the SQLite storage class, the only one on the Workers free plan; a pure reducer as the sole writer; an action semaphore of n = 1; finished sessions in D1 as one gzipped JSONL row each. R2 was dropped because it is the one Cloudflare product whose free tier demands a card.
WebMCP tool director
Three AbortControllers, one per lifetime: entry, session, chamber. One real toolchange listener reading getTools() drives both the manifest panel and the agent's rendered body, so the animation can never drift from the registry it draws. Both APIs, by a rule: declarative for the shared notepad a human can also submit, imperative for everything only the agent can do.
Cross-origin delegation
apps/archive is a separate Cloudflare Pages project embedded with allow="tools"; its tools carry exposedTo: [gameOrigin] and are invisible to a default getTools() read. Verified end to end over the Chrome DevTools Protocol against the live production domains, not only in local development.
Four chambers, four loop shapes
Airlock: a relay. Signal Room: relay plus agent-side computation and a trust decision over a vandalised page (1,956 ordered subsets, about 10.9 bits). Blind Panel: a closed feedback loop over a hidden permutation (384 hypotheses, about 8.6 bits), with one cross-linked dial and gauges that drift. Concord Lock: sustained coordination under a stamina window sized from measured latency.
Real-time 3D client
Three.js cutaway station, four lights, ACES filmic tone mapping, no post-processing; every material and texture built by one module so the twenty-colour palette stays locked; fixtures ease toward the server's last reported state instead of playing animations. 46 KB entry bundle gzipped, and the engine chunk loads only on shift start.
Error taxonomy and description budgets
Seven error codes, each returning text an agent can act on rather than a bare rejection. Chrome's character recommendations (500 per description, 150 per parameter, 30 per name, 1,500 per output) are held by a test over the real tool objects, which also pins every annotation: readOnlyHint on every non-mutating tool, untrustedContentHint on the three that genuinely return content of uncertain provenance.
The ablation and the Cooperative Benchmark
bench/ plays sessions in-process under three conditions over twenty fixed seeds. The agent-alone condition draws uniformly from the consistent-world set at every step, so it is a ceiling no real model beats and the gap it reports is a lower bound. Four scripted partners (oracle, vague, slow, wrong) then measure how joint performance degrades as the partner does.
Synthesised spatial audio and accessibility
One AudioContext, every cue placed in normalised room coordinates, no audio asset files; every cue carries a text equivalent from the same branch of code, including the detent count the Blind Panel is built on. Keyboard-reachable throughout, the two channel colours verified against protanopia, deuteranopia and tritanopia by simulation on every test run, and no green anywhere in the palette.
Zero PII, by construction
No accounts, no email, no persistent identity: a session is an opaque server-generated id plus a name the agent chose for itself. The replay projection drops every state_delta before a browser sees it, because a raw replay of a seed would be a solution key for every future session on it.
Results
3.80 of 4 chambers
Cleared by the pair, averaged over twenty fixed seeds. The agent alone clears 1.25 and the human alone 0.00: two bars near the floor and one far above it is the whole thesis, measured rather than asserted.
90% vs 0%
Escape rate with a partner versus without one. The agent-alone figure is a ceiling: it draws uniformly from the worlds its own tools can't distinguish, so no real model does better and the gap is a lower bound on the real one.
863 tests
Passing across the workspace as of the last verification pass: the possible-worlds proof, chamber solvability for every seed, the state machine, tool contracts, description budgets, and a browser proof over CDP against the live deployment. Typecheck, lint, palette lock and bundle budget run in CI on every pull request.
0.68 · 0.53 · 1.00
Chambers cleared relative to an accurate partner when the partner is vague, six seconds late, or confidently wrong a quarter of the time. The wrong partner looks free at the headline and isn't: its descriptions carry 2.36 bits each against the oracle's 5.24, because a confident mis-description scores negative.
46.3 KB
Entry JavaScript bundle, gzipped, against a 400 KB budget enforced at build time. The 147.5 KB Three.js chunk is fetched only once a shift begins, so a browser without WebMCP never downloads the engine.
18 tools, 3 lifetimes
One tool on the landing page (begin_shift), eight for the length of a shift, nine that exist only inside the chamber that needs them, and a final registry that is empty on purpose: the last toolchange of a session fires with no tools at all.
0 asset files
Every texture generated at boot, every geometry built in code, every sound synthesised. One deliberate exception, a self-hosted display typeface, documented in NOTICE.md so the rest of the repository stays plain MIT.
121 of 260 commits
My share of the ten-day build by the repository's own history; Ghassen's 139 include the tool director, the benchmarks, the audio and the accessibility layer. Live on two custom domains plus a Worker, entirely on Cloudflare's free tier.
Moments






What I took away
- 01
The proof failing on its first run was the best thing that happened to the project. It failed on the state where an agent has pulled two of three wrong levers, and it was right: exhaustive search defeats underdetermination no matter how clean the projection is. The instinct in that moment is to widen the test until it passes. Scoping the claim instead, and publishing the scope inside the test file, is the difference between a proof and a decoration.
- 02
The second clause does the work. It isn't enough that several worlds are consistent with what the agent perceives; they have to disagree about what the agent should do. A version of the test with only the first clause stayed green on the failing state and would have proven nothing.
- 03
A renderer rewrite kept every rule and broke every number. Moving from the 2D tile renderer to 3D, every design rule transferred without argument, and every tuned constant was wrong: exposure, fog, light intensity, camera distance, gauge size, the screenshot tour's own wait. Six hundred-odd tests passed throughout. After a medium change, treat every constant as unverified, including the ones in the instruments.
- 04
The game is the test. Each playtesting pass found defects that hundreds of passing tests hadn't: doors standing in solid walls, captions covering the mechanisms they described, a camera compounding its own idle drift. A renderer's real defects are visible in a frame and in nothing else, which is why the screenshot tour became part of verification rather than marketing.
- 05
Server authority is what makes a benchmark number worth anything. A client-side timer is one debugger statement from infinite, and a solution in the client is one DevTools tab from public. Putting the truth in one Durable Object per session cost nothing extra and made the replay viewer, the benchmark corpus and the Archive's ghosts fall out of a single log.
- 06
State the limitation in the README, not in a footnote. The asymmetry is a design contract at the tool layer, not a security boundary against an agent with a screenshot, and the accessibility mirror deliberately hands an agent with page access part of the human's half. Saying both plainly, and shipping the mirror anyway, was an easier decision than it looks once you accept that a game a blind player can't start is the wrong game to protect.
- 07
Ten days is enough to ship a documented product if the documentation is treated as part of the product. Consolidating twelve planning documents into two the night before the deadline reads as polish and is actually the submission: a judge with a stack of entries reads the README, the architecture, and whatever the screenshots show.