Skip to content

aibox v2: persistent per-project sandboxes, sliced home volume, phone/browser sessions - #4

Open
repalash wants to merge 26 commits into
mainfrom
claude/lib-revamp-requirements-qm0fh4
Open

aibox v2: persistent per-project sandboxes, sliced home volume, phone/browser sessions#4
repalash wants to merge 26 commits into
mainfrom
claude/lib-revamp-requirements-qm0fh4

Conversation

@repalash

Copy link
Copy Markdown
Contributor

Full rewrite of bin/aibox per the requirements in REVAMP.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 down data-loss footgun is gone. Containers are removed only on image/layout change (immediate recreate) or restore. Plain docker run/exec; compose, generated YAML, and project-dir writes are all gone.

Storage & isolation. One global aibox-home volume (replacing per-image aibox-auth-* volumes), mounted in volume-subpath slices (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.json keys), everything else stays put, idempotent, with a hash-collision sentinel per slice.

Command surface. aibox / claude = run claude; generic aibox 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 detached claude --remote-control process: 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 doctor and v1's global flags.

Dev servers. A shared Caddy proxy (127.0.0.1:80, 8080 fallback) serves http://<port>.<project>.aibox.localhost for 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 aibox network; 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.sh merges v1 aibox-auth-* volumes and old backup folders no-clobber; must run before the first v2 run (README documents the ordering).

Packaging. aibox-cli 0.6.0 → 2.0.0; npm files ships the migration script; release sed stamps only the CLI_VERSION= assignment; image tag aibox:<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-control under script(1) in a container, subpath mounts + shared login behavior, and the Ghostty open -na invocation.

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

claude added 26 commits July 30, 2026 01:24
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants