Architecture
Chrysalis is one program and a folder of files. The program serves a web page; everything you use inside that page is an app, and every app is plain files the agent can change.
#The parts
#Where your files live
Every account has its own folder under data/users/ in the Chrysalis home folder (chrysalis paths prints it):
data/
users/<name>/ your workspace, a git repository
apps/<app>/
manifest.json
index.html, src/ the app's page
plugins/<id>/ the app's server code
data/ what the app stores
node_modules/, dist/ installed packages and build output, not in git
settings.json active app, plugin approvals
persona.md
credentials/<name>/ outside the workspace
auth.json model keys
connections.json model connections
mcp.json MCP servers
app-upstream/ each app's install source and update baseline
sandbox.json the agent shell's internet switch
The split matters. The agent, its shell and every app can only reach the workspace, so keys, MCP servers and the files updates compare against are out of reach by where they are stored, not by a rule each tool has to remember.
#A request, start to finish
Here is what happens when you send a message in an app like Roleplay:
- The app's page calls
fetch("/v1/apps/roleplay/…"). The page runs in a frame that cannot make network requests, so a small bridge script in the frame hands the request to the shell. - The shell checks the path against the list of routes apps may use, then sends it with your session. The engine checks the same list again.
- The engine offers the request to the app's plugins. Each plugin's
handleRouteruns in the sandbox until one answers. - The plugin asks for a model reply and returns right away. The engine applies any changes other plugins in the app make to model requests, adds the tools they offer, and calls the provider with your key. The text streams to the page over the WebSocket as it arrives.
- When the reply is done, the engine runs the plugin again with the result. The plugin saves the chat to the app's
data/folder and answers the page. - The engine notices the changed file and tells every open copy of the app, so a second tab or your phone can refresh.
The plugin system covers steps 3 to 5 in detail.
#Why apps run in a frame
An app's page is served from /app/<name>/<app>/ into an iframe with the sandbox attribute and no same-origin access. That gives it an opaque origin: no cookies, no session, no view into the shell. Its content security policy sets connect-src 'none', so it cannot open connections of its own.
The bridge script that runs first in every app page replaces fetch, WebSocket, localStorage, sessionStorage and document.cookie with versions that ask the shell. To the app they behave like the real thing:
fetchreaches the app's own/v1/apps/<app>/…routes and a short shared list: reading the model list, generating images and speech, embeddings settings and shared assets.- A
WebSocketto/v1/wsreceives only this app's events. localStorageis kept in the shell's own browser storage, separately for each account and app.
Images and other media from outside websites cannot load directly. Apps load images through /v1/apps/<app>/img?url=…, which fetches only from hosts the app's plugins are allowed to reach, and serve other media through a plugin route. The full list of limits is on Security and privacy.
#Why the browser does the building
Two jobs that run untrusted code happen in your browser instead of on the machine running Chrysalis:
- Building apps. Bundling, Tailwind and any Tailwind plugin an app brings run in a sandboxed builder frame. The engine only reads the app's files for it and writes the output back. App packages are downloaded with their install scripts turned off, so nothing from npm runs as a program on your computer.
- The agent's shell. Commands run in a WebAssembly shell with Python, inside a sandboxed frame, on every platform. Changed files are synced back through the engine.
The trade-off: both need a Chrysalis tab open. With no page open, apps do not rebuild and the agent has no shell, though its file tools still work.
#What the engine is built on
| Part | Uses |
|---|---|
| Engine | Bun, a single executable per platform (a native library inside the Android app) |
| Plugin sandbox | QuickJS compiled to WebAssembly, in a worker thread |
| Builder | esbuild (WebAssembly), Babel's React Refresh transform, Tailwind v4 |
| Agent shell | A WebAssembly shell with bash-style commands and Python (Pyodide) |
| Workspace history and app installs | A git implementation in JavaScript, so no git program is needed |
| Shell and apps | React and Tailwind |