aibox v2: persistent per-project sandboxes, sliced home volume, phone/browser sessions - #4
Open
repalash wants to merge 26 commits into
Open
aibox v2: persistent per-project sandboxes, sliced home volume, phone/browser sessions#4repalash wants to merge 26 commits into
repalash wants to merge 26 commits into
Conversation
Requirements for the aibox rewrite: always-yolo single container per project, persistent background lifecycle, shared aibox-home volume, built-in backup/restore, wildcard *.aibox.localhost dev-server proxy, and a standalone v1 data migration script. Records settled decisions, the reduced command surface, deletions from v1, and acceptance criteria. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Corrects the command count (16, not ~20), documents the mode-switch down path and project-dir mutation by up/init, the greedy global flag parser, missing host dotfile mounts, and compose-YAML-via-stdin. Adds v2 requirements derived from the audit: verbatim claude arg passthrough, TTY only when attached, docker build --pull, and a persistent-home note for git identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Corrects browser support for *.localhost (Safari needs macOS 26 Tahoe), makes the node default track Active LTS (24; 22 is in maintenance, 20 EOL), pins the exact Claude native-installer paths and confirms .claude.json lands inside CLAUDE_CONFIG_DIR, documents the real macOS low-port publishing mechanics per runtime, and adds an empirically tested Caddyfile for the wildcard proxy routing plus the tar-while-running backup caveat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Complete rewrite of bin/aibox (2,379 → 572 lines) per REVAMP.md: - 9 commands: claude (default), shell, stop, status, backup, restore, update, version, help. Always yolo; args after claude pass through verbatim; TTY allocated only when attached so 'claude -p' works piped. - One container per project (aibox-<slug>-<hash6>), project bind-mounted at its host path. Exiting a session never touches the container; 'stop' preserves all state; containers are recreated only when the image changes, and never while sessions are active. - One shared home volume (aibox-home) at /home/aibox holds sessions, login, and the claude binary (installed by the entrypoint on first start so self-updates persist; image-baked installs would be shadowed by the mount). - Dev-server proxy: shared Caddy container routes http://<port>.<slug>.aibox.localhost to any container port over the Docker network - no published ports, no restarts, WebSockets included. Falls back to 8080 when host port 80 is unavailable. - Built-in backup (tar of the volume via read-only helper) and restore (confirmation + automatic pre-restore safety backup). - Image: node:<version>-bookworm (default 24, configurable in ~/.aibox/config) + zsh/sudo/ripgrep/fzf/jq; ~/.aibox/Dockerfile.extra is appended for custom layers; built with --pull. - scripts/migrate-to-v2.sh: standalone one-time merge of all v1 aibox-auth-* volumes and old backup folders into aibox-home. No-clobber file union, newest-wins .claude.json project merge, optional --map path rekeying, idempotent, sources read-only, prints cleanup commands without running them. Removed: compose orchestration, safe mode + firewall, isolation modes, named instances, repo clone, port-forward sidecars, init/WebStorm integration, clean/nuke/doctor/volumes/disk, auto-down on session exit. Tested end-to-end against a live Docker daemon: cold start with full image build, 0.6s re-attach, proxy routing (HTTP 200 via Host header, no published ports), state across stop/start, image-change recreation idle + active-session guard, 5-way parallel invocation race, backup/ restore round-trip, and migration merge including idempotent re-run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Three subagent audits (bin/aibox internals, migration data-safety, spec/docs/packaging conformance) empirically verified every finding against a live daemon; all fixes below are regression-tested the same way. Critical (data loss, all reproduced before the fix): - restore could wipe the volume after a silently-failed safety backup: command substitution suppresses set -e, so _do_backup swallowed every error and printed a path that did not exist. Every step in _do_backup now propagates failure and callers die loudly. - restore wiped the volume before validating the archive; a corrupt tar.gz left it empty. The archive is now validated (tar tzf) before confirmation, and the tarball is mounted at a fixed path instead of splicing its name into sh -c (which was also a quote/injection bug). - migration --map could rm -rf destination project dirs: identity and escape-colliding maps are refused, the merge branch now moves entries of any type no-clobber, deletes only byte-identical duplicates (cmp -s), and keeps + warns about anything that differs. - migration read the destination's live .claude.json from the wrong path, so re-runs rebuilt it from old sources only, dropping v2-era projects and OAuth. Correct path; verified surviving a re-run. High: - release.yml stamped all three __CLI_VERSION__ occurrences, turning the two dev-build sentinels always-true: every released build would have identified as 'dev' with a dead update mechanism. sed is now anchored to the assignment line (stamping simulated in tests). - npm users never received scripts/migrate-to-v2.sh (absent from package.json files); README now points at the shipped copy and a raw URL fallback. - aibox update used 'npm update -g', which does not cross the 0.6->2.0 major boundary; now 'npm install -g aibox-cli@latest', and brew/npm failures abort instead of printing a false success. - concurrent proxy creation: the race loser rm -f'd the winner's healthy proxy and fell back to 8080; now it re-checks state and adopts the winner (verified with parallel first-runs). - container recreation is serialized with a mkdir lock (TOCTOU window observed live in the audit). - migration staged same-mtime sources over each other (stage_json ran in a pipeline subshell so its counter never advanced) and aborted outright on null/non-object .claude.json sources; --map matching now respects path boundaries (myapp no longer captures myapp2). Medium/low: --init so stops take 0.3s not 10s (sleep ignored SIGTERM as PID 1); entrypoint ready-marker so first-run claude installs are not raced; ANTHROPIC_* forwarding via compgen (multiline env values forged variable names under set -u); stop --all filters by label instead of name prefix (no longer touches unrelated aibox-* containers); config parser keeps the last line without trailing newline and skips empty values; AIBOX_URL_BASE computed at exec time with the collision- resolved label and actual proxy port; stable UTC ordering for slug labels; CDPATH unset; ':'-in-path guards for docker mounts; stop rejects unknown args; version prints docker state; status shows uptime; unrelated-image node:bookworm curl added explicitly; migration copies symlinks, uses temp+rename, warns on newline filenames, stages both .claude.json layouts, warns about running v1 containers, pins alpine:3.20, and lists v1 port-forward sidecars in cleanup output. Docs: README migration/proxy-port/image notes, CONTRIBUTING stamping caveats (incl. unstamped Homebrew), REVAMP accepted-deviations appendix, stale v1 entries removed from .gitignore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Driven by three further audits (simplification, first-time-user UX walkthrough, cold-read maintainability), all verified against a live daemon. Bugs fixed: - Dev-server URLs were broken on modern Docker: docker inspect does not expand \t in --format (unlike docker ps), so _project_rows parsed zero rows, the Caddy map generated empty, and every dev URL returned 502 while status showed '-'. Space-separated format now; routing re-verified end-to-end (HTTP 200). - A busy proxy port left a dead Created-state container that the race recheck adopted as healthy: proxy 'running' with no host port bound and the 8080 fallback unreachable. The absent branch now discriminates by 'running' state, removes the corpse, and actually falls back. - The first interactive shell opened zsh-newuser-install; the entrypoint now seeds a minimal ~/.zshrc into the home volume. Simplifications (audit-verified safe): always pass --dangerously-skip-permissions (claude tolerates duplicates, drops the dedup scan); one in-container wait replaces the exec-per-second claude poll; one inspect fetches state+image together; hash fallback collapsed; one mkdir for CONFIG_DIR; unified is-running idiom; migration gains _copy_into_dst/_read_json helpers, halves .claude.json docker runs, and collapses the cleanup-print loops. The migration --map rename pass is deliberately kept: .claude.json keys are rekeyed unconditionally, so print-only handling would orphan pre-map sessions. UX (from the walkthrough): mental-model summary at the top of help; project names instead of container ids in stop/status; status table leads with PROJECT and de-jargons docker status; proxy start announced (was the one silent first-run gap); 'nothing was changed' on refused restores; real file size in the backup message; concise unknown-command error; actionable daemon-down hint; version prints 'dev' instead of the raw __CLI_VERSION__ placeholder; help documents shell's 1-arg vs N-arg semantics and AIBOX_URL_BASE. Maintainability: ~20 one-line comments for load-bearing constraints (container-name/proxy-label coupling, Caddy label indexing, bash-3.2 idioms, escape() must match Claude's own path escaping, timeout units, stderr-capture ordering), IMG_VER renamed VERSION_TAG, honest section headers, migration header renumbered to match its body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
mktemp -d lands in /var/folders on macOS, which Colima does not share into its VM, so bind-mounting the staging dir produced an empty mount and the merged .claude.json write failed. Pipe the file over stdin instead — works regardless of the runtime's mount configuration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
aibox claude now passes --allow-dangerously-skip-permissions (prompts on, bypass selectable in-session). A new aibox --yolo flag swaps it for --dangerously-skip-permissions, restoring the old skip-everything behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
aibox --copy runs claude in a fresh container per run on a docker-cp'd snapshot of the project at the same absolute path — the real directory is never mounted, so the agent cannot touch it. The copy shares the aibox-home volume (login, sessions, claude binary), gets its own dev URLs under <slug>-copy, and is removed by an EXIT trap when the session ends; work is kept by committing/pushing from inside. Default behavior (bind mount, persistent container) is unchanged, and --copy composes with --yolo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
One generic launcher (cmd_run) now owns flag parsing, container choice, and exec: aibox / aibox claude dispatch to 'run claude'. --copy works for any program; --yolo stays claude-specific (rejected otherwise, since other harnesses have their own approval flags that pass through as normal args). cmd_claude is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
…apshot Four-angle review (reuse/simplification/efficiency/altitude) of the recent flag work. Extract shared helpers (_docker_run run-spec, _wait_running, _ensure_net_vol, _dev_url) so persistent and copy containers can't drift; snapshot now streams a tar as the container user instead of docker cp + full-tree chown (one traversal instead of two); proxy setup moved before the snapshot so its latency hides under the copy; cmd_run gets a standard -- sentinel so a literal --copy/--yolo can reach the target program. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
The aibox-home volume is now mounted in volume-subpath slices (Engine 26+, checked): shared slices carry the Claude login/settings and binary; each project gets a private home and its own transcripts. Other projects' data is simply not mounted — the only boundary that holds against full sudo. todos/file-history/shell-snapshots stay shared by design. Old flat volumes are detected by a layout marker and migrated in place on the next run: safety backup first (abort if it fails), only recognized files move, transcript dirs are matched to projects via the real paths in .claude.json, root dotfiles are copied into each existing project's home, and the migration is idempotent. Containers predating the layout are recreated via the existing image-change path (aibox.layout label); restore now removes containers instead of restarting them so restored data always pairs with fresh mounts. Riders: --hostname <slug> (readable remote-control session names), cleanupPeriodDays=3650 seeded into shared settings.json (transcripts no longer auto-delete after 30 idle days), and CLAUDE.md notes teaching any session to grep past sessions and revive one via --resume --remote-control. Migration body is testable outside docker (AIBOX_MIG_ROOT); covered by dummy-volume tests (shape, matching, orphans, idempotency, edge cases) plus stub-docker tests for the mount table and backup-before-migrate ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
aibox serve keeps 'claude remote-control' running in the project container under a small supervisor (pty via script(1), restart loop, gives up after 3 quick exits with a pointer to the log) so phone/claude.ai sessions survive closed terminals. Bare restarts within Anthropic's ~4h window re-serve previous sessions automatically. Extra args pass through (--spawn, --capacity, ...); aibox serve stop kills the supervisor, server, UI, and revived sessions. Alongside it, a single-file Node sessions UI (written into the container, port 45789, reachable at http://45789.<project>.aibox.localhost): lists this project's transcripts with title/age/message count, Resume spawns a detached 'claude --resume <id> --remote-control' (strict id validation, argv-only spawn), live sessions link to https://claude.ai/code/<id>. Tested: UI served real transcript fixtures (titles from string and array content, sorting, live detection), resume spawn argv verified via stubbed script(1), hostile ids all 400 with zero spawns; stub-docker covers the supervisor/UI start calls, URL surfacing, stop path, and the full command regression matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
aibox status now shows a MEM column (one docker stats sample across all running aibox containers), a DISK column (rw-layer size from docker ps -s), and a daemon-wide 'Docker disk' summary line (images/containers/volumes with reclaimable amounts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Server mode is gone. aibox serve now just runs the sessions UI; every live session — brand new or revived — is one detached 'claude [--session-id <uuid>|--resume <id>] --remote-control' process. New session spawns with an explicit --session-id so live detection and the claude.ai URL work uniformly; live rows gain a Stop action; sessions with no transcript yet still get a row. serve stop kills the UI and every session. The supervisor, flap guard, rc.log tailing, and passthrough args are deleted; the 4-hour resume-window concern disappears with them — recovery is always Resume-from-transcript. CLAUDE.md notes teach the --session-id variant so any live chat can start a fresh session on request. Tested: /api/new spawn argv and returned id, /api/stop by id, hostile ids 400 with zero spawns, page plumbing, stub-docker serve start/stop, arg rejection, 9/9 command regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Serve/UI (three reviewers converged, repro'd empirically): - serve stop was a total no-op reported as success: the sh -c wrapper's own cmdline carried both pkill patterns, so the first pkill killed its parent shell. Each pkill now runs as direct exec argv. - The '# aibox-serve-ui' tag was erased by exec, so the start guard, stop, and status never matched — every serve spawned a duplicate UI and a stale server could outlive rewrites. The tag now rides as an ignored node argv, visible to pgrep/pkill. - Two UI crash-the-server paths fixed: statSync on a transcript deleted mid-poll (per-file try/catch) and 'GET //' throwing in new URL() (parse guarded, 400). - /api/resume now 409s when the session is already live (double-resume attached two claudes to one transcript); spawns get error listeners; message counts only count user/assistant lines and are cached by size+mtime; meta/caveat lines no longer become titles; project name HTML-escaped; container-wide bind trade-off documented. Migration data-safety (adversary agent, all repro'd on synthetic volumes): - Only JSON KEYS in .claude.json claim transcript dirs — a value-position path (MCP args) could steal a project's transcripts into a slice nothing mounts. - Dotfile seeding is decoupled from transcript matching and staged atomically (tmp+rename), so a crash mid-copy is healed by the re-run instead of blessed as complete; the project that triggers migration gets dotfiles too. - Unclaimed transcript dirs are announced loudly instead of stranded silently (covers >200-char and non-ASCII path encodings). - Fresh-volume detection now means EMPTY volume; anything else without a marker migrates (with backup) instead of being walled in. - The running-container guard filters by volume, not label — the oldest aibox containers carry no labels. - Path-hash collisions between projects are caught by a .aibox-path sentinel per slice instead of silently merging homes. Robustness (bash reviewer, repro'd on live docker): - A failed docker run left a wedged Created container that nothing ever recreated; Created state is now recreate-eligible. - Layout detection fails closed on docker errors instead of proceeding to guaranteed-broken container creates; slice preparation is fatal on failure with a real message. - Migration lock steal raised to 1h (a safety backup can take minutes), with a stale-lock hint, and the marker is re-checked after acquiring; backup/restore refuse to run mid-migration. - --hostname truncated to 63 chars (kernel limit; >63-char project dirs previously failed container creation). - Migration move loop no longer skips dangling symlinks. External-facts audit confirmed: volume-subpath semantics and the Engine-26 gate, Claude Code's exact transcript-dir encoding and state paths, --session-id/--resume/--remote-control flag parsing (any hex uuid accepted), cleanupPeriodDays (binary itself suggests 3650), docker stats/ps/df format parsing, /proc uuid format, and that the entrypoint chown is load-bearing for root-created mountpoint dirs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
aibox sessions runs a small Node server ON THE HOST (node is guaranteed — aibox installs via npm), loopback-only, foreground: it opens the browser on start and stops on Ctrl-C or the page's Close button. No daemon, no lifecycle machinery. Listing comes from a throwaway container with the volume mounted read-only (per-project slices, .aibox-path, titles from first user message). Each session offers three actions: Terminal (opens Ghostty via 'open -na', Terminal.app via osascript as fallback; macOS only), Copy (resume command to clipboard, client-side), and Phone (runs the new 'aibox rc-resume <id>' in the project dir — ensure container, spawn detached claude --resume --remote-control). Action requests are vetted against the last listing, so only real (path, id) pairs reach a shell. Being host-side and loopback-bound, this page has none of the sibling-container reachability of the in-container serve UI. Tested: lister against fixture volumes, server end-to-end with stubbed docker/aibox (page, listing, unknown-pair 400, macOS-only fallback message on Linux, rc action invoked in the right cwd, Close exits the process), rc-resume validation + exec argv via stub docker, 10/10 command regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
docker run of the aibox image ran the container entrypoint first, which tried to install claude into the empty image-local home before the lister executed — minutes of 'Loading...' on every page load. --entrypoint node skips it. Empty state now explains the not-yet-migrated-volume case with the exact commands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
The legacy flat layout (and post-migration leftovers) are listed too, with real paths recovered from .claude.json keys using the same rule as the migration; dirs with unrecoverable paths still show, copy-only. So aibox sessions is useful the moment it's installed — including while old-layout sessions are still running — instead of being gated on migrating first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
…er-side Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Row titles now prefer Claude Code's own summary records (the same text its resume picker shows), falling back to the first user message; each row adds an italic one-line preview of the most recent assistant response. The lister reads a head and a tail window per transcript (not the whole file), so multi-MB sessions stay cheap and tail-only summaries are still found. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
…rden server UI: rows were a cramped horizontal flex where three action buttons crushed the title/preview to nothing on a narrow window. Rebuilt as a vertical stack — title and a two-line last-reply preview get full width and wrap; actions sit on their own wrapping row with real labels. Live-session guard (the user's exact scenario — 10 sessions running while browsing): sessions with mtime within ~2 min show an 'active now' badge, and Open-in-terminal / Send-to-phone on them require a confirm, since resuming starts a second claude on a transcript being written right now. Security + robustness (from the two-agent review): - osascript Terminal branch built a shell command with JSON.stringify, which doesn't neutralize $()/backticks — a project path or a volume-planted .aibox-path could execute arbitrary host commands. Both path and id are now POSIX single-quoted; id is also regex-vetted in vetted(); the Copy command is quoted the same way. - Host-header check rejects non-loopback Host values (blocks other sites and DNS-rebinding from driving the server); action 400s return JSON so the client stops mis-reporting 'Server stopped'. - EADDRINUSE (a stale server from the earlier buggy run holds 45790) now prints a friendly hint instead of a raw stack trace. - Terminal/phone actions use the aibox binary resolved on the host (command -v / realpath $0), passed positionally to the Ghostty shell — so PATH gaps in a launchd-spawned Ghostty login shell don't cause 'aibox: command not found'; the Ghostty window is held open on failure. - Lister/docker errors are surfaced as a page banner instead of the misleading 'volume not migrated' empty-state (which had told the user to stop their live sessions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
… last reply The list alone could not tell you what a session was. Rows are now expandable: the collapsed row keeps title + two-line preview + meta, and clicking it opens Summary (Claude Code's own summary record when the transcript has one), Your last message, and the full Last reply (up to 2000 chars, scrollable). Lister emits the extra fields from the same head/tail windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
… search A search box filters the list as you type over titles, summaries, previews, paths, and ids. Pressing Enter runs a deep search: the lister re-runs with AIBOX_SEARCH (env through argv, no shell) and scans entire transcript files chunk-wise, case-insensitive, with chunk-boundary overlap — so a phrase you remember from mid-conversation finds the session even when no preview shows it. Clearing the box restores the full list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
REVAMP: add sessions/rc-resume to the command surface, amend D11 and the size target/acceptance criterion for the post-spec UI additions, correct the verbatim-passthrough rule (--copy/--yolo consumed, -- escape), the mount list (five volume-subpath slices), and the restore steps (containers removed, not restarted). README: per-project history wording, isolation claim scoped to home+transcripts (shared bookkeeping stays common), serve stop scoped to the project, migration ordering note (merge BEFORE first v2 run — slicing strands data merged later), curl in the basics list. migrate-to-v2.sh: stale flat-layout comment replaced with the ordering requirement. bin/aibox: comment no longer references a shipped 'test harness', just the env override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
…allbacks Deep search decodes chunks through StringDecoder so a multi-byte codepoint split at a 4MB boundary can't corrupt a straddling match (verified with an emoji planted across a real boundary). The server's action guard now unions deep-search subsets and only full listings replace it, and clearing the search box reloads — so rows re-rendered from the client cache stay actionable; listing errors keep the previous guard instead of rejecting everything visible. info()'s error path returns all four fields (a directory named *.jsonl no longer makes the literal word 'undefined' searchable). AIBOX_BIN resolution gains readlink -f so a relative-path invocation without realpath can't hand the actions a path that breaks after cd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full rewrite of
bin/aiboxper the requirements inREVAMP.md(included): v1's 2,379 lines / 16 commands become a single ~1,700-line file (core ~500 + two embedded Node UIs) built around durability — nothing is ever destroyed implicitly.What changed
Lifecycle. One persistent container per project dir (
aibox-<slug>-<hash6>), created on demand and kept running after sessions exit — the v1 auto-docker compose downdata-loss footgun is gone. Containers are removed only on image/layout change (immediate recreate) orrestore. Plaindocker run/exec; compose, generated YAML, and project-dir writes are all gone.Storage & isolation. One global
aibox-homevolume (replacing per-imageaibox-auth-*volumes), mounted involume-subpathslices (Engine 26+, checked): shared Claude login/settings/binary, private per-project home + transcripts. One login, one backup tar, and project-vs-project isolation enforced at the mount level — the only level that means anything with full sudo inside. Legacy flat volumes migrate in place automatically on first run: safety backup first (abort if it fails), only recognized files move (transcript dirs matched via.claude.jsonkeys), everything else stays put, idempotent, with a hash-collision sentinel per slice.Command surface.
aibox/claude=run claude; genericaibox run <prog>(e.g. codex);--yolo(start in bypass-permissions; default is prompts-on with claude's allow flag),--copy(disposable no-bind-mount snapshot container, removed on exit),--ends aibox flag parsing.serve(in-container sessions UI, port 45789 — every phone/claude.ai-drivable session is one detachedclaude --remote-controlprocess: new/resume/stop),sessions(host-side, loopback-only all-projects page, port 45790, foreground: expand for summary/last reply, full-transcript search, open-in-terminal / copy / send-to-phone),rc-resume(plumbing),shell,stop [--all],status(now with live mem/disk per container + Docker disk totals),backup,restore,update,version,help. Deleted:up down build init port-forward volumes disk clean nuke doctorand v1's global flags.Dev servers. A shared Caddy proxy (
127.0.0.1:80, 8080 fallback) serveshttp://<port>.<project>.aibox.localhostfor any container port — replaces socat sidecars; no ports published on project containers.Security model. The container boundary is the sandbox (full passwordless sudo inside); v1's unused firewall/safe-mode/restricted-sudo are deleted. The host sessions page is loopback-bound with a Host-header check, session ids/paths vetted against the live listing, and volume-derived strings shell-quoted everywhere they touch a shell. The in-container serve UI binds container-wide for the proxy — reachable by sibling containers on the
aiboxnetwork; documented trade-off (endpoints only affect that project), to be closed by moving it host-side (#3).Backup/migration. Built-in
backup/restore(read-only tar of the volume; restore validates, safety-backups, replaces, removes containers for recreation).scripts/migrate-to-v2.shmerges v1aibox-auth-*volumes and old backup folders no-clobber; must run before the first v2 run (README documents the ordering).Packaging.
aibox-cli0.6.0 → 2.0.0; npmfilesships the migration script; release sed stamps only theCLI_VERSION=assignment; image tagaibox:<ver>-node<N>triggers rebuild+recreate on any release.Review & testing
No in-tree test suite; verification was done during development with disposable harnesses: the migration body and lister expose env hooks (
AIBOX_MIG_ROOT,AIBOX_LIST_ROOT) and were exercised against synthetic volumes (layout shape, key-matching, orphan preservation, crash-recovery idempotency, UTF-8 chunk-boundary search); the host/serve servers were driven end-to-end with stubbed docker (routing, vetting, injection attempts, error paths); a 15-case command regression matrix ran against a logging docker stub. The branch went through several multi-agent adversarial review rounds (bash/busybox correctness, migration data-safety, host-shell security, external-facts verification against real docker and the real claude CLI) with all confirmed findings fixed in-branch.Still to verify on real hardware (macOS/Colima): the volume migration end-to-end, detached
claude --session-id/--resume --remote-controlunderscript(1)in a container, subpath mounts + shared login behavior, and the Ghosttyopen -nainvocation.Tracking issues: #2 (other harnesses), #3 (host-side overview page; absorbs the serve UI).
🤖 Generated with Claude Code
https://claude.ai/code/session_01XoXWywkXvRMxamQ7RjNAYq
Generated by Claude Code