Chrysalis

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

The engineThe one program you run. It signs people in, keeps model connections and keys, answers every request over HTTP and one WebSocket, runs plugins, calls model providers and watches app files for changes.
The shellThe page you open in your browser: the launcher, tabs, settings and the agent's chat. Apps open inside it.
AppsFolders in your workspace. The web page of an app runs in a sandboxed frame inside the shell.
PluginsThe server side of an app, run by the engine in a WebAssembly sandbox with the permissions you approved.
The builderTurns an app's source code into the page you see, inside your browser, and pushes changes into open apps as you edit.
The agentA coding agent with tools for reading and editing files, building apps, reading their console and running a command shell in your browser.

#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:

  1. 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.
  2. 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.
  3. The engine offers the request to the app's plugins. Each plugin's handleRoute runs in the sandbox until one answers.
  4. 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.
  5. 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.
  6. 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:

  • fetch reaches 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 WebSocket to /v1/ws receives only this app's events.
  • localStorage is 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

PartUses
EngineBun, a single executable per platform (a native library inside the Android app)
Plugin sandboxQuickJS compiled to WebAssembly, in a worker thread
Builderesbuild (WebAssembly), Babel's React Refresh transform, Tailwind v4
Agent shellA WebAssembly shell with bash-style commands and Python (Pyodide)
Workspace history and app installsA git implementation in JavaScript, so no git program is needed
Shell and appsReact and Tailwind

Edit this page on GitHub