The full reference for Shell:B Web: every feature, where its code lives, its API routes and its tables. The
README is the tour; this is the manual. Shell:B reads this file to describe herself
(shellb_self action=about lists the bold names under What it does), so keep it current when a feature changes.
Contents: Access & networking · What it does · Connectors, MCP servers and cloud AI · Login · Projects and skills · Layout · Operating it · Not done yet
URL: https://<tailscale-device>.<tailnet>.ts.net:8443 (Tailscale HTTPS) or the Cloudflare tunnel hostname ·
service shellb-web (systemd user unit, port 8200). HTTPS only: server/serve.py listens on the addresses in
SHELLB_HOSTS (default 127.0.0.1; this Spark adds the docker0 gateway 172.17.0.1, where hub-cloudflared and the hub's
_probe reach host.docker.internal:8200). There is no plain-HTTP LAN address; http://localhost:8200 still works on
the Spark itself for tools, shot.sh and the kiosk.
HTTPS is configured via tailscale serve (tailnet only, Tailscale's certificate, persists across reboots): --https=8443 →
http://127.0.0.1:8200, and Studio dev-server previews --https=8410…8417 → http://127.0.0.1:8210…8217 (82xx is
already bound on all interfaces by the preview proxy, so HTTPS uses 84xx; ui/src/studio/Canvas.tsx adds 200 when the
page is HTTPS). tailscale serve sets X-Forwarded-For, so auth.py treats these requests as remote and they need the
owner login; cookies get the Secure flag from X-Forwarded-Proto. On plain http the mic button reopens the chat on
the HTTPS address (ui/src/secureUrl.ts). The certificate covers only the full name: https://<device>:8443
fails with ERR_SSL_PROTOCOL_ERROR, so http://<device>:8200 (MagicDNS short name) redirects page loads to the HTTPS
address (_ShortNameRedirect in app.py). Check with tailscale serve status; remove with tailscale serve --https=8443 off.
-
Agents (edit them in the Agent Lab): Shell:B (orchestrator), Hephaestus (engineering, ShellB-Forge = Qwen3-Coder-Next), Minerva (research and analysis), Heimdall (vision), NYX (generative art), Spark (quick answers), Mimir (knowledge: offline wiki, Codex, past chats), Atlas (Worlds and cartography), Magpie (asset scout) and Daedalus (Studio, hidden from chat lists); the local ones run on ShellB-Swift (Qwen3.6) unless noted. Cloud: Claude (
claude-code/opus) and Gemini (antigravity/gemini-3.1-pro-high), see Cloud AI. Each agent has its own model, prompt, tools, temperature, reasoning effort and context size. The Agent Lab lists the tools you pick per agent; tools Shell:B adds by rule (weather and shopping for agents that search, wiki and law once installed, the library, Codex, follow-ups, circles, the browser and screen in live chats…) appear under Added automatically with their rule, andGET /api/toolsreports each tool'sscopeandautotext. -
Long chats (
server/compaction.py): when the next prompt would fill about 70% of the agent's context, the oldest messages are summarized in the background after a reply and the summary is applied at the start of the next turn. Nothing is deleted: summarized messages getsummarizedByand stay in the chat (under a "Summarized N earlier messages · Show originals" divider), in full-text search and inrecall_conversations; only the prompt leaves them out, and the system prompt says honestly that the summary is lossy. The Compact button does it now. A context meter under the composer shows the estimate ("41k / 128k", amber from 70%);GET /api/conversations/<id>/context. -
Secretary (
server/secretary.py,#/secretary): meeting transcription (HERMES Whisper) with speaker separation. Voices are told apart by neural voice prints (HERMESPOST /speaker_embed: WeSpeaker ResNet34-LM, ONNX on CPU, 256-d), compared by cosine distance (strict 0.40, balanced 0.50, relaxed 0.62); clips under 1.5 s can't start a new speaker, live meetings re-cluster every 25 utterances, and fragments under 4 s fold into the nearest speaker. Renamed speakers keep their names when re-clustered. Meeting audio is kept indata/secretary/<meeting>(deleted with the meeting, voice prints too) so Separate Voices can re-identify from audio; meetings without audio re-cluster their stored features. If HERMES is down, the old pitch/timbre features are used ("Voice ID: basic"). -
Real agent loop: streams reasoning and text from Ollama, runs tool calls in parallel, and loops until the model answers (
maxToolRoundscaps it). Sub-agent delegation goes throughask_agent. -
Tools: web search through VASSAGO/SearXNG; page reading via trafilatura, with local addresses blocked; a Python/Bash sandbox (Docker,
--network none, with a per-conversation/workfolder); versioned artifacts (HTML, SVG, Markdown, Mermaid, code); FLUX images via NYX/ComfyUI, which evicts idle models if memory is tight; MusicGen via ORPHEUS; and long-term memory (see Memory below). -
Memory (
server/memory.py,ui/src/components/MemoryManager.tsx; Settings → Memory, the Memory app,#/memory): what Shell:B knows about the owner. Each memory has a category, origin, source chat, optional expiry and a status (active / pending / retired). Always known memories (identity, work, preference, or pinned) go into every prompt as a profile; the rest are recalled by similarity (qwen3-embedding, ≥ 0.5, top 6). After every interactive turn the utility model reads the user's message and proposes add / update / retire actions (memoryLearning: auto applies them and shows "Remembered …" with Undo under the reply; review queues them for the owner and posts to Activity; off). Only the user's own words count: pasted text, task instructions and the reply are ignored, and update/retire can only target memories the learner was shown. Corrections retire the old memory ("replaced"), so everything can be restored. Agents getmemory_save(category,replaces,expires),memory_forgetandmemory_search. Learn from past chats reads earlier chats (not Studio/Code, to-dos or plan steps) and proposes memories for review. API:/api/memory(list, add,PATCH /{id},POST /{id}/{forget|restore|accept|reject|undo},/accept-all,/learned/{msg},/backfill);/api/statusreportsmemoryPending. -
Past conversations (
server/episodes.py): episodic memory. An FTS5 index (msg_fts, kept in sync by triggers onmessages) covers every message, and a background sweep has the utility model write a 1-3 sentence summary of each conversation once it has been quiet for a minute (conv_summary, with an embedding; it waits while anyone is chatting). Agents with memory getrecall_conversations(searchmerges full text and summary similarity;readreturns the transcript, optionally around a message id), and their prompt lists the six latest substantial conversations with summaries. Scouts don't get it: they read untrusted pages. The sidebar search also searches inside messages ("In messages", with highlighted snippets) and jumps to the matching message (ui/src/jump.ts). API:/api/chats/search?q=,/api/chats/summary-status. -
Privacy (
server/privacy.py,ui/src/components/Incognito.tsx): Incognito chats (the eye-off button beside New chat, "New incognito chat" in Ctrl K, or/incognito;POST /api/conversations {"incognito": true}, columnconversations.incognito) get no memory in the prompt and no memory tools, teach the learner, feedback and "Learn from past chats" nothing, are never summarized or found by past-conversation/sidebar search, can't join a chat Project, and are deletedincognitoHours(default 24) after their last message by the episodes sweep; forks stay incognito. Aphrodite chats (columnconversations.adult, set when she first answers) are kept like normal chats and listed in the sidebar, but are hidden like incognito ones from everything shared between chats (privacy.HIDDEN_SQL,is_hidden): no shared-memory learning or recall, no summaries, search, follow-ups, chat messages, shares, circles or chat Projects. Only she answers in one (other agents get "Start a new chat"); forks stay hers; deleting one deletes its pictures. She keeps her own memory of the user's tastes (tableadult_memories, toolaphrodite_memory, up to 200 short facts, each screened like a request, put in her prompt; listed with a forget button in Tools & Connectors → Aphrodite,GET/DELETE /api/adult/memories), which no other agent or memory view sees. In an incognito Aphrodite chat she neither reads nor saves it. Her prompt ends with the hard limits and her play rules (adult.pyGUARDRAILS,PLAY): never judge or moralise, play along, and no therapy: no guessing at mood or mental health, no wellbeing check-ins or hotlines, out of character only when the user steps out (or says plainly that they are in real danger). Local-only memories (the lock on a memory row;PATCH /api/memory/{id} {"localOnly": true}, columnmemories.local_only) never reach a cloud agent's prompt or its memory_search, even withcloudMemoryon; a correction or merge of one stays local-only. -
Voice mode (
ui/src/components/VoiceMode.tsx; the waveform button beside the mic in the chat box, HTTPS only): a hands-free conversation. The mic stays open (browser echo cancellation and noise suppression); a simple energy VAD learns the room's noise floor, starts an utterance after 160 ms of speech and ends it after 900 ms of quiet; the clip goes to HERMES Whisper and is sent withvoice: true. The server then addsVOICE_NOTEto the system prompt andVOICE_REMINDERto the message (the model ignored the system note alone), so the reply is a few spoken sentences. The reply is read aloud sentence by sentence as it streams (HERMES Kokoro, prefetched in order;speakable()drops code, tables, links and Markdown). Talking over it (a higher threshold while audio plays) stops playback and an unfinished reply; what you said is sent once the turn has stopped. Pause mic, Skip, Esc to end. Spoken messages are stored withvoice: trueand shown as "Spoken". -
Feedback (
server/feedback.py,ui/src/components/Feedback.tsx): thumbs up/down under every finished reply. Thumbs down opens "What should be different?" (quick reasons plus a note); either thumb can carry a note. With "Remember this for next time", the utility model turns it into at most one lasting preference rule (apreferencememory, originfeedback, so it is in every prompt from the next message on), possibly replacing an older rule. It learns nothing from feedback about this one answer ("wrong date") or from vague feedback (a bare "wrong tone" with no note), and it says so. "Try again with this feedback" sends the complaint as a follow-up. Ratings are stored in tablefeedback(one per message) and in the message'sfeedbackdata. API:POST /api/feedback(msgId,ratingup|down|null,reasons,note,remember),GET /api/feedback/stats?days=. -
Nightly reflection (
server/reflection.py; Memory page → "Tidy up now", "Every night at"): once a night (memoryReflection,memoryReflectionHour, default 3 AM local, only while nobody is chatting) Shell:B expires dated memories, rewords first-person ones in the third person, merges near-duplicates (cosine ≥ 0.72, model decides; pairs judged separate are not asked about again) and asks about facts older than 21 days that look temporary ("about to", plans whose dates passed) as "Forget?" items in the review queue (never re-asked within 90 days). Rewrites and merges keep the original date, followmemoryLearning(auto applies, review queues), and retire the originals, so they can be restored;replacesmay list several ids. Weekly, an Activity digest lists what was learned, forgotten, tidied, what waits for review, and the week's thumbs with repeated complaints per agent. API:GET /api/memory/reflection,POST /api/memory/reflection/run?digest=. -
Documents, pictures and OCR (
server/doc_tools.py):read_documentextracts text from PDF, DOCX/DOC, XLSX/XLS, PPTX/PPT, ODT/ODS/ODP, RTF, EPUB, HTML and images (PyMuPDF; LibreOffice converts legacy formats), OCRing pages that have no text layer;view_documentrenders pages or pictures for the vision model (text-only agents get a written description instead);ocridentifies text with RapidOCR (PP-OCR on ONNX, CPU, with confidences, boxes, regions and en/latin/japanese/chinese/korean/cyrillic models) orengine=visionfor tables, forms and handwriting. All three take/work/…,/project/…or an http(s) URL;web_fetchhands PDFs and images toread_document. Uploaded PDFs and Office files arrive with their text layer inlined, and Project knowledge OCRs scans and images when it indexes them. -
Rich links (
ui/src/components/RichLinks.tsx): agents can hyperlink words in a reply.[w](tip: …)shows a Markdown/math tooltip;[w](widget:#key)plus a```widget keyfence pops up a live, sandboxed HTML control (shellb.ask()inside it sends a follow-up);[w](artifact:id)previews an artifact on hover and opens it on click;[w](ask: …)is a one-click follow-up chip;[w](file:path)opens a Studio file. The agents learn the syntax fromRICH_LINKSinserver/agent.py. -
Composer shortcuts (
server/composer.py,ui/src/components/ComposerShortcuts.tsx): in any chat box,/at the start opens commands:/new,/effort instant|low|think|deep,/agent X(switches the chat), tool nudges (/image,/music,/search,/fetch,/run,/artifact,/remember,/recall,/libraryadd a "use this tool" note to the turn), skills (/<skill>), and saved prompt templates, whose{{blanks}}you fill in by pressing Tab to jump between them ({{date}},{{clipboard}}… fill in on their own).@Agentanywhere answers that one message as that agent (routeOnly; the chat keeps its agent; edits and regenerations re-read the mention).#inserts variables (#date,#time,#now,#weekday,#clipboard,#agent, plus saved ones; typed#names expand on send) or references a chat artifact, a library item or a URL. A reference becomes an attachment of kindref, and the server inlines it when the turn starts (artifact text, a listing plus a pointer to thelibrarytool, or the fetched page). Saved variables and prompts live in kvcomposer_vars/composer_promptsand are managed in Settings → Shortcuts. API:/api/composer,/api/composer/{variables,prompts},/api/composer/refs?q=&conv=. -
Plan mode (
server/planmode.py): the Plan button in the chat box (or Shift+Tab, or/plan;/plan <request>plans just that one message) makes the agent research with read-only tools and reply with a## Planinstead of doing the work. This is enforced, not just asked for: write tools are left out, calls whose action writes (library import,codex add,http_request POST…) are refused, MCP tools are dropped, and sub-agents reached throughask_agentinherit the mode. The reply gets a card with Approve and run, which turns plan mode off and has the agent that wrote the plan carry it out, and Keep planning. It works in chats and in the Studio/Code chat. Messages carrymode: "plan"(request bodymode), and unattended turns ignore it. -
Library (
#/library, sidebar → Library, or Ctrl K): one gallery of everything Shell:B has built. It shows the latest version of every chat artifact across all conversations, plus every Studio project and every Code workspace that has anindex.html, with live thumbnails, search, an All/Artifacts/Web apps switch and type filters (GET /api/library). An artifact opens in its chat with the panel showing, or full-window in a new tab (#/view/<conv>/<artifact>, still sandboxed). A web app launches in a new tab, or opens in Studio or Code for editing. Code is inui/src/components/Library.tsx. -
Shared library (
server/library.py): every agent, wherever it works (chats, Studio, Code, chat Projects, scheduled runs, to-dos, mega plan steps), gets thelibrarytool. It reaches everything Shell:B has made: Studio designs and Code workspaces (files), research notebooks (sources by S#, notes, findings, outputs, original files), chat Projects (knowledge files), chat artifacts and generated media. Actions:list,search(titles, file names, file contents, plus semantic search over notebook sources),browse,read(PDF/Office text extracted),view(images to the vision model) andimport, which copies a file or whole folder into the current project (defaultassets/<name>) or the chat's/work. It copies rather than hot-links, so designs and exports stay self-contained. Refs areproj-…,rb-…,grp-…,conv-…/<artifact>,media, or a unique title. The system prompt lists the 20 most recent items.run_codesees the same stores read-only under/library/{projects,research,chat-projects,media}. Cloud agents get none of this unless Share the library with cloud agents (cloudLibrary) is on. Turn it off for everyone with thelibrarytool switch. -
Wiki (
#/wiki;server/wiki.py,ui/src/components/Wiki.tsx): offline Wikipedia, Wiktionary and Wikivoyage from Kiwix ZIM archives in~/shellb/wiki, read with python-libzim. Title suggestions, full-text search and a native reader. The article HTML is never shown raw: only the parser output is kept, lxml_html_clean plus an attribute allowlist strip scripts and styles, ids get aw-prefix, internal links become#/wiki/<source>/<path>, and red links are unwrapped. Images come from/api/wiki/res(image types only, under a no-script CSP). Kiwix's tiny stub pages for redirects to a section are followed. Every agent gets thewikitool (search / read with an optional section) once an archive is installed, which is read-only in plan mode. Its output names the snapshot date and gives a[Title](#/wiki/...)link. -
Offline data (
#/data, gear menu;server/datasets.py,ui/src/components/OfflineData.tsx): the big downloaded files behind Worlds and Wiki, with their size, build, download date and history (state indata/datasets.json). The Earth basemap (Protomaps daily build,~/shellb/maps/<YYYYMMDD>.pmtilesbehind theplanet.pmtilessymlink) and the Kiwix archives can each be updated off / weekly / monthly (monthly by default). The loop checks a dataset when its period has passed since the last check (or the download) and queues the newer build. Downloads run one at a time in a thread, resume from<file>.partwith Range requests (including after a restart), refuse to start without room for the file plus 10 GB, verify Protomaps' md5 or Kiwix's.sha256, then swap atomically and delete the old copy. Results go to Activity (sourcedata). Installing, scheduling, cancelling and deleting need the owner. The planet mosaics and map fonts are built locally, so they are only measured. -
Legal (
server/legal.py; toollegal,/api/legal/{sources,search,read}): offline US and EU law from a full-text index,~/shellb/legal/index.db(SQLite FTS5; one row per section, article, recital, annex chunk, tariff heading, screening-list entry, Federal Register document chunk or court-opinion chunk). Sources: US Code, eCFR (EAR/CCL in 15 CFR 730-774, ITAR, OFAC, Customs), Statutes at Large, Public Laws, HTS, the trade.gov Consolidated Screening List, EUR-Lex legislation (the Cyber Resilience Act, NIS2, GDPR, AI Act, dual-use, customs), and, oncebuild_index.py frhas run, the Federal Register (daily issues, 2020 on;FR_FROMsets the start year) and, through~/shellb/legal/build_caselaw.py(streams the 52 GB CourtListener bulk file), published opinions of the Supreme Court and the federal courts of appeals, first 60k characters each. Both started indexing on 2026-10-10 (run-20261010-index.out). The files are fetched by~/shellb/legal/fetch_legal.py(resumable, outside this repo) and indexed by~/shellb/legal/build_index.py(per-file, skips what is already indexed, so re-run it after a download). The tool opens the index read-only per call and is added to every agent once it exists (tool_switches.legalturns it off). Results carry the cite and the snapshot date; the model is told it is reference, not legal advice.searchranks heading > act title > text, puts an explicit citation first (15 CFR 740.2,9 USC 2,HTS 8471) and ranks non-legislative EU documents below acts;readtakes a cite or a whole-act name/CELEX number (outline). -
Downloads indicator (
ui/src/components/Downloads.tsx, top bar): a progress ring that appears while anything large is downloading (offline data and its queue, Ollama model pulls), fromdownloadsin/api/status. While something is running it polls/api/datasets/downloadsevery few seconds. Click it for per-item bars, speed and time left. -
Mimir (built-in agent
mimir, "Knowledge"): the reference desk. It answers factual questions from sources in this order: the owner's Codex/memory/past chats/Library, then the offline wiki, then the web (only for things newer than the snapshot). Every fact is attributed and dated against the snapshot. Other agents reach it throughask_agentfor fact-checks. -
Asset libraries (
#/assets, sidebar → Assets;server/assets.py,ui/src/components/Assets.tsx): reusable, editable sets of sounds, music, textures, sprites, tilesets, pictures, icons, models and fonts, shared by every chat, Studio design and Code workspace. Files live indata/assets/<lib id>/(each library also gets alibrary.jsonmanifest); metadata lives in tablesasset_libsandassets: title, category, tags, description, license, author, source URL and free-formmeta(frameWidth/frameHeight/fpsanimates a sprite sheet's preview;bpm,loop…). On the page you can upload files or .zip packs (unpacked into separate assets; identical files are detected by SHA-256), filter by text, category, type and tag, preview (pixel-crisp sprites, audio, video, three.js models, font specimens), edit one asset in the side drawer or many at once (tags, category, license, copy to another library, delete), fork or lock a library, download it as a .zip withlibrary.jsonandCREDITS.md, and copy assets into a Studio or Code project. The Studio/Code file pane has an Add from an asset library button (the shapes icon) for the same thing. Every agent gets theasset_librarytool (switch:asset_library; cloud agents only withcloudLibrary): search, list, info, view (images to the vision model), import (copies into the project or /work, with a CREDITS.md), create, fork, add (files/globs from the working folder, URLs, or per-file items), update, remove, edit_library, dispatch and jobs. Locked libraries are read-only to agents, which fork them instead.run_codesees the libraries read-only at/library/assets/<lib id>/. Asset scouts: a scout job (Send a scout, orasset_library action=dispatch) runs an agent unattended in a new chat titled "Asset scout: " with 60 tool rounds, one job at a time (it waits while someone is chatting). The default scout is Magpie (built-in agent). Scout runs getscrape(server/scrape.py),web_search,web_fetch,run_codeandview_documentwhatever agent you pick.scrapelists a page's files and links (browser=truerenders the page in headless Firefox, driven over Marionette with a throwaway profile whose proxy settings block local and private addresses; it can scroll and press "load more"), downloads up to 60 URLs per call (300 MB per file; local addresses refused on every redirect) and unpacks zip/tar packs. A job's license policy (CC0 only / open / any) goes into its instructions, and every asset records its license, author and source page. API:/api/assets/libs[/{id}],…/upload,…/bulk,…/fork,…/zip,…/to-project,/api/assets/items/{id},/api/assets/search,/api/assets/jobs,/api/assets/file/{lib}/{path},/api/assets/thumb/{id}./api/statusshows a running job asassetJob. -
Saved app data (
server/appstate.py): HTML artifacts and Studio/Code web apps run in sandboxed iframes with an opaque origin, where the browser'slocalStoragethrows. Every such page instead loads/api/appstate/boot.js?s=<scope>first in<head>, a blocking script that installs alocalStoragepreloaded with the saved data (plus an in-memorysessionStorageand an async JSONwindow.shellb.storage) and sends writes back, batched, with a beacon on page hide. Scopes areart:<conv>:<artifact>(shared by all versions) andproj:<project>(shared by every page, injected by/raw/into page loads only, never into?__srcorfetch()source reads). Library thumbnails load read-only (ro=1). The data is stored in tableapp_state(scope, key, value)(10 MB per scope) and deleted with its chat or project. The artifact panel's eraser button clears it. API:GET/POST/DELETE /api/appstate?s=. -
Favorites (
#/library/favorites, the sidebar's Favorites section, or Ctrl K): one starred list across chats, artifacts, Studio/Code projects, chat Projects, research notebooks and mega plans. Click the ☆ on a sidebar chat, the chat header, the artifact panel, Library cards, Studio/Code workspace cards, and Project, notebook and plan cards and pages. Drag to reorder on the Favorites page. Titles resolve live, so renames show up right away, and a favorite whose target was deleted drops out. API:GET/POST/DELETE /api/favorites,POST /api/favorites/order; tablefavorites(kind, ref, sub)(sub= artifact id). Code:server/favorites.py,ui/src/favorites.ts(shared store),ui/src/components/Favorites.tsx(FavoriteStar, page). -
Studio (
#/studio): a design environment in the spirit of Claude Design. Each project is a real folder of files (data/projects/<id>/, git-versioned: every agent turn, manual save, upload and restore is a commit). The three-pane workspace has files/history, a live canvas and the agent chat. The canvas does device sizes and fit-zoom, and has a CodeMirror editor plus viewers for images, fonts, PDFs and media. Comment mode (ui/public/studio-inspector.js) lets you click any element and say what to change; the selector and HTML go to the agent. Daedalus (Qwen3.6, vision) gets file tools (server/studio_tools.py: list/read/write/edit/rename/delete,view_image,screenshot),generate_imagesaving intoassets/generated/, and the sandbox mounted on the project (.gitread-only). Every agent works this way inside a project.screenshotrenders the page with headless Firefox in viewport tiles: Firefox--screenshotcan't scroll, so later tiles shift the page with a transform. The images go straight to vision models; text-only models get a written review from the vision fallback instead. The review lists each requirement from your latest message and checks it pass/fail. Kinds: website, deck (1920×1080 slides), print (US Letter), mobile (390×844), dashboard, design system (tokens.css/tokens.json + living style guide), logo & brand (SVG marks + brand board), social graphics (platform-size frames), email (table-based, inline styles), motion, game (canvas game loop,?demoattract mode for screenshots), 3D (three.js) and blank. Export is a zip. three.js is vendored at/vendor/three/(import map:three→three.module.js,three/addons/→addons/), and.glb/.gltf/.obj/.stlfiles open in/vendor/three/viewer.html?src=…in the canvas. generate_3d (server/tools.py, every Studio agent gets it) is text- or image-to-3D on NYX: FLUX reference image → BiRefNet cut-out (models/background_removal/birefnet.safetensors) → Hunyuan3D-2 (models/checkpoints/ hunyuan3d-dit-v2.safetensors) → GLB, with smooth normals and a material added afterwards. Takes about 2-3 minutes. Output is untextured geometry inassets/models/, plus-ref.pngand a numpy-rendered-preview.png(front, 3/4 and side views). Headless Firefox has no WebGL, soscreenshotcan't show WebGL canvases. That preview is how agents see their meshes. -
Creative asset tools (
server/studio_assets.py; every tool saves into the Studio project, else/api/files):generate_sprite(FLUX on a plain backdrop → BiRefNet mask → trimmed RGBA PNG inassets/sprites/;pixel_size+colorssnap it to a pixel-art grid and palette),generate_texture(FLUX → half-offset cross-blend so it tiles, plus a brightness-derived-normal.png, inassets/textures/),generate_sfx(engine auto/stable/audiogen. Stable Audio Open 1.0 runs onshellb-stableaudioat 127.0.0.1:8003: 44.1 kHz stereo, up to 46 s, ~17 s per take, and it unloads after each call. It has its own venv,~/shellb/stableaudio/venv, which reuses the main venv's torch and transformers through a .pth file. It replaces diffusers' torchsde noise sampler, which recurses forever on this build, with plain Gaussian noise. The fallback is AudioGen-medium on ORPHEUSPOST /generate/sfx, 16 kHz mono; one-shots are trimmed and peak-normalized,loop=truecrossfades,variations1-4, inassets/audio/sfx|ambience/) andgenerate_voice(Kokoro on HERMES, single line or a batch oflines, inassets/audio/voice/).generate_musicnow saves into the project too (assets/audio/music/,loop=true), andgenerate_image/generate_sprite/generate_texturetakequality=highfor FLUX.1-dev (models/checkpoints/flux1-dev-fp8.safetensors, Comfy-Org repack, ~1 min) instead of schnell. Licenses: FLUX.1-schnell is Apache-2.0; FLUX.1-dev, MusicGen and AudioGen weights are non-commercial licenses, which fit Shell:B's personal, non-commercial use. ORPHEUS also hasPOST /unload. -
Live dev servers (
server/devserver.py): Studio projects with adevserver.json({"command", "port", "env"}; the Web app + back-end kind scaffolds FastAPI + SQLite inserver/, front-end inpublic/) get a real back-end. It runs in the sandbox image with--network none(no route to the host's unauthenticated services) and socat bridgesdata/devservers/<pid>/app.sockto the app's port. A proxy inside shellb-web listens on :8210-8217 (one stable port per project,kv devserver_ports), so each app has its own browser origin. The proxy needs the owner session or localhost, stripsshellb_sessionfrom requests and dropsSet-Cookies that would overwrite it. It also proxies WebSockets and injects the screenshot snippet and inspector (/__shellb/inspector.js). Servers stop after 30 min idle, at most 4 run at once, and they are removed with the project. API:GET/POST /api/projects/<pid>/dev({action: start|stop|restart}),GET …/dev/logs. Agent tools:dev_server,http_request,screenshot(server=true). The canvas shows a Server control (live/files toggle, start/restart/stop, logs, and "Ask to fix"). auth.py now refuses localhost writes that carry a foreignOrigin, so a preview page can't use localhost trust against the main app. The sandbox image (sandbox/Dockerfile) added ffmpeg, sox, fluidsynth + GM soundfont, socat, soundfile/pydub/mido/ pretty_midi/pyloudnorm, trimesh, uvicorn/SQLModel/passlib/pyjwt, and global express/sql.js/ws/zod (NODE_PATH). The previous image is taggedshellb-sandbox:prev. -
Studio kinds added: Web app + back-end (
fullstack), Music & audio (music), Illustration & comics (illustration). Daedalus is now the all-round creative studio (front-end, back-end, games, audio, art) with every generation tool. NYX gets the new asset tools too. -
Code (
#/code): a coding partner in the spirit of Claude Code. A workspace is a real git repository in the samedata/projects/<id>/machinery as Studio (kindcode), so it shares the file tree, editor, history/diff/restore, uploads and export. Start empty, upload a.zip, or clone a public https repo (POST /api/projects/clone; the server clones, the sandbox stays offline). The Remote tab (server/gitremote.py) ties the workspace to an https remote (GitHub, GitLab, Gitea…): URL, branch and a Codex secret holding the token (user:tokenor a bare token; it reaches git throughGIT_ASKPASSfrom the environment, never the command line, with credential helpers off). Fetch and Pull (fast-forward only; a diverged branch is reported) and Push (never forced) need the owner; local, LAN and tailnet remotes are refused. Agents in the project getgit_remote: status, fetch and pull on their own, while a push shows an approval card (kindgit) listing the commits and waits for you. Every operation lands indata/ssh/audit.jsonl. Routes:GET/PUT/DELETE /api/projects/<id>/git,POST …/git/{fetch,pull,push}. Hephaestus is the default partner, with an engineering prompt (code_contextinserver/agent.py): read before editing, focusededit_filechanges, run the tests withrun_code, checkproject_diffbefore replying, cite file:line. Extra project tools:search_files(regex grep),find_files(glob),project_diff(uncommitted changes). The Terminal drawer runs bash in the same sandbox (POST /api/projects/<id>/exec) and commits what it changes. The sandbox image now carries git, gcc/make/cmake, Node 20 + npm, TypeScript/tsx, vitest, eslint, pytest, ruff, mypy and ripgrep. File listings respect.gitignore. -
Self-awareness and self-modification (
server/selfmod.py, toolshellb_self, on for Shell:B and Hephaestus): Shell:B can look at herself:about(agents, server modules, features, version, recent deploys),status(units, services, memory, what's running),logs(journalctl for the shellb-* units) andsource(read, list or regex-search her live code;data/, the venv, node_modules, build output and.gitare off limits). For reviews she stays read-only on the live tree (server/codemap.py):outlinemaps a folder (each file's size and docstring) or a file (classes, functions, FastAPI routes, TS components, Markdown headings, with line ranges);sourcereads up to 8pathsper call and searches withcontextlines;read_loglists which files and lines the current turn has read, so a review can state its real coverage. The bundledaudit-codeskill is the review procedure (map, read, prove each finding with file:line, search before calling something missing). Every partialread_file, here and in Studio/Code, ends with…[path: lines a–b of n; continue with start_line=…], and it acceptsoffset/start/limitas well asstart_line/end_line, so a read never silently restarts at line 1. To change herself she works in the Shell:B (self) Code workspace (action=workspace; kvself_project), a git clone of this tree. Its sandbox gets the liveui/node_modulesread-only, sotscruns there. She never edits the live tree directly. From any chat she edits that workspace throughshellb_selfitself:read_file,search_files,find_files,edit_file,write_file,delete_file,run(bash in the workspace sandbox) anddiff(what a deploy would ship, compared with live) call the project tools on the workspace and commit each change. Inside the workspace's own Code page she has the normal project tools.action=deploymerges the latest live code into the workspace, then runs the checks in a scratch clone:py_compile, importingapp.pyagainst a copy of the database and requesting a few routes, and, when ui/ changed,tscandvite build. Only after the checks pass does she ask the owner. The chat shows an approval card with the summary, the check results and the colored diff, and only an owner session or the admin key can approve it. Once approved, the change is merged into the live tree as oneSelf-deploy <id>commit and the UI is rebuilt. If server/ changed, a transient systemd unit (shellb-selfdeploy-<id>-deploy, scriptdata/selfmod/runner.py) waits until/api/statusis idle, restarts shellb-web and health-checks it. If the server doesn't come back, the runner restores the previous files, rebuilds and restarts again.action=rollback(also approved by the owner) undoes a deploy.action=historyandGET /api/selflist deploys, which are kept indata/selfmod/deploys.json. This tree is a git repository (since 2026-10-02). Edits made outside Shell:B to tracked files, by Claude Code or by hand, are committed automatically as "Outside edits" before each sync or deploy, so every deploy has a clean base to roll back to. Untracked files are left alone and listed in the deploy approval instead. -
Self-review (
server/selfreview.py):shellb_sourceis a read-only view of the committed source (outline, read, regex search, history, andchecks: the lastscripts/check.shresult). Paths outsidegit ls-filesare refused, so it's safe to give a cloud reviewer: data, keys and logs never leave.self_findingskeeps a ledger of what reviews found (severity, file, open / fixed / won't fix; the same title + file again refreshes it, and a "fixed" one seen again re-opens). The nightly 04:00 review (agentclaude) re-checks open findings, adds new ones and writes a report; Activity → Self-review lists them..gitignorekeepsdata/, the venv, node_modules,ui/distandui/public/vendorout. -
UI: conversations are stored on the server, so they work from any device, and they survive reloads mid-reply. Also: a collapsible thinking/tool trace; a sandboxed artifact panel with versions; attachments (images go to the vision model, files to
/work); dictation and read-aloud through HERMES; Ctrl+K palette (every app, Settings tab, Tools & Connectors tab, Activity panel and Models page); light/dark themes; mobile layout. Settings → Shortcuts lists the keyboard shortcuts (?opens it). The envelope in a chat's header is its mailbox (server/chatmail.py): notes other chats left for it, and a box to leave one for its next turn. Settings → Account shows the audit log (GET /api/audit-log), and the Agent Lab shows each agent's thumbs over the last 30 days. -
Appearance and the community gallery (Settings → Appearance;
server/gallery.py,ui/src/components/Gallery.tsx): the built-in themes, the ones Shell:B designs in chat (design_theme,server/themes.py), and Community themes from github.com/ShellB-App/Shell-B-Themes; under Screensaver style, Browse community lists github.com/ShellB-App/Shell-B-Screensavers. Repositories are fetched like Community Hub skill repositories (one tarball at a pinned commit throughcommunity.fetch_repo, cached indata/community/repos, catalogue in kvgallery_<kind>), so the gallery works offline once fetched and a refresh pulls the latest. A theme installs throughthemes.clean(same checks as a chat-designed one) and joins the custom themes with its provenance; a screensaver becomes a Screensaver project in the Studio (the folder's allowed files only, 2 MB a file, 8 MB in all) and runs in the same sandboxed frame as any Studio screensaver. Installed items show Apply/Use, and Update when the gallery has a newer version; reinstalling updates in place. Installing needs the owner (or the admin key) and is audit-logged; there is no agent tool for it. Custom themes have an Export button (GET /api/gallery/export/theme/<id>) that saves atheme.jsonready for a pull request. More sources:POST /api/gallery/sources {kind, repo, ref}(owner). The same module serves Studio → New project → Start from a template (kindtemplate, github.com/ShellB-App/ Shell-B-Studio-Templates): a template makes a new project of its kind with a copy of its files and its brief in DESIGN.md, every time (nothing is marked installed). Skills for designing themes (shellb-theme-design) and building screensavers (shellb-screensaver) live in github.com/ShellB-App/Shell-B-Skills, a default Community Hub source. Shell:B's own repositories are looked up throughgit.shellb.app/<name>(themes, screensavers, templates, skills, agents, translations;community.resolve_repo), a Cloudflare redirect to where each one lives now, so they can move without an app update; if the link can't be followed, the GitHub name above is used. -
Scheduled (
#/scheduled, sidebar → Scheduled): prompts that run on their own, once at a set time or on a repeat (daily, chosen weekdays, monthly, every N minutes/hours, or any 5-field cron). Each run starts a new chat titled " · " with the chosen agent, optionally inside a chat Project, and the agent is told nobody is watching. Times are wall-clock in the task's IANA time zone, so DST doesn't shift them. Runs go one at a time; a run missed while the server was down fires once on startup (marked "ran late") and missed runs never pile up. Cron schedules can't fire more often than every 5 minutes, and at most 50 tasks can be active. Agents with the Schedule Tasks tool (schedule_task, on for Shell:B by default) can create, list and delete tasks from chat. Settings →timeZonedrives this and the agents' clock; the UI fills it in from the browser. Code is inserver/schedules.pyandui/src/components/Scheduled.tsx; the tables areschedulesandschedule_runs. -
Research (
#/research, sidebar → Research): research notebooks in the spirit of NotebookLM. A notebook has three panes. Sources: upload files (PDF, Office, text, e-books, images; scans are OCR'd), add web pages, paste text, or Discover them with a private web search. Every source is extracted, chunked and embedded (qwen3-embedding), and gets a source guide (summary plus key topics). Checkboxes choose which sources the chat and Studio use. Chat answers only from the selected sources, citing exact passages as numbered chips: hover to read the passage, click to open the source with it highlighted. "Save to note" keeps an answer, with its citations turned into[S#]source links. The notebook also gets an overview, a title and emoji, and suggested questions. Studio generates, from the selected sources: an Audio Overview (a two-host podcast; the LLM writes the script, HERMES/Kokoro voices Alex and Sam, saved as MP3 with a seekable transcript), a briefing doc, study guide, FAQ, timeline, report, an interactive mind map (click a topic to ask about it), flashcards and a scored quiz. Each can be customized with a focus, and the podcast with a length. Notes hold saved answers, your own notes, and Minerva's findings and research report. Deep research hands the question to Minerva in her own chat. Herresearch_boardtool adds the pages she reads as sources (auto-captured fetches included), plus findings and a cited report, andsearchlets her read the notebook's indexed text. Agent chats link to a notebook with[text](board:<id>). Deep links:#/research/<id>/view/<output>,#/research/<id>/source/<source>. Generation uses Minerva's model. Background work (Studio, source guides) takes one Ollama slot, so chat stays responsive. Running jobs show undernotebookin/api/status. Files live indata/research/<id>/(originals, extracted text, audio). Code:server/research.py(board, items, agent tool),server/notebook.py(ingestion, retrieval, chat, Studio, audio),ui/src/components/research/. Tables:research_boards,research_items,research_links,research_chunks,research_chat. -
Mega Plans (
#/plans, sidebar → Mega Plans): for jobs too big for one chat. Describe the goal, and an orchestrator agent (Shell:B by default) breaks it into sequential steps. Each step has a brief, acceptance criteria, the agent that does it, an optional phase label, and an optional checkpoint. You review and edit the steps on the board, then start the plan. From then on the plan runs by itself:- each step runs in its own chat with its worker agent. The worker sees the goal, the plan outline, and the
handoff notes from earlier steps, and ends its reply with a
## Handoffreport; - the orchestrator then reviews the step in its own chat. It gets the brief, the criteria, the worker's report and
the step's git diff, and can read files and run tests. It calls
mega_plan action=reviewwithaccept(plus a handoff summary later steps rely on),revise(the feedback goes back to the worker, up to the plan's attempt limit) orfail; - the plan pauses for you on a checkpoint step, a failure, a step that runs out of attempts, or a review with no verdict. You can accept or send back a step yourself, redo, skip, edit, reorder or add steps, then resume;
- when every step is done, the orchestrator checks the whole result and writes a final report onto the board.
Plans are built for long unattended jobs on one box, trading speed for quality:
- Automated checks. A step can have a
checkcommand (e.g.pytest -q tests/test_x.py), and the plan can have a regression check (e.g.pytest -q). After every attempt the plan runs these itself, in the offline sandbox, from the workspace root. A failure goes straight back to the worker with the output, without spending a review turn. The orchestrator only reviews work whose checks pass, and it can't accept a step whose checks fail (you can override). The planner is told to set up a test runner first and to give each step a check. - Quality (per plan). Thorough, the default, runs every turn at high reasoning effort and gives workers 48 tool rounds instead of 16, so they can iterate until the work is right. The planner prefers more, smaller steps (8-40), because local models do excellent work on focused steps. Standard uses each agent's own settings.
- Fresh-context reviews. Each review, phase review and final report starts from a clean slate: the plan's state is in the system prompt, and the step's evidence is in the message. A 40-step plan's history never crowds out the diff. The orchestrator chat still shows everything.
- Phase reviews. When a phase ends, the orchestrator runs the regression check, checks that the phase's steps fit together, and can add fix-up steps or reshape the steps ahead before the next phase starts. Each phase is reviewed once.
- Resilience. A turn that errors (model crash, out of memory) is retried after 30 s and again after 2 min before the plan pauses. Plan turns wait while a person is chatting with any agent, so the box stays responsive. Plans pick up again afterwards.
By default a plan gets a new git-versioned Code workspace that every step works in, so you can see each step's changes as a commit. You can also point a plan at an existing Studio/Code project, or run it with no shared workspace. The board has a Board (kanban) view, a Timeline view, the orchestrator's live status and an activity log. Like scheduled tasks, only one agent turn runs at a time. A restart pauses running plans, and you resume them from the board. Agents with the Mega Plans tool (
mega_plan, on for Shell:B) can create plans from chat and link them with[text](plan:<id>). The orchestrator always gets the tool in its plan chat. Code is inserver/megaplan.pyandui/src/components/MegaPlans.tsx; the tables aremega_plans,mega_steps,mega_eventsandmega_links. - each step runs in its own chat with its worker agent. The worker sees the goal, the plan outline, and the
handoff notes from earlier steps, and ends its reply with a
transcribe(tool, on for Shell:B, Minerva and Heimdall;server/transcribe.py): speech to text with timestamps for an audio or video file in /work or the project, or a direct media URL (public addresses, 500 MB). The host has no ffmpeg, so the file is transcoded to 16 kHz mono WAV by ffmpeg inside the sandbox image (--network none), then HERMES Whisper transcribes it; the transcript is saved beside the file as<name>.transcript.txt. YouTube and other streaming sites (YouTube, Vimeo, SoundCloud…) are fetched by yt-dlp in the server's venv, audio only, one video, ≤ 300 MB / 4 h, so the sandbox never gets a network. Secretary accepts video uploads the same way, and a research notebook given a direct media link transcribes it as a source.edit_image(tool, on for NYX and Daedalus;server/studio_edit.py):cutoutremoves the background with BiRefNet (RGBA PNG),upscaleruns an ESRGAN-family model from Models → Store → Upscalers,inpaintrepaints a masked area from a prompt, the mask being a black/white image (white = repaint) orbackground/subject(BiRefNet picks the area); FLUX.1-dev with inpaint conditioning when installed, else an SDXL checkpoint named by theeditCheckpointsetting (never Aphrodite's). Results go toassets/edited/in a project or/api/files.- LoRA:
generate_imagetakeslora(a file in~/comfyui/models/loras, downloadable in Models → Store → LoRAs) andlora_strength. - Import history also reads a Google Takeout of Gemini activity (My Activity → Gemini Apps, JSON format): one chat per day of prompts (Takeout keeps the prompts and some replies).
Settings → Notifications → Email → Inbox (server/inbox.py): the mailbox the agents may read. Host, port (TLS),
user, folder, an app password (written to data/email/imap.pass, mode 0600, never returned) and a check interval.
Every few minutes the headers of new messages are stored (uid, Message-ID, from, subject, date, a text snippet,
seen/flagged, attachment names; 2000 kept) and each new message posts to Activity (source email, level attention by
default). Rules (from/subject substrings or /regex/, an agent, a template with {{from}} {{subject}} {{date}} {{body}} {{snippet}}) start an unattended turn when a matching email arrives, the email handed over as the sender's
data, not instructions; the Activity item then links to that chat. Agents in the owner's chats get read_email
(unread, list, search on the server, read one message's text and attachment names, sync, mark read/unread); cloud
models only with Cloud AI → Let cloud agents read the inbox (cloudEmail). Replies go through send_email with
in_reply_to set to the Message-ID, so they thread. Guests and the adult agent never see mail; cells never mount it.
Routes: /api/email/inbox (status, PUT config), /test, /sync, /messages[/{uid}], /rules.
Right-click a chat (sidebar) or an artifact → Share read-only link… (server/shares.py, ui/src/components/ SharedView.tsx). A share is a snapshot taken at that moment (messages as text, or one artifact version) in
data/shares/<token>.json; later edits and messages never show. The link is /#/s/<32-hex token>; GET /api/share/<token> is the only public route behind it (auth.PUBLIC) and answers 404 for anything revoked, expired
or unknown. Incognito, guest, API-key and Aphrodite chats can't be shared. Links expire after 30 days (days on
POST /api/shares, 0 = never); Settings → Account → Shared links lists them with view counts and Revoke.
Making a share needs the owner's session (not localhost trust) and is audited.
Hosting. ai.shellb.app sits behind Cloudflare Access, so a share link there asks the reader to log in. Settings →
Network → Share host names a host that reaches this machine without Access or login in front: share.shellb.app
(set up 2026-10-10) goes through hub-shellb-edge, which serves only the app shell, /assets/*, /vendor/* and
/api/share/* with cookies stripped; links are built on it.
Access on phones. When the Access session runs out, every request gets a redirect to Cloudflare's login, which a
fetch can't follow. ui/src/net.ts (reloadIfEdgeLogin) spots that (the redirect, or a 401/403 on /api/auth/me,
which Shell:B itself always answers with 200) and reloads the page so Access can sign in, instead of showing
"Can't reach Shell:B" forever. The manifest link carries crossorigin="use-credentials", since browsers fetch it
without cookies otherwise and Access turns that away, which stops phones installing the app.
The shared page renders Markdown and sandboxes HTML/SVG exactly like the owner's view, and makes no other API calls.
The Worlds app (#/worlds, ui/src/maps/) is the offline atlas: the Sol orrery, Earth from the OpenStreetMap planet
(~/shellb/maps/planet.pmtiles, Protomaps build) and 21 planets and moons from USGS mosaics (~/shellb/maps/bodies),
with elevation for Earth, Mars, the Moon and Mercury (~/shellb/maps/terrain). Server side: server/maps.py (tiles,
glyphs, sprites, terrain, 3D models), server/planetary.py (gazetteer, ephemeris, traverses, telemetry),
server/sky.py (spacecraft), server/flights.py, server/weather.py, server/worldnotes.py (cards),
server/earthview.py (imagery and radar proxies).
- Layers (the Layers menu,
ui/src/maps/overlays.ts): Live: flights, spacecraft, earthquakes, day & night, satellite imagery, rain radar. Places: landmarks and landing sites, rover traverses, wonders, shipwrecks, painted caves, Neolithic sites, spaceports, webcams, Atlas places. Reference: relief, 3D terrain, 3D buildings, Ice Age coast, grid, starfield. Only what the current world offers is listed; choices are kept per browser. - Satellite imagery is NASA GIBS (MODIS Terra since 2000, VIIRS since 2015, false-colour bands 7-2-1, the Blue Marble
composite, Black Marble night lights). It follows the map clock's date: rewind the time bar to 14 August 2003 and the
layer shows that day. Rain & snow is NASA's GPM IMERG precipitation, every half hour, four to seven hours behind
real time (a slider picks the frame; RainViewer's free radar tiles stopped answering in 2026, see the module). All of
it goes through
server/earthview.py, which adds the User-Agent, keeps the owner's browser off those services and caches tiles underdata/earth/(bounded to about 40,000 tiles), so a day once viewed stays viewable offline. - 3D buildings extrude OpenStreetMap heights from the planet tiles (
buildingslayer:height,min_height) from zoom 14; the map tilts when the layer is turned on. 3D terrain uses the elevation tiles with the exaggeration scaled by the world's radius. - Measure draws great circles in km, miles or nautical miles and, on worlds with terrain, an elevation profile
along the path (
ui/src/maps/elevation.tsreads the terrarium tiles in the browser) with the climb and descent. - Cards: clicking a feature opens its card. Below the curated text sits whatever Atlas (or the owner) attached:
summary, facts, sections, pictures, sound, video, 3D models and links (
data/worlds/notes.json, media copied todata/worlds/media, GLB allowed, no SVG/HTML). Edit on the card removes items; "Ask Atlas" sends a research request. - Atlas places: a card note with
lat/lon/kindis a point of its own in the Atlas places layer (pink pins), so Atlas can put a wreck, a battlefield or a crater on the map that no curated layer has, complete with its card. The owner can remove a place from its card. - 3D models (
~/shellb/maps/models,fetch_models.pythere): 51 NASA 3D Resources GLBs (Apollo Lunar Module, Saturn V, Perseverance, Ingenuity, InSight, Viking, the Mars Exploration Rover, Cassini-Huygens, Voyager, Juno, Galileo, Dawn, Magellan, MESSENGER, LRO, MRO, Odyssey, MAVEN, the ISS, Hubble, Webb, the Shuttle, Skylab, Mir, Parker, Pioneer 10, NEAR, Rosetta, Stardust, Deep Impact, OSIRIS-REx and asteroid Bennu, Chandra, Kepler, TESS, SDO, SOHO, Terra, Landsat 8, Gemini, Freedom 7, Friendship 7, Apollo-Soyuz, a DSN dish and more). The catalog maps each model to landmark ids, spacecraft names and NORAD numbers, so the Apollo cards show the Lunar Module, the Perseverance card the rover and the helicopter, the ISS card the station. The Bookmarks drawer's 3D models tab lists them all; any opens full screen (three.js, loaded on first use: drag to turn, scroll to zoom).GET /api/maps/modelslists the installed ones;/api/maps/models/<id>.glbserves them. NASA says the assets are free and without copyright; keep the credits. - Inside a world: the dossier drawer (
?dossier) now has a cross-section of the interior (core, mantle, crust, oceans and ice shells,ui/src/maps/interior.ts) for the Sun, the planets and the major moons, from published interior models. - Tours: a map block with a
tourlist plays as a guided fly-through (ui/src/maps/TourPlayer.tsx): stop to stop, with the narration shown and optionally read aloud by the house voice. Atlas writes tours on request ("give me a tour of the Apollo sites"). - Import and export (Bookmarks drawer): GeoJSON, KML or GPX files appear on the map as an overlay; bookmarks export as KML (opens in Google Earth) or GeoJSON.
- Dive view and 3D terrain (
ui/src/maps/DiveView.tsx): the Dive button on a shipwreck card, 3D on landmark, wonder and Atlas-place cards, the mountain button on a dropped pin, or Atlas'sview3dkey open a three.js view of the terrain around a point, built from the same elevation tiles as the relief layers (elevation.tssamples a 128 × 128 grid, bilinear). Under the sea on Earth it is a dive: the sea floor lit from above, the surface seen from below, marine snow (slow-drifting organic flecks, not weather: nothing falls from the sky down there), a dashed line from the target up to the surface, depth-banded colours. On land and on Mars, the Moon and Mercury it is a fly-through with the world's own sky colour; on Mars a Dust storm toggle thickens the haze and sends dust past the camera (an impression, not a forecast). Area 20 to 320 km, vertical exaggeration 1 to 8 (always shown), the cursor reads height and distance from the target. Cells are coarse (GEBCO ~300 m at 40 km, MOLA ~2.5 km at 320 km): shapes, not details. - Sky & haze: each world's atmosphere colours MapLibre's sky (
ui/src/maps/atmosphere.ts): Earth blue, Mars butterscotch, Venus yellow-white, Titan orange, Pluto and Triton a faint blue haze, airless worlds black. Visible as the limb glow on the globe and at the horizon when the map is tilted. The same palette lights the 3D views. - Plumes and dust storms (
ui/src/maps/effects.ts, a custom WebGL layer like the spacecraft): Enceladus' geysers rise along the four tiger stripes and Io's plumes from Pele, Loki, Prometheus, Tvashtar and four more, at the real vents; Mars gets drifting dust clouds over the basins where regional storms start (Hellas, Argyre, Chryse, Noachis, Isidis, Syria). The vents and basins are real, the motion is an impression, and the Layers menu says so. Plume heights are capped at a fifth of the radius so they stay on screen when zoomed in. - The giant planets' aurorae live in the orrery (
ui/src/maps/aurorae.ts), since the giants have no surface map: select Jupiter, Saturn, Uranus or Neptune and press Show the aurorae (true scale, centred, zoomed to the disc), or zoom in yourself in true scale. Ovals ring the magnetic poles: the spin poles come from the IAU rotation models, the planet turns with the map clock (W = w0 + w1·d) and the dipole leans from the spin axis by its real tilt, 9.6° on Jupiter, 0° on Saturn, 59° on Uranus and 47° on Neptune, so the ice giants' ovals wander far from the geographic poles and swing round as the clock plays. The glow flickers, Jupiter shows Io's flux-tube footprint at Io's longitude, Saturn gets its C, B and A rings and Uranus its ε ring, drawn behind and in front of the disc. False colour: these aurorae shine mostly in ultraviolet. The dipole longitudes are approximate, so the phase of the swing is indicative. - Wind (Earth, Live;
ui/src/maps/wind.ts,WindPanel.tsx,server/windfield.py): the earth.nullschool.net view. Thousands of particles ride the NOAA GFS wind and leave fading trails on a canvas over the map, on the globe (hidden behind it) and the flat map alike; a colour overlay underneath shows wind speed, temperature (palette slid per height), sea-level pressure, cloud cover, humidity or gusts. Heights: surface (10 m), 850, 500 and 250 hPa (the jet stream). The forecast hour follows the map clock, 3-hourly to +240 h from the latest run, so playing the time bar moves the weather; before the run's time the analysis shows. With a pin dropped the chip reads the wind there. Data: GFS 0.25° GRIB2 from the NOAA archive on AWS Open Data (keyless, public domain), one message per field by byte range from the.idx, decoded with ecCodes (pip install eccodes, in the venv), thinned to 0.5° and cached as Int16 planes underdata/earth/wind/<run>/(GET /api/earth/wind/meta,/api/earth/wind/<product>/<hour>.bin, formatSBWF+ header JSON + planes). NOMADS' OpenDAP, which such sites used to read, was retired in 2025. A fresh field is about 1 MB and takes a second to build the first time. Tests:tests/windfield_test.py. - Aurora (Earth, Live): NOAA's OVATION 30-minute forecast, a probability per 1° cell, painted as green-to-violet
ovals over the poles (
ui/src/maps/aurora.ts; proxyGET /api/earth/aurora, cached 5 minutes), with the forecast peak shown in a chip. Pair it with Day & night to see where the lights would be visible.
Atlas is the agent behind the Worlds app. The copilot drawer sends every question with a context line: the world, the view, the open card (any kind: landmark, wreck, prehistoric site, traverse waypoint, quake, spacecraft, Atlas place), the layers that are on, globe or flat, the clock when it isn't live, the Ice Age setting, the dropped pin, a followed flight, and how many places are annotated. Atlas answers with text and a ```map block; the copilot applies the block to the live map at once, and the "Open in Maps" button on a chat card does the same.
Scene keys of a map block (besides markers, lines, body, center, zoom, globe): pitch, bearing,
layers ({"terrain3d": true, "flights": false}), time (ISO date-time or "now"), years_ago (Ice Age coast),
focus (opens that feature's card), model (shows a 3D model by catalog id), view3d (lat, lon, label,
span_km, depth_m: opens the dive / 3D terrain view there) and tour (stops with title, text, lat, lon,
zoom, pitch, bearing, body, seconds). The keys are documented for every agent in
server/prompts.py (MAP_CARDS); Atlas's own prompt (server/defaults.py) tells it how to drive the app and the cards.
Card tools: read_map_card (what a card already holds, or every annotated card) and annotate_map_feature (summary,
facts, sections, media with credit/license/source, links; remove to drop items; lat/lon/kind to make a new
place; replace to start over). The owner can do the same with POST /api/worlds/notes; GET /api/worlds/places
and GET /api/worlds/notes/all list places and annotated cards. Tests: tests/worlds_test.py.
Shell:B speaks English and French. The language applies to the interface and to the agents' replies: when it is French, every agent gets a note to answer in French unless you write to it in another language or ask for one.
- Yours: Settings → General → Language. Automatic follows the browser; pick a language to fix it. The choice is remembered per browser so the login screen comes up in it on the next visit, and the server tells every device through the auth check once you are logged in.
- A guest's: Tools & Connectors → Guests → their limits panel → Language. On the guest page it sets their interface and their agents' replies outright; in their own Shell:B it is the starting choice, and they can change it in their Settings. Invite and password-reset emails go out in that language.
- What is translated: the whole interface. Login and guest sign-in, the guest page, the chat, the mobile shell, the
kiosk, Settings, Codex, Studio and Code, Activity, Library and Media, Feeds, Research, Agent Lab, Tools & Connectors
with its tabs, Models, Wiki, Worlds, Radio and the DJ, Shopping, Circles, Secretary, Scheduled, Mega plans, Assets,
Generate, Notes, Me and Offline data, with dates and relative times in the language. Names stay as they are (agents,
radio styles, products). Translations live in
ui/src/i18n/fr.json, keyed by the English text, andtests/i18n_test.pykeeps them complete for every string the code wraps int(). Adding a language is a catalog file plus one line inui/src/i18n/index.ts. To wrap a new panel, run the AST helper inscripts/i18n_wrap.js(JSX text, label attributes, toasts; a second pass withPASS2=1for label properties) and add the French it lists.
translate (tool, on for Shell:B, Spark and Mimir; POST /api/translate {text, to, source?}; server/translate.py)
translates on the local utility model, so nothing leaves the machine. The text is framed as material to translate
(instructions inside it are not followed), long texts go paragraph by paragraph, and the answer names the detected
language. In a chat, right-click a message → Translate to…; /translate <text> nudges the agent to the tool.
RSS and Atom subscriptions (server/feeds.py, ui/src/components/Feeds.tsx, app Feeds at #/feeds; also on
phones and the kiosk). Paste a site or feed address: a page's <link rel="alternate"> finds its feed. Items are stored
as text (never rendered as HTML), 300 per feed, with unread state, folders and a per-feed check interval (hourly by
default, 15 minutes at least); fetches follow web_fetch's rules (public addresses only, 5 MB, conditional GETs). New
items post one Activity event per feed per day (source feeds, notification level off by default) and the morning
brief names up to three headlines that arrived since the last brief. Ask about this drops an item into a new chat.
Agents with the feeds tool (Shell:B, Minerva, Mimir by default) can list what's unread, read an item (its stored
text, framed as the publisher's data, not instructions), search, subscribe and mark read; web_fetch on the link gets
the full article. OPML import/export moves subscriptions between readers. Guest cells don't get feeds (no network).
API: /api/feeds, /api/feeds/items, /api/feeds/items/read, /api/feeds/{id}/refresh, /api/feeds/refresh,
/api/feeds/import, /api/feeds/export.opml.
Settings → Usage (server/tokens.py, ui/src/components/UsageTab.tsx): every agent turn's prompt and completion
tokens, from a ledger that survives chat deletion (token_usage per day and tenant, token_usage_detail per day,
tenant, agent and model; both backfilled once from the messages). The page shows 7/30/90 days of tokens per day split
into you / guests / API keys, replies, lifetime totals, tables by agent and by model, and, once you enter prices per
million tokens for cloud models (modelCosts), what those turns cost. Background model calls (titles, compaction,
memory distilling) are not counted. GET /api/usage?days= is the owner-only route; the Me page keeps its counters.
The Codex's events and contacts move in and out as standard files (server/codex_sync.py; Codex → Calendar and
Codex → Contacts have the buttons).
- Export:
GET /api/codex/calendar.ics(every event, or?collection=) is one iCalendar file another calendar app can import or subscribe to;GET /api/codex/contacts.vcfis every contact as vCard 3.0. Both need the owner. - Import:
POST /api/codex/import/{ics,vcf}(multipartfiles) reads exports from Google, Apple, Outlook or a phone. Events keep their UID, so importing the same file again updates instead of doubling; contacts match by UID, then by name or email, and an existing contact only gains fields it was missing. Times with a time zone or in UTC are converted to Settings →timeZone; all-day events stay all-day;RRULEs that are a plain daily/weekly/monthly/ yearly map to the recurrence picker, anything else becomes Custom rule and is kept as the rule text (meta.rrule). AVALARMtrigger becomes the event's reminder. - Recurrence: daily, weekly, monthly, yearly and custom RRULE events are expanded with dateutil for the agents' "Upcoming Schedule" block and for the calendar grid (custom rules are expanded server-side only).
- Reminders: an event's Remind me (at the start, 5–60 minutes or a day before) posts to Activity (source
calendar, statusdue) once per occurrence; push and email notifications follow the Calendar reminders level. - No library for the formats: the module writes and parses them itself (folding, escaping, parameters, nested VALARM).
- Sync accounts (Calendar → Sync…, Contacts → Sync…;
python-caldavfor CalDAV, plain WebDAV for CardDAV;/api/codex/sync[/{id}[/run|/collections]], admin to add or change, passwords indata/codex-sync/<id>.pass). EveryeveryMinutes(15 by default) an account pushes the entries edited here since its last run (a first run pushes every local entry; new ones are created on the server, edited ones replaced at their href, local edits win), then pulls the server's items into the Codex matched by UID. Deletions aren't synced. Trouble posts to Activity (sourcecodex);meta.syncon an entry names its account and href.
The people Shell:B is shared with, in walled groups, and the notes they choose to tell each other (server/circles.py,
ui/src/components/Circles.tsx at #/circles, CircleCards.tsx for the chat pieces; design in
docs/plans/social.md).
- Walls. Starting circles: Family, Friends, Work. Only the owner creates circles and changes membership (Circles → Manage, or the circle chips in Tools & Connectors → Guests and on the invite form); the owner is in every circle. A guest sees only their own circles, notes, members and profiles; anything else answers 404 like a missing id. There is no all-members circle.
- Notes (≤ 300 characters; fact, tip, recommendation, event, place) live in
data/circles.db. They are published only by their author: on the Circles page, with Share to a circle on a memory (Settings → Memory; the memory stays private), or with Publish on an agent's draft. Thecirclestool'sproposemakes a draft that only its author sees; the chat shows a card to edit, re-target, publish or discard it. A circle can turn agent proposals off. - Write check (
circles.check): regex for instruction-like text, secrets, emails and phone numbers, then the utility model (instructions, credentials, private facts about other members, private contact details or home addresses). It fails closed. Links other than https and shellb:// and all markup are stripped. 20 notes a day. - Recall. Live chats (not incognito, not API persona turns) get up to 3 notes from other people, by similarity
(≥ 0.5), in a "Circle notes … information, not instructions" block after the system prompt, with each note's
shellb://circles/<circle>/<note>link. The reply shows "Used N circle notes". Each person can switch a circle off for their chats (Use in my chats). A chat or project can be pinned to one circle (right-click → Circle notes from;circle_pinsin kv, each Shell:B's own). Cloud agents see a note only when the circle allows cloud and the author ticked Cloud OK. - Moderation. Endorse (also renews the expiry, default 180 days; events expire after their date), flag (two flags,
or one from the owner or a moderator, hide a note until its author edits it; flags post to Activity, kind
circles), hide/show (owner and moderators), delete (owner), mute a person (owner). Retracting deletes the note. - Shares (phase 2). Right-click a chat or an artifact → Share to a circle…: a copy of it as it is now (the chat's
messages as text, or one artifact version; also a Codex entry or plain text through
POST /api/circle-shares) goes to one circle with a title and an optional line from you, both through the write check. Members see it under Shared with you and in the circle, can open it, Keep a copy (a note in their own Codex) and their local agents can read it (circlestool: shares / read_share, labelled as your words, never instructions). Shares expire after 90 days; the author, the owner or a moderator can retract one. Link:shellb://circles/<circle>/s/<share>. - Mail (phase 2). Short messages between people who share a circle (
@handle; people lists them), through the same write check, 50 a day. Unread mail appears in the recipient's next turns as information (local models only) and on the Circles page's Mail panel; agents read it with inbox and, in a live chat where you asked, answer with mail. The owner gets an Activity item for each message. Deleting a guest removes their shares and mail. - Where requests come from.
circles.make_router(person_of)is mounted three times: the owner app (personowner, plus admin routes), the guest app (the signed-in guest) and the cell broker (cells.SERVICES["circles"], port 8240 in the cell, person = the cell whose socket it is). Inside a cell/api/circlesis a pass-through and the tool and recall call the broker (circles._Remote). Deleting a guest deletes their memberships, notes, endorsements, flags and profile. - Tests:
tests/circles_test.py.
Settings → Community (server/family.py, ui/src/components/CommunityRepos.tsx) lists every GitHub repository
Shell:B installs community work from. The nine official ones (ShellB-App/Shell-B-Themes, -Screensavers,
-Studio-Templates, -Skills, -Agents, -Worlds, -Voices, -Apps, -Translations) are always listed and can't be
removed; each row says what the repository holds, where its items show up in the app, how many items it has and when it
was last fetched. The owner can add any public GitHub repository laid out like the official one of its kind (a folder
per item plus index.json; forking an official repository is the quickest start) and remove it again. Adding a theme,
screensaver or template repository registers it as a gallery source (gallery.add_source), a skills repository as a
Community Hub source (community.add_source); the kinds without an installer yet (agents, worlds, voices, apps,
translations) are listed with where their items will land. Fetching reuses community.fetch_repo (one tarball at a
pinned commit, public-internet guard, size cap); counts come from index.json (items) or, failing that, the kind's
folder, and a repository whose index.json says another kind is flagged. kv family_repos holds the owner's list,
family_stats the per-repository counts. GET /api/family (?refresh=1 fetches them all), POST /api/family
{kind, repo, ref?, name?} and DELETE /api/family/<id> (owner), POST /api/family/<id>/refresh.
shellb://<route> names a place in the app, where the route is the app's hash route without #/:
shellb://c/<chat>, shellb://codex/notes, shellb://studio/<project>, shellb://wiki/<page>. A link means the
same thing in every Shell:B (the owner's and a guest's): it opens in the reader's own app, and that app's server decides
what they may see. Parsing lives in ui/src/links.ts (linkRoute, shellbLink); routes are limited to URL-safe
characters and only ever set the hash, so a link can't leave the app or carry markup.
- In chats: Markdown links to
shellb:/web+shellb:and bareshellb://…in text open in the same tab (remarkShellbLinks; code spans and blocks are left alone). The sidebar's chat menu has Copy Shell:B link. - From other apps: browsers only let a web app claim
web+schemes, so both manifests registerprotocol_handlersweb+shellb→/?link=%s;desktop.ts(owner) andguest/main.tsxroute the launch, throughlaunchQueuewhen the app is already open. The browser asks once before the app may open them. - Bare
shellb://on a desktop:clients/shellb-link/registers the scheme with the OS and hands links on asweb+shellb:(install-linux.sh [address]→~/.local/bin/shellb-link+x-scheme-handler/shellb;shellb-link.regfor Windows, per user;install-macos.shbuilds a small AppleScript handler app). WithSHELLB_URLset (~/.config/shellb/link.conf, or the installer's argument) the Linux helper opens<address>/?link=…instead.
Agents can look at your screen while you share it (server/screen.py, ui/src/screenShare.ts).
- Screen button by the message box: Attach a screenshot (one picture of a screen, window or tab, attached to the message) or Share screen live. A live share stays on across chats until you press Stop (or the browser's own Stop sharing); a red dot and "Sharing …" sit above the composer, and a toast appears each time an agent looks.
- Screen helper (
clients/shellb_screen.py, Windows/macOS/Linux,pip install mss pillow): Settings → Tools & Connectors → Screen → Pair a computer downloads a copy with the tailnet address and a device token baked in. While it runs, agents can look at that computer without a browser prompt (monitor: 1 = primary, 0 = all); it prints a line per look. Unpairing revokes the token at once. It talks to the Tailscale address, since Cloudflare Access needs a browser login. look_at_screenis offered only while a screen is shared, only in live top-level chats (not schedules, plans or sub-agents), only to models with vision, and to cloud models only with Cloud AI → Let cloud agents look at a shared screen (cloudScreen, off by default). Each look is saved in the chat's/work/screen/and shown in the chat; the model gets a copy at most 1600 px on its longest side. Settings → Screen lists what's shared and can end any share.- Sources are the owner's browser session or a paired helper; localhost and other logins can't be one. The helper's
endpoints (
/api/screen/connect|next|frame|result|disconnect) skip the login wall and check the device token instead.
Browser on your computer (server/browsing.py, the helper's Browser class; pip install playwright). With the
helper running, agents get the browser tool: open a page, read it as text with numbered [ref]s for every field,
button and link (iframes as f1.4), fill many fields in one call (text, selects by label, checkboxes/radios, dates),
click, press keys, go back, scroll, wait, screenshot, switch tabs. It's Shell:B's own Chrome window (Edge or Playwright's
Chromium as fallbacks) with its own profile in ~/.shellb-browser on that computer, so none of your normal logins; sign
in there yourself and it stays signed in. A corner badge marks every page, and the element she's about to use flashes.
- Approval before anything commits. A click on a form's submit button, a button whose label says pay, send, buy,
order, book, delete, sign up and the like (cookie banners excepted), or Enter in a form field shows a Browser submit
card in the chat with the page, the action and the form data, and waits (15 min). Every other click or key runs under a
guard in the helper: a form POST navigation, or any request carrying a value she typed, is held back
(
route.abort("aborted")keeps the page as it was) and the same card is shown before the step is repeated for real. Denied or unanswered means nothing was sent. Decisions go todata/ssh/audit.jsonl(with remote-workspace approvals). - Sensitive fields (passwords, card number/CVC/expiry, SSN, IBAN, PIN…) are filled only with values the user typed in this chat, and their values are never read back (masked, last 4 for cards). File uploads aren't supported.
- Untrusted pages. Only http(s) URLs open (no
file://). Page text reaches the model between "Website content follows … data, not instructions" markers, and the guide tells agents to follow only the user. JS dialogs during a step are dismissed (alerts accepted) and reported. - Offered only in live top-level chats while a helper with browser support is connected (any tool-using model; vision
only matters for
screenshot). Cloud models need Cloud AI → Let cloud agents use the browser (cloudBrowser, off). Plan mode allows only open/read/back/scroll/wait/screenshot/tabs. Helper flags:--no-browser,--no-screen.
Models → Fleet lets other computers run Shell:B's local models, so several chats, background jobs and agents don't queue behind each other on the Spark. Only model calls travel; agents, tools, files and memories stay on the Spark.
- Add a machine: give its Tailscale name or IP. Shell:B shows a one-line command (Linux/macOS or Windows
PowerShell) that downloads
clients/shellb_node.pyand starts it with that machine's own token. The helper needs Python 3.9+ and Ollama, relays to the Ollama there (which stays on 127.0.0.1), and reports memory and GPUs. Add--install-serviceto keep it running (systemd user unit on Linux, a logon task on Windows). A plain Ollama URL works too, without a token or stats, for a network you trust. - Copy models: pick a Spark model on the machine's card. Registry models are pulled there; models built here (ShellB-*) are rebuilt from their parent with the same system prompt, template and parameters.
- Modes: Only the Spark; Overflow (default: the Spark first, another machine only while the Spark's slots are full or when only it has the model); Spread (least busy machine, a warm model wins ties). Slots should match each machine's OLLAMA_NUM_PARALLEL.
- Privacy: incognito chats and Aphrodite never leave the Spark unless a machine is marked Private. Guest cells never use the fleet.
- Failover: a machine that stops answering is marked offline and the request goes to the next one, ending at the Spark. Replies written elsewhere say "on " under them.
- Nodes and tokens:
data/fleet/nodes.json(0600). Code:server/fleet.py,clients/shellb_node.py.
Open Tools & Connectors in the sidebar.
- Connectors are one-click presets for MCP servers. On this machine: Folder (
~/shellb/mcp/shellb_files.py), SQL Database (shellb_sql.py, read-only, for SQLite, Postgres and MySQL), n8n workflows, Knowledge Graph and Sequential Thinking (both via npx). Online: GitHub and Notion. Also any custom MCP server, by URL (Streamable HTTP or SSE) or by command (stdio). - MCP servers lists live connections with their status and tools. Switch individual tools on or off there. To give an agent
a server, tick it in the Agent Lab: it shows up as a tool entry with the id
mcp:<server>. Its tools reach the model asmcp__<server>__<tool>. Server code is inserver/mcp_hub.py, and stdio server logs are indata/mcp-logs/. - Cloud AI is off by default. With it on, the built-in Claude agent (
claude-code/opus, through the Claude Code CLI login on this machine) and Gemini agent (antigravity/gemini-3.1-pro-high, through the Antigravity CLI) work; API-key models (anthropic/…,gemini/…,openai/…) can be picked per agent too. Any agent can also use a cloud model, and local agents may consult cloud agents throughask_agent. Cloud agents get no long-term memory unless you allow it. Provider code is inserver/providers.py: Claude goes through the Anthropic SDK; Gemini and OpenAI use OpenAI-compatible endpoints. - Webhooks (Tools & Connectors → API,
server/hooks.py, docs/API.md § Webhooks): inbound hooks give n8n, GitHub, Home Assistant or a cron job a URL that starts an agent turn from a payload (template with{{payload.x}}fields, secret or HMAC signature, 202 or?wait=1); outbound hooks post Activity events and waiting approvals as signed JSON to a public URL you run. Local, LAN and tailnet targets are refused. - Admin key: connectors can launch programs and API keys cost money. So adding or editing servers, saving keys and
changing cloud settings need the owner's login, or the key in
data/admin.key(cat ~/shellb/web/data/admin.key). Localhost on its own doesn't count as admin. Turning servers on or off, restarting them and editing skills need admin too. - Discover (
server/community.py,ui/src/components/Discover.tsx): browse and install what others published: MCP servers from the official MCP Registry, and Agent Skills from GitHub (Anthropic's skills, the official Claude plugin marketplace, or any repo added under Sources:owner/repo[@ref][/folder]). Everything is unverified, so the detail view shows the full SKILL.md and its scripts, or the exact command/URL a server runs as. Installs need admin and arrive switched off. Package servers (npm, PyPI, OCI) run in a throwaway container (--cap-drop ALL, 1 GB, theshellb-mcpnetwork, secrets passed by environment); remote servers are HTTP/SSE connections. Repos are fetched as a tarball at a pinned commit and indexed intocommunity_items, so browsing works offline. Agents getfind_capability(search, details, propose); a proposal is an approval card, never a silent install.
Friends and coworkers can have their own Shell:B on this machine, without access to anything of the owner's (server/guests.py, server/guest_app.py, ui/src/guest/).
- Where: the same process listens on
SHELLB_GUEST_PORT(8220, loopback and docker0 like :8200). Connections on that port reach a separate app that mounts only guest routes; the owner's routes, cookie, localhost trust and admin key don't exist there. Publish it through the Cloudflare tunnel as its own hostname, with no Access policy (guests sign in to Shell:B itself). - Invites and accounts (server/guest_accounts.py): Tools & Connectors → Guests → Make invite gives a one-time link (7 days). The guest opens it and chooses a username (case-insensitive), a password (10+ characters, common and guessable ones refused, scrypt-hashed) and optionally an email; that spends the invite. There's no open registration. Sessions last 30 days from last use with "Remember me", 12 hours idle without. A device that signed in with the password can make a 10-minute device code for another device. Pause, sign out or delete a guest there; deleting removes their chats, files, memories and sign-in history.
- Two-step and lockouts: guests can turn on authenticator-app codes (TOTP) with 10 one-time recovery codes under Account and security, where they also change their password (other devices are signed out) and see their sign-in history. 10 wrong tries per address per 15 minutes are throttled, and an account locks for 15 minutes after 8 wrong passwords or codes in a row, which posts "Guest account locked" to Activity. The guest's row in Settings shows username, last sign-in, two-step, lock state and a 90-day sign-in log, with Unlock, Turn off two-step (for a lost phone) and Make reset link (one-time, 24 h, signs them out everywhere; pass it on yourself, nothing is emailed). Guests who joined by invite before accounts existed keep their devices but must choose a username and password before anything else works.
- What guests get (default since 2026-10-10): their own full Shell:B. Every guest's visit opens the complete
Shell:B UI running as their own instance in a sealed container (Own container, below): chat with the local agents
whose model is on the guest model list, Studio, Code, Codex (notes, links, snippets, files, contacts, calendar),
Library, Favorites, Generate, Research, Shopping, Circles, Assets, Mega plans, Scheduled, Activity, Wiki, Worlds,
Radio, their own memory, Agent Lab, themes and screensavers, pictures, music, sound, Kokoro voices, the code sandbox
and web reading. In a cell the UI knows it is one (
cellon/api/auth/me,ui/src/cell.ts): the host's apps (Models, Offline data, Secretary, Gibberlink, Feeds) and tabs (API keys, Network, System, Backups, MCP connectors, Discover, Cloud AI, Screen, API, Guests) are not shown, the gear menu, Settings → Account and the command palette carry the gateway's own panels (Account and security, Your devices, About and limits, Report to Evie, Sign out), each reply has a Report flag, and the sidebar shows whose Shell:B it is. While the container boots, the sign-in screen says "Starting your Shell:B…" (GET/POST /api/guest/cellon the gateway) and offers Try again if it fails. Inside a cell, host-only/apipaths answer a JSON 404 and the status probe asks the broker for the sandbox instead of Docker. Dev servers and screenshots work through the broker (below). - The lighter guest app (untick Their own Shell:B on a guest's row; also what guests had before): an installable app (guest.html) with chats, web search, offline wiki and law, maps and sky tools, pictures, music, sound, Kokoro voices, the code sandbox, artifacts, file uploads and voice mode, plus Notes (their own Markdown notes with [[links]], server/guest_notes.py answering the Codex paths the Notes page uses), Circles, Wiki and Worlds (the machine's offline Wikipedia, maps, sky and weather; read-only, so the owner's Worlds notes show but can't be edited: guest_app shares only the GET routes of those routers), and Radio / DJ. Each guest has their own memory (guest_memories), learned from their chats and editable in "What Shell:B remembers". Chats made in one mode don't show in the other (the cell keeps its own database).
- What neither gets: cloud agents, the owner's memory, Codex, Library or projects, MCP connectors, Discover, SSH, self-edit, screen and browser, model downloads. Their sandbox mounts only their own /work. Nothing they do asks the owner for approval.
- Aphrodite for a guest (off by default): tick Aphrodite (18+) on a guest's row, only for guests you know are
adults. It counts only while Aphrodite is on for you (Tools & Connectors → Aphrodite) and the guest has their own
Shell:B; the lighter guest app never runs her. Their cell then keeps her (on the host's model for her), her picture
tool, her memory and
/api/adult/filesand/api/adult/memories, and the guest gets an Aphrodite tab in Tools & Connectors where they switch her on themselves after ticking "I'm 18 or older". Everything in adult.py applies in the cell as it does for you (limits, background checks that fail closed, kept-but-hidden chats, her own memory in their cell, no delegation, interactive turns only). The broker checks the permission on every request (guests.adult_ok): only then does it serve her model and her picture graph, with your checkpoint swapped in for the placeholderaphrodite, previews instead of saved files, the keyword screen on the prompt, and each picture deleted from ComfyUI's temp folder once the cell has fetched it. Pictures need an SDXL checkpoint (guests never get the FLUX.1-dev fallback). Changing the permission, your switch or the checkpoint restarts the affected cells on their next visit. - Fair use: one guest reply at a time (configurable), started after the owner's own turns or
ownerWaitseconds; daily caps on pictures and audio and a storage quota per guest. Messages and tokens are unlimited unless the owner sets a cap (0 = no limit). - Reports: "Report to Evie" (header, and the flag under each reply) lands in Tools & Connectors → Guests →
Reports and in Activity (kind
guest, pushed to the owner's devices). The chat is attached only if the guest ticks the box. - Privacy: guest chats are incognito on the owner's side (never learned from, searched, summarized or listed), but they are stored in this machine's database; the guest app says so.
- Own container (server/cells.py, cell_boot.py, cell/): on for every guest unless the owner unticks "Their own
Shell:B" on their row in Tools & Connectors → Guests (new guests and all existing ones were moved to it on
2026-10-10, kv
guest_cells_default); their visit gets the full Shell:B UI running as their own instance in a container (shellb-cell-<id>, imageshellb-cell:latestbuilt from cell/Dockerfile). The container has no network, a read-only root, no capabilities, its own uid (61001+), 3 GB / 4 CPUs / 512 pids, and mounts only the code and UI (read-only), its own data (data/cells//data), its broker sockets and the offline wiki, maps and law (read-only). Model services are reached only through the host broker, which allows chat and embeddings for the guest models (no pulls, deletes or unloads), Shell:B's own image/3D/cut-out ComfyUI graphs (output under output/cells/), Kokoro speech, transcription, music and sound with clamped lengths, SearXNG and Valhalla. Generation waits behind the owner's turns and shares the guest slots; picture and audio caps count there. The gateway signs the guest in, then proxies to the cell with a per-start gate secret; frames are routed by their key. Cells start on demand (a few seconds), stop after 30 idle minutes, at most "Guest containers running at once" (default 3), and restart by themselves if the container dies. The code sandbox runs through the broker (the cell'srun_codeasks the host, which runs the sandbox image as the cell's uid with only that chat's or project's folder mounted; one run at a time per cell), and web access goes through a filtered proxy on the host (cells._Egress: public addresses only, ports 80/443, per-cell caps), so web reading, Research sources, Shopping, Codex snapshots andgit clonework; the cell reports each turn soturnsPerDay/tokensPerDayapply. Dev servers (phase 2b, 2026-10-10): the cell'sdevserver.pyasks the broker (POST /dev/start|status|stop|logson the sandbox socket); the host runs the project's container (devserver.run_args, labelshellb.celldev=<gid>) as the cell's uid with only that project bound at /work and the socket indata/cells/<gid>/web/dev/<pid>/app.sock, a folder the cell made (0755, socket mode 666) so the cell reaches its own app (probes,http_request) and the host's preview proxy serves the guest's browser on the project's preview host (p-<id>.<previewHost>) with a ticket the host signs; the proxy connects only to a socket the cell's uid owns (cells._CellSocket), never through a symlink. Dev servers shareMAX_RUNNINGand the idle reaper with the owner's; a cell's stop and a guest's deletion remove them. Screenshots:projects.screenshotanddevserver.screenshotpost to the broker's/shot; the host's Firefox renders a static page through the guest gateway under the project's frame key (/api/k/p-<pid>.<mac>/projects/<pid>/raw/…, signed with the cell's frame secret) and a live page through the preview proxy's port; the page's height beacon lands on the gateway'sPOST /api/projects/_shotinfo/<token>, accepted only for tokensprojects.shootmade. Every bind source inside the cell's data is checked from a root helper to be a real directory with no symlink in its path (cells._real_dir, the sandbox too). Deleting the guest removes the container and wipes its data.
One owner password guards the app (server/auth.py, ui/src/components/AuthGate.tsx).
- First visit: the login screen asks for the admin key (
cat ~/shellb/web/data/admin.key) and a new password. To reset it from the shell, run~/shellb/web/venv/bin/python ~/shellb/web/server/auth.py set-password. That also signs out every session. - Two-step verification (Settings → Account): after the password, a code from an authenticator app (TOTP;
server/otp.pyis shared with guest accounts). Setup shows a QR code and, once a code matches, ten one-time recovery codes./api/auth/loginthen answersneedsCodewith a 10-minute ticket bound to your address, and/api/auth/login/codetakes the code or a recovery code; wrong codes count toward the same throttling as wrong passwords, and an accepted code is never accepted again. Localhost trust and the admin key are unaffected. Lost phone: a recovery code, orauth.py disable-2fa(andset-passwordclears it too). Routes:GET/POST /api/auth/2fa,/2fa/{begin,confirm,recovery,disable}, all owner-session only. - Passkeys (Settings → Account → Passkeys;
py_webauthn): sign in with the device's screen lock or a security key instead of the password. Registering needs a signed-in session plus the password; a passkey is bound to the host it was made at (the relying-party id is the address without its port) and counts as its own second factor, so no TOTP step follows. Routes:GET/DELETE /api/auth/passkeys[/{id}],/passkeys/register/{begin,finish},/passkeys/login/{begin,finish}, publicGET /passkeys/available. Tableauth_passkeys. - Sessions are an HttpOnly
shellb_sessioncookie (SameSite=Lax, Secure over https) that lasts 30 days from your last visit. They're stored hashed in theauth_sessionstable. Settings → Account signs you out, changes the password (which signs out every other device) and lists the signed-in devices. Five wrong passwords from one IP slow further attempts down, with an increasing wait of up to 15 minutes. - Localhost is trusted. A request from 127.0.0.1/::1 with no
X-Forwarded-For/Forwardedheader needs no login, so headless-Firefox screenshots,shot.shandcurl localhost:8200/api/statuswork as before. The LAN, Tailscale and the Vite dev server (xfwd: true) must log in. - Cookie-authenticated writes must come from the same origin. Other apps on this host (
:80,:3000, …) count as "same-site" to the browser, so theOriginheader is checked as well. - Sandboxed frames (artifacts, Studio canvases, Library thumbnails) have an opaque origin, and browsers don't send the
cookie from them. So each session has a frame key, and
/api/k/<frameKey>/…stands in for/api/…, but only for project pages (projects/<id>/raw/…), chat workspace files,/api/filesand app state (appstate,boot.js). It never grants admin, and it dies with its session. A Studio page framed with the cookie gets a 302 under that prefix, so its relative links and assets keep working.srcDocinArtifactPanel.tsxloadsboot.jsthroughframeApi(). - Public: the UI shell and static assets,
/api/auth/*,/api/_hold, and/api/files/<name>(generated media with random names).
- Projects (sidebar → Projects) group chats with shared instructions and knowledge files (PDF, Word, Excel,
PowerPoint, text, code). Knowledge under 40k characters goes into every chat's prompt; anything larger is chunked,
embedded with the memory model and searched with
project_search. Files are also read-only at/projectin the sandbox. Chats in a project can read each other (project_read_chat) and save notes back (project_save_file). Use the folder icon on any chat in the sidebar to move it into or out of a project. Internally these are called "groups" (server/groups.py,/api/chat-projects,conversations.group_id) so they don't collide with Studio's design projects. Data lives indata/groups/<id>/. - To-dos (on each project's page): tasks you preload for agents to run on their own. Give each a title, optional
detailed instructions, an agent and a priority (high, normal or low; click an item's flag to change it); paste a list to
add one per line. The list is sorted by priority and, within a priority, by the order you arrange it. Run list works
through the open items in that order, one at a time, each in a new chat in the project titled "To-do #n: …", with nobody watching. The run's system prompt
shows the whole list with earlier items' results, so later items build on earlier ones (save shared output with
project_save_fileor artifacts). The agent ends by callingproject_todoswithaction=finishand a status of done, blocked (needs you) or failed, plus a summary; a turn that completes without reporting counts as done. A blocked or failed item stops the list. You can also run one item now, re-open, skip, reorder or edit items, or press Stop to abort. Project chats get theproject_todostool, so you can also say "add these to the to-do list and start it". A restart puts the running item back in the queue. Code is inserver/todos.pyandui/src/components/ProjectTodos.tsx; the tables areproject_todosandproject_todo_lists. - Skills (Tools & Connectors → Skills) use Claude's SKILL.md format: write one, upload a
.zip/.skill/.md, or import the skills installed for Claude in~/.claude. Agents see each enabled skill's description and load it withuse_skill. Bundled files are read-only at/skills/<name>in the sandbox. Starting a message with/skill-nameinvokes a skill directly. Narrow an agent's skills in the Agent Lab; the opt-insave_skilltool lets an agent write new skills. Code is inserver/skills.pyand data indata/skills/.
server/ FastAPI: app.py (HTTP API + static UI), agent.py (loop, prompt assembly), tools.py, db.py, defaults.py
ui/ React 19 + Vite + Tailwind 3; `npm run build` → ui/dist (served by the server)
sandbox/ Dockerfile for the code sandbox image
data/ shellb.db (SQLite), files/ (generated media), workspaces/<conv>/ (sandbox /work), cells/,
projects/<id>/ (Studio files + .git), project-meta/<id>/ (cover.jpg, shots/)
systemctl --user restart shellb-web # after editing server/
cd ~/shellb/web/ui && npm run build # after editing ui/ (no restart needed)
cd ~/shellb/web/ui && npm run dev # hot-reload dev server on :5173, proxies /api to :8200
docker build -t shellb-sandbox ~/shellb/web/sandbox # (re)build the code sandbox
journalctl --user -u shellb-web -fNode is a user-local install at ~/.local/lib/nodejs (symlinked into ~/.local/bin).
HTML artifacts can load /vendor/{tailwind,chart,mermaid,marked}.js; refresh them with npm run vendor.
./shot.sh 'http://localhost:8200/?shot=5000#/c/<conv>/<artifact>' out.png 1440 900 takes a headless Firefox
screenshot. ?shot holds the page's load event so the screenshot captures a settled page, &theme=light
switches theme, &peek=N opens the Nth rich-link popover, and &modal=agents|tools|settings|memory|system opens a panel.
- One owner. Guests have their own accounts and data (see Shell:B Guest), but the owner's side is a single account.
- An artifact that hard-codes
/api/workspace/<conv>/…(rather than going through the frame-key prefix) can't load that file inside its sandboxed preview, because no cookie is sent from there. - The microphone needs HTTPS (browser rule), so dictation works only over HTTPS: the Tailscale
https://URL or the Cloudflare tunnel (ai.shellb.app, behind Cloudflare Access).