Essays · Replit Designathon

A Real Computer, In A Link

How I built a multiplayer operating system that runs in a browser tab — and what it taught me about where software development is going.

Khizer Husain

August 2026

10 min read

"There were maybe six or seven points in this project where the honest assessment was 'this is a multi-week detour and probably not worth it.' In a world where I'm typing every character, most of those don't get built. The project ends up smaller and less interesting.

They all got built. Not because the AI is magic, but because the cost of attempting something dropped far enough that ambitious detours stopped being irresponsible."

— Khizer Husain

The Premise

Every "collaborative" tool I've ever used shares the same defect: it lets you look at something together, not do something together.

Screen sharing is the purest example. Someone streams a rectangle of pixels at you. You can see their terminal. You cannot type in it. You watch a cursor you don't control move through a filesystem you can't touch. When you want to help, you say words out loud and hope they translate into keystrokes on the other end.

Remote desktop protocols solve this by giving you input — but they're still fundamentally one machine, streamed. There's a host somewhere. Latency is a function of distance. The machine belongs to a datacenter.

I wanted to know what happened if you removed the datacenter entirely.

Not "moved it closer." Removed it.

What Cometwork Actually Is

Cometwork boots a real x86 Linux machine inside your browser tab. Not a container. Not a VM in the cloud with a video feed. A genuine x86 emulator — v86, compiled to WebAssembly — executing real instructions against a real Linux kernel, on your own CPU, in a Web Worker isolated from the UI thread.

Then it makes that machine multiplayer.

You share a URL. Whoever opens it lands inside the same running machine. Same terminal buffer. Same filesystem. Same processes mid-execution. The desktop is captured as a video track and published peer-to-peer over WebRTC — the media path never touches my infrastructure. My server relays handshake signaling and nothing else.

The result is a thing I didn't have a good name for at first: a computer that exists as a link.

How It Works

Booting an OS in a tab

v86 emulates a 486-class x86 machine in WebAssembly. You give it a kernel image and a disk image and it boots. The interesting engineering isn't in getting the emulator running — copy did that work and it's excellent — it's in everything around it.

Before anything boots, the client measures the host machine and sizes the allocation to what the hardware can actually sustain, then asks for explicit consent before starting. This matters more than it sounds: an unthrottled x86 emulator will happily eat a laptop alive. The consent gate is keyed per workspace, not globally — an early bug had it global, which meant the second workspace you opened silently skipped the hardware check and inherited an allocation sized for a different machine.

Why not just stream a VM?

Because then it's someone else's computer.

The entire security posture of Cometwork rests on the OS never leaving your device. Once you accept a remote VM, you've accepted that a server operator can read your filesystem. I wanted the opposite default. Your machine runs on your silicon. The only thing that crosses the network is a video track going peer-to-peer to people you invited, and encrypted object data going to storage.

The control lease

Multiple people inside one machine is chaos unless exactly one person can drive at a time.

Cometwork enforces a single-writer control lease. One participant holds it. Everyone else watches. To take over, you request; the holder grants; the lease moves with an epoch bump that fences stale writers.

Critically, this is server-authoritative, not a UI convention. The lease is issued by the backend and required on every mutation. The host browser only accepts input events originating from the current lease holder. A viewer poking at their own DOM cannot inject keystrokes — the boundary is real, not front-end theater.

Getting this right took several passes. An early version had an idle auto-yield that would release the lease from someone who was actively driving the VM over the peer-to-peer data channel, because the yield timer only watched application-level events and was blind to raw VM input. The fix was a control-activity heartbeat on the P2P path.

CRDTs for everything that isn't the VM

The Linux machine syncs as video plus input. Everything around it — documents, spreadsheets, whiteboards, window positions, the graphing calculator — syncs as Yjs CRDTs over a dual transport: WebSocket to the server for persistence, WebRTC data channel for low-latency peer updates.

There's a genuinely nasty bug lurking in this design that took me an embarrassing amount of time to find. The ws library drops frames that arrive before a 'message' listener is attached. If you await anything during connection setup — an auth check, a database lookup — before attaching your handler, you lose whatever the client sent first. Which is the Yjs syncStep1 handshake. Which means the document silently never syncs, with no error anywhere. Attach the message listener before any await.

End-to-end encryption

Encryption is opt-in per workspace. A passphrase is stretched with PBKDF2 into a non-extractable AES-GCM key that lives only in a module-scoped Map in the browser. It never serializes, never hits storage, never crosses the network. The server receives ciphertext and a non-secret salt.

Two things I got wrong here and had to fix:

The random-IV save loop. AES-GCM requires a fresh IV per encryption. That means encrypting identical plaintext twice produces different ciphertext. If your autosave loop compares "current ciphertext" against "last saved ciphertext" to decide whether anything changed, the answer is always yes and you write forever.

Recovery codes wrap the passphrase, not the key. The key is deliberately non-extractable — you can't export it to wrap it. So recovery has to operate one level up, on the passphrase itself.

There's also a subtle consequence for the branch-comparison feature: because ciphertext is IV-randomized, the server literally cannot diff two encrypted workspaces. Object-presence comparison returns garbage. The fix was to zero out server-side diffing entirely for encrypted workspaces and recompute the comparison in the browser, where the plaintext exists.

Egress and the firewall

The emulated machine has network access via a raw-ethernet WebSocket bridged to real Node sockets on the server. This immediately creates an SSRF machine: an in-browser Linux box that can ask my server to connect anywhere.

The gateway runs default-deny with an explicit allowlist, and the SSRF block beats every allow rule — you cannot allowlist your way to 169.254.169.254. The bridge is a lossless link, which means no retransmit logic is needed, which is one of the few places this project got a freebie.

The private-browsing proxy has its own set of traps worth documenting. Node skips DNS resolution for literal IP addresses, so IP-literal SSRF bypasses hostname validation. Happy Eyeballs with {all: true} returns multiple addresses and you must check every one. IPv6 needs bracket handling. Each of these was a real bypass I found and closed.

The agent sandbox

KhizAI can run shell commands. Those commands execute in an unprivileged bwrap sandbox entered via setpriv with capabilities dropped, /proc bind-mounted, and symlink escapes guarded by real-path containment checks. It fails closed — if the sandbox can't be established, the command doesn't run.

WebGPU over a serial port

This one is my favorite, because the constraint is so stupid and the workaround is so satisfying.

I wanted the guest OS to be able to use the host's GPU for compute. The clean way to do this is memory-mapped I/O. v86 doesn't support MMIO.

What v86 does support is serial ports.

So the GPU passthrough transport rides ttyS1 as a CRC32-framed protocol. The register specification is the actual contract; the serial port is just how bytes get there. Host and guest have to stay in exact lockstep on the framing, which means the two implementations are permanently coupled — but it works, and a guest process can dispatch compute to the host GPU through what is functionally a 1980s peripheral interface.

The AI Is A Participant, Not A Feature

This is the part I think is genuinely new, and it's not because the model is special. It's because of where it sits.

KhizAI joins the workspace as a named participant. It gets a presence entry, a color, and a cursor. It can hold the control lease. When it does, it drives the actual machine — opening terminals, running commands, editing files. It narrates its reasoning aloud through server-side neural TTS. Every action is written to a typed event log.

It is not a chat box adjacent to your work. It is in the room.

Consequence isolation

Here's the mechanism I'm proudest of.

When consequence isolation is enabled, the first time KhizAI attempts something irreversible — a file write, a destructive command, a delete — it does not do it. Instead it forks the entire live machine into an isolated branch and continues working there.

Branching a live machine means server-side copying the VM snapshot blob and inserting a snapshot row pointing at the same VM allocation. Miss the second half and the branch boots a fresh OS instead of the frozen state, which looks like it works right up until you notice none of your files are there.

At the end of the run, it diffs the branch against the original and shows you a risk-scored summary: what was added, changed, removed. Promote the branch and your session switches into it. Discard it and it's gone. The live machine was never modified.

The branch creation must fail closed. If forking fails and the agent proceeds anyway, the entire safety property evaporates — you now have an agent doing irreversible things to a machine it was supposed to be protecting.

Abandoned branches

Agent-created branches that are never promoted get swept after a TTL. User-created branches are exempt, distinguished by an agent_created flag. This is a small detail that only exists because the first version deleted a user's manual fork.

What Else Is In There

The scope grew because every answer produced another question.

  • Time travel — fused snapshot and event timeline. Restore any moment, or branch from it.
  • Session replay with AI narration — the event log is fed to a model that emits chapters; the browser speaks each one and auto-advances a scrubber. Exportable as MP3 (concatenated TTS frames, no re-encode) or WebM (canvas + MediaRecorder + WebAudio).
  • A full multiplayer app suite — Docs (TipTap), Sheets (HyperFormula), Whiteboard (tldraw bound to a Yjs records map), and a collaborative graphing calculator with presenter mode.
  • Voice control — push-to-talk over Web Speech, with TTS-echo turn-taking so the agent doesn't hear itself.
  • Mini-apps — ask the agent for a tool and it generates a self-contained app rendered in a locked-down iframe (sandbox="allow-scripts", host CSP default-src 'none'), with a postMessage bridge for persistence.
  • Peer compute grid — opted-in viewers run sandboxed work units for the host over the existing data channel. Verification is spot-check, not Byzantine-tolerant; I'll say plainly that this is a trust-the-room model, not a trustless one.
  • Spatial audio huddles — P2P A/V mesh with a PannerNode positioned by each peer's cursor location.
  • Windows 98 — because once Linux worked, someone was going to ask.

Limitations, Honestly

I'd rather say these myself than have them found.

Emulation is slow. v86 is a software x86 emulator. This is not a workstation. It's a real Linux machine, and it runs like a real Linux machine from 1998.

Encryption doesn't cover everything. Object content and VM snapshots are sealed. Live WebRTC media, terminal I/O in flight, and realtime presence are not. If your threat model includes an adversary observing the session in real time, E2E encryption here does not save you. I'd rather state that clearly than let the badge imply more than it delivers.

No lazy paging. v86 can't demand-page a memory image. Instant resume works by restoring a locally cached snapshot when its ID matches the server's latest — not by streaming pages. Cold reopen on a new device is a full download.

Peer compute is not trustless. Spot-check verification catches lazy cheaters, not determined ones.

Mesh doesn't scale. The default multiplayer topology is a WebRTC mesh, which is fine for a handful of people and falls apart beyond that. There's an opt-in SFU path for larger rooms, but it means media transits a third party — which is exactly the property the mesh exists to avoid. Pick your tradeoff.

TURN isn't free. Some networks won't traverse without a relay. There is no free public TURN infrastructure. Relay is opt-in and config-driven, and self-hosters need their own coturn.

The Part About Replit

I built all of this with Replit Agent.

I want to be precise about what that means, because "AI wrote my app" is a sentence that has been devalued by people using it to describe a to-do list.

This is a production system with a WebAssembly OS layer, a WebRTC multiplayer topology with SFU fallback, CRDT-backed collaborative applications, an end-to-end encryption scheme with recovery codes and Ed25519-signed trust manifests, a sandboxed agent runtime, an egress firewall, a public API, and a mobile companion. It has real security boundaries that had to actually hold.

Agent wrote it, ran it, read its own logs, found its own regressions, and iterated. I directed. I made architectural calls, said no to things, and caught the places where a plausible-looking implementation had a hole in it. That division of labor turned out to be extremely productive.

The thing I keep coming back to isn't that the code got written faster. It's that the ceiling moved. There were maybe six or seven points in this project where the honest assessment was "this is a multi-week detour and probably not worth it" — the serial-port GPU transport, the encrypted branch-diff rework, the egress firewall, the recovery-code scheme. In a world where I'm typing every character, most of those don't get built. The project ends up smaller and less interesting.

They all got built. Not because the AI is magic, but because the cost of attempting something dropped far enough that ambitious detours stopped being irresponsible.

I think that's the actual story here, and it's bigger than my project. The constraint on what individuals can build has, for my entire career, been throughput. That constraint just got materially weaker. What's left is knowing what's worth building and being able to tell when something is subtly wrong — which, it turns out, is most of the job.

Try It

Boot a workspace, share the link, hand someone the keyboard. No installs, no accounts.

cometwork.com

If you break it, I'd genuinely like to know how.