# cherry π > A terminal desktop where agents ask first. Shells, vendor agents such as Claude Code and Codex, and Picache, cherry π's own agent, work side by side. A workspace is split into layers, each with its own folder, panes and conversation. Every call an agent makes into the workspace goes through one interface: allowed, asked or refused by policy, and recorded either way. ## Docs - [Field manual](manual.html): how the workspace works, in plain language, as of 2 November 2026 - [Field manual as Markdown](manual.md): the same text, with each diagram as its description - [Extensions](extensions.html): addons, .pi apps and Cellar Door worlds - [Extensions as Markdown](extensions.md): the same text, with each diagram as its description - [What if…](whatif.html): technical questions about failure and attack, answered with what is built - [What if… as Markdown](whatif.md): the same text - [Everything in one file](llms-full.txt): this index followed by the manual, the Extensions page and What if --- # Field manual How cherry π works, section by section. The diagrams illustrate the ideas behind the screens. ## 00 / Quick start These four steps take you from a project folder to work you can check and undo. 1. **Open a workspace.** Run `cherrypi` in a project folder. The launch screen shows where the data folder will be before anything is made ([Open a workspace](manual.html#opening)). 2. **Ask Picache.** Picache is the agent you talk to in a Chat. Type in the prompt band above the status line and press Enter. Alt+Enter steers a turn that is already running, and Esc stops it ([Send, steer or stop](manual.html#steer)). 3. **Launch a vendor agent.** Pick one in the rail, such as Claude Code or Codex. It starts in a terminal pane, inside a box that limits what it can reach ([Launch a vendor agent](manual.html#launch-agent)). 4. **Check and undo.** Every Picache turn takes a snapshot of the folder before it starts and after it ends, so you can see what it changed and put the folder back ([Undo a turn's changes](manual.html#undo)). ``` picache snapshots changes turn: # what a turn changed picache layer restore turn::start # the folder as that turn found it ``` When the status row shows `N NEEDS YOU`, something is waiting for your answer ([When something needs you](manual.html#needs-you)). ## 01 / The workspace A workspace is a folder that cherry π manages, with its work split into layers. ``` cd ~/src/api cherrypi # open the workspace here, or make one ``` The program has two halves. The **harness**, `cherrypi.ape`, is a Lua runtime built on redbean. It decides what is allowed and keeps every record. The **desktop** is a Go terminal program that draws panes. The harness keeps running when you quit the desktop, and the next launch reconnects to it. Each project **layer** is bound to a subfolder, with its own Picache conversation and its own panes. Layer Zero is a scratchpad. A layer can also be a git worktree, and removing it never deletes its branch. Panes hold shells, vendor agents, files, editors, runs, models and more. You can split, stack, zoom or move them. ### What runs where Four kinds of process make up cherry π. - The desktop is the Go program you launch. It draws panes and makes no decisions. - There is one harness per workspace. It decides what is allowed, keeps the records and runs the work. Desktops attach to it on `run/harness.sock`. - Each terminal has a **pump**. The pump holds the terminal and its recent output, so a shell or an agent keeps running if the desktop or the harness goes away. - **Sidecars** run beside the harness. The signer holds the keys, the P2P sidecar talks to the network, and model servers run as jobs. > **Diagram: What runs where.** Desktops, this window, another window on the same workspace, or a paired machine's desktop over P2P, connect to one harness per workspace through run/harness.sock. The harness, cherrypi.ape, holds policy, grants and budgets, runs turns and Picache, keeps the stores, receipts and chains, runs jobs, Conduit and the P2P core, and is the one door for capsules. Each terminal is held by a pump of its own; a program in a pane reaches the harness through picache.sock with its pane's token. Sidecars: the signer holds the keys, the P2P sidecar runs on Bare in a box of its own, and llama-server runs as a job. Capsules, app handlers, scripts, clean rooms and Run one, start through the one door. A desktop quitting leaves the harness and pumps running; a new build replaces the harness and the pumps stay; a reboot ends everything. > > What runs where, and what outlives what. A program in a pane talks to the harness with the `picache` command ([section 04](manual.html#picache)), over `picache.sock`. Only you can read that socket. Every call carries its pane's token, which names who is calling ([section 03](manual.html#callers)). Anything confined, such as an app's handler, a script or a clean room, starts as a capsule through one door in the harness ([section 09](manual.html#capsules)). ### One file `cherrypi.ape` is one file that carries everything it needs for macOS on Apple silicon and Linux on x64 and arm64, including the Raspberry Pi. When you run it at a terminal, it starts this machine's desktop. The desktop is inside the file too. It is built natively for macOS and statically linked for Linux, and it draws with the Ghostty terminal library. On Linux, nothing the file carries needs a system loader, so it runs on NixOS. The file is in the Actually Portable Executable (APE) format, and on a Linux machine with no APE loader it writes its own. The same commit builds the same bytes from any checkout. > **Diagram: One file carries it.** cherrypi.ape is one file of about 150 MB: the Lua runtime on a pinned redbean base, Bare for each platform, the signer and P2P bundles, printimage and ptv, scope and pasta for Linux, the docs Picache reads, and the Go desktop for each platform on Ghostty's terminal library, with a manifest of every file that may come out. On first use, this machine's share comes out, checked by sha256, atomically and never through a link: macOS on Apple silicon, Linux on x64, or Linux on arm64 such as the Raspberry Pi. The same commit gives the same bytes from any checkout. > > The hermetic runtime: what it carries, and what each machine takes out of it. ### Open a workspace Run `cherrypi` in a folder. It looks upward for a `.cherrypi/`, the way git looks for `.git`, and stops below your home folder, so the home workspace, `~/.cherrypi`, never swallows every project under it. The launch screen offers the workspace in this folder or above it, a new workspace here, another folder, the home workspace and recent workspaces. Each choice shows where its data folder will be before anything is created. Then you pick the latest conversation, another one, or a new one. Flags let you skip these questions: ``` cherrypi -state ~/src/api/.cherrypi # this data folder cherrypi -session # this conversation cherrypi -private # a harness of its own, ended with the desktop cherrypi -remote studio:web # a paired machine's workspace cherrypi -remote studio:web:L1 # ... one layer of it cherrypi -remote studio:web:terminal: # ... one terminal cherrypi -stop-harness # stop this workspace's harness ``` Inside cherry π, Open does what `-remote` does, and shows the result in a pane ([section 08](manual.html#network)). Each data folder carries a marker that says it belongs to cherry π. When it picks a data folder, cherry π only takes one that is empty, already marked, named `.cherrypi` or already holding its database. It refuses any other folder that holds files, and a data folder or marker that is a symbolic link. - In a git work tree, it offers to add the folder to `.git/info/exclude`, which is never committed. - Layer folders are stored relative to the workspace, so you can move the whole tree. - You're warned about synced folders such as iCloud Drive and Dropbox, because SQLite is fragile there. Deleting a project folder deletes its history too, because the data folder is inside it. Settings warns you about this, and cherry π itself never deletes a data folder. ### The data folder A workspace keeps everything it knows in its data folder. What belongs to you or to the machine lives in the home data folder, `~/.cherrypi`. Your project's own files hold only what you put there, such as a saved layout or an agent you declared for a folder. ``` project/.cherrypi/ .cherrypi-workspace-v1 the ownership marker cherrypi.db the workspace: desktop, grants, settings, tasks, offers, its evidence chain layers/.db one store per layer: its conversations, runs, receipts and its own chain scratchpad/ Layer Zero's folder trash/ what Files deleted run/harness.sock where desktops attach run/tty/ one socket per terminal pump picache.sock where picache calls arrive harness.log what the harness said apps/ exports/ logs/ backup-…/ ~/.cherrypi/ the home data folder, and the home workspace identity.db who this machine is; the keys stay in the signer p2p/p2p.db pairs, contracts, the fleet p2p/rooms.db rooms and their messages ai/models.json models, referenced where they are preferences.json theme, glyphs, keys: yours, in every workspace workspaces.json recent workspaces, for the launch screen floats.json where your floats were ``` ### Layers Each layer has an id that survives renames and moves, and a SQLite store of its own. The store holds the layer's conversations, runs and receipts, and its own evidence chain, which is anchored in the workspace's chain ([section 10](manual.html#records)). Because a layer's history is in its own file, a layer can leave the workspace whole. It can be exported, shared, forked or kept in step with another machine. ### Export and import a layer ``` picache layer export L1 --out api-layer.db # history and folder picache layer export L1 --history-only # history alone picache layer export L1 --git # include .git picache layer import api-layer.db # as a working layer picache layer import api-layer.db --as reference ``` An export is one SQLite file, signed by this machine. It holds the layer's history, its evidence chain and its folder: files of any size, empty folders, symbolic links that stay inside it, modes and modified times. Files are split into chunks of up to 4 MiB and stored by SHA-256 hash, so a chunk that many files share is stored once. The signature covers the hashes, and every chunk is checked as it's read, so an import refuses a chunk that has been changed. > **Diagram: A layer leaves as one signed file.** A layer's folder and its store go into one SQLite file signed by this machine. Files are cut into chunks of at most 4 MiB, stored by SHA-256, so a chunk two files share is stored once, and the signature covers the hashes. The file can be imported as a working layer or as a read-only reference that you can attach as a brain, shared with a paired machine inline up to 10 MiB and by your Drive past it, or forked into a new layer beside the original. Measured on a Mac: 2 GiB plus 2,000 small files exported in 25 seconds, verified in 9 and imported in 23, with peak memory about 106 MB. > > A layer's export: its history and its whole folder in one signed file. On a Mac, exporting 2 GiB plus 2,000 small files took 25 seconds. Verifying the export took 9 seconds and importing it took 23, and memory use peaked at about 106 MB. An export leaves out `.git` unless you pass `--git`. It also leaves out folders that can be rebuilt, such as `node_modules`, `.venv` and `target`. You can set your own list with `layer.export_excludes`. The archive lists everything it left out. An import as a working layer writes the files to a new folder and opens the conversation it had in front. An import as a reference is read-only. Importing an archive signed by a machine you haven't paired asks you first. Grants ([section 03](manual.html#grants)) never travel. They stay in the workspace that gave them. ### Share or fork a layer ``` picache layer share L1 studio # offer it to another workspace or machine picache layer fork L1 # a copy beside it, under new ids ``` When you share with a paired machine, up to 10 MiB travels with the offer. Anything larger goes through your own Drive, which needs no cloud and no account. Each chunk goes to the Drive once, and the receiver fetches only the chunks it doesn't already hold. The share command says how many chunks were new. If an accept stops part way, it picks up where it stopped. A fork copies the folder, history and conversations. Nothing the fork does reaches the original, and both evidence chains record the fork. You can also attach any layer or reference to the layer in front as a **brain**. Picache and agents can search a brain, and nothing writes to it. ### Undo a turn's changes Every Picache turn takes a snapshot of the layer's folder before it starts and after it ends. ``` picache snapshots # list them picache snapshots changes turn: # what a turn added, removed, changed picache layer restore turn::start # the folder as that turn found it ``` Snapshots are stored as hashed chunks inside the layer's store, so a turn that touched one file costs the space of one file. A restore takes a snapshot first, so you can undo the restore. It also tells you whether the result is exact. Snapshots never touch `.git` or rebuildable folders. Files over 32 MiB are listed by name, but their contents are not kept. When you export or fork the layer, its snapshots go too. On cherry π's own source repository (1,296 files, 421 MB), the first snapshot took 2.65 seconds and the next one 0.02 seconds. | Setting | What it does | |---|---| | `layer.snapshots` | Turns snapshots on or off | | `layer.snapshot_max_file` | The largest file kept | | `layer.snapshot_keep` | How many to keep (1000 by default) | ### Share a layer live A live layer stays the same on several machines. Each member's edits and conversations reach the others within a few seconds. ``` picache layer live L1 studio # offer L1, live picache layer live L1 # list the members picache layer live L1 --remove laptop # owner only picache layer live L1 --leave # keep your copy, alone ``` Only the owner can offer a live layer. Whoever accepts it as a working layer becomes a member. The owner's machine adds you when your accept reaches it, and from then on you can write. Every two seconds, each member runs a round: - It snapshots its folder and sends the files that changed. - It writes the other members' changes into its folder, even in the middle of a turn. - A file you changed since the snapshot is left alone and goes out in the next round. Several members can edit one text file at once, and everyone keeps every change ([Edit a file together](manual.html#live-edit)). Files travel as hashed chunks, and a chunk you already hold isn't fetched again. The rest come from the machine that wrote them, or from its Drive when that machine is offline. Copying to the Drive never holds up a round. Sometimes a file can't be written, because its chunks can't be fetched or because it's a folder on one machine and a file on another. The file then waits, and you're told once. Every round tries it again. When a member's turn ends, the next round sends it, and it shows up in your copy as a read-only conversation. Message bodies over 1 MiB are cut. These never travel: - grants, and the layer's panes - `.git` and the folders in `layer.export_excludes`, such as `node_modules` - files over 32 MiB (`layer.snapshot_max_file`) Live layers pick up where they left off when P2P starts. The owner can remove members but can't leave. A member who leaves keeps the folder as a layer of their own. > **Diagram: A live layer, one round.** Every two seconds, each member runs a round. It snapshots its folder, sends what changed since the last round to the layer's log, and writes what the other members sent, unless it changed that file too. The log runs in each machine's P2P sidecar. It holds text edits, whole files, deletes and finished turns. Every text edit is kept on every member; for other files, the later write wins everywhere. Chunks come from the member's own store, then the writer's machine, then the writer's Drive, each checked by its hash. Grants, desktops, .git, rebuildable folders and files over 32 MiB never travel. Only the owner offers the layer, and a member writes once the owner's machine has added it. > > A live layer: each round, text edits and whole files go out to one log and come back in, the same on every member. ### Edit a file together When members of a live layer edit one text file at once, every member ends up with every change. That holds for changes to different lines or different words, and even for one word changed two ways, where both new words stay, one after the other. > **Diagram: Two members edit one line at once.** You and studio both start from the line "the cat sat on the mat". You change cat to dog; studio changes mat to rug, before either sees the other's save. Each save goes to the layer's log as edits, not as the file: delete these characters, insert this text after that character. The edits name characters, not positions, so both fit. Every member ends with "the dog sat on the rug", whatever order the edits arrive in. When both change the same word, both new words stay, one after the other. Binary files, files over 1 MiB and paths in the layer's .lww file are replaced whole instead, and the later save wins. > > Two saves at once: each goes out as edits that name characters, so both fit, on every member. Each save goes out as a set of edits, not as a new copy of the file. The file is kept as a CRDT, a structure built so that every copy agrees whatever order the edits arrive in. Nothing is merged after the fact, and no change is lost. If your editor saves a copy from before others' changes arrived, their changes are kept. If you delete exactly the text another member just added, it can come back once. If it does, delete it again. > **Diagram: A save from an old copy.** Your editor opens plan.txt with three lines. Studio changes the second line, and the change is written into your file, but your editor still holds the old copy. You change the third line and save. Cherry-pi compares the save with the file's recent versions. Against the newest, the save would also undo studio's line. Against the copy your editor opened, it is one change, yours, so only that goes out. Every member ends with both lines changed. If you delete exactly the text another member just added, it can come back once. > > A save from an editor that held an old copy: only your change goes out, so studio's stays. Edits from a save go out in the next round, every two seconds, not as you type. If two members create the same new text file at once, both texts are kept in it, and one of the two members is told. Other files are replaced whole. Every member ends up with the version that comes last in the layer's shared log. This applies to: - files that aren't text: not UTF-8, containing a NUL byte, or over 1 MiB - paths that match the layer's `/.lww` file, which takes gitignore patterns. List logs and generated files there. If your change to such a file is replaced, you're told, and the notice gives the command that shows your version, such as `picache snapshots show 14 logo.png`. A change made by someone who had already seen yours loses nothing, so it arrives without a notice. For any file, you're also told when a delete removes your change or a change undoes your delete. ### Arrange the desk The layer in front is a tree of tiles. Any pane can also **float** over it, like a window. ``` picache split --right --label tests -- make test # a new tile picache float --cascade # pile up the floats picache new-layer ~/src/site --label site # a new layer ``` Actions → Float this pane lifts a pane out, and the tiles close up. Drag a float's title to move it and its edges to resize it. Programs such as lazygit, btop or htop open as floats, with a splash screen until they draw. Floats stack. If you drop one on another's title, or use Actions → Cascade the floats, they pile up with every title showing. A float buried under others still shows its agent's badge. A buried terminal's title ends with its last line of output, so you can read a cascade of monitors without raising any of them. - ◌ in a title makes a float see-through. - To dock a float, drag its title and hold `Space`. Drop it on a pane to join it as a tab, or on an edge to make a new tile. - After a restart, floats come back where you left them and run their programs again. > **Diagram: The desk: a rail, a tree of tiles, floats over it.** On the left, the rail is a tree of the layers and their panes, the conversations and the machines; F3 hides it. The layer in front is a tree of tiles: a terminal, an Actions pane acting on that terminal, and a chat where Picache asks before it changes the desk. Over the tiles sits a cascade of floats: two buried terminals show only their titles, each ending with the last line it printed, and the front float, lazygit, has the keyboard. > > A layer in front, with a cascade of floats over its tiles. Scripts can drive layers with `picache new-layer`, `close-layer`, `focus-layer`, `label-layer` and `move-layer`, and they receive layer events. The Actions list can stay open as a pane (Open Actions as a pane). Each action then applies to the pane you were in before, and the header names that pane. ### Save a layout ``` picache save-layout dev # or Actions → Save this layer as a layout picache apply-layout dev --right # beside the current pane picache apply-layout dev --new-layer docs # as a new layer ``` A layout goes into the layer's folder as `.cherrypi/layouts/dev.json`, and you can undo the save. It keeps every kind of pane, with each terminal's command and folder. When you apply a layout, a terminal that's still running is reattached instead of started twice, unless `layouts.live_panes` is `fresh`. An agent's pane resumes its own conversation, using the conversation id the agent's hooks reported (Claude Code, Codex, Pi). > **Diagram: A layout, saved and applied.** A layer named dev, with a shell, an editor and an art pane, is saved with save-layout as .cherrypi/layouts/dev.json in its folder, with an undo. The file keeps every kind of pane and each terminal's command and folder. It is applied either beside the focused pane, splitting it and closing nothing, or as a new layer on a folder you choose. A terminal still running is reattached by default, or started fresh when the setting layouts.live_panes says so. > > A layout opens beside a pane or as a new layer. ### Terminals keep running Each terminal runs in its own small process, its pump. A shell or an agent keeps running when you quit the desktop, when a new build replaces the harness, or when you reload the desktop (Actions → Reload cherry-pi). It comes back in its pane with its recent output. Closing a pane ends its program, and Actions → Stop all terminals ends them all. A reboot ends them too. ### Use sudo Your own shell can sudo, and sudo asks for your password in the pane. An agent's pane, and any pane an agent opens, can't use sudo. | Setting | Default | What it does | |---|---|---| | `terminal.sudo.shells` | on | Your shells may sudo | | `terminal.sudo.agents` | off | Agents' panes may sudo | | `terminal.sudo.forget` | on | sudo forgets your password before an agent or Picache types | | `terminal.sudo.unboxed_agents` | password | macOS: an agent outside its box needs your password at sudo, or (`refuse`) is refused | A change to these settings applies to panes opened after it. On Linux, cherry π can never gain privilege. redbean sets no-new-privileges on itself, and everything it starts inherits the flag. So a pane that may sudo is started by your systemd user session instead, on a terminal that cherry π opened for it. The pane's environment is passed in a file only you can read, never on a command line. Even then, cherry π holds that pane, its record and its output. Without a systemd user session, no pane can sudo. macOS has no such flag, so there an agent's box itself refuses sudo, su, login and the administrator prompt's tools. > **Diagram: Who starts a pane's program.** On Linux, cherry π's runtime has no-new-privileges, and it holds every pane: its terminal, its relay, its record and its token. For your own shell's pane it asks your systemd user session, which is not the runtime's child, to start the shell on that terminal; the shell has no flag, and sudo asks for your password there. While terminal.sudo.agents is off, the default, an agent's pane is started by the runtime itself and inherits the flag, so sudo is refused. On macOS there is no flag: an agent's box is a Seatbelt profile that refuses sudo, su and login, and for an agent outside a box the setting terminal.sudo.unboxed_agents decides. Before a send or send-key that is not yours, an agent's or Picache's, types into any pane, and after you approve it, sudo is told to forget your password. > > Who starts a pane's program, and what stands between an agent and root. | Pane | macOS | Linux | |---|---|---| | Your own shell | sudo, asking for your password | sudo, through your user session | | An agent in its box | refused by Seatbelt | never: bubblewrap sets no-new-privileges itself | | An agent outside a box | `terminal.sudo.unboxed_agents`: `password` (default) or `refuse` | refused: the pane keeps no-new-privileges | On macOS, an agent's box is a Seatbelt profile. Only a Seatbelt profile of its own could refuse sudo to an agent outside a box, and Seatbelt boxes don't nest. An agent that brings its own sandbox, as Claude Code does, could then not start that sandbox. So the setting for an agent outside a box, `terminal.sudo.unboxed_agents`, is your choice. sudo remembers your password for a few minutes. So before an agent or Picache sends text or keys to any pane, cherry π tells sudo to forget it, and sudo asks you again next time. If sudo can't be told, the typing is refused. Typing from another of your machines needs the As me grant ([section 08](manual.html#remote-actor)), so that it counts as yours. None of this covers an agent CLI you start by hand in your own shell. It can do what that shell can, including use a password you typed a minute ago. ### Open several desktops If you open the same workspace from a second terminal window, both desktops attach to one harness. The status row shows `CONNECTED · 2 desktops`. | Shared | Each desktop's own | |---|---| | Terminals: output shows on both, and either can type | The layer in front | | Each layer's layout | Focus | | The Picache conversation, and any turn so far | Zoom | A question Picache asks appears on every desktop, and the first answer counts. The desktop you used last plays notifications. If two desktops change a layout at the same moment, the later save wins. A paired machine's desktop can attach too, over P2P: to the whole workspace, to one layer or to one terminal. It acts as that machine, not as you, and holds only what you grant it ([section 08](manual.html#remote-actor)). Another machine's desktop opens here in a pane of its own, framed in the remote colour and marked ◎. ## 02 / The desktop The desktop draws your work as a tree. Each part of the tree is a single pane, a stack of panes shown as tabs, or a split. Panes tile exactly, and a terminal resizes with its pane. A pane's kind decides how it draws and which keys it takes. Floats are panes that sit over the tree like windows ([section 01](manual.html#arranging)). ### Kinds of pane | Pane | What it is for | |---|---| | Terminal | A shell, a vendor agent or any other program, on libghostty and held by its pump ([section 01](manual.html#terminals)) | | Chat with Picache | A conversation with Picache, with its input at the foot. Several can be open at once. | | Editor, Viewer | A file, read through the harness. Markdown is rendered, code is coloured, and a save is checked against the bytes it read. | | Files | A folder as a tree beside your work. You can open files beside it, copy, move, trash or archive them, and see each file's git change. | | Runs | What ran in this workspace, when it waited on you, and how it ended | | Inbox | Issues that you and the agents write in, and the offers, asks and tasks between layers, for you to accept or reject | | Goals | Goals given to vendor agents, with each one's steps, Picache's review and the whole diff, for you to accept or reject | | Models | What this machine can run, as measured, and each seat's model and whether it's loaded | | Keys | Your API keys, sealed on this machine and shown by their last four characters | | Downloads, Tools, Media | Durable jobs: downloads with exact bytes and checksums, the managed media tools, and imports and conversions | | Art | Clips, pictures, and video played by ptv | | Machines, Capsules | Your other machines, with pairing and shares, and every capsule, here or on another machine, each with Stop | | Rooms, The Bakery | People on paired machines, and public rooms you join by name | | Extensions | Installed `.pi` apps, what they ask for, and the panes and programs they add | | Actions, Errors, Settings | Actions lists every action and can stay open. Errors lists what failed or was refused. Settings holds every setting, on seven pages. | When a new pane asks what it should be, you can pick any of these kinds. You can also save it in a layout and find it by name with Go to ([Find anything](manual.html#find)). ### Mouse and keys Everything you can do has something to click, and the keys are shortcuts. A pane's header is a small toolbar with split, zoom, resize, move and close. When space runs short, the least important controls are removed from the header, so nothing you can't see is still clickable. - Drag a border to resize. - Drag a title onto another pane to swap or dock. - Drag across any text to copy it. - Click outside a dialog to close it. - Press `F6` to hand the pointer back to your terminal, so it can make its own selection. | Key | Does | |---|---| | `F1`, `Ctrl+P` | Actions: every action, filterable | | `F3` | Hide or show the left rail | | `F5` | Move the keyboard between a terminal and the prompt | | `F8` | Zoom the focused pane or float, and back | | `F10` | Show, hide or focus the floats | | `Ctrl+Alt+P` | Go to: layers, panes, files, conversations, receipts, runs | | `Ctrl+Alt+A` | The next agent that needs you | | `Ctrl+Alt+T`, `Ctrl+Alt+E` | A Chat on this conversation, or a new conversation | | `Ctrl+Alt+R` | The Inbox of the layer in front | | `F4`, `Ctrl+Alt+F` | The layer's Files pane, or a new one beside | | `Ctrl+Alt+X` | Pick up the focused pane and move it with the arrows | | `Ctrl+Alt+S` | Send to: choose where the prompt goes | | `Alt+Enter`, `Esc` | Steer Picache's running turn, or stop it | ### The rail The rail is the column on the left (`F3` hides it). At its top is the layer's banner, then a tree of where you are, then AGENTS, the agents you can launch, and NEEDS YOU, everything that waits on you. Each layer has its own banner. Under the picture are four buttons. The first shows how the picture is drawn (`▣` pixels, `▀▄` half blocks, `#` ASCII, `⣿` braille) and steps to the next. `☰` chooses the banner: the theme's mark, one of cherry π's logos, or a picture of yours (png, jpg, gif or webp, up to 8 MiB). `‹` and `›` step through the logos. The choice is kept in the layer's own store, so it goes with an export or a fork. While Settings is open, the rail shows the banner of the layer you opened it from. The tree has three sections: - **Layers**: each layer holds its panes as its tiles hold them (a stack's tabs, its floats, and a pane's own tabs), an Agents branch with the agents launched in it and the goals given in it, and the surfaces over it. Open Settings is a row of its own, `⚙ Settings`, because it belongs to no layer. - **Sessions**: the conversation you are in and the recent ones, each saying where it is open. - **Machines**: this machine with its workspaces, each with its harness, then your paired machines and what each shares with you. Click a row to go there. `Tab` from the prompt gives the tree the keys: the arrows move and open, `Enter` does what a click does, `/` filters it, and `Esc` gives the keys back. What you open and close is kept for the next start. Each row says what is going on in it, in the shapes the agent badges use ([Agent status](manual.html#agent-states)): | Mark | Means | |---|---| | `!` | Waits for you. An agent's or a terminal's own row says on what, such as `! Bash: rm -rf build`. | | `⋈` | A machine asks you for a share, or asks to pair. A machine asking to pair is listed until you answer. | | `✗` | The program reported a failure. | | `●` | It finished while you were looking elsewhere. | | `◔` | Working, with the program's own progress (`▰▰▱▱▱ 40%`) when it reports one. A conversation works while one of its turns is under way. | A plain terminal shows these too when its program reports its state over OSC 7501. A closed row carries the most urgent mark of everything under it, with how many wait when there is more than one: `!2`, counted as NEEDS YOU counts them. So a closed layer, workspace or section never hides what needs you. `rail.motion` (Settings, Desktop, *The tree's activity*) sets how the marks move. With `full`, the default, a working row's `◔` turns, and a row lights up for a moment when news arrives while you look elsewhere. `calm` holds the marks still, and `off` draws none. The rail ticks only while something on it moves. ### Manage files The Files pane shows a folder as a tree and opens files in a Viewer pane beside it. Its menu row and its File list hold the same actions. Actions that don't apply are dimmed and show the reason. Every change (copy, move, rename, trash, archive, unpack, chmod) goes to the harness, which checks it, keeps it inside the folder and writes a receipt ([section 03](manual.html#mediated)). The desktop can't write your files itself. Anything you trash can be restored. - A paste of many files is saved as a plan before the first one moves. If the paste is interrupted, Operations… shows it, and Finish completes the rest. - In a git checkout, the header names the branch, and each changed row shows its state. Show change opens the diff beside it. - If a file changed after you opened it, the save is refused and your edit is kept. ### Find anything Press `Ctrl+Alt+P` for Go to. It searches as you type, and every word you type must match. It finds layers and panes by name, agent, kind, file or title. Through the harness it also finds file names and text in every layer's folder, saved conversations, messages, receipts and recorded runs. Text indexing has published limits, and the workspace's own data is never indexed. The harness refuses to search the whole disk (`/`). You set search excludes and the cache budget on the Search page of Settings. ### When something needs you The status row shows `N NEEDS YOU` while the harness waits on you. It may be waiting for you to grant a capability that something asked for ([Grants](manual.html#grants)), or for you to deal with work a crash left behind ([section 10](manual.html#records)). Failures and refusals are counted in red on the status line until you see them. While cherry π runs, the Errors pane lists the last 500. The harness also keeps its own refusals as receipts, so Go to finds them. A turn stops if Picache asks you something and nobody answers within 10 minutes, if no desktop is open, or if cherry π restarts. The question then waits in Recovery, marked *asks you*. When you Approve or Decline it there, the turn goes on from that call. The call is never made twice. The setting `picache.approval.unanswered` chooses what happens to an unanswered question. It can wait (the default, stored as `park`), or refuse the call and go on without it. For agents that need you, see [section 05](manual.html#agent-states). ### The prompt and the Chat You write a governed turn ([How a turn runs](manual.html#turn)) in the prompt band above the status line. The band is always one row high. For a long prompt, a chip opens a bigger window (`Ctrl+Alt+L`). The band names where the turn goes, and Send to changes that without moving you. Each layer has its own Picache conversation, so switching layers switches the Chat. A Chat pane follows the newest reply until you scroll up. A reply can include a form (A2UI) that you fill in with the pointer or the keys. Its button sends your choices as your next message and does nothing else. ### Pictures, video and icons Terminals that draw pictures with the Kitty graphics protocol, such as Ghostty, kitty and WezTerm, show an image as a real picture, fitted to its pane. In other terminals, printimage draws it as text. Video plays in an Art pane through ptv, as text with sound. Converting a video for ptv runs as a job. Both programs ship inside the APE, `cherrypi.ape`. Icons need no patched font. Where the terminal supports the Glyph Protocol, cherry π registers its own shapes with it. Elsewhere it uses plain shapes. There are seven themes, each a palette of named roles. Terminal Mono uses no colour at all, and nothing relies on colour alone. ## 03 / What gets governed The harness governs three things call by call: Picache's tools, file operations, and every `picache` call. A vendor agent such as Claude Code is different. It runs in a box that limits what it can reach ([section 09](manual.html#capsules)), but what it does inside the box is up to it. Only what it asks of the harness is governed. ### How a turn runs > **Diagram: A governed turn.** Six steps. 1, record: what was asked becomes a durable task row before anything runs. 2, gate: what it means, one intent from a closed set, observe, mutate or converse. 3, commission: the allowed capabilities are frozen and checked again when used. 4, execute: only what was frozen runs, its effect saved before the call. 5, answer: the model, with tools, for up to 8 steps, each tool call a verb the policy decides, so you may be asked. 6, receipt: one entry in the hash chain binds every receipt of the turn, refusals included. A model may choose an intent, prose or a tool call; it never chooses a capability, a grant or a receipt. A steer and a stop land between steps, and a second prompt waits its turn. > > A governed turn, in the order the harness runs it. Each step is separate code, and the steps run in this order. 1. The request becomes a durable task before anything runs. 2. The gate maps the request to one intent from a closed set (observe, mutate or converse), using keywords or a model. It discards any answer outside the set, so a model can pick an intent but never its own capabilities. 3. The commission freezes the capabilities that intent allows. Each one is checked again when it is used. 4. Execution records the intended effect before each call, so a crash can be told apart from a finished call ([section 10](manual.html#records)). 5. Only then does the model write its answer. What the turn found is labelled as data, so the model does not take it as instructions. Picache answers with tools, in up to eight steps. Each tool call is one `picache` verb, decided by the same policy as when you type it. You're asked whenever the policy says so. Without a model configured, the answer is a fixed template. Any model failure falls back to it, so a broken model never fails a turn. ### Send, steer or stop One turn runs at a time in a conversation, whichever desktop sent it. | During a turn | What happens | |---|---| | Send a message | It waits as the next turn | | `Alt+Enter` | It steers the running turn, read before its next step | | `Esc` | The turn stops at its next step | Waiting messages are kept in the workspace, so the queue survives a harness restart or a new build. A steer whose turn died runs first, and then the waiting messages run in order. A call that changes something is written on the turn's task before it runs. That includes calls you approved and calls a script makes. If a crash cuts such a call off, it stays on record, marked as [in doubt](manual.html#in-doubt). Reads aren't written this way, because running a read again changes nothing. The turn ends with one entry in the hash chain, and that entry binds every receipt the turn wrote ([section 10](manual.html#records)). ### Effect classes Every `picache` verb declares what it does to the world, as one effect class. The policy decides by that class and ignores the verb's name. | Class | Means | For example | |---|---|---| | read | Changes nothing | `ls`, `cat`, `grep`, `search`, `panes`, `layout`, `events`, `wait`, `history` | | record | Writes cherry π's own records and settings | `offer`, `accept`, `ask`, `task`, `pair`, `link`, `box`, `app`, `loop` | | reversible | Changes a file and keeps the way back | `write`, `patch`, `undo`, `save-layout` | | arrange | Changes only your desk | `float`, `focus`, `move`, `resize`, `swap`, `zoom`, `label`, and their layer forms | | irreversible | Cannot be taken back | `run`, `send`, `send-key`, `split`, `close`, `apply-layout`, `new-layer`, `close-layer` | ### File operations Reading, writing, renaming, moving, deleting, extracting and running all go through the harness. Each change passes three checks, in this order: 1. A capability check. 2. The path jail, which keeps the path inside its root. 3. One receipt, saved in the same transaction as the change. > **Diagram: A change to a file.** The Files pane, picache write and patch, Picache's patch tool and an accepted offer all go the same way. 1, authority: a capability such as drive.write, refused with a capability code or asked of you. 2, the jail: the path must stay inside its root, walked by descriptor, with no symlinks and no parent steps, or it is refused as path_escape. 3, evidence: one receipt in the same transaction, committed, refused or failed. A refusal is a receipt too, and the order says whose fix it is: a missing grant is yours to give; a path that escapes is refused whoever asks. > > Three checks, in this order, for every change to a file. Because the checks run in this order, a refusal's code tells you which one failed. `needs_confirmation` or `needs_operator` means a grant is missing, and you can give it. `path_escape` means the path left its root, and no grant fixes that ([When a call is refused](manual.html#troubleshooting)). Every operation opens its root, walks each part of the path without following links, and acts on the file where the walk ended, so nothing can be swapped in between the check and the act. - A copy keeps the source's mode and streams in 64 KiB blocks. - A save keeps the file's mode and is flushed to disk before and after its rename. It's refused as `stale_write` if the file changed since it was read. Every change ends as `committed`, `refused` or `failed`. Refusals are recorded too, so you can find out whether anyone tried to delete a file. ### Grants When something lacks a capability, you're asked, and you can give one of three answers: - *Just this once* - *Until cherry π closes* - *Always in this workspace* Grants are stored in the workspace's own database. If you refuse Picache, that kind of call stays refused for the rest of the turn. Changing your files is a capability that belongs to you, the operator. Only you grant it, from the desktop, and no extension or app can hold it. Grants never travel. An imported layer, or an app installed from elsewhere, has to be granted again here. ### Vendor agents are observed A vendor agent in a pane runs its own harness, with its own permissions and model. For each one, cherry π starts it in a box, watches its state, records its run and tells you when it waits on you. It doesn't approve what the agent does inside the box or write receipts for it. So an agent's pane is marked *observed*, and a turn in the prompt band is marked *governed*. What the agent asks of cherry π is governed call by call, under its pane's token ([What a caller may do](manual.html#callers)). This holds whether it types a `picache` verb or calls the verb over MCP ([section 04](manual.html#mcp)). An agent can also join as a cohort, with its own write and shell tools turned off ([section 05](manual.html#cohort)). ### What a caller may do Each pane gets a token that names who is calling and from which folder. The policy weighs the caller's ceiling (the most that caller may do), the workspace's grants and a budget per token. You are asked in your client and never in the agent's own terminal, so an agent can't approve its own request by answering a prompt there. | Vendor agent's call | What happens | |---|---| | Scoped read | Allowed through the read interface. | | Direct write or patch | Refused, except on a goal: then only in the goal's copy of the layer ([section 05](manual.html#goals)). | | split, send, send-key, close, apply-layout, new-layer, close-layer | Asks you in your client, for every call. | | Arrange panes: float, move, resize, zoom | Allowed without asking on its own panes and the ones it opened. Any other pane asks, and so does moving your keyboard. Both are per-agent settings (`picache box claude arrange`). | | run | Refused by the cohort ceiling. | | cohort, bringing in another vendor agent | Refused by the cohort ceiling. | Picache can make reversible writes within its grant, and asks otherwise. Its irreversible calls ask every time, and so does any change it makes to your desk. Approving a call covers that call only. It never becomes a standing grant. > **Diagram: Who is asked, for what.** Who is asked for what. Reads are free for everyone; a vendor agent asks to read a pane that is not its own. Writing a file: you and Picache write inside the workspace's grant and are asked outside it; an agent's writes are refused, except in the copy of its goal. Arranging panes: free for you, asked each time for Picache, free for an agent on its own panes. Moving your keyboard: asked for Picache and, by default, for an agent. Opening, typing into or closing panes and layouts: a destructive one is confirmed by you, and asked each time for Picache and an agent. Running a command: Picache asks each time and runs it in its box; an agent cannot. > > Who is asked, for each caller and kind of call. These are the defaults. An agent's arranging and focus are settings. ## 04 / picache and Picache **Picache** is the agent you talk to in a Chat, and **`picache`** is the command it uses as its hands. Picache is pronounced *pee-KAH-cheh*. The `picache` command is available in every pane cherry π opens, and it offers the harness's own tools as 88 verbs. Every tool Picache calls is one of those verbs. When Picache calls one, it is run by the same handler, decided by the same policy and recorded in the same receipt as when you type it. > **Diagram: One registry, every way in.** Four callers reach the same verbs: you in a shell typing picache split, Picache making the split tool call in a Chat, a vendor agent typing picache split in its pane with its own token, and the same agent calling split as an MCP tool through picache mcp, with the same token. All go to one registry of 88 verbs, each with one handler, an effect class and a capability. The policy decides each call from the token's actor, the workspace's grants and a budget per token: it runs and the receipt names who called, or you are asked, at your terminal or in your client, or it is refused with the reason. For this split, yours runs, and the other three are asked. > > A verb takes the same path whoever calls it. ### Who can call a verb The same verb, from you, from Picache and from a vendor agent: ``` # you, in any pane picache split --right --label tests -- make test # Picache, in a Chat: the same verb, as a tool call split {"side": "right", "label": "tests", "command": "make test"} # → you're asked in the client; on yes, the same handler runs, # and the receipt names picache as the caller # a vendor agent, in its pane: the same verb, its own token picache split --right -- make test # → you're asked in the client, naming the agent and its pane ``` | Who calls | How | Who the harness sees | Where it is asked | |---|---|---|---| | You, or a program in your shell | `picache …` | the operator | At your terminal, for a destructive command or a write the workspace has not allowed. `--yes` answers in advance. | | Picache | A tool call: one verb, named parameters | `picache`, with a token made for the turn and revoked when it ends, in the folder of the layer in front | In your client: Allow this once, or No. A write the workspace has not allowed also offers Until cherry π closes and Always in this workspace. | | A vendor agent in a pane | `picache …` in its own pane | `agent:claude`, from its pane's token | In your client, naming the agent and its pane, and never in its own terminal, which the agent controls | | A vendor agent over MCP | An MCP tool, through `picache mcp` in its pane | `agent:claude`, from its pane's token | As for its pane: in your client, never in its terminal | `picache` itself is a few lines of shell. It passes its arguments to `cherrypi.ape cli`, which talks to the harness with the pane's token from `PICACHE_TOKEN`. Output is text for people, or `--json` for programs. The policy decides each call from who made it, what the workspace has granted and the token's budget. The grid in [section 03](manual.html#authority) shows the outcome for each caller. A refused call exits non-zero, says why, and leaves a receipt like any other call. ### Find a verb ``` picache help # every verb picache help split # one verb picache help --json # every verb, for a program picache status # who you are to the harness, and your folder picache panes # every layer's panes and their state ``` A few you'll use often: ``` picache wait claude --state blocked picache send claude --wait "run the tests" picache float --cascade picache save-layout dev picache apply-layout dev --new-layer docs picache events --json terminal ``` ### Give an agent the verbs over MCP `picache mcp` is an MCP server over standard input and output. Add it as an MCP server command in the agent, from the pane the agent runs in. ``` # for example, in Claude Code claude mcp add cherrypi -- picache mcp ``` Its calls carry that pane's token, so the harness sees the agent as the same caller whether it types a `picache` command in the pane or calls a tool over MCP. > **Diagram: An agent's MCP server, in its pane.** In the agent's pane, the agent talks MCP to picache mcp on standard input and output. picache mcp carries each request to the harness over picache.sock with the pane's token. The harness lists only the tools this actor may ever be allowed: the reading tools, ask, the Inbox's tools, Cherrytop proposals and the desk, and the writes only while it works on a goal, but never run or cohort. Each call goes through the same policy, the same asks in your client and one receipt, and comes back with structured data, checked against the tool's output schema where it declares one, or as an error saying why it was refused. Spec 2026-07-28, stateless, with a handshake for 2025-11-25 and 2025-06-18. > > `picache mcp`: the verbs as MCP tools, under the pane's token. It follows the MCP specification of 28 July 2026 and accepts clients of earlier versions. It offers tools only, and each caller sees only the tools it could ever be allowed to call. An agent sees the reading tools, `ask`, the Inbox's tools, Cherrytop proposals and the desk. The desk is the set of tools that drive panes, layouts and layers ([Picache's tools](manual.html#picache-tools) lists them). Arranging its own panes needs no approval, but you are asked before it opens a pane or types into one. It sees the writes only while it works on a goal ([section 05](manual.html#goals)). It never sees `run` (which needs a terminal an MCP client doesn't have), `history` (which reads Picache's conversation), `search` and `jobs` (which read past its folder) or `cohort`. A result is checked against the tool's output schema where the tool declares one, and a mismatch is an error. `picache mcp` is also the only MCP server a cohort gets. A cohort is a vendor agent you bring into a Chat ([section 05](manual.html#cohort)). ### Picache's tools Picache has 46 of the verbs as tools. With them it can: - read the workspace and its panes, and its own conversation exactly ([below](manual.html#context)); - change files with an undo; - run a command as a job in its box ([section 09](manual.html#capsules)), or run a clean room or a script; - ask another layer or a vendor agent ([section 05](manual.html#cohort)); - drive the desk: open, type into, arrange and close panes, save and apply layouts, and open and arrange layers; - propose opening, showing or arranging things on Cherrytop, for you to accept ([When an agent proposes](manual.html#web-proposals)); - read and answer issues, see who an issue could go to, and write to agents ([Agents write to each other](manual.html#messages)); - read and stop the jobs it started. A conversation starts with the core tools (`status`, `layers`, `ls`, `cat`, `find`, `grep`, `write`, `patch`, `undo`, `run`) and one more, `tools`, which lists the rest in groups. | Group | For | |---|---| | `panes` | Reading panes, typing in terminals | | `arrange` | Where panes sit, and layouts | | `layers` | Opening, closing and moving layers | | `memory` | History past the window, search, cherry π's own docs | | `work` | Scripts, a clean room, asking a layer or an agent, its jobs | | `forms` | Showing you a form for a choice or an input | | `inbox` | Issues and replies, who an issue could go to, what an agent's hooks were seen doing, and agents' messages | | `web` | Cherrytop: what is open, and proposals for you to accept | | `app.` | One per installed app | A group loads when Picache asks for it or calls one of its tools, and stays for the conversation. Loading a group changes what the model is told and leaves what it may do as it was. With groups loaded this way, the start of each request is about 6,350 characters instead of about 20,200. > **Diagram: Picache's tools, and yours.** Left, the 46 verbs Picache calls as tools: reading (status, layers, ls, cat, find, grep, search, panes, brain, kb, history), changing files (write, patch, undo), running (run, cleanroom, script, jobs), issues and messages (issue, reply, assign, message, skill), asking another layer (ask) or a vendor agent (cohort), and the desk (send, split, send-key, wait, close, zoom, float, resize, swap, move, label, focus, layout, save-layout, apply-layout, new-layer, close-layer, focus-layer, label-layer, move-layer), proposals to Cherrytop (web), plus the tools of a granted .pi app. Right, the 42 verbs that are yours to type: the fleet, identity, offers, links and what each layer accepts from them, agents' goals, boxes and evals, an agent's MCP server, apps, layer export, import, share, fork, restore and snapshots, events and reports, scripts and loops. Picache is told their names every turn, so it can say which to type. > > The 88 verbs, split by who calls them. `picache help` lists the full set. An installed `.pi` app adds its own tools while you grant it. In a thread that only reads, such as another layer's question, Picache gets only the reading tools. The other 42 verbs are yours to type. With them you: - pair machines and manage the fleet, your identity and profile; - answer offers, link workspaces and set what each layer takes from them; - give agents goals ([section 05](manual.html#goals)), set agents' boxes, run evaluations and install apps; - export, import, share, fork and restore layers, and list their snapshots; - watch events, run your own scripts, and start and stop loops ([below](manual.html#loops)). Two of them are not for typing. `connect` is what a confined agent's proxy asks about a connection, and `mcp` is the MCP server you give an agent. Each turn, Picache is told the names of these 42 verbs. When you need one, it names the command for you to type instead of trying it. ### How Picache learns cherry π Picache learns from an atlas built from the same registry: every verb and tool, its effect, when it asks and how to use it. The atlas is rebuilt whenever the registry changes, so it can't describe a verb that isn't there. Next to it are an operating shelf and these docs, packed into the runtime. Picache searches them with its `knowledge` tool before answering how to do something. You and your other agents can search them too. ``` picache kb search "share a layer" # the docs and the atlas picache kb index on # also search this layer's files picache kb semantic on # ... by meaning, not just words ``` Your other agents learn the command from `picache skill setup`. - It gives Claude Code, Codex, OpenCode and Pi a skill that explains cherry π. - It gives Claude Code, Codex, Cursor Agent, Devin and Kimi CLI hooks, so each one's pane shows its state. The hooks of Claude Code and Codex also tell a new session which pane it's in. Each change is marked as cherry π's, and `picache skill remove` takes out only those. ### What the model sees A conversation is never compacted. Every message, receipt and checkpoint stays in the layer's store as written, and the model gets a window over that record. > **Diagram: A window over a record that is kept whole.** Left, the conversation's record in the layer's store, never compacted: every message, the receipt of every call, and a checkpoint for every turn. Middle, what the model is handed: one system message with Picache's rules, the latest checkpoint and the conversation's origin; the newest messages, by default 8 of up to 1600 bytes each and 8192 in all; and the request, whole. Right, Picache can search and read the record exactly with history_search and history_read, a chunk at a time with a digest, and each turn's receipt names the context version and its fingerprint. > > The record stays whole. The model gets a bounded window and can look up the rest exactly. Each turn leaves a **checkpoint**. It records how the turn came out (never rounded up to completed), which tools it had, any effect in doubt, the files, jobs and calls it touched, and the start of its reply. The next turn starts from it. The model's one system message holds the latest checkpoint and the conversation's first request. Then come the newest messages that fit: | Setting | Default | What it limits | |---|---|---| | `context.recent_limit` | 8 | Newest messages in the window | | `context.message_chars` | 1600 | Bytes of each message | | `context.window_budget` | 8192 | Bytes in all | Your current request is always kept whole. The checkpoint counts what the window left out, and a checkpoint written when Picache had different tools is marked stale. Beyond the window, Picache can look things up exactly. The harness fills in which conversation a lookup reads, so a call can't reach another conversation. - `history_search` searches its conversation's messages, receipts or checkpoints. It returns two previews with ids and a cursor. - `history_read` returns one record exactly, a chunk at a time, with a digest that shows if the record changed between chunks. Everything the model is handed is versioned: the instructions, the tool declarations, the window's defaults and the strategy. Each turn records its version in a receipt, so you can tell which context a reply was made under. ``` picache context # exactly what the model was given on its last call ``` ### Run a loop A loop works toward one goal over several turns in the folder your pane opened in, checking each step with the layer's tests. ``` picache loop start --goal "make the API tests pass" --steps 10 picache loop # list runs picache loop status picache loop continue --steps 4 picache loop stop ``` > **Diagram: A loop's steps.** picache loop start with a goal begins a run. Each step is a whole Picache turn, given the goal, a compact memory of the steps so far and the run's PROGRESS.md. Then the layer's test command runs as a job owned by the step. If it passes, the run is done. If it fails, its output is what the next step works on, until it passes or the steps run out, six by default, and the run stops as blocked for picache loop continue. One step runs per tick, and never while an effect in its conversation is in doubt. > > A loop: a whole turn per step, checked by the layer's tests. A run works in that folder. Each step: 1. is a whole Picache turn in the run's own conversation, listed as *Loop:* and the goal, and governed and receipted like any turn; 2. gets the goal, a short memory of earlier steps and the run's `PROGRESS.md`, and is told not to redo finished work; 3. ends by running the layer's test command as a job (`loop.test_command.`, else `loop.test_command`). If the tests pass, the run ends. If they fail, the next step works on their output. When the steps run out (six by default), the run stops as blocked, and `loop continue` gives it more. With no test command, a run takes its steps and stops for you to review. A run stops as blocked before its next step while anything in its conversation waits on recovery ([section 10](manual.html#records)). After a restart, a running loop stays blocked until you continue it, unless recovery mode is automatic. Anyone can list and read runs. Only you can start, stop or continue one, and a script cannot. An app you granted `ai.redloop.manage` ([section 09](manual.html#apps)) can start, stop or continue the runs it started, and you are asked each time it starts or continues one. ## 05 / Agents There are two kinds of agent in cherry π, and they differ in who governs them. - A **folder's agent** is Picache, or an agent you define for a folder. The harness governs it: it decides what the agent may do and records what it does. All of its tools are governed `picache` commands, so it can act only within the permissions the workspace has granted. - A **vendor agent**, such as Claude Code, Codex or Pi, runs under its own harness in a terminal pane, confined in a box ([section 09](manual.html#capsules)). Its run is observed and recorded, but cherry π does not govern what the agent does. ### Declare a folder's agent ``` picache agent new docs --role "Keeps the API docs in step with the code" picache agent new docs --library # keep it in the workspace, not the repo picache agent list picache ask L1/docs/api "which endpoints have no examples?" ``` Every folder is answered by an agent: its own, if one is defined for it, otherwise the nearest parent folder's, then the layer's, then the workspace's, and Picache when none of those exist. `picache agent new` writes the agent's definition to `.cherrypi/agent/agent.json` inside the folder. It names a persona, a preferred model, and a folder of private Markdown notes, `kb/`, that only this agent reads. The definition lives in the folder, so it goes with the folder wherever the folder is copied or shared. Use `--library` to store the definition in the workspace instead of in the folder. The workspace records which folder paths it belongs to, so the repository contains no agent files. The persona applies only within cherry π's own rules and cannot override them. The agent's permissions always come from the workspace's grants, never from its definition file. Its actions are recorded in receipts under its own name. When you send a question or a task to a layer, the layer's agent answers it. You can also address a question directly to a folder inside a layer, whether that folder is in this workspace, in a linked workspace, or on another machine. ### Test and share an agent ``` picache eval # run the cases picache eval schedule on # run them while the model is idle picache agent pack --out docs.pi picache agent install docs.pi ``` `picache eval` runs test cases against an agent: a standard set of core cases, plus any cases the agent defines itself. An evaluation cannot change anything: read-only operations run for real, and every other operation the agent attempts is recorded but not carried out. The core cases check that the agent: - reads a file before answering a question about it; - searches the folder to find a file before trying to read it; - looks at the pane when it is asked what a pane shows; - asks before running a command, instead of running it; - when a job belongs to the operator, names the command the operator should run (for example, `picache pair`) instead of attempting the job itself; - answers how-to questions about cherry π, such as how to unpair a machine or give a folder its own agent. `picache agent pack` bundles an agent into a single signed `.pi` file, and `picache agent install` checks that signature wherever the file is installed. It refuses an agent that is unsigned or was changed after signing. An agent signed by someone you do not know needs `--untrusted`. ### Launch a vendor agent The rail lists the vendor agents cherry π can launch: Claude Code, Codex, Gemini CLI, Cursor Agent, Copilot, Pi, and others. Next to each one, it shows whether the agent is installed and whether an update is available. When you launch an agent, cherry π starts it in a new terminal pane opened at the layer's folder. By default it runs confined in its box ([section 09](manual.html#capsules)), and the run is recorded ([section 10](manual.html#runs)). The agent keeps its own conversation transcript, and cherry π does not take it over. ### Ask a vendor agent in a Chat If you start a Chat message with `@claude`, Claude Code answers that message instead of Picache. A vendor agent brought into a Chat this way is a cohort. ``` @claude what does the build script do? # in a Chat picache cohort # who can be asked, and who is running picache cohort claude "what is in here?" # ask once, from a shell picache cohort stop all # stop a conversation's cohort ``` In a Chat, cherry π talks to the agent over ACP (Agent Client Protocol), and the agent works in the layer's folder. It is confined in the same box it would use in a terminal pane, with the same launch setting, network access, and login, but it has no terminal. The agent's own tools for writing files, editing files, and running commands are turned off, which leaves it Read, Glob and Grep. Its only MCP server is cherry π's own ([section 04](manual.html#mcp)), reached with a token made for it alone, so anything it tries beyond reading is checked against the same policy as any other agent's work. If the agent asks for permission to do something, cherry π asks you, and your answer covers that one call only. If you do not answer, the call is refused. > **Diagram: A vendor agent as a cohort.** A message in a Chat that starts with @claude goes to Claude Code instead of Picache; the turn around it stays Picache's, with its task, receipts, ledger, snapshots and events. Claude Code's ACP program runs in its box, the one its terminal pane would have, without a terminal: the harness talks to it over a socketpair. Its own tools are only Read, Glob and Grep; writing, editing and the shell are turned off in its own settings. Its other tools are cherry π's, through picache mcp under a token of its own. What comes back is its words, marked external and unverified, with what it says it ran and what it asked; a permission it asks for is asked of you, per call. It stays for the conversation until it has been idle for 15 minutes, its folder changes or you stop it, and at most four stay at once. > > `@claude` in a Chat: a vendor agent over ACP, in its box, with cherry π's tools. The reply holds the agent's answer, marked as external and unverified, with the tools it says it ran and any questions it asked. The turn still belongs to Picache and is recorded like any other. Your next message goes to Picache unless it also starts with `@claude`. Picache can read Claude Code's answer as part of the conversation and knows that Claude Code wrote it. Claude Code stays running for that conversation, keeping its context, until it has been idle for 15 minutes, the conversation moves to a different folder, or you stop it. At most four of these agents can stay running at one time. When a fifth is needed, the oldest one is stopped. Bringing in a vendor agent cannot be undone: your question, and anything the agent reads, is sent from this machine to the agent's vendor. You can bring one in directly at any time. Picache can do so only with your approval for that call, and an agent brought in this way cannot bring in another one. A vendor agent can be asked in a Chat only through an ACP adapter. Claude Code is the only agent with one, `@agentclientprotocol/claude-agent-acp`. ### Give an agent a goal A goal gives a vendor agent a piece of work to complete on its own. The agent works in a copy of your layer, and none of its changes are applied to the real layer until you accept them. ``` picache goal @claude "make the login tests pass" --steps 8 picache goal # the goals, newest first picache goal status picache goal accept # merge its work into the layer picache goal reject # leave it unmerged picache goal stop picache goal continue --steps 4 ``` A goal works like a loop ([section 04](manual.html#loops)), except that the vendor agent, not Picache, takes each step. A step is one turn for the agent, like a message you address to it with `@claude` in a Chat, and ends by running the layer's test command. A goal has six steps unless you set another number. The agent works in the `.worktrees/goal-` folder inside the layer, on a branch named `cherrypi/goal-`. The copy includes the layer's dependencies and existing build output, and is copy-on-write where the file system supports it. > **Diagram: A goal, from copy to merge.** You give a vendor agent a goal with picache goal. It works in its own copy of the layer, .worktrees/goal-id, on a branch cherrypi/goal-id, and writes there and nowhere else. Each step is its turn, then the layer's tests; a test failure becomes the next step's work, and every step is committed to the branch. When the tests pass or the steps run out, Picache reviews the branch, reading only. Then you decide: accept merges it into the layer, reject keeps the branch for the record. The layer is untouched until you accept, and a conflict refuses the merge whole. Without git, the copy is the whole folder, snapshots stand in for commits, and the merge is three-way, with an undo. > > A goal: worked in a copy, reviewed by Picache, decided by you. - The agent acts with a token created only for this goal. Using cherry π's `write` and `patch` commands, it may change files only inside the goal's working folder. Every change can be undone and is recorded with a receipt. - It cannot write to the copy's `.git` folder. - At the end of every step, the agent's changes are committed to its branch. The run stops when the tests pass, when its steps run out or a time limit you gave it passes, or when a step's turn fails or needs your review. Failing tests do not stop it: their output is the next step's work. Picache then reviews the branch. The review is read-only: Picache compares the agent's work with the goal, points out anything that looks wrong, and recommends whether you should accept the work. Accepting the work merges the branch into the layer. The merge is refused if either folder contains uncommitted work or if the merge would conflict, and a refused merge leaves both folders unchanged. Rejecting the work leaves the branch unmerged, so the agent's work stays on record. Either way, the working copy is removed and the branch is kept. If a layer is not in git, the agent still works in a copy of the whole folder, but each step ends with a snapshot instead of a commit. Accepting the work performs a three-way merge against the snapshot taken at the start, and the merge is refused entirely if there is any conflict. The layer is snapshotted before the merge, so you can undo the merge afterwards. If you set `goal.workspace` to `worktree`, or the layer's folder is below the top of its repository, the agent works in a git worktree of the layer instead of a copy. The Goals pane lists every goal and shows its steps, Picache's review, and the complete set of changes. Only you, the operator, can create, stop, continue, accept, or reject a goal. Agents and scripts cannot. ### Agent status Each agent shows both its state and how that state was determined, because a badge that said *working* while the agent waited for your answer would mislead you. The state comes from several signals, ordered from the most reliable to the least: 1. The process itself: whether it has exited, whether it has started a child process, and whether it printed anything in the last moment. These checks know nothing about the particular agent, so they work for an agent cherry π has never seen. 2. The agent's own reports: its terminal title, and reports sent through the Program Status Protocol (OSC 7501), where the agent states whether it is working, idle, finished, failed, or blocked waiting for a permission, an answer, or a sign-in. 3. Hooks installed by `picache skill setup`, which make Claude Code, Codex, Cursor Agent, Devin and Kimi CLI report their state in the same way. With these hooks, every one of them but Cursor Agent reports a permission dialog as blocked, together with the tool and the command it concerns. > **Diagram: How cherry π knows what an agent is doing.** The ladder, certainty first: 1, the process exited, a fact; 2, it started a child process, a fact; 3, it printed in the last 1.5 seconds, a fact; 4, the terminal's title, reported; 5, its own report through OSC 7501 or its hooks, reported. The badge is one glyph naming your next act: working, wait; idle, type; unseen, read it; blocked, answer; error, inspect; exited, nothing; unknown, look. An inferred state is drawn dim, and the detail pane says why. An observed exit takes precedence over an agent's report, and a report takes precedence over inference; only a report can mark an agent blocked. > > The status ladder and the badge. ● is a result you haven't seen. The status badge is one symbol for what you should do next. An inferred state is drawn dim, and the agent's detail pane says how the state was decided. The more reliable signal wins: an observed exit overrides an agent's report, and an agent's report overrides an inference. Only an agent's report can mark it *blocked*. The badge never guesses that state, because silence cannot distinguish a model that is still thinking from a question that is waiting for your answer. If two panes run the same agent and a report could belong to either pane, the report is withheld so that it never appears on the wrong pane. The rail's tree shows the same marks on each pane's row, and a closed row carries the most urgent of what it holds ([The rail](manual.html#rail)). A script or shell can wait until an agent needs you: ``` picache wait claude --state blocked # until it needs you ``` ### Notifications When an agent needs your attention or finishes its work while you are not looking at it, cherry π notifies you once and plays a sound. When several agents need you at the same time, one notification covers them all. Press `Ctrl+Alt+A` to jump to the agent that has been waiting for you the longest. Questions are shown first, then failures, then results you have not yet seen. A result remains marked as unseen until you focus its pane. Each agent can have an expression: a picture, a sound, and a line of text for each state. The picture appears next to the agent's name in the rail and on its profile card. You choose how notifications are delivered: through the terminal cherry π runs in, through the operating system's notification system, or not at all. The setting applies in every workspace. ## 06 / The software factory The factory makes `.pi` apps ([section 09](manual.html#capsules)). You describe what to build in a sentence or two, and a vendor agent builds it in a layer of its own. Checks that the agent never sees decide whether the app passes, and nothing is accepted until you accept it. - A **schematic** describes one kind of app: a working skeleton to start from, what every app of its kind must keep, the capabilities it may ask for, which agent builds it, and how much that agent may spend. - A **product** is one app a schematic made. It is a git repository and a layer of its own, beside the layer it was made from. - The **builder** is the vendor agent working on it, as a goal ([section 05](manual.html#goals)). - The **hidden checks** are the schematic's scenarios ([Hidden checks](manual.html#factory-checks)). The harness runs them against the packed product, and the builder hears only what failed. - A **Try** runs a product before it is installed, on data of its own. ### Make a product Add a **Product** pane in any layer. In a layer that is not a product, the pane is where one is made: - **Schematic.** The layer's schematics, and an **Add** button for each starter cherry π ships: `data-app` (a collection in the app's own tables, with verbs, a pane and pages), `pane-program` (a program in a terminal pane, drawn from keys) and `web-app` (pages that are the product, with a Cherrytop widget). Adding one copies it into `.cherrypi/schematics/`, where you can edit it. - **Builder.** The vendor agents that can work over ACP, as a cohort does ([section 05](manual.html#cohort)). The schematic's own is chosen unless you pick another. One that is not installed on this machine is named but not offered. - **What it spends.** Before anything starts, the pane says how many steps and minutes the builder may use. The steps are the builder's own turns, from its vendor, not Picache's. **Make a product…** asks for the product's name (its layer and its app's id), then what it should be. Nothing starts until you have answered both, and then the pane shows the new product. From a shell: ``` picache factory # this layer's schematics, the starters, the products made picache factory add data-app # copy a starter into .cherrypi/schematics/ picache factory run data-app books "a reading list with ratings" picache factory run data-app books "…" --agent @claude --steps 8 ``` ### While it is built The builder works as a goal does, in a copy of the product, one step at a time. Each step ends with the schematic's public test. After a step, the result of the last hidden check (in the schematic's words) is part of what the builder is told next. The Product pane says where the build stands: | The pane says | What it means | |---|---| | building · step *n* of *m* | The builder is working. **Stop** ends its run, and you then accept or reject what it did. | | Picache is reviewing it | The builder stopped. The hidden checks run on its copy, and Picache reads the result and the changes. | | waiting on you | Picache's review is shown. **Accept** merges the build into the product. **Reject…** leaves it out, with your reason if you give one. **Open its chat** shows its steps and the review. | | its build was accepted / rejected | Decided. A rejected build's branch is kept for the record. | A build is accepted only on a current pass of the hidden checks, on exactly what is in its copy now. A pass on an earlier version, or under scenarios that have changed since, does not count. A run stops for review when its steps or its minutes run out. `picache goal continue --steps n --minutes n` gives it more. Each step's commit says what made it, as git trailers: `Cherry-pi-Goal`, `Cherry-pi-Agent` (with the name and version the agent gave), `Cherry-pi-Launch` (`confined` or `standard`) and `Cherry-pi-Factory-Run`. The merge that accepts it says why: the goal, Picache's review, how many hidden checks passed on which candidate, and who accepted it. ### Hidden checks A scenario is a small Lua data file in the schematic's `scenarios/` folder. The harness packs the product once and runs each scenario on its own copy, in a capsule. When the product asks for its own tables, the request is carried out for real. Any other request is answered from the scenario's `answers` (or the schematic's `finish.answers`) or refused. | Kind | Checks | |---|---| | `call` | A verb or tool answers with this text, pattern or data | | `asks` | What it asked the host for: these capabilities, never those | | `rows` | After these calls, its own tables hold these rows | | `page` | A page answers with this status, type and content | | `program` | These keys give a screen that contains this, and fits the pane | | `static` | Its manifest is valid, it asks only for what the schematic allows, it packs and its signature verifies, and it is small enough | The builder never sees a scenario. With `feedback = "summary"` (the default) it hears which scenarios failed, what was expected and what was observed. With `"verdicts"` it hears only which failed. A builder launched `confined` cannot read them. One launched `standard` could, and every verdict records which it was. To run the hidden checks now, use **Run hidden checks** in the pane, or `picache factory finish `. ### Try a product **Try it** runs what is in the product now, before it is installed, on data of its own and only with what the schematic allows. The pane then offers what the product has: - each of its panes, and each program in a terminal pane; - its pages: **Open in Cherrytop**, or `https://try-.pi`; - **Open its console**: type a verb and its arguments, `tool `, `tables` or `rows `, and see the answer, what it asked for, and what changed in its tables. `keep ` saves the last call as a new hidden check that expects what you just saw. **Close Try** ends it, and its data goes with it. A Try never installs anything, and `.pi` stays the installed app's. ``` picache factory try books picache factory call books add "Dune" picache factory close books ``` ### Pack for review **Pack for review**, or `picache factory pack `, runs the hidden checks. If they pass, and only then, it packs the product as a signed `.pi` onto the review shelf, `/factory/shelf/`. The product's evidence is inside the signed file: the order, the schematic, every check and scenario result, and whether the builder was confined. `picache factory` says how each schematic is doing: its runs, how many were packed, accepted and rejected, and how often its hidden checks passed. ### Turn it off The factory is a first-party addon, on by default. `picache addons disable factory` turns it off, and `picache addons enable factory` turns it back on. While it is off, there is no `picache factory` and no Try, and the Product pane says the addon is off and how to turn it back on. A build the factory started cannot be accepted while it is off, because its hidden checks are not there to pass it. ## 07 / Models and scripts Picache runs on a model you choose, and you can script cherry π in Lua. ### Choose a model Open Actions → Open Models. Its two tabs are This machine and Catalog. | Row | Shows | |---|---| | Machine | The chip, its memory and cores | | GPU | How much of its memory is in use | | Runtime | cherry π's own llama.cpp (b11819), with Install when it's missing | | Seats | Picache, Small, Search, and Cloud in hybrid mode: each one's model, with Load and Empty | | Thinking | Whether a reasoning model thinks first (off by default) | Each model gets a verdict: runs, tight, too large or won't load. llama.cpp measures it from the model's header, so a catalog model is measured before you download it. Try loading confirms it with one real load. A model loads only into free GPU memory, with 1 GB to spare. If there isn't room, cherry π stops models it started to make room: those in no seat first, then the oldest, and never the one serving Search. If even that would not make room, the load is refused before anything stops, and the reason names what holds the memory. Model servers cherry π didn't start are listed, with Stop. Seats' models stay loaded side by side while they fit. Empty clears a seat, and unloads its model if no other seat uses it. Search takes only an embedding model, and no other seat takes one. The pane also finds or imports models you already have, and uses them where they are. Downloads are checksummed, resumable jobs, and `llama-server` runs as a job too. Picache can also use any OpenAI-compatible service. Without a model, it answers with a fixed template ([section 03](manual.html#turn)). Vendor agents ([section 05](manual.html#agents)) keep their own models, subscriptions and settings. ### Your API keys Open Actions → Open Keys. The signer seals your API keys on this machine ([Where your secrets go](manual.html#secrets)), and the pane shows each one only by its last four. | Tab | Holds | |---|---| | Providers | Hugging Face, OpenRouter, Qwen Cloud, DigitalOcean inference | | Agents | Each vendor agent's own keys, such as a login token you handed in | A row reads one of these: - stored (`••••abcd`); - an environment variable (`$HF_TOKEN ••••9876`), which is read but never copied in; - not set; - locked, when the sealing key has changed. A stored key wins over the variable. Listing your keys opens none of them anywhere else, because the pane gets only the last four from the signer ([Where your secrets go](manual.html#secrets)). Press Enter to set a key, masked as you type, and x to clear one, which asks first. Only this machine's own desktops can see or change keys, and no verb reaches them. Each change is a receipt naming the key, never its value. An agent's keys go into its environment when it starts, or stay with the signer when its Key is set to inject ([Keep the key out of the box](manual.html#keys-out)). Provider keys belong to the Cloud seat ([Which model answers](manual.html#routing)). ### Which model answers Without a router, every step goes to the Picache seat with the workspace's thinking setting. A retry or a continued turn stays on the same model, so the prompt cache holds (`route.sticky`). A router is an installed app's `route` hook. It proposes a seat, thinking and limits, or handing the turn to a vendor agent. Each proposal is checked before it is used: - the app is granted that seat (`route.seat.small`, …); - the seat's model is loaded; - the model has qualified for the job; - anything leaving the machine is allowed. A proposal that fails a check is overruled, and the record says why. A router never answers a permission prompt or changes a tool call. ``` picache eval qualify small # a seat's model must pass before a router uses it picache layer egress ask # may this layer's requests leave the machine? ``` Leaving the machine means the Cloud seat, in hybrid mode with a key from Keys, or a handoff to a vendor agent. It's off by default (`route.egress`). The Chat says when a step was sent to a cloud model or handed to an agent. ### Run a script A Lua script runs in one of two places, depending on who wrote it. ``` picache do layout.lua name=dev # yours: runs as you picache script check.lua --budget calls=20 # anyone's: runs in a capsule ``` | Compared | `picache do` | `picache script` (code mode) | |---|---|---| | Written by | You | A model, or anyone you don’t vouch for | | Runs | In your terminal, as the `picache` command you started | In a capsule: an empty folder of its own, confined by Seatbelt on macOS | | Can use | Lua’s whole library, including `io.popen` and `os.execute` | Computation, JSON and the verbs it was given. No files, no programs, no network. | | Governed | Each `picache.*` call, as the pane | Each verb call, as its caller, within a fixed list and budget | `picache do` takes its scripting shape from Rex, the Superlogical terminal. Both run a local Lua script that can arrange terminals, wait for events and run commands. Rex removes `io.popen` and `os.execute`; cherry π leaves them working. Cherry Pi also applies its own pane authority and receipts to calls made through `picache.*`. Blocking them would not protect anything, because the script is your program running in your terminal, and redbean's `unix` library does the same with `fork` and `execve`. A script reaches what you can reach at the prompt, and no more. When an agent runs `picache do` in its pane, the script runs in that agent's box. A script you didn't write belongs in `picache script`, where the whole environment is the limit. Picache's `script` tool is that same capsule, limited to the tools its turn has, so a script there can do no more than Picache could do one call at a time. Each `picache.*` call carries the pane's token and leaves a receipt. A command started with `io.popen` never reaches the harness, so, like a command typed in the pane, it leaves no receipt. Still, prefer `picache.exec` over `io.popen`: - It takes a list of arguments instead of a shell line, so nothing needs quoting. - It has a timeout. - A command it starts stops when the part of the script that started it ends. - The script keeps reading events while it waits. `io.popen` blocks the whole script until its command ends. ## 08 / Machines, offers and rooms Conduit is how cherry π talks to itself and to others: layer to layer, workspace to workspace and machine to machine. The three differ only in how the bytes travel. The records, an offer's lifecycle, the receipts and the authorization are the same in all three. > **Diagram: One stack, three carriers.** Conduit is a stack, layer on layer: features (asks, offers, tasks, rooms, shares) on Conduit (typed offers, outbox, inbox, receipts) on a typed session (signed envelopes, stored before any effect) on a carrier, which moves bytes and says what it is. Three carriers: local, to another layer such as L1, inside one harness; direct, to another workspace on this machine such as web:L1, over a Unix socket once the two are linked; fleet, over Holepunch, to another machine such as studio:web:L1, over Noise, forward-secret, between paired machines. The records, lifecycle, receipts and authorization are the same; only the carrier differs. > > Layers, workspaces and machines differ only in how the bytes travel. | Address | Means | |---|---| | `L1` | A layer in this workspace | | `L1/docs/api` | A folder inside it | | `web:L1` | A layer in a linked workspace on this machine | | `studio:web:L1` | A layer on a paired machine | Every machine keeps a durable queue, so all of this works without a server. - An offer is written to the outbox before the network is touched. - It counts as delivered only once the receiver has saved it. - A repeated message gets its stored answer instead of running twice. - An offer that doesn't arrive stays queued and is sent again. A refusal is final. - Both ends keep the record. ### Offers, asks and tasks From a shell, the three look like this: ``` picache offer L2 message "the schema moved to v3" picache offer L2 patch src/api.ts oldName newName picache ask L2 "which tests cover login?" picache task L2 "update the client for the v3 schema" ``` > **Diagram: An offer, from outbox to applied.** Layer L1 sends: an offer, say a patch to layer runtime, made by you, Picache or an agent, is written to the outbox, queued and then sent, before the network is touched. Any carrier takes it; delivered means the receiver committed it. Layer runtime receives it in its inbox, with a count on its rail, and you answer there. Accept applies it through runtime's own patch verb, its jail and its undo; Reject changes nothing. How it ended goes back to the sender. Nothing changes in a layer until it is accepted there, and every step is a receipt. > > An offer between two layers. An **offer** is a message, a patch or a whole file for another layer. Nothing changes there until you accept it in that layer's Inbox (`Ctrl+Alt+R`). A patch is applied through that layer's own `patch` verb, jail and undo, and it applies in full or not at all. Anyone may offer, agents and Picache included, but only you answer. An **ask** puts a question to the other layer's agent. The question runs in a thread of its own, which can only read and can't see either layer's chat. The answer comes back labelled as coming from that layer, and it is treated as information, never as instructions. The thread opens as a tab on your Chat. A **task** is work. Once accepted there, it runs as a thread with the usual tools. Its report comes back marked *external_unverified* until you promote it. Writes never cross layers directly. Reads can, limited to the other layer's folder: ``` picache cat --layer runtime README.md ``` ### Link two workspaces ``` picache link ~/src/web --accept message,patch,ask ``` The command links this workspace to another one open on this machine and names the kinds of offer it may send. The other workspace is asked to link back. Once linked, its layers are addressed as `:`. An offer to a workspace that isn't open waits in the outbox until it opens. A layer can decide for itself what it takes from one linked workspace or paired machine, or from `all`. ``` picache accepts L1 # show the lists picache accepts L1 web message ask # only these kinds from web picache accepts L1 web none # nothing from web picache accepts L1 all inherit # hand the choice back ``` A list can allow fewer kinds than the link allows, or more. A list for one peer takes precedence over the list for `all`. A layer with no list takes what the link allows, and any kind from a paired machine. Only you set these lists. ### Identity and the signer An installation has one identity. It is made of the machine's keys (Ed25519, Curve25519 and secp256k1) and a key for each agent, Picache first, ready to be named on Nostr. Workspaces and layers have no keys, because they're addresses under the machine. The private keys never reach the Lua runtime. They are held by a separate signer process on Bare, which receives what cherry π wants signed and sends back a signature. ``` picache identity # the node id and each agent's public key ``` ### Pair your machines P2P is an addon, off until you turn it on. It runs the Bare and Holepunch stack in a sidecar, boxed apart from the signer ([section 09](manual.html#capsules)). ``` picache addons enable p2p_holepunch picache fleet new # on the first machine: make a secret picache fleet join # on each other machine picache pair studio # ask; it shows a code picache pair approve # on studio: the code, typed ``` Asking with `picache pair studio` shows a code on your machine. The code is typed on the machine you're joining: its operator reads it off the asking machine and approves with it. Studio holds the request for ten minutes. Studio's list of requests never shows the code, so a stranger who asks under your machine's name can't be approved by someone copying a code from it. A code typed before its request has arrived is kept for ten minutes. The asking machine asks again while it waits, and it is approved when the request comes in. P2P belongs to the machine, so a pairing made in one workspace holds in all of them. A **fleet** is the set of machines that can find this one. Fleet machines announce themselves on rednet, the public peer-to-peer network, under a topic hashed from the fleet's secret. Only machines with the secret can work out the topic. - Without a fleet, and with the open directory off, a machine is listed nowhere. - The open directory stays off until you turn it on (`picache fleet open on`). - On one local network, fleet machines also find each other over mDNS, and they keep working with no internet. The mDNS beacon shows the machine's id and key to everyone on that network. Only its fleet is hashed. `picache fleet lan off` stops the beacon. - A beacon is only a hint. Seeing one never makes a machine trusted. Pairing pins each side's key and sets up a contract in each direction. Each contract has exact scopes and can be revoked. It lasts 30 days and is renewed for as long as you stay paired. Unpairing revokes what this machine issued. > **Diagram: Finding a machine, then pairing with it.** Finding: picache fleet new makes a secret, and fleet join adds each other machine. Fleet machines announce on a rednet topic hashed from the secret, and on one local network also send an mDNS beacon that hashes only the fleet tag. The open directory is off unless you turn it on. Pairing: the laptop runs picache pair studio and sends a signed invite that spends a one-time nonce. Studio holds it as a request for ten minutes, and its operator approves it with pair approve. Studio answers with its acceptance and a contract offer, and the laptop sends its own contract back. Each side pins the other's key, and each direction has a contract of six exact scopes, for 30 days, renewed while the machines stay paired. A beacon is a hint, never trust: nothing admits an unpaired machine. > > Finding a machine takes a shared secret, and pairing with it takes the other operator's yes. A paired machine may ask this one to run a capsule ([section 09](manual.html#capsules)). The request is refused unless you allow it for that machine (`picache machines host studio on`). ### Open another machine's desktop ``` picache machines open studio # its workspace picache machines open studio:web:L1 # one layer picache machines open studio:web:terminal: # one terminal ``` You can also use **◎ Open** on that machine's card in the Machines pane. Either way, the workspace opens in a pane here. The pane has the remote colour (theme roles `remote` and `remote-focus`), and its title starts with ◎, so you won't mistake it for a local pane. Close the pane to leave. Only you can open one. An agent can't, and neither can a desktop that is itself remote. While desktops from other machines are attached here, the status line says so, as in `1 from studio (view)`. The other machine lets the desktop in when its workspace accepts remote desktops and its grant to you allows it. Its operator sets this: ``` picache desktops edit # on the other machine: off, view, edit or full picache desktops off ``` A desktop given the whole workspace reaches the files, git and trash of its layers' folders, by root, never by path. Folders outside the layers stay out of reach, such as folders you searched or pinned, or a job's scratch folder. A grant therefore never grows with what you do after giving it. This machine's own data folders (`.cherrypi` and `~/.cherrypi`) are never reachable, even with the As me grant ([below](manual.html#remote-actor)). ### Share one layer or one terminal ``` picache machines desktop studio edit --layer L1 picache machines desktop studio view --terminal ``` You can let a machine's desktop into one layer and nothing else, with the first command above or with **Desktop per layer…** on the machine's card. A layer's grant works even when the workspace takes no desktops. When several grants cover what was asked, the narrowest one decides. That machine can also ask for a layer as a share, which you approve (**Actions → Share this layer with paired machines**). It then opens the layer with **◎ Open** or `picache machines open`. The other machine sees that layer's view, conversations, terminals, files, evidence, offers, goals and git, and nothing else. Its terminals open in the layer's folder, and it can't add, close or move layers. Changing a grant closes at once any desktop it no longer covers. Presence names the layer, as in `1 from studio (full, L1 alone)`. Narrower still, you can share one open terminal: use **Actions → Share this terminal with paired machines** and approve it, or grant it with `--terminal` as in the second command above. A terminal without a grant of its own uses its layer's grant. The other machine sees only that terminal, in one pane. The grant and the desktop end when the terminal closes. ### What a remote desktop may do A desktop from another machine acts as that machine, not as you, and it's recorded as that machine (`node:`, `desktop on studio`). Its grant is a preset over seven capabilities: `read`, `files.write`, `chat`, `layout`, `git.write`, `terminal.type` and `goals.decide`. | Grant | Holds | |---|---| | Watch | Reading | | Edit files (where a new grant starts) | Reading, changing files, talking to Picache, saving layouts | | Full | Everything except typing in terminals | | As me (your own machines only) | Everything, acting as you. The record still names the machine | ``` picache machines desktop studio view # a preset picache machines desktop studio read,git.write # a custom grant picache machines desktop studio inherit # back to the workspace's ``` You can also use **Desktop…** on the machine's card. **Custom grant…** takes a list such as `read,git.write`. The workspace's own setting caps the grant for the whole workspace: **No desktops**, **Let them watch**, **Let them edit files** or **Let them act**. A desktop closes at once if its grant loses a capability it came in with, or moves to or from As me. Only As me can type in a terminal, because a shell runs as you, with all of your home, and can commit to git. Other desktops, custom ones included, can watch terminals but can't open any. A watched terminal's header says *watching*. Messages to Picache from a remote desktop get the same limits. A turn it starts or steers can read, and it writes files only if the grant allows. It can't run commands, type, arrange the desk, or start an agent or a goal. ### When another machine's words reach a chat Once another machine's words reach a conversation, its later turns stay held until you acknowledge them. A held turn reads only what that machine could read. Anything it would change is asked of you as usual. If two machines' words reach one turn, it gets only what both grants allow. The chat says whose words they were, with an **Acknowledge** button. Click it once you've read what those turns did. It works only on this machine's own desktop, and not while a turn is running. ``` picache history acknowledge # the same, from a shell ``` The command works only in a shell you opened by hand on this machine, and the desktop asks you to confirm it. Panes an agent asked for, As me panes and other machines' panes can't use it. An agent that you start yourself, in a shell you opened by hand, can acknowledge. ### What stays with this machine Every call names the capability it needs. A call from another machine is answered only when its grant holds it, and anything undecided is refused. Some things stay with this machine's own desktops, even under As me: - settings, pairing and grants; - approvals and recovery; - installing onto this machine; - running jobs, capsules and app panes; - speaking as this machine in rooms and the lobby; - stopping the harness. On a trusted network, a machine with no grant may watch but can never change anything. An explicit `off` still holds there. You mark a network as trusted like this: ``` picache desktops trust 192.168.1.0/24 ``` On the remote desktop, what its grant leaves out stays visible but dimmed, with the reason underneath, and can't be clicked. > **Diagram: A remote desktop acts as its machine.** Studio's desktop is open on alpha's workspace, with an Edit files grant. It edits README.md: the call needs files.write, which the grant holds, so alpha answers it and records it as studio's node, from its desktop, not as alpha's operator. The grant leaves out git.write, terminal.type and goals.decide, so on studio's desktop Stage and Commit are drawn dimmed, with why under them, and never sent. As me, for the operator's own machines, acts as the operator, and the record still names the machine. > > Who a remote desktop acts as. > **Diagram: One layer, one grant.** A paired machine, studio, opens alpha:work:L1 in a pane and asks for one layer. On alpha the grants are read narrowest first: L1's own grant lets studio act there, L2's own lets it watch, and the machine's grant for the whole workspace is off. The narrowest grant that covers what was asked decides, so studio's desktop is admitted to L1 alone. It sees L1's view, conversations, terminals, files, evidence, offers, goals and git; Layer Zero and L2 are not there for it. Changing a grant closes a desktop it no longer covers. > > A desktop given one layer. ### Profiles and shares A machine shows its paired machines a profile: a name, a handle, a status, a bio, a time zone, an avatar and a banner. Its avatar and banner are kept in the machine's [Drive](manual.html#share-layer) and checked by hash. ``` picache profile set name "Studio" picache profile avatar ~/me.png picache shares add layer L1 request # paired machines may ask for L1 ``` A machine also shows whatever you share: a workspace's desktop, a layer, a terminal, a room, an agent or an app. A share is asked for, approved and revoked like a pairing. A shared workspace, layer or terminal then has **◎ Open** on its card. The Machines pane does all of this too, and runs the same commands underneath. ### Issues An **issue** is a thread in the Inbox that you and your agents write in, like mail. | List | Holds | |---|---| | **Waiting on you** | Every decision waiting on you, as the rail's NEEDS YOU lists them | | **Issues** | Open issues | | **Conversations** | What agents wrote to each other, and to you or Picache ([below](manual.html#messages)) | | **Offers** | Offers between layers | | **Done** | Issues done or declined | Start an issue with **New issue** in Actions (`F1`) or the Inbox menu, or press `n` in the Inbox. Then type in the deck at the bottom of the screen. The first line is the title, and the rest is the body, in Markdown. Each issue is numbered (`#12`) and tagged with your current layer. While an issue is on screen, what you type in the deck is a reply to it. **Done**, **Decline** and **Reopen** move it between lists, and the thread notes each move. ● marks an issue an agent answered since you last read it. Agents write from a shell: ``` picache issue # the open issues echo "Login fails on Safari." | picache issue new "Safari login" picache reply 12 "Fixed in the session cookie." ``` An agent sees only the issues tagged with its own layer and the ones assigned to it. What it writes shows its name and the end of its pane's run id (`@claude·3f2a`), and is marked unverified. Only you can move or retag an issue. > **Diagram: An issue sent to another machine.** On the machine laptop you send issue 12 to the machine studio. It goes as an issue offer, written to the outbox first and retried while studio is away, and arrives in studio's Inbox as mail does: open and unread, numbered 4 there, from laptop, with no accept step. Replies on either side reach the other as issue events, an agent's marked unverified. Only studio moves it: Done or Decline there comes back, and laptop's copy follows and says they decide its state. Each side keeps its own thread. > > An issue sent to a paired machine. ### Send an issue to another machine To send an issue to a linked workspace or paired machine, use **Send to…** on an open issue, or: ``` picache issue send 12 studio ``` The issue arrives in the other Inbox like mail, open and unread, and marked with where it came from (*from laptop*). The receiving side has no step to take it in, because pairing or linking already gave consent. A layer's own list (`picache accepts`) can still refuse issues, and a linked workspace takes none unless its link allows `issue`. Each side keeps its own thread with its own number, and every reply reaches the other side. An agent's reply is still marked unverified and names its machine (`@codex·91c0 (studio)`). The receiving side moves the issue, and the sender's copy follows. If a machine can't be reached, the issue stays queued and is retried, and the issue reader shows that it is waiting. Only this machine's own desktops and As me desktops write in a shared issue. Another machine's desktop attached here with any other grant can only read it. ### Agents write to each other An agent in one pane can write to an agent in another, the way Claude Code's SendMessage writes to another session. Writing takes one command, and the message reaches that one pane, not every agent of that kind: ``` picache message who # the agent panes you can write to picache message codex·91be "The API is ready on main." picache message read # what waits for you, now ``` An agent is named by its vendor and the end of its pane's run id (`codex·91be`), or by the vendor alone (`codex`) when only one is open. You can write the same way from your own shell, to any agent. > **Diagram: How a message reaches an agent.** The agent claude·3f2a runs picache message codex·91be. cherry π keeps the message in a conversation in the Inbox, between the two of them, and queues it for that one pane run; codex·91be's header shows an envelope and a count. The agent's own hooks hand it over: when its next turn starts, when its turn ends (the Stop is held with the message), or when it runs picache message read. An agent idle at its prompt fires no hook, so cherry π types one line into it once you pause there. A message is information, not an order, delivered once, to one exact pane. > > One message, one pane, handed over by that agent's own hooks. Each message carries a type. An answer names the question it answers, and a handoff names what to take up. | Type | For | |---|---| | `message` | Anything (the default) | | `question` | Something the other agent should answer | | `answer` | An answer, with `--re ` naming the question | | `handoff` | A result for the other to take up, with `--ref` a branch, commit, path or goal | | `blocked` | The sender can't go on without something | | `done` | The sender finished what it was asked | ``` picache message codex "Is the build green?" --type question picache message claude "Green on main." --type answer --re ev_81c2 picache message codex - --type handoff --ref fix/parser < notes.md ``` `picache message` on its own lists your conversations, and `picache message show codex` shows one. An agent that opened another agent's pane also hears from cherry π about that pane, the way herdr tells a parent. It hears *done* when the other agent finishes a turn (with what it last said), *needs you* when it waits on your permission, and *exited* when it exits. Each message spends the sender's budget for offers, so two agents can't keep writing to each other forever. Reading spends nothing. Waking an idle agent The agent's own hooks hand a message over: when its next turn starts, when its turn ends, or when it runs `picache message read`. An agent sitting at its prompt fires no hook, so on its own it would get a message only when you next type to it. To avoid that wait, cherry π types one short line into its pane, which starts its turn: ``` [cherry-pi] message from @claude·3f2a (in your context) ``` Like herdr's `hc send`, the wake waits for you. If you're typing in that pane, it waits until you stop. > **Diagram: Waking an idle agent.** A message arrives for codex, which is idle at its prompt. You are typing in its pane, so its header says the envelope waits after pause. Once three seconds pass without a key there, cherry π types one line and Enter. Its turn starts, and its hook hands the message over. It never types into an agent that waits on you or a pane that has exited; after two minutes without a pause it gives up, and the message waits for the next prompt. A second message while one wake waits types nothing more. > > A wake waits for you to pause, and never types into a pane that waits on you. If that agent's hooks have never been seen handing a message over, the line asks it to run `picache message read` instead. Tell Picache, and hear back You can also leave the command to Picache. Tell it in plain words, such as *ask codex to check the build*, and it writes the message itself, from your chat. The agent writes back with `picache message picache …`, and Picache's next turn in that chat has the answer. > **Diagram: Picache asks an agent, and hears back.** In a chat you ask Picache to have codex check the build. Picache writes to codex·91be from that chat, signed picache·a1b2. Codex's next turn starts with it. Codex answers with picache message picache, which reaches only a chat that wrote to it. Picache's next turn in that chat has the answer, and the chat shows it to you dimmed: from codex·91be, an answer, build is green. > > Picache writes from your chat. The answer comes back to that chat, and you see it too. An agent can write to a chat only after Picache has written to it from that chat, so no agent can start talking into your conversations. As an address, `picache` means the newest such chat, and `picache·a1b2` names a particular one. What you see | Where | What | |---|---| | An agent pane's header | `✉ 2` while two messages wait for its next turn, and `✉ after pause` while a wake waits for you to stop typing there. It clears when the agent takes them. | | **Inbox → Conversations** | Every conversation between agents, and with you or Picache, each message with its type (*question*, *answer to ev_81c2*, *handoff · fix/parser*) | | The deck, with a conversation selected | It says MESSAGE, and what you send goes to every agent in it whose pane is open | | A Picache chat | What an agent wrote back, dimmed, at the turn that took it. It stays in the chat's history and isn't fed into later turns | | The agent's own pane | Whatever that vendor's app draws for context its hooks add, and the wake line | Who may write to whom By default an agent writes only to agents in its own layer. Each layer can let other layers in, or keep them out, for itself: ``` picache layer messages from web # this layer takes web's agents' messages too picache layer messages none # its own agents only picache layer messages any # any layer's picache layer messages default # as the workspace says picache layer messages # what it says now ``` > **Diagram: Who may write to whom.** Three layers: web with claude, api with codex, docs with gemini. Within a layer agents write to each other. The api layer ran picache layer messages from web, so claude in web writes to codex in api, but gemini in docs is refused. The workspace's inbox.messages is layer by default, or workspace, or off; a layer's own word narrows or widens it, off stays off, and you write to any agent. > > The receiving layer decides who writes into it. | Setting | Default | What it does | |---|---|---| | `inbox.messages` | layer | Whom an agent may write to: its own layer, any agent pane here (`workspace`), or no one (`off`) | | `inbox.message_notes` | on | An agent hears when an agent pane it opened finishes a turn, waits on you, or exits | | `inbox.message_wake` | idle | An idle agent is woken with one line once you pause in its pane. `off` leaves the message for its next prompt | Picache has the same commands as tools (`message` and `messages`, in the `inbox` group), and so does an agent given the verbs over MCP. The web desktop doesn't wake an idle agent, so a message to one there waits for its next prompt. How a vendor's app draws what its hooks add is up to that vendor. Messages stay on one machine. To reach another machine, send an issue ([above](manual.html#send-issue)). ### Rooms and the Bakery Rooms are for people on paired machines, across the internet. - Each message is signed by the machine that wrote it. - It's accepted only from a member. - It waits in an outbox while a machine is away. Beside each room is a side room with the same people, where agents may be asked in. An agent named there with `@` answers once. It reads the room's last few messages and its own folder, and it changes nothing. The Bakery is public: a set of rooms on rednet, joined by name like IRC, and off until you join. Each person goes by a name generated from their key. Nothing is kept unless you choose, and agents are never allowed. Rooms carry messages between people. A single layer can be kept in step with another machine ([section 01](manual.html#layer-stores)), but a whole workspace can't. ## 09 / Confinement and .pi apps A **capsule** is a confined process that can talk only to the harness. Every capsule starts through a single entry point, which this manual calls the door. The door picks the strongest confinement the machine has. A machine that can't confine runs no capsules, and says why. | Platform | Confinement | |---|---| | macOS | Seatbelt | | Linux | bubblewrap where it starts, otherwise Landlock | Capability checks govern what a capsule asks of the harness. The confinement limits what it can do on its own. ### The signer and the P2P sidecar Two processes start outside the door: the signer, which holds the keys, and the P2P sidecar, which faces the network. The sidecar has a box of its own, apart from the signer's keys. It can write only in its own folder. The only local sockets it can reach are those in its own socket folder (its own, each workspace's remote-desktop socket and the ones desktops dial from) and the system's name lookup. - On macOS, the box is Seatbelt. - On Linux, it's bubblewrap. On kernel 6.12 and later, Landlock stops the sidecar from connecting to the host's abstract Unix sockets, though it can still bind a name there. Otherwise it gets a network of its own behind `pasta`, which keeps it from them entirely. With neither, it's boxed but can reach those sockets, and its log says so. - Where only Landlock is available, the sidecar runs unboxed and logs that. > **Diagram: One door, or nothing.** Agents, scripts, app handlers, clean rooms and capsules from paired machines all pass through one door. On a machine that can confine they run confined, by Seatbelt on macOS or bubblewrap or Landlock on Linux. On a machine that cannot, they are refused with the reason. A capsule has no unconfined path. The signer and the P2P sidecar are not capsules and start outside the door; the sidecar has a box of its own. > > Everything that runs confined starts through one door. > **Diagram: The P2P sidecar's own box.** The signer, which holds the keys, starts the P2P sidecar through cherrypi.ape p2p-box, apart from itself. On macOS the box is Seatbelt. On Linux, bubblewrap binds the sidecar's own socket folder whole, never the folder that holds the signer's socket. The sidecar can't connect to the host's abstract Unix sockets: through scope, keeping the host's network, where the kernel has Landlock's scope (6.12 and later), otherwise by a network of its own behind pasta. With neither, it is boxed but can reach them, and its log says so. Under Landlock alone the sidecar runs unboxed, and its log says so. > > The P2P sidecar is boxed apart from the signer's keys. Hosting capsules for other machines is opt-in, and a host that can't confine refuses them. ### Capsule profiles A capsule is defined by a named profile with five parts. Both the operating system's boundary and the harness's seal (the verbs the capsule may ask the harness for, fixed when it starts) are built from that one profile, so the two can't disagree. | Part | Decides | For example | |---|---|---| | image | What runs inside | cherry π's own APE running Lua, an agent's CLI, or a command | | ground | The files it sees | Sealed (copies only), one folder, or the pane's folder and named read-only roots | | network | Where it can connect | Nowhere, a proxy with a list, or open (labelled as such) | | broker | What it may ask the harness for | Verbs sealed at start: a script's are its caller's tools, a clean room's none | | limits | How much | Seconds, CPU time and file size. A script's budget also caps its calls, writes and runs. | The seal can only narrow what a capsule may do. A request outside it is refused, and a request inside it still goes through the policy for its verb. A capsule can never do more than whoever started it could do directly, and it ends when whoever started it ends. If the harness itself dies first, a clean room ([below](manual.html#clean-room)) on macOS, or under Landlock on Linux, runs on, still confined, until cherry π next opens. Then it is stopped. Its out/ folder is kept and its copies of the inputs are removed. Each capsule's record names the confinement that held it, which is not always the one that was asked for. ### Run a clean room A clean room runs a command, or a Lua script with `--lua`, on copies of files. Use it to open an archive you were sent, run a repository's tests offline, or convert a document. ``` picache cleanroom invoice.zip -- unzip invoice.zip -d out/ picache cleanroom notes.docx --for 15m --tools -- pandoc notes.docx -o out/notes.md picache cleanroom data.csv --lua summarize.lua ``` - It has no network, and no view of your folder, your home or cherry π's state. - What it writes to `out/` is kept in cherry π's state, and the reply lists it with its path. Everything else is thrown away, and the originals are never touched. - It runs for five minutes by default. You can set another time with `--for`, and anything longer than an hour is cut to an hour. - It can use the system's own tools. With `--tools` it can also use what Homebrew installed, read-only. > **Diagram: A clean room, from copy to out/.** picache cleanroom copies its inputs, which must be inside your folder, into a capsule's ground: a folder of its own. The command runs there with no network and nothing to ask the harness, under Seatbelt on macOS, or bubblewrap or Landlock on Linux. It lives five minutes by default and an hour at most. When it ends, out/ is kept in cherry π's state and listed in the reply, with the end of its log and its exit; the capsules chain records its start and end; everything else is deleted. Your originals are never touched. With no way to confine here, the run is refused, never run unconfined. > > A clean room takes copies in and gives out/ back. Nothing else passes in either direction. Picache also offers the clean room as a tool. In the Capsules pane, **Run one** starts a capsule for a purpose: one prompt and its answer, a room for inspecting files, or a fifteen-minute agent-code session. The capsule runs on this machine, or on a paired machine when this one can't confine. An interactive capsule runs only on this machine. ### Agent boxes A vendor agent such as Claude Code, Codex or Pi starts confined in a box by default. The whole CLI runs inside it, including its hooks, its file tools and the MCP servers it starts. ``` picache box claude # its box settings picache box claude launch standard # or confined, the default ``` | The agent can | The agent can't | |---|---| | Write its pane's folder and its own configuration | Read the rest of your home | | Read its own install | Read cherry π's state | | Use its terminal and `picache` | Become root ([Use sudo](manual.html#sudo)) | Launch is a per-agent setting with two values, `confined` and `standard`, and only you can change it. A standard launch starts the agent without the box, and its record says so. When an agent starts, cherry π looks through its folder for cherry π state and repositories. It goes at most 8 folders deep, through at most 100,000 entries, for at most five seconds. In a `node_modules` folder it checks each package but does not walk every file inside it, so a `.git` or cherry π state deeper inside a package isn't found. If cherry π can't look through the whole folder, or can't open a folder inside it, the agent won't start in a box there. Start it in a smaller folder, or launch it standard. On Linux, the box hides the cherry π state it knows of, and any it finds in the agent's folder at the start. State made there later isn't hidden. On macOS, an agent can commit, merge and stash in the repositories its folder holds at the start. It can't change their config or hooks, or make a `.git` anywhere in its folder. So `git init`, cloning into its folder and updating submodules fail there. Do those yourself, outside the box. It also can't rebase. Both kinds of rebase (including `--apply`), `git am` and a cherry-pick of several commits fail in the box. Each would leave state behind that your next `git rebase --continue` or `git am --continue` would pick up and carry on with as you. Rebase outside the box. On Linux, those repositories are read-only, so the agent can't commit. It can make a new repository in its folder, though. When you run git in that repository, git runs its hooks and settings, such as `core.fsmonitor`. Check a repository an agent made before you run git in it. On either system, an agent can leave a bare repository in its folder. Git accepts one in any folder, with no `.git` in its path, and the box doesn't catch it. Run `git config --global safe.bareRepository explicit` so your git won't use one unless you name it. On macOS, when a boxed agent ends, cherry π waits up to three seconds for what it started to finish, and stops the rest. Then it looks again through the places in the agent's repositories where git writes, for a link or a file with a second name (a hard link). Your git would write through such a file as you. If it finds any, the agent's terminal shows each path: ``` cherry-pi: the agent's box left in its repository what your git would write through, as you, and git does not make. Look at each and remove it before you run git there: /path/to/project/.git/logs/refs/heads/x/main ``` The same text goes into `git-sweep.txt` in the agent's run folder, under `confine/agents/` in cherry π's state. Remove each path before you run git there. The agent's next box in that folder won't start until you do. Before it makes its own git writes in a repository that a box guarded, cherry π looks there too. Its own writes are staging or committing in the Changes pane, and accepting a goal. If it finds anything, the write is refused with `confine_git_sweep`, naming each path. Git you run yourself isn't checked, so don't run git in an agent's repositories while the agent runs. When the agent ends, cherry π doesn't wait for a process the agent started in a process group of its own. The first look at a repository goes through at most 200,000 entries, for at most five seconds. After a look that finds nothing, cherry π looks again only in folders that have changed since. A look that is cut short counts as finding something. When an agent won't start in its box, it says why. These codes concern its repositories: - `confine_agent_gitdir`: a `.git` file or link, or a repository's `commondir`, leads to a place in its folder the agent could make or swap. Such a place is a folder that doesn't exist yet, a file, a path through a link, or the agent's folder itself. Your git would use whatever the agent put there. Point the `.git` at the repository itself, or remove it. - `confine_agent_git_link`: on Linux, a `.git` in its folder is a link. The box keeps what it leads to read-only, but not the link, which the agent could swap. Replace it with a `.git` file that says `gitdir: `. - `confine_agent_filesystem`: on macOS, its folder isn't on APFS, or its file system can't be identified. The box matches names the way APFS finds them. On HFS+, for example, git finds `.git` under names the box doesn't match. Use a folder on APFS. - `confine_agent_git_sweep`: a repository in its folder still holds what an earlier box left, as described above. The message names each path. Remove them. For any of these codes, you can also launch the agent standard. > **Diagram: Box the agent, not its commands.** Left: an agent's own sandbox boxes only its shell commands; the agent, its hooks, MCP servers and login stay outside, so a hook can read ~/.ssh. Right: cherry π boxes the whole agent with its hooks and MCP servers; only its folder, its config and its own temp folder are writable, and the same hook stops at the box wall. > > The agent's own sandbox is drawn dashed, and cherry π's box solid. ### Network access When an agent's network setting is `ask` or `allowlist`, the only thing it can reach from the box is a proxy that cherry π runs outside it. The proxy carries connections to what the agent's list names. ``` picache box claude allow registry.npmjs.org # add a rule picache box claude allow '*.githubusercontent.com:443' picache box claude unallow registry.npmjs.org picache box claude network ask # for anything else ``` | `network` | For a host not on the list | |---|---| | `ask` | You're asked (the default) | | `allowlist` | Refused | | `open` | Allowed, with no list (below) | Each list starts with the vendor's own endpoint, measured under the box. Names that resolve to this machine or a private network are refused unless a rule names that address. The proxy decides by host name. It carries HTTPS connections, which it never reads inside, and plain HTTP requests. When you're asked, your answers are kept as receipts, and the proxy's log keeps the rest. With `open` there is no list. A proxy runs only when the agent's key is injected, to carry its vendor's hosts to the injector. The agent can reach this machine's loopback and your local network, and it can listen on TCP ports. It can also reach other agents' proxies on 127.0.0.1, which don't check who is calling. If another agent has its key injected ([Keep the key out of the box](manual.html#keys-out)), an `open` agent can send requests through that agent's proxy, and they go out with the other agent's key. On Linux, `open` needs kernel 6.12 or later, where Landlock stops the agent from connecting to this machine's abstract Unix sockets. An X server's socket, for example, takes keystrokes. On an older kernel, an `open` box is refused. > **Diagram: The network is a list.** In ask mode, the agent's only way out of the box is a proxy outside it. A listed host goes through. An unlisted one is put to the operator, who answers always, and it joins the list. A name that resolves to a private address is refused. A connection around the proxy has no route. Your answer is a receipt; the proxy logs the rest; a connection around it is recorded nowhere, because it never leaves the box. > > An agent's network in ask mode, the default. ### Sign-in in the box Claude Code keeps its macOS login in the Keychain, which the box can't reach, so in the box it keeps its login in a file in its own config. **Sign in…** on the agent's Profile menu runs its own sign-in command in its box, with the network open for that command alone. **Token…** keeps a token from `claude setup-token` sealed instead. On Linux the box is bubblewrap. With `ask` or `allowlist`, the proxy is bridged into the agent's own network namespace. An agent that hasn't been measured under bubblewrap won't start in a box there. The pane says why, and you can launch it standard. The box has two limits: - A confined agent can read and write a config file that a dotfile manager links from elsewhere. - The box shares the kernel with the rest of the machine. It isn't a virtual machine. ### Keep the key out of the box A key handed into the box sits in the box's environment. Anything the agent runs can read it, such as a test runner, a package's install script or an MCP server. ``` picache box claude credentials inject # a stand-in goes in the box picache box claude credentials hand-in # the real key goes in the box ``` When an agent's **Key** setting is `inject`, it gets a stand-in key of the same shape (`sk-ant-oat01-cherrypi-…`) and starts as usual. 1. When it calls its vendor's API, the proxy hands that connection to cherry π's signer, outside the box. 2. The signer, which already holds the key sealed, opens it and swaps it in for the stand-in. 3. It sends the request on to the vendor over verified TLS. The answer streams back unchanged. The key is never in the box, and outside it only the signer opens it. > **Diagram: The key stays out of the box.** Inside the box the agent holds a stand-in key and trusts the run's certificate authority. Its request to api.anthropic.com leaves the box carrying the stand-in, goes through the proxy to the signer's injector outside the box, which opens the real key, swaps it in, and sends the request to Anthropic. A script in the box that reads the stand-in gets nothing it can use, and a request carrying any other key is refused. > > Claude Code's request to its API, with `credentials inject` set. Until an agent has run injected on this machine, the default is hand-in ([below](manual.html#keys-out)). The agent trusts a certificate made for this run alone, which can vouch only for its vendor's hosts. That certificate's private key is deleted as soon as the run's certificates exist. The signer's injector ends when the agent does. ### Which sign-ins can be injected Claude Code can be injected when its token or API key is kept in cherry π. With inject set, the login file in its config is hidden from the box, so the real key isn't beside the stand-in. Codex can be injected when it uses an API key from Keys. Its ChatGPT sign-in can't be, because Codex renews that sign-in with a token in the request body and the injector changes only headers. With inject set, that sign-in's file is hidden from the box and Codex uses the API key. To use the ChatGPT sign-in, choose hand-in. You give the agent its key the same way for both, with **Token…** on its Profile menu or a key in Keys. Choose between inject and hand-in with the **Key** row in the agent's Profile, or with the commands above. An agent defaults to `hand-in` until it has run injected on this machine, and to `inject` after that. If the setting is inject and a launch can't inject, the launch is refused with the reason. It does not fall back to handing the key in without saying so. ### Where your secrets go A key, a token or a password is opened only where it's used. Everything else gets its name, its last four characters, or nothing. > **Diagram: Where a key goes.** A key comes in three ways: typed masked into the Keys pane, piped to picache box for an agent's token, or piped to picache app set for an app's secret. The signer seals it into the credentials store, 0600, under its own root key. It's opened only into one of four places: the harness's request to a provider, an agent's box when its key is handed in, the signer's injector when it's injected (never the box), and the call that hands an app its own secret. Receipts, hooks, cherrypi.db, command lines, the P2P sidecar, the program that opens a browser and the key log never hold it. The Keys pane shows the last four, which the signer gives it. > > A key is kept in one store and opened by the signer alone, into one of four places where it is used. A token or an app's secret typed on the command line is refused, because it's then in your shell's history and in the list of running processes. Pipe it in instead: ``` pbpaste | picache box claude token pbpaste | picache app set my-app api_key picache app set my-app api_key --clear # forget it ``` Every `picache` call is recorded as a receipt, so what a receipt keeps passes through two filters. First, a verb that knows an argument is secret hides it. Then anything shaped like a key, wherever it appears, is cut down until only what kind of key it was remains. > **Diagram: What a receipt keeps.** A call's arguments pass two filters before they're receipted or handed to an app's hooks. First the verb's own: picache box claude token with a value becomes claude token and an ellipsis, and the call is refused. Then a shape filter over every argument and every detail: a known key prefix such as sk-ant- keeps the prefix and loses the rest, a bearer token loses its value, a variable named as a key keeps its name, and a password in a URL is cut. What's left goes to the receipt and the hooks. A verb whose whole answer is a secret, such as fleet secret, gives hooks no text at all. > > A call passes two filters before it is recorded. Each pane's `PICACHE_TOKEN` is that pane's authority. The pane's record keeps only a digest of the token, so a copy of `cherrypi.db` gives nobody a pane's rights. > **Diagram: A pane's token, across a restart.** The harness mints a token for a pane and puts it in the pane's environment; the program there calls picache with it. The pane's record in cherrypi.db keeps only the token's SHA-256 digest. When the harness restarts and adopts the running pane, it restores the digest; the first call whose token hashes to it gets the pane's identity back. The digest itself is not a token, and is refused. > > After a restart, the record lets a pane get its identity back, but the digest in it can't be used as the pane's token. Files that hold a secret, or might hold one, are open to your user alone: | File | Mode | Holds | |---|---|---| | `cherrypi.db`, its `-wal` and `-shm`, and its backups | 0600 | Receipts, pane records | | `secrets/credentials.db` | 0600, folder 0700 | Your keys, sealed | | `identity/` | 0700 | The signer's root key and identity keys | | A job's log | 0600 | What a job printed | | The key log (`CHEWY_KEYLOG`) | 0600 | Keys pressed, with typed text kept only as `` | Other processes get no more than they need. A capsule inherits no environment. The P2P sidecar gets none of the signer's variables and no variable named as a key, and neither does the program that opens a browser. On Linux, a pane that may use sudo starts through your systemd user session. The session reads the pane's variables from a file only your user can open, which is removed once it has been read, so no value appears on a command line. An agent's key is handed into its box by default until that agent has run injected on this machine ([Keep the key out of the box](manual.html#keys-out)). Text an agent types into a pane with `picache send` is filtered only by its shape, and a password has no shape the filter can recognise. ### How it compares Agents that ship a sandbox put the commands they run inside it and leave the agent itself outside. The box in cherry π holds the whole agent, for every vendor alike. Each row was checked against that tool's documentation on 18 October 2026: | Tool | What is boxed | On by default | Network | |---|---|---|---| | cherry π | The whole agent, any vendor, and every capsule | Yes | Per agent: ask, list only or open | | Claude Code | Its shell commands | No | A proxy whose domain list starts empty | | Codex CLI | The commands it spawns | Yes | On or off | | Gemini CLI | Tool calls, or the whole CLI in a container | No | Open, or a proxy you supply | | Cursor | Terminal commands | Not stated | A domain list, plus its defaults, or all | | Pi | Nothing in core. A community extension boxes its tools. | No | Rules, plus a reviewer per connection | | Docker Sandboxes | The whole agent, in a microVM | When used | A host proxy with policy modes | Compared with these tools, cherry π has no virtual-machine tier for hostile binaries. Like Docker Sandboxes, it can keep the key out of the agent's reach ([above](manual.html#keys-out)) when the agent's Key setting is inject. Sources: [Claude Code](https://code.claude.com/docs/en/sandboxing), [Codex](https://learn.chatgpt.com/docs/sandboxing), [Gemini CLI](https://geminicli.com/docs/cli/sandbox/), [Cursor](https://cursor.com/en-US/blog/agent-sandboxing), [pi-sandbox](https://pi.dev/packages/@erichll/pi-sandbox), [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/security/isolation/). ### Addons and .pi apps There are two tiers of extension: addons and .pi apps. The [Extensions](extensions.html) page covers both in full, along with Cellar Door's worlds. | Compared | Addons | .pi apps | |---|---|---| | Made by | cherry π (first-party) | Anyone | | Ship | Inside `cherrypi.ape`, with no folder to drop one into | As one `.pi` file | | Plug into | Registries the core owns: P2P backends, peer discovery, signers, model backends, knowledge, worlds, terminal carriers, verbs, goal gates, app providers | Verbs, tools, panes, terminal programs, hooks | | Run | In the harness, and may run a sidecar | In a capsule, holding only what they were granted | ``` picache addons # list them picache addons enable p2p_holepunch ``` The core itself is never a plugin. It is made up of authority and policy, receipts and the ledger, confinement, the addon loader and the verb registry. > **Diagram: A .pi app.** A .pi app is one SQLite file. Signed: its manifest, which holds the signature, its handlers, its assets and any other table it ships. Unsigned and written in place: the tables it declares for its own data, and a journal of every write, so its data never breaks its signature. Install: signed here or by a paired machine installs; anything else asks first; a file changed since it was signed is refused. Run: handlers run only in the app's capsule, with each capability granted by you, among them app.terminal, app.hook and appdata:own. Hooks may refuse, warn, note or suggest, and never approve, change or widen. > > A .pi app is one file. Its signature covers what was shipped and never the app's own data. A `.pi` file is one SQLite file that holds the whole app: its manifest (which holds its signature), its handlers and its assets. Outside the signature, it holds tables the app declares for its own data. Those tables are written through the harness's gate ([section 10](manual.html#app-work)), one statement at a time, and journaled. When a new version is installed over the old, they are carried over and the old file is kept as a backup. A version that changes a table's shape without a migration is refused before anything is written. An app signed by this machine or a paired one installs at once. Anything else, including an unsigned development folder, asks first and is marked as such wherever it shows. Each capability it asks for is granted separately. ### What an app can add An app can add: - `picache ` commands - tools for Picache - panes drawn from A2UI - its own program in a terminal pane, which needs a grant of its own, `app.terminal`, because a program there hears every key - hooks, which need `app.hook` A hook hears each call before it runs and its result afterwards. It may refuse a call, warn, add a note or make a suggestion, and whatever it does is labelled as the app's. A hook can never approve a call, change a call or its result, or answer a question cherry π asks you. A hook that is slow or fails counts as silent. Meta, the first app, inspects and packages `.pi` files, including itself, and asks you each time it signs. After a crash, an app's work is recovered the same way as a tool's. Work that takes longer than one call runs as a loop the harness steps, and never as app code left running ([section 10](manual.html#app-work)). ### An app's own data, on every machine An app with `appdata:own` keeps rows in the tables it declares. Its pages in Cherrytop, the browser desktop, read and write those rows by revision. A write names the revision it read. If another window wrote first, the write is refused and the reply gives the revision that is there now, so nothing is overwritten unseen. Every other page of the app is told to read again. > **Diagram: An app's rows on two machines.** On this machine a page of notes.pi writes a row by its revision. The change is kept in the app's outbox in the same write. Once you've shared the app with a paired machine and approved its request, the two send each other changes every two seconds, and at once after a page writes, as rows, never as SQL. The paired machine applies the row and its pages read again. When both machines wrote the same row, every machine keeps the same one, the later write; the other is kept as a conflict and you're told in the Inbox. > > Both machines keep the same row, and the losing write is kept as a conflict. To keep an app's rows the same on a paired machine that has the same app installed: 1. Share it here: `picache shares add app `. 2. Ask for it there: `picache shares ask `. 3. Approve the request here: `picache shares approve `. From then on each side sends the other its changes. To see what syncs, run `picache app sync`. To see the writes that lost a conflict, run `picache app conflicts `. To stop syncing, run `picache shares revoke `. Only the two apps linked by the share, signed by the same publisher, take each other's rows. ## 10 / Recovery and evidence After a crash, cherry π can say what happened and what didn't, because it keeps two records apart: how far a task got, and whether its effect changed anything. | Record | Values | Says | |---|---|---| | A task's status | queued, running, blocked, completed, failed, cancelled | How far the work got | | An effect's outcome | `applied`, `not_applied`, `uncertain` | Whether the world changed | A step that ran hasn't necessarily had its effect. ### Settle an effect in doubt > **Diagram: An effect in doubt.** Before a tool runs, its intent is saved; then the call runs. Its effect ends applied, not applied, or uncertain, which is what a crash in the middle of the call leaves. An uncertain effect blocks a retry until someone settles it: you decide in Actions, Review recovery, with Mark Applied, Mark Not Applied, Retry Safely (offered only when repeating is harmless) or Stop. After a crash, running tasks go back to queued, a resume is a new turn, and work in doubt never reruns. A decision is checked against a fingerprint of what you were shown and committed to the chain. > > Three-valued effects and the retry gate. The harness writes down the intended effect before the call runs. If the call is cut off midway, its effect is left uncertain, along with any output it had saved. An uncertain effect blocks a retry until someone settles it. Actions → Review recovery lists the waiting work of every conversation, including the ones you don't have open. It shows what's in doubt and the output saved before the stop, then offers the harness's choices: Mark Applied, Mark Not Applied, Retry Safely (offered only when repeating is harmless) or Stop. Your choice is checked against a fingerprint of what you were shown. If the view changed under you, nothing is done and you're told so. ### Resume interrupted work When a harness opens, any work it finds still marked as running goes back in the queue, whatever the recovery mode (below). This restores the record and reruns nothing. Running the work again is another matter, because its effect may already have happened. Who resumes it is a setting, Actions → Recovery mode, and the recovery list's title shows the current mode. | Mode | Resuming | |---|---| | Manual | A person, task by task | | Supervised | A person: the default | | Automatic | Unattended, within a budget, gated | A resume is a new turn. The old ids and receipts stay as the record of the interruption. A task holding an effect in doubt is refused. It is neither resumed nor skipped. - The new turn picks up where the old one stopped, in the same folder and under the same limits, and never beside a running turn. - It's told what the interrupted turn already changed and how that came out, so it doesn't repeat it. - The old task is marked as resumed by the new turn, so it isn't offered again. Actions → Resume all ready runs the ready queue up to a budget and reports each outcome, as in `2 resumed · 1 deferred · 1 in doubt`. A command that Picache runs is a job (see [Jobs](manual.html#jobs) below). The job is tied to the call that started it before that call begins waiting on it. If the harness dies during the wait, the job keeps running and writes its own exit status. When the harness opens again, an effect whose job has exited is settled from that record and the job's output, the same way you would settle it. If the job is still running, the effect stays in doubt until the job ends. If it was cancelled or is gone, the effect is left for you to settle. ### Apps after a crash > **Diagram: An app's work after a crash.** An app's handler runs in its capsule and reaches nothing, so each thing it changes it asks the harness for. Each ask passes one gate: the capability granted to the app, your answer when it must ask you, and, inside a turn, the write saved on the turn's task before it runs. The write ends applied, not applied, or uncertain, which a crash in the middle leaves; an uncertain one names the app in Recovery, and the app is not upgraded or removed until you settle it. Work longer than one call is a loop run the app starts under ai.redloop.manage: its own definitions, a layer named by id, asked of you, stepped and recovered by the harness. > > An app's asks and the retry gate. An app's handler runs in its capsule ([section 09](manual.html#capsules)) and can reach nothing from there. It asks the harness for every change, one at a time, even changes to its own tables. Inside a turn, each ask is saved on the turn's task before it runs, like Picache's own calls ([section 03](manual.html#turn)). That holds whether the ask comes from a tool, a command or a hook. A crash midway leaves the ask uncertain, so it is never done twice without anyone noticing. Review recovery names the app and what it asked. The resumed turn is told whose write it was and goes on as a new turn. The app's call is not revived. Until you settle it, the app can't be upgraded or removed, so the version you are deciding about is always still there. Work that takes longer than one call never runs inside the app. The app starts a loop run instead, and the harness steps it, saves between steps and recovers it like any other run ([section 04](manual.html#loops)). Two grants control this. | Grant | Lets an app | |---|---| | `ai.redloop.manage` | Write and delete its own loop definitions, and start, continue or stop its runs | | `ai.redloop.read` | Read loop definitions and its own runs | The app names its definitions under its own id and names a layer by id, never by path. The answers it gets contain no folders or file paths. You're asked each time the app writes or deletes a definition, or starts or continues a run, and it sees only its own runs. The approvals inside a run stay yours, and so does the scheduler. ### Jobs Long work is a job: downloads, tool installs, conversions, captures, indexing a layer's files, and the commands Picache runs. A job is a durable record of its kind, state, phase, exact bytes, rate and log. The record is updated as the job changes, and each change is announced as an event. - Many run at once. - Each can be cancelled, and a download can also be paused and resumed. - Each survives a restart. - A finished download is checked against its pinned SHA-256 before it is moved into place. Panes only show jobs. That is why the Downloads and Tools panes never disagree about how far a job has got. ### Runs Every terminal is a run, and every run has a record. The Runs pane keeps these records across restarts. Each one says: - which agent or program, in which layer and folder; - when it started; - each time it waited on you, and why; - how it ended: closed by you, exited with its code, or ended when cherry π closed. ### The evidence chain Receipts are stored as rows. The ledger is separate, and it holds commitments. Each governed turn commits one ledger entry, `turn_receipt`, in the same transaction as its last receipt. The entry's digest binds every receipt the turn wrote. A file operation commits an entry of its own, and so does settling an effect in doubt. The entries form a hash chain. Each entry's id is a BLAKE2b-256 hash of its body, and each body names the entry before it. The newest entry therefore commits everything before it, and changing a covered receipt breaks verification. > **Diagram: Evidence that travels with its layer.** The workspace's chain, in cherrypi.db, runs genesis, a turn, an anchor for layer L1, a file operation, an anchor for layer L2, and its tip. Layer L1's own chain, in its own store, runs genesis, a turn, a turn and an export. L1's genesis names the workspace's tip when it began, and the workspace's anchor names L1's tip. Verify evidence chain checks every chain entry by entry and each layer against its anchor, with BLAKE2b-256 per entry. Each entry hashes the one before it, and a layer carries its own chain when it leaves. > > The workspace chain and a layer's chain, anchored both ways. Each layer keeps its own chain in its own store, and the workspace's chain anchors it. The layer's first entry names the workspace's newest entry, and the workspace records an anchor that names the layer's newest entry. A layer that leaves the workspace takes evidence with it that verifies on its own. Actions → Verify evidence chain checks every chain and anchor, and says where a chain broke. Actions → Seal this session records a receipt in the chain that is bound to a conversation's messages and receipts. Seals are unsigned. A seal proves only that the covered rows haven't changed since it was made. It isn't third-party attestation, and it proves nothing against an owner who rewrites the whole store. ## 11 / In a browser: the web desktop and .pi names `cherrypi -browser` opens Cherrytop, this machine's web desktop, in your browser, signed in, starting its web server first if needed. `cherrypi.ape --browser` opens the lander instead. ``` cherrypi -browser # Cherrytop; -web and -gui do the same cherrypi.ape --browser # the lander cherrypi.ape names install # serve it at https://cherry.pi cherrypi.ape web status ``` The lander is the cherry mark on black. Click it and you hear a short boot sound, then Cherrytop opens. - `/site/` is this website, which the APE serves itself with no network involved. - `/guide/` is the build's own docs. - `/machine` lists the workspaces and the apps served here. There's one web server per machine, and it belongs to no workspace. It listens only on this machine's loopback and answers only the names it serves. A request for any other name is refused, so a page elsewhere can't point its own name at the server and read what it serves. Its pages are open to read. The web desktop and its APIs need a browser that you have signed in (see [Sign a browser in](manual.html#web-signin)). Before you [install the names](manual.html#install-names), the lander is at `http://127.0.0.1:3141`. > **Diagram: One request to https://my-game.pi.** Four steps, all on this machine. One: your browser asks where my-game.pi is. Two: the system sends every question under .pi (on macOS /etc/resolver/pi, on Linux a drop-in for systemd-resolved or NetworkManager's dnsmasq) to cherry π's DNS on 127.0.0.2 port 5314, which answers A 127.0.0.2 for any .pi name and nothing else, forwarding nothing. Three: 127.0.0.2 port 443, and port 80, go to the web server's port 3141: on macOS by two pf rules in the anchor com.apple/250.cherrypi, on Linux by a systemd socket. Four: the web server, loopback only and one per machine, completes TLS with my-game.pi's own certificate, chosen by SNI, and the Host picks what answers: cherry.pi gets the lander, Cherrytop, the website and the guide; my-game.pi gets its own files, read-only, at its own origin; any other name is refused with 421. > > How a `.pi` name reaches this machine's web server. Every step stays on this machine. ### Install the names ``` cherrypi.ape names script # read what will run as root cherrypi.ape names install # asks for your password (twice on macOS) cherrypi.ape names status # check it as a browser would ``` `names install` puts the lander at `https://cherry.pi`, with no port in the address. On Linux it asks for your password once. On macOS it asks twice: once for administrator rights, and once more to trust the certificate authority in your login keychain. On macOS it asks in a dialog, then puts four things in place: 1. The address `127.0.0.2` on the loopback, so cherry π's names leave `127.0.0.1:443` to whatever else on this machine already uses it. 2. Two rules for pf, the macOS packet filter, that send that address's ports 443 and 80 to the web server. They load under an anchor macOS already provides, so `/etc/pf.conf` is never edited. 3. `/etc/resolver/pi`, which sends lookups for `.pi` names, and only those, to cherry π's DNS. 4. A LaunchDaemon, `pi.cherry.names`, that restores the address and the rules at boot. Then it asks to trust this machine's certificate authority in your login keychain. ### Install the names on Linux On Linux, sudo asks for your password once, in the same terminal. Lookups for `.pi` names, and nothing else, go to cherry π's DNS through the system's own resolver. The files that do this belong to cherry π, and which ones it adds depends on the system: - Where systemd-resolved runs (Ubuntu, Fedora), a drop-in for it. - Where NetworkManager writes `/etc/resolv.conf` instead (Debian's desktop), drop-ins that turn on its dnsmasq and forward `.pi` names. A systemd socket passes ports 443 and 80 on `127.0.0.2` to the web server, and it does so at boot too. This machine's certificate authority (see [A certificate authority that can only sign .pi](manual.html#pi-ca)) goes into the system's certificate store (`update-ca-certificates` or `update-ca-trust`). Your own NetworkManager settings are never edited. If you set `dns=none` or `rc-manager=unmanaged` yourself, install leaves it alone and tells you. On macOS and Linux alike, browsers with their own certificate store (Firefox everywhere, Chrome and Chromium on Linux) get the authority too, through NSS's `certutil`. If `certutil` is missing, install names the package that has it. `names status` explains any failure. What was installed is recorded in `~/.cherrypi/web/names.json`. ### A certificate authority that can only sign .pi > **Diagram: This machine's CA, and what it may sign.** Each machine makes its own certificate authority with the system's openssl. It may sign names under .pi only, and no authority below it; its key is kept with owner-only permissions in ~/.cherrypi/web/tls, and you trust it once: in your login keychain on macOS, in the system's store on Linux. It issues one certificate per name: cherry.pi, and one for each served app such as my-game.pi, each for 825 days. A certificate for any name outside .pi, such as bank.com, falls outside its constraint and is refused. No private key ships in the APE, and no *.pi wildcard is used, since browsers refuse a wildcard directly under a top-level name. > > A certificate authority per machine, bounded by a name constraint, so trusting it trusts nothing outside `.pi`. Each machine makes its own certificate authority the first time a name needs a certificate, and that authority is constrained to `.pi`. It can sign `cherry.pi` and `my-game.pi`, and browsers refuse any certificate it signs for another name. Its key, which only you can read, therefore can't vouch for a site outside `.pi`, such as your bank's. `names install` prints its SHA-256, so you can check it before you trust it. The web server picks each name's certificate as the connection opens, and sends a plain `http://` request on to `https://`. Each certificate lasts 825 days, and the server issues a new one 30 days before it ends, so you never need to renew one yourself. ### Serve an app's pages > **Diagram: Serving an app at its own name.** An app's manifest names the folder of its assets that is its site, web = { root = "site" }, and asks for the capability domain.serve. You, and only you, type picache app serve my-game. That grants domain.serve for good, records the app in ~/.cherrypi/web/served.json for the web server to read, issues my-game.pi's certificate from this machine's authority, and restarts the web server if it is running. Unserving it, denying it domain.serve or removing the app takes it out again. > > Serving is off by default. You turn it on yourself, one app at a time. A [.pi app](manual.html#apps) can have pages. In its manifest, `web = { root = "site" }` names the folder of assets that is its site. It may name an `index` other than `index.html`. ``` picache app serve my-game # https://my-game.pi picache app unserve my-game ``` The app must ask for the capability `domain.serve`, and nothing is served until you run `app serve`. An agent can't run it for you. Its files are then served read-only at `https://.pi`, straight from its `.pi`, or from its `assets/` folder while you are developing it. Each app has its own origin, so no app's page can read another's or the lander's. Its pages may run their own scripts, but they can't load anything from elsewhere. An app without pages gets a card at that address instead, showing its name, description, version and signer. Names belong to the machine, and these rules apply: - `cherry.pi` is reserved, and so is `picache`, the id of Picache's built-in chat. - An app's id must be a single DNS label to get a name. - An id served from one workspace can't be served from another until it's unserved there. `app unserve` stops serving an app. Denying it `domain.serve` or removing the app also stops it. ### The web desktop Click the cherry on the lander and you're in Cherrytop, this machine's desktop in a browser tab. `cherrypi --gui` opens it directly. Cherrytop is a separate desktop from the TUI. What you do in one doesn't change the other, except where both read the same harness. > **Diagram: Cherrytop: windows over a canvas.** Left: one browser tab holds two layers. Windows, tiled or floating, hold a shell, the TUI, a served app or Picache's chat. Under them is the canvas, with widgets, icons, frames and spaces; a machine can have up to 24 canvases, each with its own windows. Pinning a window puts the same frame on the canvas, still running, and nothing reloads. Right: zoomed out, each canvas item is drawn by the width it has on screen: 240 pixels or more, live; from 96, a card with its title, a state and its last lines; under 96, an icon. A shell stops drawing while it is a card. > > Windows on top, the canvas under them, and how much of each canvas item is drawn as you zoom out. - Windows hold a shell (`/terminal`), the desktop TUI itself (`/tui`), a running terminal attached from a workspace, this website, a served app, or Picache's chat ([Picache in a window](manual.html#web-chat)). - The canvas under the windows is an infinite surface that you pan and zoom, with icons, widgets, frames and named spaces. `Ctrl`+`Alt`+`D` shows it. Press `m` to change its mode (ember, glass, graph, mono, silk), or `g` followed by a number to fly to a space. The widgets read every workspace this machine has opened. One whose harness isn't running is read from its store, with nothing live. They show: | Widget | Shows, and what you can do from it | |---|---| | waiting for you | Each question a turn stopped to ask ([When something needs you](manual.html#needs-you)): the call, why, and whether it's destructive. **Approve once** or **decline** answers exactly that question and resumes the turn, as Recovery does. | | goals | Each goal's progress. You can **stop** a running one, **accept** or **reject** a finished one's work, or **continue** one that stopped short ([Give an agent a goal](manual.html#goals)). | | running | Each open terminal run. **View** puts it on the canvas, live, and **window** opens it in a window. Both attach to the run itself ([section 12](manual.html#sakura)). | | agents, inbox, workspaces | Agents' states, active Issues, and which harnesses are running. | | apps | Served apps. An app can offer widgets of its own: pages of its site, in the same sandbox as its windows. A widget can show live data by running one verb the app names for it, but only to read: it can't make the app change anything. Meta's *Installed apps* is one (right-click the canvas, **Add app widget**). | | clock, machine, notes | The time, a count of what's here, and notes kept on this machine. | An answer, a goal verb or an attachment from the canvas goes through the same harness calls the desktop uses, so each is receipted the same way. A question is asked in the page, never in the browser's own pop-up. The windows and canvases are kept in `~/.cherrypi/web/web.db`. Every change is recorded as an event, so another open tab shows it at once. Choosing **Open in browser** on a terminal pane in the desktop shows that same running terminal in a browser tab, already signed in for you. Typing in either shows in both, and closing the tab leaves the program running ([From a browser](manual.html#sakura-browser)). ### Canvases, pinning and zoom - You can have several canvases. Make, rename and remove them from the canvas button in the bar or from the palette (`Ctrl`+`K`, then `canvas`). Each canvas has its own windows. `Ctrl`+`Alt`+`]` and `[` go to the next canvas and the one before. Canvas Zero is always there, and you can have up to 24 in all. - You set placement from the canvas's right-click menu: free, snap to grid, or locked. You can also turn on *push others aside when dragging*. A single item can be locked on its own with **Lock in place**. - Pinning a window (with `⇣` in its header, or *Pin focused window to the canvas* in the palette) turns it into a canvas item that keeps running. Popping it out again turns it back into a window. The frame is never reloaded, so a shell keeps its session and an app keeps what it holds. - When you zoom out, items turn into cards and then icons, depending on the width each has on screen. A run's card shows the agent's own report, or that the run ended. Otherwise it says *no report* and does not guess. A shell's card shows its last lines, and the shell stops drawing until it's live again. ### Picache in a window Picache is the one app built into Cherrytop. Open it from the apps and it talks to the harness of a running workspace. You pick the workspace and the conversation, and the conversations are the same ones the TUI lists. A new conversation you start here shows up in the TUI too. > **Diagram: How Picache's chat reaches a harness.** The chat runs in a sandboxed frame that can run scripts and nothing else, and that holds no sign-in. It asks Cherrytop, which is signed in, and Cherrytop carries each request to the harness of a workspace this machine has opened. There, your words start a turn in the same conversation the TUI lists, and Picache's tools work in the workspace's folder. The turn's events ring the workspace's bell and stream back to the chat as they happen; the model's words are shown as text, never as markup. When the turn asks a question, declining from the chat is sent as asked; approving opens Cherrytop's own dialog, showing the call as the harness states it. > > The chat holds nothing of yours. Cherrytop carries what it says, and approving always happens in Cherrytop's own dialog. The chat is sandboxed because it shows a model's words, and a model's words are untrusted text. Even if something in them got past the page, it would reach nothing, because the frame has no sign-in and can only ask Cherrytop. - A turn streams into the chat as it runs: your words, each tool call, a question, and the reply. **stop** ends the turn at its next step. Something you send while it runs is queued for when it ends. - A question the turn asks shows in the chat. Pressing **decline** answers it at once. Pressing **approve once…** opens Cherrytop's dialog, which shows the call as the harness states it, not as the chat page shows it. Only that dialog can approve. - The chat's card on a zoomed-out canvas shows the last exchange. The chat's tools work in the workspace's own folder. Every model call, the generic `openai_compatible` backend's included, runs in the inference worker, so the harness keeps answering while a model thinks. ### When an agent proposes Cherrytop belongs to you. An agent can see what's open on it, but it can't open, focus or move anything there. Instead it makes a proposal, with a note, and you answer it in the browser. > **Diagram: An agent proposes; you answer.** An agent runs picache web open with a surface and a note, or picache web arrange with a plan of where items go. Cherrytop records the proposal in its own store, web.db: three waiting per agent, twelve in all, each for ten minutes. The browser shows it at once with the note, as text. An arrangement can be previewed as ghosts where each item would go; a locked item stays. Accepting does it as your own click would, so it's recorded as yours; declining does nothing. The agent reads your answer. > > Nothing changes on Cherrytop until you accept. | Command | Does | |---|---| | `picache web status` | Whether this machine's web server is serving, how many canvases, and how many proposals wait (yours among them) | | `picache web canvases` | The canvases, by id and name | | `picache web instances [--canvas ]` | What's open on one canvas: its windows, and each item by the id `focus` takes | | `picache web open [--note …]` | Proposes opening `terminal`, `tui`, `site`, `activity`, `app:` or an attached run | | `picache web focus [--note …]` | Proposes showing one item on the canvas | | `picache web arrange [--note …]` | Proposes where items go: `[{ item, x, y, w?, h? }]`, as the argument or on stdin | | `picache web proposals [--all]` | An agent's own proposals still waiting. `--all` adds the answered ones, with your answer. You see everyone's. | | `picache web withdraw ` | Takes back one of your own proposals that is still waiting | An arrangement moves canvas items and leaves windows where they are. A paired machine's turn can't propose. Picache has the same commands as tools, in the `web` group ([Picache's tools](manual.html#picache-tools)). ### Sign a browser in The web server listens only on loopback, but every program on this machine can reach loopback, including an agent. So the desktop's pages and their APIs need a browser that you have signed in. ``` cherrypi --browser # opens Cherrytop, signed in cherrypi.ape web login # sign a browser in to the desktop cherrypi.ape web login --print # print the link instead of opening it cherrypi.ape web sessions # the browsers signed in, each with an id cherrypi.ape web logout # sign that one browser out cherrypi.ape web logout # sign every browser out ``` > **Diagram: Signing a browser in.** Three steps. One: a launcher on this machine, cherrypi --browser or web login, mints a one-time code and writes only its digest into ~/.cherrypi/web/login, good for two minutes. Two: the browser opens /login with the code in the fragment, which no server log sees; the page removes it from the address bar and posts it to /api/login. Three: the server consumes the code, so it works once, and opens a session for this browser: a random cookie, HttpOnly and SameSite=Strict, recorded on this machine by a MAC of it under the secret in ~/.cherrypi/web/access.key, so no cookie is kept on disk. Below, what is refused: a request with no cookie gets 401 login_required; a request that changes something without an Origin header gets 401; another site's page gets 403. A confined agent's box doesn't see ~/.cherrypi, so it can't sign itself in. > > Only someone at this machine can sign a browser in. Each browser gets its own session, and `web logout` ends them all. - Opening `/cherrytop`, `/terminal`, `/tui` or `/machine` without signing in takes you to `/login`, which explains how to sign in and then brings you back. - `http://127.0.0.1:3141` and `https://cherry.pi` are different sites to a browser, so each is signed in on its own. `web login --loopback` signs in the first of the two. - A browser's session ends after 30 days without use, or after 400 days in all. **Sign this browser out** in Cherrytop's palette ends only that one. - Apps shown in Cherrytop run sandboxed in a separate origin: an app's script can't call the desktop's APIs or read its page. ### Commands | Command | What it does | |---|---| | `cherrypi -browser`, `-web` | The same as `cherrypi --gui`: starts the web server if needed and opens Cherrytop, signed in | | `cherrypi --gui` | Opens Cherrytop, the web desktop, signed in | | `cherrypi.ape web login` | Signs a browser in with a one-time link; `--print` prints it instead of opening it | | `cherrypi.ape web sessions` | Lists the browsers signed in: an id, which browser, where, and when each was last used | | `cherrypi.ape web logout ` | Signs that one browser out | | `cherrypi.ape web logout` | Signs every browser out | | `cherrypi.ape names install` | Sets up the address, the ports, the resolver and the boot job, and trust for the certificate authority in the system and in browsers' own stores (one password on Linux, two on macOS) | | `cherrypi.ape names script` | Prints what the install runs as root | | `cherrypi.ape names status` | Tries `https://cherry.pi` as a browser would, ticks off each piece and store, and lists when each certificate ends | | `cherrypi.ape names remove` | Takes every piece away and stops trusting the authority | | `cherrypi.ape web start`, `stop`, `restart`, `status` | The web server's lifecycle. `stop` waits up to five seconds before it forces the server to stop | | `picache app serve ` | Serves an app's pages at `https://.pi` | | `picache app unserve ` | Stops serving them | Serving pages this way opens no port to the network. ## 12 / The Sakura protocol Sakura is how a client reaches something that's already running in a workspace without starting a copy of it. That something is a terminal: a shell, a vendor agent, the desktop TUI in a pane. A client attaches to that run, gets its current screen, follows its output, types into it and leaves, and the program never notices. The browser speaks it, and so does the Go client's library. The desktop's own panes don't use it: they reach their terminals directly. A browser tab showing a terminal, Cherrytop's terminal widgets and the desktop's **Open in browser** are Sakura attachments to the same run your pane shows ([section 11](manual.html#web-desktop)). The rest of this section is the wire reference: the frame, the handshake, the terminal stream, services, recovery, the browser carrier, and limits and errors. ### Three planes over one run A terminal has exactly one owner, its pump. The pump is the small process that holds the terminal's PTY and a ring of its recent output ([section 01](manual.html#workspace)). Sakura adds no second owner. Over each run it carries three planes: - The control plane decides who may attach to which run, and for how long. It covers tickets, the handshake, detach, resume and revoke. - The stream plane carries the screen: a snapshot first, then live output in order, sent within the credit the client grants. - The service plane carries typed calls with receipts. Typing, resizing and reading go here, so the client doesn't have to guess an input's outcome. > **Diagram: Three planes over one run.** Left, clients: the Go client and a browser tab. Middle, one Sakura connection context per client, with three planes on it. Control, on channel zero: HELLO, CHALLENGE, AUTH, ATTACH, DETACH, RESUME. Service, also on channel zero: SERVICE_CALL and SERVICE_RESULT for the terminal's read, input, resize and resume. Stream, on channel one: snapshot, READY, DATA, EVENT, with ACK and WINDOW back. Right, the host side: the harness admits each connection and the run's pump owns the PTY, its output ring and a headless terminal engine that makes snapshots. Several clients attach to the same run; the program runs on when they leave. > > Each client gets its own connection context over the same run. Control and services share channel 0, and the terminal stream is channel 1. ### Carriers Sakura frames are bytes. A carrier moves them and says who the caller is, but adds no authority of its own: the frames still authenticate, and the host still checks the run. | Carrier | Who uses it | How a frame travels | |---|---|---| | `local-control/1` | The Go client on this machine | The harness's private control socket, which only a holder of its 0600 client key can use. `sakura_open` scopes a ticket to a run, `sakura_exchange` carries one frame each way as base64, and `sakura_next` takes the stream's next frame. | | `browser-http/1` | A browser on this machine | `POST /api/sakura` on the machine's web server, which forwards only `exchange`, `next` and `revoke` into the harness's `sakura_browser` entry. Every request carries the attachment's bearer ([below](manual.html#sakura-browser)). | Both carriers work only on this machine. A desktop on a paired machine can't call any Sakura entry point. It attaches over the remote-desktop path instead ([section 08](manual.html#remote-desktop)). ### The frame Every frame is a 40-byte header and a payload of at most 64 KiB. Integers are big-endian. The Go codec (`internal/sakura/frame.go`), the Lua host (`runtime/addons/sakura/frame.lua`) and the browser (`sakura-client.js`) check the same golden vectors. > **Diagram: The SKRA v1 frame header.** A 40-byte header, drawn to scale in two rows of 20 bytes. Row one: bytes 0 to 3, the magic SKRA; byte 4, version 1; byte 5, the frame type, 1 to 31; bytes 6 and 7, flags, bit 0 response and bit 1 error; bytes 8 to 11, the channel, 0 for control and services, 1 for the terminal stream; bytes 12 to 19, seq. Row two: bytes 20 to 27, ack; bytes 28 to 31, the payload's length, at most 65536; bytes 32 to 39, the epoch. The payload follows at byte 40: JSON for control and service frames, raw bytes for DATA and snapshot chunks. > > The header, to scale: each box's width is its size in bytes. | Field | Meaning | |---|---| | magic, version | `SKRA`, then 1. Anything else is refused. | | type | One of the 31 types below. An unknown type is refused. | | flags | Bit 0: a response (a service result). Bit 1: an error, after which the context is gone unless the error says it is recoverable. Other bits are refused. | | channel | 0 for control and services, 1 for the terminal stream. | | seq, ack | Per channel and per direction. Control frames carry 0 and 0. Service calls count from 1. Stream frames count from 1 after the stream's generation begins. | | length | The payload's size. Trailing bytes after it are refused. | | epoch | The host's generation. A client's first HELLO carries 0. Every later control and service frame carries the host's. A host restart changes it, and an old attachment is then refused. Stream frames carry the stream's generation instead, which a fresh snapshot changes. | ### Frame types Thirty-one types are defined. The terminal attachment uses the ones marked *used*. The host refuses the ones marked *refused*. | Code | Type | Direction | Handling | |---|---|---|---| | 1 | `HELLO` | client → host | used: version, nonce, features | | 2 | `CHALLENGE` | host → client | used: host nonce, principal, carrier, limits | | 3 | `AUTH` | client → host | used: ticket and proof | | 4 | `ATTACH` | client → host | used: the run, channels, window | | 5 | `ATTACHED` | host → client | used: channels, input acknowledgement, resume token | | 6 to 8 | `SNAPSHOT_BEGIN`, `_DATA`, `_END` | host → client | used: the current screen, in chunks | | 9 | `READY` | host → client | used: the stream is live | | 10 | `DATA` | host → client | used: output bytes | | 11 and 12 | `INPUT`, `RESIZE` | client → host | taken on channel 1 after READY, refused on channel 0. The Go client and the browser type and resize through services instead | | 13 | `EVENT` | host → client | used: a resize of the run | | 14 | `WINDOW` | client → host | used: more credit | | 15 | `ACK` | both | used: a frame taken, or credit granted | | 16 and 17 | `PING`, `PONG` | client → host, host → client | used: liveness, and renews the lease | | 18 | `CLOSE` | host → client | used: the program ended | | 19 | `ERROR` | host → client | used: a refusal, fatal or recoverable | | 20 and 21 | `DETACH`, `RESUME` | client → host | used: leave without closing, or come back | | 22 to 29 | `OBJECT_BEGIN` … `OBJECT_ABORT` | both | refused: reserved for object transfer | | 30 | `SERVICE_CALL` | client → host | used: a typed call | | 31 | `SERVICE_RESULT` | host → client | used: its result or refusal | ### The handshake An attachment begins with a ticket: 32 random bytes written as 64 hex digits, scoped to one connection context (each client gets its own) and one existing run. A ticket is good for 60 seconds and one authentication attempt. Four exchanges follow, all on channel 0 with seq and ack set to 0: > **Diagram: The handshake, and the ticket spent at AUTH.** A sequence between a client on the left and the host on the right. One: the client sends HELLO with epoch 0, version 1, a 64-hex-digit client nonce and the features it speaks, service.json/1 and terminal.ghostty-snapshot/1. Two: the host answers CHALLENGE with its own nonce, the real principal, local-uid followed by the user id, the carrier and its limits. Three: the client sends AUTH with the ticket and a proof, an HMAC-SHA256 under the ticket of the protocol name, the epoch, the connection, principal, carrier, run and both nonces, each length-prefixed. The host compares in constant time and the ticket is spent: one attempt only. A browser is answered with a fresh bearer here, and from then on the pasted ticket authorizes nothing. Four: the client sends ATTACH naming the same run, channel 1 and an initial window; the host answers ATTACHED with the stream's generation, the input acknowledgement and a resume token. The snapshot follows on channel 1. > > HELLO, CHALLENGE, AUTH, ATTACH. The proof binds the ticket to this connection, this run, this principal and both nonces. The proof is HMAC-SHA256, keyed with the ticket's 32 bytes, over: ``` "sakura.local/1\0" ‖ epoch (u64) ‖ len‖connection ‖ len‖principal ‖ len‖carrier ‖ len‖run ‖ len‖client nonce ‖ len‖host nonce # each len a u32 ``` - The principal is the host's real `local-uid:`, never a field the client sends. - A lost answer is safe to retry: the identical frame gets the identical answer, and nothing runs twice. A retry with different bytes is a new request and is judged as one. - ATTACH must name the run the ticket was scoped to. Asking for another is refused with `sakura_session_scope` and ends the context. ### The terminal stream Channel 1 carries the run's screen, using the codec `terminal.ghostty-snapshot/1`. Nothing is live until the client holds a whole, checked snapshot. 1. `SNAPSHOT_BEGIN` gives the codec, the exact native ABI, the size (at most 16 MiB), its SHA-256, the grid, and the output cursor and resize revision the snapshot was taken at. 2. `SNAPSHOT_DATA` chunks of at most 16 KiB follow, in order. 3. `SNAPSHOT_END` repeats the SHA-256 and gives the scrollback marker. The client checks size and digest, and decodes it fully, before it shows anything. 4. `READY` commits it. From then on, `DATA` carries output bytes (at most 16 KiB each) and `EVENT` carries the run's resizes, in the order they happened after the snapshot. A snapshot holds both screens, the saved primary screen, scrollback, cursor, title, working directory and any half-parsed escape sequence, so the first live byte continues exactly where the snapshot ends. The pump's own terminal engine makes it, and answers the program's screen queries itself, so a browser never sends a reply a native client also sent. > **Diagram: ACK and WINDOW on the stream.** The host's stream on channel 1 sends frames in sequence: SNAPSHOT_BEGIN as seq 1, SNAPSHOT_DATA as 2 and 3, SNAPSHOT_END as 4, READY as 5, then DATA as 6. The host keeps one unacknowledged frame and sends it again if asked again, so a lost frame is never skipped. The client answers each frame with ACK, which retires it, and separately with WINDOW, which grants byte credit for what it has drawn; the host answers a WINDOW with an ACK of its own. A duplicate WINDOW grants nothing twice. The host sends DATA only within its credit; when the credit is spent it waits, and the program keeps running, its output kept in the pump's ring. > > ACK and WINDOW are separate: one retires a frame, the other grants credit. A slow tab holds back its own stream, never the program. - The initial window, the credit the client grants in ATTACH, is between 16 KiB and 1 MiB. Credit never exceeds 1 MiB. - If the client falls further behind than the pump's ring holds, the host says so with a recoverable `ERROR`, `sakura_snapshot_required`. The client asks for a fresh snapshot ([see below](manual.html#sakura-recovery)). The screen it already shows stays until the new one is ready. - A run that has drawn Kitty graphics can't be snapshotted: the host refuses with `snapshot_graphics_unsupported`, so it never sends a screen with its pictures missing. ### Services The Go client and the browser send typing and resizing as service calls on channel 0, so each one gets an answer. The host also takes `INPUT` and `RESIZE` frames on the stream after READY, and refuses them on channel 0. A call and its result look like this: ``` SERVICE_CALL seq=n ack=last result {"service": "terminal", "op": "input", "request_id": "browser-7", "args": {"data": "", "seq": 12}} SERVICE_RESULT flags=response seq=m ack=n {"request_id": "browser-7", "result": {"input_ack": 12}} ``` | Service | Operations | Notes | |---|---|---| | `terminal` | `input`, `resize`, `read`, `resume` | Input is at most 16 KiB per call. Resize asks for 1 to 500 columns and 1 to 300 rows. Input and resize share one writer sequence, `seq` in `args`, counted from the attachment's `input_ack`. | | `sakura.service` | `describe` | Lists the services and their limits. | - Each context has one call outstanding at a time. Calls count from 1. Results have their own sequence and acknowledge the call they answer. A gap, a future ack or a changed duplicate ends the context. - `input_ack` means the pump admitted the bytes to the program's input queue, in order. It doesn't mean the program ran them. - The host supplies the run and the attachment. Fields in `args` can't name another run or another lease. - A refusal in a result (because the queue was full, for example) is still an answer. The sequences advance and the attachment stays. Only an `ERROR` frame that isn't marked recoverable ends it. ### Lost answers, detach and resume Sakura's rule is that an operation is never repeated by accident, and never silently dropped. - If an answer is lost, the client sends the exact same bytes again. The host recognises them and returns its stored answer, so nothing runs twice. Until that answer arrives, nothing later may overtake it: the browser pauses input and offers **Retry**. - If the pump can't say whether an input was admitted (`command_outcome_unknown`, `command_backpressure`), the client resolves that same input, with the same writer sequence, in a new service call. The pump acknowledges a duplicate it already has without typing it again. - `DETACH` leaves the attachment, its counters and the run as they are. `RESUME` names the same run and the resume token, with the last frame the client took, and the host answers `ATTACHED` with its counters. With `snapshot: true`, or when the client's history is gone, the stream starts a new generation with a fresh snapshot. Frames from the old generation can't touch the new one. - A harness restart makes a new host and changes the epoch. Old attachments are refused, and a client never resubmits uncertain input under a new attachment. Instead it gets a new credential and starts from a snapshot. - An attached context holds a lease that lasts five minutes without a call, and any call, poll or ping renews it. A host keeps at most 64 contexts, counting those still waiting to authenticate. Leaving, expiry and revocation end the attachment, but the program keeps running. ### From a browser A browser attaches with a credential the harness issues for one run. You can get one in two ways without ever seeing it: - **Open in browser**, from a terminal pane's menu or Actions, opens a one-time sign-in link to `/terminal/attach?workspace=&run=`. That page asks the web server for its own credential, so the credential never passes through the clipboard, the terminal or a recording of it. - Cherrytop's **running** widget opens the same page as a widget or a window. A script can also ask for one: ``` cherrypi.ape desktop -state /.cherrypi -browser-attach ``` That prints the credential, which is a secret, for you to paste into `/terminal/attach` within a minute. It looks like this: ``` {"protocol": "cherrypi.browser-attachment.v1", "carrier": "browser-http/1", "connection": "<32 hex>", "ticket": "<64 hex>", "epoch": 71, "session": "", "principal": "local-uid:501", "workspace": "", "expires": …, "max_payload": 65536} ``` > **Diagram: Open in browser, end to end.** Four steps. One: in the desktop, Open in browser asks the harness for a link, sakura_browser_link, which returns a one-time sign-in link and no credential. Two: the browser opens /login with the code in the fragment; the page drops it from the address bar and redeems it once for the sign-in cookie, then goes to /terminal/attach for that workspace and run. Three: the signed-in page posts to /api/desktop/act, kind attach; the web server checks the workspace is catalogued and asks the harness, over a one-call connection, for a credential scoped to that one run; the answer goes straight into the page's session. Four: the page speaks Sakura over POST /api/sakura with the bearer: HELLO, CHALLENGE, AUTH, which rotates the bearer, ATTACH, then next and exchange polls. The web server forwards only exchange, next and revoke to the harness's sakura_browser entry, which checks the bearer on every request. > > From the pane menu to a live terminal in a tab. The only thing that travels by URL is a code that works once, for two minutes. Each request on the browser carrier looks like this: ``` POST /api/sakura Authorization: Bearer {"workspace": "", "connection": "<32 hex>", "operation": "exchange" | "next" | "revoke", "frame": ""} 200 {"frame": "" | ""} # AUTH's answer also has "bearer" 400 a request the host will refuse again 401 no bearer 403 bearer refused, or the context expired 503 the harness can't be reached: retry ``` - The ticket is spent at AUTH. The answer to AUTH carries a new bearer that only the page holds. After that, the pasted credential can do one thing: repeat that exact AUTH if its answer was lost, until the attachment moves on. A copy left in a clipboard history can't read, type or revoke anything. - The workspace is named by its id, a SHA-256 of its data folder, and must be in this machine's catalogue. A filesystem path, an actor or an RPC method in the body is never forwarded. - `/api/sakura` doesn't need the desktop's sign-in cookie. The bearer is its own authority, and the page sends no cookies with it. Getting a credential from `/api/desktop/act` does need the cookie ([section 11](manual.html#web-signin)). - The page polls `next` when it has nothing to do, backing off from 25 ms to 400 ms, and wakes at once when you type. Leaving the page revokes the attachment, with a request that outlives the tab. - **Fit to window** resizes the shared run for every viewer, so the page never resizes on its own. Joining keeps the grid the run already has. ### Limits and errors | Limit | Value | |---|---| | Frame payload | 64 KiB | | `DATA`, snapshot chunk, one input | 16 KiB | | Snapshot | 16 MiB | | Stream window | 16 KiB to 1 MiB | | Browser input queue | 64 KiB. A larger paste is refused whole. | | Grid a browser may ask for | 500 columns × 300 rows (a run may report up to 500 rows) | | Ticket | 60 seconds, one attempt | | Idle attachment | 5 minutes, renewed by any call | | Connection contexts per host | 64 | An `ERROR` frame's payload is `{"code": …, "recoverable": …}`, and `recoverable` appears only when it is true. Without it, the context is gone, but the run is not. | Code | Means | |---|---| | `sakura_auth_refused` | The ticket or the proof was wrong. The ticket is spent either way. | | `sakura_session_scope` | ATTACH or RESUME named another run or token. | | `sakura_epoch_mismatch` | The host restarted. Get a new credential. | | `sakura_sequence_gap` | A service call skipped or acknowledged the future. | | `sakura_snapshot_required` | Recoverable: the client has fallen behind the ring. Resume with a snapshot. | | `command_outcome_unknown` | Recoverable: resolve the same input again before anything else. | | `snapshot_graphics_unsupported` | The run has drawn Kitty graphics, which snapshots can't carry. | | `sakura_connection_limit` | The host already holds 64 connection contexts. | | `terminal_not_persistent` | The run has no pump to attach to (when opening a credential). | | `sakura_invalid_state` | A frame the attachment's phase doesn't allow. | ## 13 / Reference This section lists every `picache` verb and every setting in the Settings pane. Read from cherry π's source at `66461bb`: 88 verbs, 46 of them Picache's tools, and 24 settings. `picache help` lists the verbs as they are now. ### Every verb Run `picache help ` for a verb's usage and what it needs. A verb's effect class says what it does to the world, and it decides when you're asked ([section 03](manual.html#authority)). | Verb | Called by | Effect | What it does | |---|---|---|---| | `accept` | you | record | Accept an offer to your layer (a patch is applied here, undoable) | | `accepts` | you | record | What a layer takes from a linked workspace or a paired machine, narrowed per layer | | `addons` | you | record | The first-party addons in this build; `addons enable ` grants what one needs and starts it | | `agent` | you | record | The agent for a folder: which answers here, declare one, attach or detach one | | `app` | you | record | .pi apps: what is installed, install and remove, pack a dev folder, grant what one asks for | | `apply-layout` | Picache, you | irreversible | Open a layout's panes beside a pane (yours unless `--at`), or as a new layer; it closes nothing | | `ask` | Picache, you | record | Ask another layer a question; it is answered there, reading its files, in a thread | | `ask-operator` | you | read | Put a form in front of the operator (A2UI JSON) and print their answer | | `assign` | Picache, you | record | Assign an issue to the agent in a pane run, and see or decide its delivery (the operator's) | | `box` | you | record | How each agent is started (confined or standard, its network, its list, its login) and how far it reaches on the desk | | `brain` | Picache, you | read | Brains attached to your layer: other layers or references, read-only | | `cancel` | you | record | Take back an offer from your layer that nobody has answered | | `capture` | you | read | What a pane shows: `panes read`, by Rex's name | | `cat` | Picache, you | read | Print a file (bounded at 256 KiB) | | `cleanroom` | Picache, you | read | Run a command or Lua script on copies of files, with no network and nothing else to reach; out/ is kept | | `close` | Picache, you | irreversible | Close a pane you opened (the operator may close any) | | `close-layer` | Picache, you | irreversible | Close a layer: its panes' programs end (Layer Zero stays) | | `cohort` | Picache, you | irreversible | Ask a vendor agent one question over ACP, in its box, with cherry-pi's tools; the answer is marked external | | `connect` | you | read | A confined agent's proxy asks whether a connection may go out | | `context` | you | read | Exactly what Picache's model was given on its last call | | `desktops` | you | record | Desktops on paired machines: whether this workspace takes them, from which trusted networks, and who is attached | | `do` | you | read | Run a Lua script whose calls go through your token: picache.call, .split, .wait, .on … | | `eval` | you | record | Measure an agent: its core and own cases, non-executing, with a report kept | | `events` | you | read | The harness's events as they happen, one per line; `--json` for programs | | `factory` | you | record | Make .pi apps from a schematic: a product layer from its skeleton, a builder's goal, and checks that pack it for review | | `find` | Picache, you | read | Files whose path contains (under your folder, or a subfolder) | | `fleet` | you | record | The fleet this machine is in: who can find it on the network | | `float` | Picache, you | arrange | Float a pane over the desk, out of the tree, raise or lower a float, hide and show the floats, or cascade them | | `focus` | Picache, you | arrange | Give a pane the keyboard: by name, or left, right, up, down, next, prev | | `focus-layer` | Picache, you | arrange | Bring a layer to the front: by name or place, or next, prev | | `goal` | you | record | Give a vendor agent a goal to work on in its own copy of this layer; Picache reviews it, and you accept or reject its branch | | `grep` | Picache, you | read | Lines containing in files under your folder | | `help` | you | read | The verbs, what each may do, and how to call them; `help ` describes one | | `history` | Picache, you | read | This conversation's own messages, receipts and checkpoints, searched or read exactly, past what the context holds | | `identity` | you | record | This node's identity and its agents' keys (`identity init` makes them) | | `inbox` | you | read | Offers to your layer that wait on an answer (`--all` for every one) | | `issue` | Picache, you | record | The Inbox's issues: list, open, show, reply; the operator moves and tags them | | `jobs` | Picache, you | record | Long work the runtime is doing: downloads, conversions, model servers, measurements | | `kb` | Picache, you | read | An agent's knowledge: search it (the atlas of every verb and tool, the operating shelf, the docs), list its sources, or refresh it | | `label` | Picache, you | arrange | Give a pane a title | | `label-layer` | Picache, you | arrange | Give a layer a name | | `layer` | you | record | Export a layer whole (history and files, signed), import one, fork one, share one live, list references | | `layers` | Picache, you | read | The workspace's layers, their folders, and the offers waiting in each | | `layout` | Picache, you | read | The layer in front as a tree: how it splits, each split's share, each pane; `--json` as a layout, `--saved` the saved ones | | `link` | you | record | Link another workspace (its folder): it may offer this one what `--accept` names | | `links` | you | read | The workspaces linked to this one, and what each may offer it | | `loop` | you | record | A goal pursued over several turns in this folder, each checked by the layer's test command, until it passes or the steps run out | | `ls` | Picache, you | read | List a folder (default: your pane's folder; `--layer` for another layer's) | | `machines` | you | record | This machine's fleet: paired machines, requests to pair, the directory; `machines name` names one | | `mcp` | you | read | This pane's verbs as an MCP server on stdio, for the agent the pane runs | | `message` | Picache, you | record | Write to another agent's pane, as SendMessage: it gets it at its next turn or as its turn ends; read yours, see whom you can write to | | `models` | you | read | The models this machine has, and which one Picache uses | | `move` | Picache, you | arrange | Dock a pane beside another: its old place closes up | | `move-layer` | Picache, you | arrange | Put a layer at a place in the rail, from 2 (Layer Zero stays first) | | `new-layer` | Picache, you | irreversible | Open a new layer on a folder (the caller's unless named), in front unless `--focus`=false | | `offer` | you | record | Offer another layer a message or a change; it is applied only if accepted there | | `outbox` | you | read | Offers from your layer that nobody has answered yet (`--all` for every one) | | `pair` | you | record | Pair with a machine, or answer one that asks (`pair approve [] `, `pair decline `, `pair decline-all `) | | `panes` | Picache, you | read | The panes on the desk, or `panes read ` for what a terminal shows | | `patch` | Picache, you | reversible | Replace text in a file (once, or exact with `--all`); whole lines match despite spacing or quotes; the old bytes are kept for undo | | `profile` | you | record | This machine's profile -- name, handle, bio, status, timezone, avatar, banner -- which paired machines see | | `reach` | you | record | Reach an address another machine serves: a loopback port here that connects to it | | `reject` | you | record | Reject an offer to your layer | | `reply` | Picache, you | record | Reply in an issue's thread (markdown; the text, or stdin) | | `resize` | Picache, you | arrange | Make a pane wider, narrower, taller or shorter by cells, or `--to` a size: it moves the divider beside it | | `run` | Picache, you | irreversible | Run a shell command, after the shell policy has classified it | | `runs` | you | read | What ran in this workspace, newest first (an agent, or a folder's own: what ran in its folder) | | `save-layout` | Picache, you | reversible | Save the layer in front as a layout in the folder (.cherrypi/layouts/.json) | | `script` | Picache, you | read | Code mode: a Lua script in a capsule whose only way out is the verbs, each one decided and receipted | | `search` | Picache, you | read | The workspace's search: conversations, runs, receipts and files | | `send` | Picache, you | irreversible | Paste text into a terminal pane and press Enter (a shell runs it); `--wait` answers when the turn it starts has settled | | `send-key` | Picache, you | irreversible | Press keys in a terminal pane: enter, tab, esc, up, ctrl+c … | | `serve` | you | record | Serve a port, or an installed app from its own capsule, at an address that is a key: to paired machines, approved ones, or anyone holding it | | `shares` | you | record | What this machine shares with its paired machines, and what they share with it | | `skill` | Picache, you | read | Teach Claude Code, Codex, OpenCode and Pi cherry-pi: show the guide, install the skill and hook | | `snapshots` | you | read | Your layer's folder as each turn found and left it: list, what changed, a file as it was | | `split` | Picache, you | irreversible | Open a terminal pane beside yours, or as a float over the desk, running a command or a shell | | `status` | Picache, you | read | Who you are to the harness, your folder, and Picache's model | | `surface` | you | arrange | Arrange a temporary team surface; dismissing never stops its agents | | `surfaces` | you | read | List lightweight pane arrangements in the caller's project | | `swap` | Picache, you | arrange | Two panes trade places; the tree keeps its shape | | `task` | you | record | Give another layer a task; it runs there once accepted, and its report comes back to you | | `undo` | Picache, you | reversible | Put back what a write or patch replaced, if the file is still as it left it | | `unlink` | you | record | Withdraw this workspace's link to another: it may no longer offer this one anything | | `wait` | Picache, you | read | Wait for a pane's program to end (its exit status), for text to appear, or for its state | | `web` | Picache, you | record | Cherrytop, the browser desktop: what is open where; propose opening or showing something | | `write` | Picache, you | reversible | Write a file from stdin or a text argument; the old bytes are kept for undo | | `zoom` | Picache, you | arrange | Fill the window with a pane, or restore it: `--on`, `--off`, or toggle | ### The Settings pane Ctrl+Alt+Q opens Settings, a surface of its own, where you left it. Its Settings pane has seven pages, drawn as cards, and each card says whether it's for this workspace, this machine or you. A choice saves at once. A choice for this workspace is also recorded as a receipt. | Page | Holds | |---|---| | Web | Whether `https://cherry.pi` works, setting it up, and the certificate authority | | Terminals | `terminal.sudo.*` | | Agents | Recovery, goals, Picache and routing, the Inbox, and new agents | | Sharing | `clients.remote`, and Machines | | Desktop | Yours alone: theme, images, notifications, sounds, hints, layouts | | Search | The search cache, its size, and what search leaves out | | Workspace | The data folder, layer snapshots, and the rest | Each setting in the next table has a fixed set of choices. Others are described where they apply, such as [`context.*`](manual.html#context) and [`loop.test_command`](manual.html#loops). | Setting | Choices (default in bold) | What it sets | |---|---|---| | `terminal.sudo.shells` | **on**, off | Your shells can sudo | | `terminal.sudo.agents` | **off**, on | Agents' panes can sudo | | `terminal.sudo.forget` | **on**, off | Forget your sudo password before an agent types | | `terminal.sudo.unboxed_agents` | **password**, refuse | An agent outside its box, at sudo (macOS only) | | `clients.remote` | **off**, view, edit, full | What a paired machine's desktop may do here | | `recovery.mode` | **supervised**, manual, automatic | Work interrupted by a crash | | `goal.workspace` | **copy**, worktree | Where an agent works on a goal | | `picache.patch.match` | **forgiving**, exact | How Picache's edits find their text | | `picache.approval.unanswered` | **park**, refuse | When nobody answers Picache's question | | `picache.model.retry` | **once**, off | When Picache's model fails to answer | | `route.sticky` | **on**, off | Keep a turn on one model | | `route.egress` | **off**, ask, on | A router may send a request off this machine | | `route.qualify` | **required**, warn | A router's model must have qualified | | `picache.run.time` | 10m, **1h**, 4h, off | How long Picache's command may run | | `picache.run.memory` | 4g, 16g, **off** | How much memory Picache's command may use (Linux) | | `picache.budget.turn` | **off**, 50k, 200k, 1m | Tokens one turn may use | | `picache.budget.cloud` | **off**, 100k, 1m, 10m | Tokens the cloud seat may use in a day | | `evidence.on_failure` | **closed**, open | When a turn's evidence can't be recorded | | `apps.dev_changes` | **hold**, follow | When a dev folder app's files change | | `inbox.delivery` | **next_turn**, when_idle, check | When an agent gets an issue assigned to it | | `inbox.messages` | **layer**, workspace, off | Whom an agent may write to | | `inbox.message_notes` | **on**, off | Tell an agent about the panes it opened | | `inbox.message_wake` | **idle**, off | Wake an idle agent for a message | | `layer.snapshots` | **on**, off | Snapshot a layer at each turn | ### When a call is refused A refused `picache` call prints a code on standard error, usually followed by a sentence, and the refusal is recorded as a receipt. ``` picache : : # exit code 1 picache: cannot reach the harness at # exit code 2 ``` With `--json`, the refusal goes to standard output as `{"ok":false,"error":…}`. In a Chat, Picache sees `refused: `. Failures of the desktop's own calls go to the Errors pane. | Code | Means | What to do | |---|---|---| | `needs_confirmation` | Your own call needs a yes: a write without the `drive.write` grant, or a command the shell policy calls destructive. | Answer *Go ahead?* in the terminal, or pass `--yes`. | | `needs_operator` | Picache or an agent needs your yes for this one call. | Answer in the client. A yes covers that call only. | | `declined`, `denied` | You said no. | Ask again if you meant yes. | | `… nobody could be asked` | No desktop on this machine could ask you, so the call failed closed. | Keep a desktop attached on this machine. | | `waiting_approval` | Nobody has answered yet, and the question is kept. | Approve or Decline it in Recovery ([When something needs you](manual.html#needs-you)). | | `path_escape` | The path, or where a link in it points, is outside the folder. Files refuses any symbolic link in the workspace. | Use a path inside the folder. No grant fixes it. | | `bad_path` | The path has a `..` in it. | Name the file without `..`. | | `unknown_verb` | There's no such verb. | `picache help` lists them ([Every verb](manual.html#verbs)). | | `cohort_proposes` | An agent tried to write without a goal. | Give it a goal, and its work comes back for review ([Give an agent a goal](manual.html#goals)). | | `cohort_ceiling` | An agent tried something irreversible. The most an agent may do is read and propose. | Do it yourself. No grant changes it. | | `operator_decides` | Only you can do this. The caller may propose it. | Do it yourself. | | `budget_spent` | A pane's caller used its share (200 writes, 50 runs, 100 offers, 500 arranges), or a script its budget. | If it is a script of yours, give it a larger `--budget`. | | `focus_never` | The agent's box says it never moves your keyboard. | Run `picache box focus ask`. | | `hook_blocked` | An installed app's hook stopped the call. | The message names the app and its reason. | | `host_cannot_confine`, `confine_unavailable` | This machine has no Seatbelt, bubblewrap or Landlock to confine a capsule. | For Run one, pair a machine that hosts capsules and send it there ([section 09](manual.html#capsules)). | | `… is not on this agent's list` | A boxed agent tried to reach a host its list doesn't name. | Allow it when asked, or `picache box allow ` ([Network access](manual.html#box-network)). | | sudo fails in an agent's pane | Agents' panes can't sudo by default. | Change the `terminal.sudo.*` settings ([Use sudo](manual.html#sudo)). | | `effect_uncertain` | A crash left a change in doubt, and cherry π won't repeat it blind. | Settle it in Recovery: Mark Applied, Mark Not Applied, Retry Safely or Stop ([Settle an effect in doubt](manual.html#in-doubt)). | | `remote_off` | This workspace takes no desktops from other machines. | Change the `clients.remote` setting. | | `remote_not_granted` | That machine has no grant here. | On this machine, run `picache machines desktop edit`. | | `remote_not_allowed` | The machine's grant doesn't give what the call needs. | Give it a wider grant ([What a remote desktop may do](manual.html#remote-actor)). | | `layer_live_read_only` | That conversation is another member's, in a live layer. | Continue it on their machine. | | `agent_bundle_unsigned`, `agent_bundle_untrusted` | An agent without a signature, or signed by a key this machine doesn't trust. | An unsigned agent never installs. With `--untrusted`, an agent from an unknown signer installs. | | `refusing nonempty unowned state root`, `state root must not be a symbolic link` | The data folder has other files in it, or is a symbolic link. | Pick an empty folder, or one cherry π made ([The data folder](manual.html#data)). | | `no harness to talk to` | `picache` ran outside a cherry π pane. | Run it in a pane. | ### Words used here | Word | Means | |---|---| | [Bakery](manual.html#rooms) | The public rooms on rednet, joined by name, like IRC. | | [box](manual.html#boxes) | Where a vendor agent runs by default. It sets what the agent may read, write and reach. | | [capsule](manual.html#capsule-profiles) | Anything confined, such as an app's handler, a script or a clean room. Each starts from a named profile, through one door in the harness. | | [clean room](manual.html#clean-room) | A command or Lua script run on copies of files, with no network. | | [cohort](manual.html#cohort) | A vendor agent asked from a Chat, as with `@claude`. Its own write and run tools are off. | | [Conduit](manual.html#network) | How cherry π talks layer to layer, workspace to workspace and machine to machine. | | [Drive](manual.html#share-layer) | A machine's own peer-to-peer file store, with no cloud and no account. Large shares and its profile travel from it as hashed chunks. | | [effect class](manual.html#effect-classes) | What a verb does to the world: read, record, reversible, arrange or irreversible. It decides when you're asked. | | [evidence chain](manual.html#chain) | A hash chain of entries. Each turn adds one that binds every receipt it wrote. | | [fleet](manual.html#pairing) | The machines that can find this one. Only machines with its secret can. | | [goal](manual.html#goals) | Work a vendor agent does on its own, in a copy of your layer. Nothing lands until you accept it. | | [grant](manual.html#grants) | A capability you give. It stays in the workspace that gave it. | | [harness](manual.html#processes) | The process, one per workspace, that decides what's allowed and keeps every record: `cherrypi.ape`. | | [in doubt](manual.html#in-doubt) | A change written down before it ran, whose outcome a crash hid. | | [layer](manual.html#layer-stores) | A project folder with its own Picache conversation, panes and store. Layer Zero is the scratchpad. | | [offer](manual.html#offers) | A message, patch or file for another layer. Nothing changes there until you accept it. | | [Picache, picache](manual.html#picache) | Picache is the agent you talk to in a Chat. `picache` is the command it, you and agents call the harness with. | | [pump](manual.html#terminals) | The small process that holds one terminal, so it outlives the desktop and the harness. | | [receipt](manual.html#mediated) | The record of one governed call, refusals included. A change and its receipt are saved together. | | [rednet](manual.html#pairing) | The public peer-to-peer network. Machines running cherry π find each other on a subnet of it. | | [seat](manual.html#choose-model) | A place in Models for one model: Picache's, Small, Search, or Cloud. | | [signer](manual.html#identity) | The sidecar that holds this machine's and its agents' keys. | --- # Extensions How cherry π grows: first-party addons, `.pi` apps anyone can write, and Cellar Door's worlds. ## 00 / Two ways to extend Extensions come in two tiers. Addons are cherry π's own parts, shipped inside `cherrypi.ape`. Apps are `.pi` files anyone can write. | Compared | Addons | .pi apps | |---|---|---| | Made by | cherry π | Anyone | | Ship | Inside `cherrypi.ape`. You can't add one. | As one `.pi` file, or a folder while you write it | | Plug into | Registries the core owns: P2P, signers, model backends, knowledge, worlds, terminal carriers, `picache` verbs, desktop methods, goal gates, app providers | Verbs, Picache tools, panes, terminal programs, hooks, layouts | | Run | In the harness, and may start a sidecar | In a capsule, holding only what you granted | | Turn on | `picache addons enable ` | `picache app install `, then grant | The core is never a plugin: authority and policy, receipts, confinement, the addon loader and the verb registry stay in cherry π itself. ## 01 / Addons An addon is a first-party part of cherry π that plugs into a registry the core owns. Addons are found only inside the APE, so nobody can drop one in. Code of your own goes in a [`.pi` app](extensions.html#apps). ### List and turn on addons ``` picache addons # every addon in this build picache addons enable cellardoor # grant what it needs and start it picache addons disable cellardoor # stop it ``` Each line of the list shows the addon's id, its state, its version and what it needs. Enabling an addon grants every capability it declares. Only you can enable or disable one, and anyone else is told `operator_decides`. Your choice is kept as the setting `addon..enabled`, so it holds across restarts. Two buttons do the same for you: **Turn P2P on** in Machines, and **Open a world on this layer** in Actions, which needs Cellar Door enabled. ### The addons in this build | Id | What it does | Starts by itself | Needs | |---|---|---|---| | `knowledge` | Keeps the agents' knowledge store, and indexes the capability atlas and the shipped docs. Picache's `knowledge` tool and `picache kb` need it. | Yes | Nothing | | `p2p_holepunch` | Runs the P2P node and its sidecar. Joining the public network is your call. | No | `sidecar.run`, `network.peers`, `p2p.peers.read`, `p2p.session.open`, `p2p.session.send` | | `cellardoor` | Runs one Cellar Door sidecar for each layer with a world open ([section 03](extensions.html#worlds)). | No | `sidecar.run` | | `factory` | The [software factory](manual.html#factory): `picache factory`, its product panes, Try, and the gate a factory build passes before its goal is accepted. | Yes | Nothing | | `sakura` | The [Sakura](manual.html#sakura) terminal carrier, which **Open in browser** and browser attachments use. | Yes | Nothing | `p2p_holepunch` conflicts with `p2p_libp2p` and `p2p_iroh`, so only one P2P backend can run. ### What an addon's state means | State | Means | |---|---| | `discovered`, `configured` | In the build, and not turned on. | | `waiting` | Turned on, but a capability it needs isn't granted. Not an error. | | `enabled`, `running` | Started, and its parts are registered. | | `error` | A step failed. What it registered is removed. | | `disabled` | Stopped by you or at shutdown. | Addons start in dependency order. Two addons that conflict are refused, and so is a missing dependency or a cycle. A failure never stops the harness. It is recorded in three places: - a line in `/logs/addons.log` - a receipt, `addon.activation_failed` - an event of the same name to the desktop. ### Where addons plug in An addon can register only in these places. Anything else is refused with `registry_unknown`. When an addon stops, everything it registered goes with it. | Registry | For | |---|---| | `p2p.backend` | A P2P transport: connect by key, listen, carry sessions | | `p2p.discovery` | A source of peers: the LAN, mDNS, a DHT topic | | `signer` | A signing service for the keys this machine holds | | `model.backend` | An inference backend for model steps | | `knowledge.store` | Where agents' knowledge is kept and searched | | `knowledge.source` | Knowledge an agent may be given: a shelf, docs, an atlas | | `knowledge.embedder` | Embeddings for semantic search. Keyword search doesn't use it. | | `world.provider` | A world run per layer: open, close, health, one-time console tickets | | `terminal.carrier` | A way to reach a running terminal: attach, read, input, resize, detach, resume | | `picache.verb` | A `picache` verb, under the same policy and receipts as the built-in ones | | `control.method` | A method the desktop calls on this machine's harness | | `goal.gate` | A check a goal must pass before it is accepted. A goal naming a gate that isn't registered is refused. | | `app.provider` | Apps reached under the provider's name: their panes, programs, widgets and data | An addon can declare only these capabilities: `sidecar.run`, `network.peers`, `p2p.peers.read`, `p2p.presence.read`, `p2p.session.open`, `p2p.session.send` and `p2p.bond.manage`. None of them can be granted to an app. ## 02 / .pi apps A `.pi` app is one SQLite file that holds the whole app: its manifest and signature, its Lua handlers, its assets and its own data. The [field manual](manual.html#apps) shows what is signed and what isn't. ### Install an app ``` picache app install notes.pi # a packed app picache app install ./notes # a folder you're writing, run in place picache app grant notes appdata:own # grant one capability, for this session picache app grant notes appdata:own --always picache # an app's verb, such as picache notes add ``` Whether you're asked depends on who signed it. | The app is | Install | |---|---| | Signed by this machine or a paired one | Installs at once | | Unsigned, signed by someone else, or a folder | Asks you first. `--yes` answers ahead. Without a terminal it's refused. | | Changed since it was signed | Refused, with the reason (`app_signature_invalid`) | A packed app is copied to `/apps/.pi`. A folder runs where it is and is marked unsigned wherever it shows. Grants are tied to the app's exact content. If the file changes, none of it runs until you review it and run `picache app accept `. For a folder you're editing, set `apps.dev_changes` to `follow` to keep its grants. Each edit is still receipted. Use the **Extensions** pane (Actions → Open Extensions) to do the same with buttons: install, grant, deny, open an app's pane, remove. ### App commands | Command | Does | |---|---| | `picache app list` | Installed apps | | `picache app show ` | One app: what it asks for, what it adds, its grants | | `picache app install [--replace [--reset]]` | Install. `--replace` upgrades and keeps its data, and `--reset` starts empty. | | `picache app remove ` | Remove it, its grants and settings. A folder itself is left alone. | | `picache app pack [folder] [--out ]` | Pack a folder into a `.pi`, signed by this machine | | `picache app grant [--always]` | Grant for this session, or always | | `picache app deny ` | Take a grant back | | `picache app accept ` | Accept a changed app after reviewing it | | `picache app settings `, `set ` | Read or change the app's own settings. A secret is piped in (`pbpaste \| picache app set `), shown only as set or not set, and `--clear` forgets it | | `picache app test [--q ] [--fixtures ]` | Run one part in a real capsule | | `picache app serve `, `unserve ` | Serve its pages at `https://.pi` ([section 11](manual.html#browser) of the manual) | Installing, removing, packing, testing, granting, accepting, changing settings and serving are yours alone. Anyone else is told `operator_decides`. ### Write the manifest An app folder has `manifest.lua`, `handlers/main.lua`, and optionally `schema.sql` and `assets/`. The manifest is data, never code: a function or `require` in it is refused. ``` return { schema = "cherrypi.app.v1", id = "notes", -- lowercase, up to 64 characters name = "Notes", version = "0.1.0", capabilities = { "app.capability", "tools.custom", "appdata:own" }, storage = { tables = { { name = "notes", mirror = "none", schema_version = 1 } } }, settings = { { key = "sort", type = "choice", default = "newest", choices = { "newest", "oldest" } } }, contributions = { verbs = { { name = "add", summary = "add a note" } }, tools = { { name = "search", summary = "search notes", parameters = { type = "object", properties = { q = { type = "string" } } } } }, }, } ``` | Field | Rules | |---|---| | `schema` | Always `cherrypi.app.v1` | | `id` | Starts with a letter and holds only lowercase letters, digits and `_ . -`. At most 64 characters, and not the name of a `picache` verb. | | `name`, `version`, `description` | Shown in the list and the Extensions pane | | `runtime` | Leave it out, or `"lua"`, the only runtime. | | `capabilities` | Everything the app may ask for ([below](extensions.html#app-capabilities)) | | `storage` | Its own tables ([below](extensions.html#app-data)) | | `settings` | Up to 32: `string`, `choice`, `boolean`, `number` or `secret`. Kept as `app..` and handed to every call. A `secret` (an API key your app calls a service with) has no default. It's sealed with your other keys, never in the settings, and opened only into your app's own call ([Where your secrets go](manual.html#secrets)). | | `web` | `{ root, index }`: pages to serve. Needs `domain.serve`. | | `contributions` | What it adds ([below](extensions.html#app-adds)) | ### What an app can add Each contribution has a `name` (lowercase, at most 48), a summary, and the capability it runs under, which the manifest must also ask for. | Kind | Runs under | You reach it | |---|---|---| | `verbs` | `app.capability` | `picache ` | | `tools` | `tools.custom` | As Picache's tool `app__`, and over MCP, while granted | | `panes` | `app.capability` | A pane drawn from A2UI, from the Extensions pane | | `panes` with `program = true` | `app.terminal` | Its own program in a terminal pane, since a program there hears every key | | `hooks` | `app.hook` | Heard on calls and model requests ([below](extensions.html#app-hooks)) | | `layouts` | `app.layout` | A saved layout it may open, with the commands its terminals may run | `handlers/main.lua` returns a table with a function for each: `verbs.(q)`, `tools.(q)`, `hooks.(q)`, and for a pane `render` and `action`, or `start`, `key`, `resize` and `tick` for a program. A layout lists every command its terminals may run, and `layout.apply` asks you each time. A plain shell or an agent pane is refused, and so is anything over 16 panes. ### Hooks A hook hears a call at one of four points. Its answer is labelled as the app's, and it can never approve, change a call or answer a question cherry π asks you. | Point | Hears | May answer | |---|---|---| | `before_call` | A call before it runs | Block it, warn, or propose another verb | | `after_result` | The result | A note | | `context` | What a model is about to be given | Text to add | | `route` | A model request: `user`, `continuation`, `retry` or `direct` | Seat, model, thinking, tokens, temperature or a handoff, each under its own `route.*` grant | A hook has two seconds to answer. An answer that is slow, fails or is unclear counts as no opinion. Its text is cut at 500 bytes. A block refuses the call with `hook_blocked`, naming the app. ### Capabilities An app holds only what you grant, checked at every call. Each capability it asks for is granted separately. | Group | Capabilities | |---|---| | Adding things | `app.capability`, `tools.custom`, `app.terminal`, `app.hook`, `app.layout`, `app.world` | | Its data and pages | `appdata:own`, `domain.serve` | | Other apps | `app.list`, `app.inspect`, `app.build` | | Loops | `ai.redloop.read`, `ai.redloop.manage`, `loop.durable` | | Models | `route.seat.agent`, `route.seat.small`, `route.seat.cloud`, `route.params`, `route.load`, `route.handoff`, `route.read.prompt`, `route.read.messages`, `route.read.tools` | | Agent services | `skills.propose`, `skills.retrieve`, `memory.query`, `memory.propose`, `search.files`, `decision.answer` | Some are never an app's at any level: your files (`drive.*`), the [addon capabilities](extensions.html#registries), and the core's own, such as `store.write` and `approval.resolve`. A manifest that asks for one is refused. A grant is for this session or always. A session grant ends when the harness restarts, and you're asked again. Deny takes it back, and a program pane running under it ends at its next call. ### Ask cherry π to do something A handler can't reach cherry π directly. It answers `{ ask = { verb, args }, cont }`, and the harness runs the verb and calls the handler again with the result. One call may ask up to 16 times, and every ask is receipted. | Verb | Under | Does | |---|---|---| | `apps.db.query`, `apps.db.exec` | `appdata:own` | Read or write the app's own tables | | `apps.list` | `app.list` | Installed apps | | `apps.inspect`, `apps.rows`, `apps.query` | `app.inspect` | Look inside another app's file, read-only | | `apps.edit`, `apps.pack` | `app.build` | Change or pack another app. Asks you every time. | | `loops.read`, `loops.manage` | `ai.redloop.read`, `ai.redloop.manage` | Read, start, stop and define loops. Defining or starting one asks you. | | `route.history` | `app.hook` | Recent routing decisions, up to 50 | | `layout.apply` | `app.layout` | Open one of its layouts. Asks you every time. | | `world.open` | `app.world` | Open the world on a layer. Asks you every time. | ### The capsule Handlers are Lua, and run in a [capsule](manual.html#capsules) sealed to the one capability the call is under. On a machine that can't confine, apps don't run (`app_unconfinable`). - They get Lua's string, table, math and utf8 libraries, plus JSON, and have no file, process or network access. - `require` reaches only the app's own handler files. - An answer has 10 seconds and at most 256 KiB. - Work longer than one call is a loop that the harness steps and recovers, so no app code is left running. A loop has at most 50 steps. ### An app's own data Tables declared in `storage` and created by `schema.sql` are the app's to write. They sit outside the signature, so writing them never breaks it. - `schema.sql` may hold only `CREATE TABLE` and `CREATE INDEX`. - Each read or write is one statement. Writes go only to the declared tables. - Writes are journaled for sync with paired machines, except in a table whose `mirror` is `"none"`. - Declaring tables needs `appdata:own`. `install --replace` carries the rows into the new version and runs its migrations. A version that drops a column or changes a table without a migration is refused before anything is written. The old file is kept in `/apps/backups/`. ### Meta, the first app Meta inspects and packs `.pi` files, itself included. Install it with `picache app install apps/meta` from a checkout. It adds the verbs `list`, `inspect`, `rows`, `query`, `pack` and `edit`, an Inspector pane and a SQL program pane. It asks you each time it signs. ## 03 / Cellar Door worlds A world is a place you walk through in a terminal pane: scenes of animated art, doors between them, and spots to chat. Cellar Door runs it, and cherry π gives it a layer, panes and, when the layer is shared live, the other members. Cellar Door comes in two parts. | Part | What it is | Comes as | |---|---|---| | The `cellardoor` addon | The engine. It runs one headless Cellar Door sidecar for each layer with a world open. | Inside `cherrypi.ape` for macOS (ARM) and Linux (ARM, x64) | | A world app (`.pi`) | A world with its layout: which panes open, and the commands they may run | A developer setup ([below](extensions.html#world-app)) | ### What a world is A world is a folder with a `world.json` and its scenes. Put it in the layer's folder, either at the top or in `world/`. - Scenes are the places, each a file under `scenes/`. - A scene holds objects: `art`, `chat` spots and `door`s. - A door leads to another scene, or to another world by its address. `world.json` names no layer, pane or path of cherry π's, so the same world runs in Cellar Door on its own. No world ships with cherry π. AFTERHOURS, from the Cellar Door repository, is the one to try: three scenes, 20 animated pieces, three chat spots and doors in a loop. ### Open a world ``` picache addons enable cellardoor # once ``` Then, in the layer, choose **Actions → Open a world on this layer**. Its sidecar starts, and you're told how to walk in. An app with `app.world` can open one with the [host verb](extensions.html#app-host) `world.open`. You're asked every time, and shown the layer and its folder. A layer has one world at most. It closes when the layer closes, and its sidecar is stopped. ### Walk in ``` cherrypi-world attach # in a pane on that layer cherrypi-world attach --wait # wait up to 15 s (at most 60) for it to start ``` The pane becomes a console on the world, drawn in text. `Ctrl+]` leaves it and gives you the pane back. | Key | Does | |---|---| | `W` `S` or `↑` `↓` | Forward, back | | `A` `D` or `←` `→` | Step left, right | | `Z` `X` | Turn left, right | | `Space` or `E` | Go through the door in reach | | `C` | Chat at the spot in reach. `Enter` sends, `Esc` closes, `Ctrl+U` clears. | | `Q` or `Esc` | Leave the world | ### Who can walk in Two kinds of pane can attach: - Your own shell, as you. - A pane an app's layout opened, as that app, while it holds `app.world`. Anything else is refused with `world_attach_refused`. The console never names a layer or a person. The harness works that out and vouches for it, and each console gets a one-time ticket that lasts 10 seconds. Attaching gives you presence in the world and no right to change it. ### Where a world's files go | What | Where | |---|---| | The sidecar's state | `.cellardoor/` in the layer's folder, beside the world, never inside it | | Its control socket | `/tmp/cherrypi-/.world.sock`, named for `/worlds/`, in a folder only you can open | | Its output | `/worlds//sidecar.log` | | Starts, restarts and stops | `/logs/addons.log`, and the events `world.sidecar_exited` and `world.sidecar_killed` | | Opening, closing, each console let in | Receipts: `world.open`, `world.close`, `world.admit` | A sidecar that exits is started again after 1, 2, 4… up to 60 seconds. Closing sends it SIGTERM, then SIGKILL five seconds later if it's still there. The sidecar runs unconfined, as a [job](manual.html#jobs) does. ### A world app's layout A world app brings its layout: a world pane, a room for chat, files beside it. It declares the layout and every command the layout's terminals may run. ``` capabilities = { "app.layout" }, contributions = { layouts = { { name = "world", asset = "layouts/world.json", runs = { { "bin/studio", "-world", "assets/worlds/afterhours", "-control", "world.sock" } } }, } } ``` Apps run Lua only, so a `.pi` can't carry Cellar Door's program. The layer's folder has to be a Cellar Door checkout with the program built. ### Share a world When a layer is [shared live](manual.html#live-layer), its world is shared too. Every member runs the whole world on their own machine. There's no world server and no relay. > **Diagram: One world, two members.** Two machines share a live layer. Each runs its own Cellar Door sidecar holding the whole world. Between them run two channels. The log carries what changes the world for everyone, the edits and chat proposed to a sidecar, in one order on every member, kept in the layer's shared history; each sidecar replays it. Motion carries where each person is, directly between the members, up to 20 frames a second, kept nowhere. Everything else each machine works out from the world's files, the log and the clock. > > Two channels keep every member's copy of the world the same. | What | How it crosses | |---|---| | Edits and chat proposed to a sidecar | The **log**: one order on every member, kept in the layer's shared history. Each sidecar replays it. | | Where each person is | **Motion**: straight to the other members, up to 20 frames a second, kept nowhere | | Everything else | Nothing. Each machine works it out from the world's files, the log and the clock. | The harness writes each event with the machine it came from. The world never says who it is. A row that lies about who wrote it is skipped on every member, so every copy stays the same. Motion goes over the connections the layer already uses. Only the layer's other members can send it. A frame is at most 1 KiB, and anything faster than 20 a second is dropped. Losing a frame is harmless, because each one carries the latest position. If your harness restarts, the sidecar replays the log from the start and carries on. A world whose characters move by themselves, such as followers, is not shared, because on two machines they'd drift apart. It runs on each machine alone, and you're told why (`world_rules_unshared`). Edits reach the log from Cellar Door's Studio, started with `-propose` and the sidecar's socket. A console proposes nothing, so chat typed in a console stays on that machine. --- # What if… Technical questions about what cherry π does when something goes wrong, or someone tries to make it. Each answer says what happens, and where the field manual has the detail. ## 00 / The model in one paragraph An agent in cherry π is limited in four places. The box ([section 09](manual.html#capsules)) limits what can run: a vendor agent runs whole inside it, with its shell, hooks and MCP servers. The token limits what that running process may ask the workspace for. Every tool is a verb, and each verb call is checked against the caller's policy and receipted. The proxy and the signer limit what can leave the machine. And you are asked about the calls that are hard to undo. None of these is meant to hold alone, and each answer below says which of them applies. ## 01 / Inside the box ### What if the agent reads `PICACHE_TOKEN` from its environment? It already has it. The token is how its pane calls `picache`, so reading it gains nothing. The token names one actor (your shell, a vendor agent, Picache) and the folder it works in. It is minted when that pane starts and revoked when the pane closes. Whatever presents the token can only call the verbs that actor may already call, and each call is checked by policy and receipted under the actor's name. Anything else running in the same box can use the token too. That is why the box has to be tight, and the next questions are about how tight it is. [Field manual, section 04](manual.html#picache). ### What if the agent just uses bash instead of your tools? Then its bash runs under the same limits as the agent. The box is around the whole agent, so its bash, Python or anything it installs is inside it too. It can write only its folder, its config and its own temp folder. With the default network setting, it reaches the network only through the proxy. Some tools sandbox only the code interpreter. There, an agent's own shell still runs outside the sandbox. Every tool is also a plain command, `picache `, so a script in any language can call the tools under the same policy, approvals and receipts. [Section 09](manual.html#capsules). ### What if it leaves something behind that runs later as me? Outside its folder, config and temp folder it can't write at all. So it can't add a cron job, a launch agent or a line to your shell profile. Inside its folder, the risk is a file your own tools later run as you. The usual one is git's: a hook or setting in the repository. The box can't write those. When the box ends, cherry π lists anything the agent left in the repository that your git would write through as you and that git itself would not have made. It tells you to look at each one and remove it before you run git in that repository. A box also can't rebase, so it can't leave a half-finished rebase for your next `git rebase --continue` to pick up as you. Git is the only tool checked. Any other tool of yours that runs a file from the project (a build script, a `Makefile`) runs what the agent wrote, as it would for any collaborator's change. Review those files before you run them. ### What if it reaches for localhost, my network or a metadata endpoint? With the network set to `ask` (the default) or `allowlist`, the agent's only way out is a proxy outside the box. A name that resolves to this machine or a private network is refused unless a rule names that exact address. A connection that tries to go around the proxy has no route. The proxy decides by host name and port. It carries HTTPS tunnels and plain HTTP, and never decrypts HTTPS. Setting the network to `open` takes the proxy away, except for the vendor of a key that is injected. The agent can then reach loopback and your local network, including other agents' proxies, and cherry π labels the setting "open, anywhere". [Section 09](manual.html#capsules). ### What if it runs sudo? An agent's pane in a box can't use sudo by default, and on Linux neither can any pane an agent opens. On macOS, an agent's pane outside a box, and a pane an agent opens, can run sudo only with your password (`terminal.sudo.unboxed_agents`, which can refuse it instead). Your own shell can use sudo. Before an agent or Picache types into a pane, sudo forgets your password, so what they type can't run on it. [Use sudo](manual.html#sudo). ## 02 / Keys and the signer ### What if a script in the box reads my API key? With the agent's key set to `inject`, there is no key in the box to read. The agent holds a stand-in of the same shape. When it calls its vendor, the proxy hands the connection to cherry π's signer outside the box. The signer opens the real key, swaps it in and sends the request on over verified TLS. A request whose key header holds anything but the stand-in is refused. An agent starts with `hand-in`, where the key does sit in the box's environment, until cherry π's record for that agent shows it running injected under this machine's kind of box. After that it defaults to `inject`. A launch set to inject that can't inject is refused, and the key is not handed in instead. [Keep the key out of the box](manual.html#keys-out). ### What if the signer, or the proxy, is the thing attacked? The signer and the proxy are the high-value targets, so they are kept small and apart. Both run outside the agent's box. The signer holds the keys, sealed. An injected key is opened only inside the signer, for a request to that key's vendor. Any other key is opened for cherry π only where it is used: a cloud model call, an agent's box when its key is handed in, or an app's own call. The peer-to-peer sidecar, which talks to other machines, runs in a box of its own, away from the signer's keys. Your answers to the proxy's questions are kept as receipts, and the proxy keeps a log. The signer answers any process running as you and does not check who is asking. The sealed store is protected by its file modes and the signer's root key. ### What if a key ends up in a receipt, a log or a command line? Every authenticated `picache` call leaves a receipt, so receipts are filtered twice. A verb that knows an argument is secret hides it first. Then anything shaped like a key (`sk-ant-…`, a bearer token, a variable named as a key, a password in a URL) is replaced by a note of what kind of secret it was. App hooks get the same filtering, or no text at all. An agent's token or an app's secret typed on the command line is refused. Pipe it in instead. On Linux, a sudo-capable pane's environment reaches systemd in a private file, removed once read, and never on its command line. The key log never records typed text. Text an agent types into a pane is filtered by shape only, and a password has no shape. [Where your secrets go](manual.html#secrets). ### What if someone copies my cherrypi.db? They get receipts with no keys in them, and pane records with no pane tokens. Each record keeps a digest of its pane's token, and the token can't be recovered from the digest. The file and its backups are readable by you alone (0600). Your keys are in a different file, sealed under a root key only the signer reads. [Where your secrets go](manual.html#secrets). ## 03 / Approvals, crashes and evidence ### What if every action needs my approval? Most don't. Each verb has an effect class. Reads run. Writes that can be undone run too, and every Picache turn snapshots the folder before and after, so a turn can be put back. Calls that are hard or impossible to undo ask you, by default. Because you are asked only about those, each question is one worth reading. [Section 03](manual.html#authority), [Undo a turn's changes](manual.html#undo). ### What if something irreversible is hidden inside a shell command? It depends whose shell it is. A command Picache runs goes through the `run` verb. The shell policy classifies the command before it runs, and `run` is irreversible, so it asks you. If the command changed files in the folder, the turn's snapshots let you put them back. A vendor agent's own shell inside its box is not a verb, so no effect class sees what it does. The box bounds it instead: where it can write and what it can reach. That is why the box is around the whole agent. ### What if cherry π crashes in the middle of an action? A call that changes something is written on the turn's task before it runs. That includes calls you approved and calls a script or an app makes. If cherry π crashes midway, the call is left marked uncertain and is never run a second time on its own. An uncertain effect blocks a retry until someone settles it in Recovery. A question nobody answered waits in Recovery too, and approving it continues the turn from that call. [Section 10](manual.html#records). ### What if nobody reads the receipts? They are still evidence after the fact. Within a turn, a hash chain binds every receipt, refusals included, so a missing or altered one shows. The desktop also uses them as they come in. Its status line counts refusals and failures, Go to finds them, and Recovery acts on uncertain effects and waiting questions. [Section 10](manual.html#records). ## 04 / The web desktop ### What if another program on my machine calls the web server? The web server listens only on loopback, but every program on the machine can reach loopback, an agent included. So the desktop's pages and APIs need a browser you signed in. A launcher on this machine mints a one-time code, and the browser trades it for a session of its own. A request that changes something must also come from one of the desktop's pages. A request without a session gets `401`. A confined agent's box doesn't see the folder where the codes and sessions live. [Sign a browser in](manual.html#web-signin). ### What if someone copies my terminal's browser credential? **Open in browser** copies nothing. It opens a one-time sign-in link, and the page then asks for a credential of its own. If you make a credential by hand for pasting, it is spent when the browser connects. The answer carries a new key that only that page holds. After that, the pasted credential can't read, type or revoke anything. [From a browser](manual.html#sakura-browser). ### What if a served app's page tries to use the desktop? Inside Cherrytop, an app's page runs sandboxed in an origin of its own, so its scripts can't call the desktop's APIs or read its page. It can send Cherrytop a message asking to open a pane. A canvas widget may run one verb its manifest names, read-only. Any write that verb would make is refused before it runs. Cherrytop takes which app is asking from the frame the message came from, never from what the message says. [The web desktop](manual.html#web-desktop). ### What if another site frames the desktop to trick a click? The desktop's pages refuse to be framed by any other site. Its sign-in cookie is also `SameSite=Strict`, so a framed copy would be signed out even if a browser framed it. ## 05 / Not covered The answers above leave these open: - Keys are handed into the box until cherry π's record for an agent shows it running injected ([above](whatif.html#wi-key)). - A vendor agent's own shell commands are bounded by the box, and nothing classifies them one by one ([above](whatif.html#wi-hidden)). - Files your own tools later run from the project, other than git's, are yours to review ([above](whatif.html#wi-persist)). - An `open` network removes the proxy and its refusals ([above](whatif.html#wi-ssrf)). - Anything that can act as you outside every box (read your home folder, run as your user) can do whatever you can. Nothing here defends against that, because it is you, or something already past every boundary on this page.