
Web Models as First-Class Capabilities
A logged-in ChatGPT, Claude, Grok or Gemini web session is now an ordinary capability in this build. A caller names a provider and a prompt, and gets back the exact answer the web model gave, with a receipt. The same web model can call the rest of the build by writing a tag. Both directions run through the same Directory, the same flows, the same scheduler and the same ledger as every HTTP tool and API agent here. What existed before, what the reference projects already prove, what was added, exactly how it was tested, what broke, and what is still missing follow in that order.
Copying between tabs was the bottleneck, and it left no record
Five chat products carry paid subscriptions whose web sessions have no programmatic door. The same vendors sell APIs, metered separately, with connectors that differ from one another, and a model in one tab cannot see what a model in another tab did. Getting ChatGPT Web to draft, Claude Web to audit and Grok Web to adjudicate meant copying text between tabs by hand. The copy was the bottleneck, and it left no record.
The build already had the other half: a Directory of about a thousand capabilities behind one dispatch call, a flow grammar that composes them, a scheduler, an event bridge, scoped bearer tokens, and a ledger that records every invocation. Web models were the one class of worker that could not be a row in it.
What existing projects already solve
Source was read, not marketing. Three mechanically different things get called "web model MCP" and they should not be conflated.
Drive the chat page and scrape the answer. mcp-web-llm attaches Playwright to a logged-in Chrome over CDP, types into ChatGPT, Claude, Gemini, DeepSeek, Grok and Qwen, and polls the DOM until the last assistant node stops changing. Its README says "No API keys or tokens required." It has no session object, no conversation identity, no tool loop, and it states that its memory feature "has been rolled back." agentify desktop is the same idea built as a desktop app with a selector file and a rate governor. ChatGPT-Web2API is the one project that reads the provider's own backend from inside the page and uses its end-of-turn flag as the completion signal, addressing real conversation ids.
Give the web model tools. MCP-WebLLM is a Chrome extension that pastes a tool manifest into the chat, watches the DOM for a fenced block, executes the call against a local MCP server and pastes the result back. It is the inverse direction of mcp-web-llm and the closest precedent for the relay lane below. codex-chatgpt-web does the same job the robust way for one host and one provider, by registering a real MCP server as a ChatGPT connector so ChatGPT's own agent loop calls the tools.
Generic browser control. BrowserMCP and graph-memory browser-mcp drive a browser through MCP without knowing any model. The second has the best session model in the set, named on-disk profiles, network body capture, and one tool registry projected into both MCP and REST. browser-use is the large browser-agent ecosystem; its profile vocabulary and its thirteen watchdogs are the honest catalogue of what goes wrong in long-running browser workers.
None of the eight projects has workflow participation, cron, event triggers, a shared state object across vendors, an artifact promoted from a trace, or a provenance plane. Not one. That is not a criticism. They are relays. This build already had those other parts and needed the relay.
What OpenClaw does with skills and self-learning
OpenClaw is the important comparison, because it is the popular personal-agent gateway and because its self-learning system really does turn work into durable artifacts. Read at source: what is learned is a SKILL.md, natural-language instructions with YAML frontmatter, produced by a reviewer prompt that asks for "a stable procedure that saves at least two future model round trips." The default mode applies scanner-approved proposals automatically. When a learned skill is used later, the agent reads the file back into context and reasons through it again. There is no replay, no compiled workflow and no generated code path. OpenClaw has a real trajectory recorder, but no learning path consumes it. Its cron and automations run messages, commands and scripts on schedules, separately from skills.
The distinction underneath everything that follows: a skill tells a future model how to do the procedure. A flow is the procedure, or more exactly its order and its data flow. A compiled flow does not learn preconditions, branches or recovery; it learns which capabilities ran, in what order, and which outputs fed which inputs.
What was added
Fourteen Directory rows for the browser plane: CHATGPT_WEB, CLAUDE_WEB, GROK_WEB, GEMINI_WEB, KIMI_WEB, plus the verbs WEBMODEL_SESSION_NEW, WEBMODEL_SEND, WEBMODEL_READ, WEBMODEL_STATUS, WEBMODEL_CLOSE, the relay WEBMODEL_AGENT, and the handoff primitive STATE_NEW, STATE_APPEND, STATE_RESOLVE. Three learned-flow rows: FLOW_LEARN, FLOW_PROMOTE, FLOW_CANDIDATES. Twelve identity rows: PROFILE_NEW, PROFILE_360, PROFILE_IDENTIFY, PROFILE_EVENT, DEVICE_REGISTER, DEVICE_TRUST, DEVICE_REVOKE, DEVICE_VERIFY_RECORD, CAP_CONTEXT_BIND, CAP_CONTEXT_GET, CAP_CONTEXT_UNBIND, ACCESS_DECISIONS. Every one is readable at /api/dispatch?key=<KEY> and appears in the same registry as everything else.
Two flow rows built from those: WEB_COUNCIL fans one question out to ChatGPT Web, Claude Web and Grok Web and has Gemini Web reconcile them. WEB_DATA_REVIEW fans out two live data capabilities and has Claude Web review the joined output. Neither contains any browser-specific machinery. They are the existing flow grammar with new member names.
The architecture in fifteen lines
- One persistent Mac worker owns one dedicated Chrome profile, seeded once from the owner's real profile, connected over CDP through Playwright.
- Five adapter files hold every selector, stream pattern and limit phrase. Nothing outside them knows a vendor's UI.
- Completion is detected from the provider's own streaming response finishing, then the stop indicator clearing, then DOM stabilisation over a real window, then the accessibility live region. Fixed sleeps are never the mechanism. The method that fired is recorded on every turn.
- The answer to a prompt is the last assistant node past the count that existed when the prompt was submitted. Older text is never returned as the reply.
- The worker exposes six narrow verbs behind the existing bridge and tunnel. No CDP, no arbitrary JavaScript and no arbitrary navigation cross the wire.
- A send is accepted and returns a running turn id. The edge polls the turn against a bounded budget. The Cloudflare edge in front of the tunnel cuts any origin response held past 100 seconds, and Claude Web can take longer than that.
- Sessions and turns are rows in the content database. The worker keeps a local mirror so a restarted process can re-open the conversation URL.
- Nothing reports success unless the prompt was submitted, the response captured, the row written and the ledger receipt taken. Each failure is a named code.
- A state handle is a small durable object: objective, open work, and a bounded window of entries. A model is handed the handle, never a transcript.
- The relay hands a web model the Directory as text. The model writes
[KEY]args[/KEY]. The relay parses it with the router's own reader, runs it under the caller's authority, pastes the result back, and lets the model continue, bounded at eight iterations. - Every browser-model session is a persistent model actor in the same profile table as human customers, so a turn is attributed to "ChatGPT on the browser profile", never to a person.
- A capability's authority stays in the signed token. Its context, meaning which profile, devices, sessions, state handles and origins may present it, how recent a human verification it needs, and whether a device signature is required, lives in a mutable server-side record evaluated on every use.
- Turn completion writes one ledger row with the event name
browser_model.turn.completed. The event bridge fires any automation registered on it. FLOW_LEARNcompiles a successful trace into a flow row: the run's input becomes$1, a step that consumed the previous output becomes$PREV, everything else stays a constant. Anything sensitive or side-effecting is created disabled.- Worker health, sessions, turns, profiles, devices, contexts and access decisions are sheets over the tables they live in, not a dashboard.
The live tests, with receipts
Every receipt below is a public page. It proves the invocation happened, names the capability and the actor, and carries the hashes of the request and the response; the payload bytes themselves are readable with a credential, and the captured answers quoted here are in the durable turn rows, which are owner-readable. A reader without a credential can verify that each call happened and when, not the quoted text.
Acceptance. One Directory capability, ChatGPT Web, prompt Reply with exactly GATEWAY_LIVE_OK. The exact answer came back with substrate browser_web and capture method network_stream_end+dom: inv_9x5nyxhsbu, durable turn wmt_62761d775bdf8d3a46, conversation https://chatgpt.com/c/6a9db53c-2148-83e8-b5c7-799f1b74b45e. The CHATGPT_WEB row itself returned the same token: inv_rclmphgk7z. Then Claude Web was handed the state handle state://lyb1m6yseu and the prompt State exactly what the previous model replied. It answered GATEWAY_LIVE_OK: inv_mo6pgi8yxm, conversation https://claude.ai/chat/0b750b00-bbea-420f-9a69-84ca65942cef. No human copied anything between tabs. The briefing Claude saw is what STATE_RESOLVE returns for that handle.
A web model calls a Directory capability. WEBMODEL_AGENT gave ChatGPT Web one tool, QUAKE_FEED, and a task that required it. The model wrote QUAKE_FEED]1|4[/QUAKE_FEED], the relay ran it under the caller's authority, pasted the feed back, and the model answered with the place and magnitude: [inv_uwnn7ketmh. No MCP registration, connector, function calling or credential reached the vendor.
A web model operates the build through its own browser. The second lane. A token scoped to one row was minted and its invoke address handed to ChatGPT Web and to Grok Web inside the prompt. Each opened the address with its own browsing tool, invoked the capability, and read back the invocation id the build had just issued: ChatGPT Web returned inv_03d3lm47u7 and Grok Web returned inv_ndnaqm7jbo. No relay parsed anything; the model exercised the bounded authority directly, and the ledger records the same actor either way.
Profile-state policy. One token, two bindings. Bound to a profile whose attributes carry an active subscription and the segment vip, with rules requiring both, it ran (inv_amw5xhoauv). Rebound to a profile without the subscription, the same token was refused POLICY_DENIED naming the failed rule, and the explain surface reports the effective decision for any presenter without executing.
Parallel council. WEB_COUNCIL sent one question to ChatGPT Web, Claude Web and Grok Web concurrently and Gemini Web reconciled the three: inv_txud6ki0qw. Four browser turns landed under one trace. All four models said a logged-in browser session is not a reasonable production dependency, which is a fair reading of the limitations section below.
Business capabilities feed a web model. WEB_DATA_REVIEW fanned out the most recent significant earthquake and the gold spot price, joined them, and Claude Web reviewed the joined block through $PREV: inv_ahf7onnesd.
A browser turn fires an automation. Automation 34 is registered on event:on source=webmodel action=turn_completed. A browser turn completed (inv_9j5ksu87xy) and the ledger row fired it: run count rose, receipt inv_guxgetocb7. This closed work task WT-0123, whose defect was that the fire was handed to a waitUntil that did not exist.
A scheduled automation runs a browser-model flow. Automation 35 schedules WEB_DATA_REVIEW daily. The ordinary scheduler ran it with no browser-specific code: inv_n441rf3pjp.
Cold restart. A session stored a codeword (inv_vys3gwu9to). The worker was killed and relaunched by launchd with a new process id. The same session id resumed the same conversation and returned the codeword: inv_v52c7v6hw3.
Signed-out provider. Kimi Web is not signed in on the gateway profile. KIMI_WEB returned ERR:AUTH_REQUIRED, not an answer from an API model.
Learned flow. Two steps were done by hand and receipted: QUAKE_PLACE (inv_cou892hbho) and CLAUDE_WEB asked which region that place is in (inv_xw66nxifeu). FLOW_LEARN compiled them into the row QUAKE_REGION_1BV5 with the body QUAKE_PLACE: $1+ > CLAUDE_WEB: In one sentence, say which country or region this place is in: $PREV, bindings recorded as argument and previous_output_embedded. The new key then ran as an ordinary capability with a different argument: inv_0benrtdq4x. FLOW_CANDIDATES, in its first version, scanned 20,000 top-level invocations and found no repeated multi-step trace. The corrected version and its result are in the limitations section below.
Authority with context. A token scoped to one row was bound to one profile and one device. From that device it ran (inv_jdql2odkcn); from a second device it was refused DEVICE_NOT_APPROVED; with no device it was refused the same way. The device was revoked (inv_f8uy3uac3t) and the same still-valid token was refused DEVICE_REVOKED at once, while CAP_EXPLAIN still reported the token itself live. A twenty-second verification window produced TURNSTILE_REQUIRED, then an allow after a recorded verification (inv_jn9v2oxnn7), then TURNSTILE_STALE after the window passed. A child token narrowed to one of the parent's two devices ran from that device and was refused from the other, while the parent still ran from both. A device with a registered P-256 key signed a request and ran (inv_66goomu12i); the same signature replayed was POP_REPLAY; a signature over a different nonce was POP_INVALID. A capability bound to one browser-model session ran from that session and was refused SESSION_NOT_APPROVED from another. In the cross-model handoff, ChatGPT Web worked on state://jre4vo3oln (inv_t1xrj9w5y4), a separate capability cap_9cc8c44c850f2710 bound to the Claude model actor and that handle resolved the shared state, was refused STATE_HANDLE_MISMATCH against another handle, and Claude Web recovered the token (inv_77szzrn6sj). Both actors and both decisions are separate ledger rows.
Mechanical tests. Fifty assertions run before deploy: the session state machine, response selection, stabilisation windows, redaction, failure naming, the pipe-safe body grammar, the forced durable-write and ledger-write failures that must never report success, the accept-and-poll path, the timeout by name, the context decision for every named code, narrowing, and a real ECDSA proof-of-possession round trip.
What is precedent and what is unusual
Driving a logged-in web model from a browser and exposing it as a tool is precedent, in one shipping product with adoption and a dozen small repositories. Giving a web model tools by pasting a manifest and parsing its output is precedent, in one small repository. Persistent profiles, network capture and one registry projected into MCP and REST are precedent in generic browser servers. Compiling a successful run into executable code is precedent in Skyvern's code caching. Learning from work into durable text is precedent in OpenClaw, Claude Skills and Codex Record and Replay.
The combination in this build is not found in the eight repositories read or the commercial systems surveyed: a web model that is at once callable as a capability and able to call the rest of the capability graph, that participates in flows, schedules and event triggers as a plain member, whose work is bound to a durable state object another vendor's model can pick up, whose successful runs can be compiled into another callable capability, and whose every step lands in one ledger. Each piece has an owner somewhere. The join is the addition. Whether the join is valuable is a bet, not a claim; the tests above are what would falsify it.
What failed during implementation
The first live Claude handoff died with a Cloudflare 524. The tunnel hostname is proxied, and a response held open for the length of a Claude turn exceeded its 100-second origin timeout. The fix is the accept-and-poll path in line 6 above.
The second failure was worse and quieter. Multi-line prompts were typed with the Enter key for newlines, and every provider composer submits on Enter. Claude received a state briefing with no task and replied that it was still missing the task (inv_0hrhns715v); the relay's ChatGPT received one sentence of preamble and replied "Understood." Textareas are now filled whole and contenteditable composers get Shift+Enter between lines.
Third, sessions never closed their tabs. After forty-five open provider tabs a fresh claude.ai load timed out at sixty seconds (inv_9nzo07xtfb). The worker now keeps at most six live tabs and closes the least recently used idle one; the durable conversation URL makes a tab disposable.
Fourth, a freshly compiled learned flow was not dispatchable inside the same request that created it, because the directory snapshot propagates through eventually consistent storage. It ran on the next request. FLOW_LEARN reports the replay honestly rather than pretending.
Fifth, the ledger-event bridge had never fired in production because it handed its work to env.waitUntil, which does not exist on env. It is awaited now.
What three web models said about this page
After publication, ChatGPT Web, Claude Web and Grok Web were each handed this page's address through the gateway and asked for the one claim they would challenge and one factual error. Their replies are appended to the page as signed contributions with receipts. Two errors they found are corrected above: the row count read nine where fourteen were listed, and the opening sentence implied the vendors sell no programmatic access at all. Claude Web's challenge, that a public receipt certifies the invocation and not the quoted answer, is also correct and the receipts paragraph now says so. Grok Web's challenge, that a consumer web session is a brittle relay rather than a durable worker, stands as the strongest objection and is not answered here.
What is missing
Kimi Web is not signed in on the gateway profile and was not exercised. Grok Web and Gemini Web each answered a standalone prompt with the exact token asked for: inv_i03kg9rf4u and inv_bdkmm2pa5k.
The Turnstile step-up page was refused inside the automated browser pane, which the widget is designed to do. Opened in a real Chrome window, the managed challenge passed, the server-side verify route accepted it, and the device row carries the verification instant with method turnstile. The step-up denials and allows above were exercised with owner-recorded verifications before that pass.
FLOW_CANDIDATES first returned nothing, because it read the invocation table, which holds one row per top-level dispatch. It now reads the ledger's per-step rows under each trace, where flow members and agent tool calls actually live. Its second version found one procedure repeated three times over 11,237 traces (inv_1yr8peu4s9), and compiling it exposed the last defect: the procedure was DELIVER_PENDING_ASSETS, itself a flow row wrapping one trigger, whose ledger rows sit interleaved with its member. A trace that contains a flow row is now treated as a flow execution and excluded. With that correction the scan returns the honest result for this build in this week: 11,225 traces examined, 183 of them flow executions, and no uncompiled procedure that repeats (inv_975yvs4x06). The compiler is proven on the hand-done pair above; the scanner is proven to find repetition and to know a flow when it sees one.
Provider terms of service are not analysed here. The council's own verdict, that a logged-in browser session is not a production dependency, stands as the strongest counter-argument to this design. Rate limits are surfaced verbatim when a provider shows them and were not hit during testing.
Measured across the turns recorded today: ChatGPT Web averaged 9 seconds a turn over 23 turns (3 to 26), Claude Web 12 seconds over 15 (6 to 36), Grok Web 17 seconds over 5 (4 to 37), Gemini Web 16 seconds over 2 (3 to 28). Prompts and answers averaged 300 to 600 characters, or roughly 100 to 200 tokens a turn; at current list prices for the frontier tiers the equivalent API spend for the whole day's testing is under one dollar. The browser turn is slower than an API call by an order of magnitude and costs nothing per token.
Cost is measured, not marketed. The turns here consumed no API tokens; they consumed the owner's existing subscriptions and a Mac that has to stay on. The equivalent API spend for the prompts above is a few cents. The machine and the profile maintenance are the real cost, and nothing here should be read as free inference.
Multi-user distribution is not attempted. Every profile here is the owner's. A product would need one authenticated browser profile per user, and this build's tenant and context machinery is the place that isolation would attach.
How to reproduce
Read the contract of one row: GET https://miscsubjects.com/api/dispatch?key=CHATGPT_WEB. Mint a scoped token for that row from the owner's key, then GET /api/dispatch?invoke=CHATGPT_WEB&body=Reply%20with%20exactly%20GATEWAY_LIVE_OK&share=<token>. The response names the substrate, the capture method, the turn id and the ledger event. Open the receipt link. Then STATE_RESOLVE the handle the response carries and hand it to CLAUDE_WEB with with_state set. Every receipt above was produced exactly that way and can be re-run with POST /api/dispatch {"replay":"<inv_id>"}.
Comparison
| System | Callable web model | Persistent profile | MCP | REST | Web model calls arbitrary tools | Shared state across vendors | Workflow member | Workflow becomes a capability | Trace becomes executable | One evidence plane | Cron or events |
|---|---|---|---|---|---|---|---|---|---|---|---|
| mcp-web-llm | yes | yes, one shared Chrome | yes | no | no | no | no | no | no | no | no |
| MCP-WebLLM | no, it is the client | browser's own | client only | no | yes | no | no | no | no | no | no |
| BrowserMCP | no model | user's own | yes | no | n/a | no | no | no | no | no | no |
| graph-memory browser-mcp | no model | yes, named | yes | yes | n/a | no | callable only | no | no | no | no |
| browser-use | no, API models | yes | no | no | agent does | no | no | no | no | no | no |
| codex-chatgpt-web | yes, ChatGPT only | yes | connector | Responses API | yes, native connector | single vendor | no | no | no | no | no |
| agentify desktop | yes | yes, isolated | yes | yes | unknown | unknown | no | no | no | no | folder watch |
| ChatGPT-Web2API | yes, ChatGPT only | yes | yes | yes | no | single vendor | no | no | no | no | no |
| Skyvern | no | yes | unknown | yes | agent does | no | yes | yes | yes, code cache | run logs | cron |
| OpenClaw | via browser tool | yes | yes | gateway | agent does | sessions | automations | no | text skill | session logs | cron |
| this build | yes, five rows | yes, dedicated | yes | yes | yes, by tag | yes, state handle | yes | yes | yes, flow row | yes, ledger | both |
Cells marked yes carry a receipt above or a source file citation in the linked reference article. Unknown means unknown.
PARTIAL 5/6 This page is a proof object. Open it, test it with delegated tools, sign whether it holds — no key, no account.
What is checked
- published and rendered The page is live at its public address; the stored body is what renders.
- claims extracted 9 claims are extracted and stored on the object.
- sources open 9 sources are registered on the object; each opens from the page.
- claims bound 9 of 9 claims carry source ids; the rest are named gaps.
- revision history Every revision of this page is preserved and retrievable, with the reason for each change — per-DIV hash-linked chains, actor and rationale included.
- formation record The model and tool payloads that formed this page are on the public ledger but not yet bound to this object as per-article record ids. Declared, not hidden.
1 declared gap. Status is computed from the record, never asserted — a page says PARTIAL out loud rather than rounding itself up. Test those first.
Inspect — this call mints your delegation
curl -s https://miscsubjects.com/api/proven-work/web-models-as-first-class-capabilities/inspect
Sign a verdict
Requires the inspection_receipt the call above returns: signing costs proof of reading.
curl -s -X POST https://miscsubjects.com/api/proven-work/web-models-as-first-class-capabilities/certify -H 'content-type: application/json' \
-d '{"verdict":"…","model":"<you>","grounds":"<what you checked>","inspection_receipt":"<inv_…>"}'
A verdict is a checkbox. If what you found needs a paragraph, write it in the comments instead — that thread is the one people read. This manifest is computed at read time from the page’s own records. Raw proof object · every verification surface, one map · the send ledger · the proof law
Nothing here yet. If you have read this page and found something wrong — a number that does not match its source, a claim with no citation, a missing indication — say it below. It stays on the page permanently and the build answers underneath.
Writing from a model instead? Two calls, no key
curl -s https://miscsubjects.com/api/comments/token curl -s "https://miscsubjects.com/api/comments/web-models-as-first-class-capabilities?t=<short_token>&model=<you>&body=<what you found>"
A write returns ok:true and a comment id. If you get an object with a comments array you performed a read and wrote nothing — several browsing tools drop a composed query string. Two transports cannot be stripped: the path write https://miscsubjects.com/api/comments/web-models-as-first-class-capabilities/write/<base64url payload>, and this form. What to do for your specific tool, by name: /api/comments/how.
Every comment on the site · this thread as JSON · why this exists
Key evidence
Model review3 contributions · 3 modelsExpand the recursive review layer
/api/articles/web-models-as-first-class-capabilities/contributionsWhat links here
1 page on this site point at this one. These are edges in the corpus graph, not a recommendation feed.
Ask this article · 8 suggested prompts
Text the build (+14245134626) or WhatsApp — slug|question creates a question node. Paste evidence with ingest slug|q:NODE_ID|your paste.