A Cloud Agent Kernel — Clients Reduced to a Browser
Code, services and agents all moved onto a cloud host, with laptop, tablet and phone reduced to a browser — one message from your phone dispatches work, ships code and accepts results; any screen can take over
- Role
- Architecture and implementation (solo)
- Scale
- One cloud kernel · four client types · ports closed
- Period
- Oct 2026 — Present
What I did
- Multi-device control: laptop (Desktop registry), tablet, phone (home-screen dashboard) and Feishu / WeChat (messages) share one cloud kernel and one session — switching devices migrates nothing
- Messages as commands: one line from a phone dispatches work, asks follow-ups or accepts results; the agent edits in the cloud and returns the diff and screenshots — development continues on a commute
- Single-writer control plane: one master owns state.db and the task board, the second node is active/passive failover — co-writing tears sessions and double-dispatches the same task
- Zero inbound ports — the gateway binds loopback, reached through a Tailscale tailnet with device ACLs and a public_url auth gate; social clients ride outbound long connections only
- A middle state on the way to AGI: the human keeps judgement, authorisation and acceptance while the agent executes and records — every step observable, interruptible and reversible
Shape: the kernel in the cloud, the client down to a browser
What I wanted was not another remote console but the whole chain moved up: kernel, code and execution environment on the cloud host, with the laptop and the phone reduced to a client that reads one web page.
- Kernel: hermes-agent v0.22.4,
hermes serve :9119exposing JSON-RPC + WebSocket, bound to127.0.0.1only, never public. - Always on:
hermes gateway installputs it under systemd so it restarts itself; one process serves every profile (multiplex_profiles), each profile a roster entry with its own memory and skills (coder / ops / research). - Clients: the laptop is Desktop’s remote gateway, in a registry that holds local, cloud and SSH connections side by side; the phone is the responsive dashboard, added to the home screen; social platforms are entrances only.
There is a deliberate trade here: hermes already ships the kernel, the messaging gateways, the dashboard and the multi-instance registry — most of the distance, roughly eighty percent. So I write no native console app and no control panel of my own. The saved time goes into the overall design and the failure paths, not into rebuilding what someone else already got right.
Multi-device control: change the screen, not the environment
Moving to the cloud was never about remote login. It is about the same working state continuing on any screen:
| Situation | Client | What it is for |
|---|---|---|
| At the office | Laptop · Desktop remote gateway | Heavy work: design docs, long runs, logs, diff review |
| Commuting / queueing | Phone · dashboard on the home screen | Check progress, approve one dangerous command, tell the agent to continue |
| Travelling / meetings | Tablet · the same URL | A second console: watch the task board during a call |
| No console at all | Feishu / WeChat · one message | “Make the login button bigger” — the diff and a screenshot come back; “looks good” closes it |
Three things have to hold at once: one session (not a repository copied to another machine and synced by hand), one permission model (a device outside the tailnet sees nothing), and one evidence trail (what changed, which checks ran, where the screenshots are — all in the session).
So “develop by messaging your phone” is not a slogan: the message is only the entrance. Execution happens inside the kernel on the cloud host, which has the code, can run the tests and leaves the record. The human keeps judgement and acceptance.
Why this matters: a middle state on the way to AGI
Model capability turns a page every few months, and today’s best workflow will likely be rewritten next year. So rather than chasing a fully automatic end state, I made the middle state solid:
- The human keeps three things: judgement (what to do), authorisation (confirming dangerous actions), acceptance (whether the result is good enough).
- The agent takes three things: execution, bookkeeping, and reporting back — every step observable, interruptible, takeable-over.
- The system guarantees two: any step can be rolled back, and a human can still rescue it when every model is down (see the bypass command layer below).
When models get stronger, the executor changes and the system does not. When a model is wrong, the damage stays inside one step. Until AGI arrives, a semi-automatic system that can be taken over reliably is worth more than an uncontrollable fully automatic one.
Access: zero inbound ports
Port 443 on the host stays closed. Every entrance is “a device already inside the tailnet”:
| Channel | How | Why |
|---|---|---|
| Tailscale tailnet | Device-level ACLs, tailscale serve for the HTTPS cert |
No exposed 443; access granted per device |
| Auth gate | public_url behind basic auth, even for the reverse-proxied loopback |
Anything reachable through a proxy gets checked again |
| Feishu / Lark | WebSocket long connection, no public callback | Approval-card buttons mean confirming dangerous commands on the phone |
| iLink bot identity, long-poll, results pushed only | Group messages often go undelivered — treat it as a notification channel | |
| Hermes Desktop | Connects straight to the cloud gateway | The desktop is still where heavy work happens |
Social platforms cap what you can do, and their rules are not mine: patching their clients is a ban risk. So the split is fixed — the console handles the heavy work, the social side handles the light work.
Control plane: exactly one writer
With a second node, the first question is not how to scale but who is allowed to write. The answer here is single-writer:
state.dbis SQLite: two nodes writing the same session means WAL contention and torn sessions — hermes refuses the second process outright.- The
kanban.dbdispatcher must have a single owner: two dispatchers spawn the same task twice. - Dangerous-command approval is in-process state: two masters each raise their own prompt, so one command gets approved twice, or approved on one side and rejected on the other.
So the second node is not another master; it is active/passive failover. Peers call each other over the API server on :8642 with API_SERVER_KEY granting one-way authority, and /v1/runs is idempotent, steerable and stoppable. Whoever dies, the other takes over — they never both work the same task.
The bypass command layer: no model in the loop
The fragile link is the model gateway: if it dies, the agent cannot think, and therefore cannot rescue itself. So the rescue was lifted out of the model and written as a script:
watchdog.shruns under cron in no-agent mode every five minutes, reads/api/status, and only prints when something is wrong; a healthy run is silent.- It costs zero tokens and its stdout goes straight to Feishu — it still arrives when no model is reachable.
- The self-heal whitelist has exactly two entries: restart the local service, export the cause of death. Cross-node, it notifies and never dispatches commands.
The backstop is backup: hermes backup snapshots to another site on a schedule, and a cold restore gets rehearsed before anyone talks about dual-active.
Providers and scheduling
Besides the kernel and the messaging gateways, the host also runs a fork of magpie (another project here: Magpie Plus), which manages the various agents and model providers in one place — account pools, balance expiry and routing behind a single entrance, so the kernel only knows one base_url.
The next step is to make scheduling part of the same system: hermes schedules and accepts, Claude Code writes the code, and sub-agents split the work to save tokens — the point being that every step of a long, multi-round task stays observable and interruptible.
Explicitly not doing
- A Flutter console app: web first, and only if it proves insufficient.
- Nodes trusting each other as mutual masters: write conflicts are not solved by trust.
- Dual-active writes to
state.db: it tears sessions. - Patching the WeChat / Feishu clients: a ban risk that buys nothing.
- Opening terminal or file writes to social clients: one mistap on a phone is an incident.
Still going
- Execution moving up too: code can still stay on my machine today (
terminal: ssh); the goal is to pull the terminal into containers on the cloud host, isolation first, until the client really is only a browser. - State aggregation: compress every node’s state into one card — who is running, who died, how many days of balance are left.
- Network hardening: tighten Tailscale ACLs down to ports, and swap the
public_urlgate for short-lived sessions. - Failover drills: turn backup-and-restore into a scripted run rather than a document read after the fact.