wacli.me alpha

Isolation by construction, not by filter

Almost every multi-tenant data leak has the same shape. One table holds everyone's rows, a column says who owns each one, and exactly one query forgets to mention that column.

-- the bug that keeps happening
SELECT * FROM messages WHERE chat_id = ?      -- whose messages?

Code review catches it most of the time. Most of the time is a strange standard to apply to somebody's private conversations.

Different databases, not different rows

So there is no shared messages table here. Every linked account gets its own directory with its own SQLite file, written by its own sync process. The gateway never queries a database directly: it runs the CLI with --store /path/for/this/account, and that path comes from the bearer token.

The consequence is that the dangerous query cannot be written. There is no row belonging to someone else within reach of the process handling your request — not filtered out, not present. A missing condition returns your own data twice, not a stranger's once.

One server object per request

The MCP session is stateless on purpose. Each request constructs a fresh server, registers tools bound to one store, answers, and is thrown away:

const server = buildServer(tenant);   // tools closed over this tenant's store
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, body);

Nothing survives between requests, so there is nothing to mix up. It costs a few milliseconds of object construction, which is invisible next to a call that shells out to a CLI. A restart cannot interrupt a session, because there is no session to interrupt.

The capability is the registration

The same idea governs write access. An account without it does not get a disabled send_message tool — it gets a server on which that tool was never registered. tools/list does not mention it, and calling it by name fails as an unknown method. There is no flag to misread at call time, because the decision was made when the server was built.

What this does not solve

It contains a mistake in request handling. It does nothing about the host itself: root on the machine reads every store, and no amount of per-tenant structure changes that. Full-disk encryption protects a stolen disk, not a live compromise. That is the honest limit of any hosted service, and it is exactly why the self-hosting path gets a real guide rather than a footnote.