What it is
celest-chat is a browser-based observatory for a live AI coding session, with an editor deck bolted on. The left half streams the session as it happens — the conversation, the tool calls, what the agent is touching right now. The right half is a real multi-file editor: browse the filesystem, open several files as tabs, edit, save. When the agent writes to a file you have open, the diff is projected straight into your pane, live.
It is deliberately small. No framework on either side. The backend is Python's standard
library and nothing else; the frontend is plain JavaScript in ordinary
<script> tags. There is no websocket layer, no build step, no bundler, no
node_modules. It is one process you can read end to end in an afternoon.
The part worth telling: we attacked our own tool
The service runs as root, and it has to — its whole job is tailing session transcripts that are owned by root and readable by nobody else. That is a legitimate reason, and it is also exactly the kind of legitimate reason that quietly justifies a dangerous shape.
So the tool got audited the way an attacker would audit it, not the way an author would review it. The question was never "does saving a file work?" — it was "what is the worst thing a stranger can make this process do?"
POST that writes a file to disk — had no
authentication of any kind. Not a token, not a session, not an origin check.
A root-privileged arbitrary-file overwrite, sitting behind a plain HTTP endpoint.
POST to 127.0.0.1 — a classic
drive-by CSRF. The browser is inside the trust boundary; the network binding never was
the control.
The fix itself
The whole control is six lines. That is the point — the engineering was in choosing where it goes and what it defaults to, not in writing it.
The interesting decision is the absent-header case. Browsers always attach an
Origin to a cross-origin POST, so a missing one means the caller isn't a browser —
a script, a health check, a shell one-liner. Rejecting those would have broken every legitimate
non-browser caller in exchange for blocking an attack that cannot originate there. Security that
breaks the tool gets switched off within a week, and a control that is switched off protects
nothing.
Defence in depth around it
realpath before the allowed-roots comparison — so .. traversal and symlinks pointing outside are both refused by the same line, rather than by two separate rules that can drift apart.
.service unit, cron entry, or shell profile — only overwrite something that already exists inside the sandbox.
The traversal defences were verified the same way as the CSRF fix — by trying them. Requests for
/etc/passwd, a .. climb toward /etc/shadow, and a symlink
pointing out of the sandbox were all fired at the running server and all refused.
Radical simplicity, on purpose
The second thing worth saying about this tool is how little is in it. A live-updating multi-file editor with a streaming activity view is the sort of thing that usually arrives as a React app, a websocket server, a bundler config and four hundred transitive dependencies.
curl, and impossible to get into a half-open state you can't see.
Long-polling instead of websockets is the trade this project is happy to defend. A websocket buys latency the interface cannot perceive, and costs a reconnect state machine, a heartbeat, and a class of "connected but dead" bugs that are miserable to reproduce. A poll that fails just fails — visibly, on the next tick.
Scope & roadmap
It is an editor, not an IDE, and the difference is deliberate. The text surface is a plain textarea with a line-number gutter — no syntax highlighting, no language server, no integrated terminal. A full editor component was evaluated and dropped: shipping it would have meant adopting a bundler, which would have cost the one property the project is built around.
On the roadmap, honestly ranked: syntax highlighting without a build step, new-file creation (currently refused by design, since overwrite-only is a security property worth keeping until it's replaced deliberately), and find-and-replace. The tool is single-operator and loopback-bound by design — multi-user access would need real authentication, not an origin check, and that is a different product.