-scale 0.5 was hardcoded on the assumption that :0 is 4K. With no monitor
plugged in X falls back to something tiny — 800x480 on this box — and halving
that served an unreadable 400x240.
Read the framebuffer width from xrandr and scale only above 2560px. When the
width cannot be read, serve 1:1: too many pixels beats a thumbnail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The desktop page has never worked on a fresh install. Two faults, both fatal.
The password could never be created. DesktopView fetches /desktop/vnc-password
before opening the WebSocket, but ensureVncPassword ran only from startSession,
which only the WebSocket triggers — so the endpoint answered "not configured",
the UI stopped, and the socket that would have provisioned it was never opened.
A new vnc:ensure-password sidecar command provisions it directly; the endpoint
asks for it instead of returning 500.
The rfbauth file could never be written either. ensureVncPassword shelled out to
tigervnc's `vncpasswd -f`, which is not installed — and, contrary to the comment
in setup-desktop.sh, is not in tigervnc-common, which ships only tigervncconfig.
The failure was swallowed because only a zero exit wrote the file, so x11vnc got
-rfbauth pointing at nothing. x11vnc writes that format itself with -storepasswd,
so the dependency is gone and a failure now throws.
Verified on the box: the endpoint returns a password, .vnc/{passwd,password} are
written 0600, and the sidecar reports mirroring :0 on 5900 with x11vnc using the
generated rfbauth file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The proxy is an opaque catch-all, so the endpoint surface wasn't perceivable from
the platform side. Add a contract header (all routes + params + SSE/response
shapes) atop the sidecar fetch handler where the routes are defined, and point
the proxy router at it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a live progress channel for the library index:
- indexer.ts: progress subscribers (onIndexProgress) + throttled emit during the
walk, and buildReport() for a final summary.
- sidecar: GET /reindex/stream (SSE) — triggers a build if idle (?trigger=0 to
watch only), streams `progress` events, ends with a `done` event carrying the
report; auto-proxied at /api/music/reindex/stream for the app. Sidecar also
writes DATA_PATH/music/.server (its port) for local tooling.
- scripts/reindex-music.ts: CLI that reads the port file, follows the SSE, prints
live progress + a final report. Run: bun scripts/reindex-music.ts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The officer-music sidecar now builds a cache tree mirroring the library (server
counterpart of the app's music-index.ts), and exposes an rsync-clean diff surface.
Indexer (indexer.ts): walks HOME_DIR/Music; per album computes a version `v` =
hash of the source signature (track name+size+mtime, cover size+mtime); ffprobe
→ meta.json (phone IndexMeta schema: file/title/artist/albumArtist/album/track/
year/durationSec); ffmpeg compresses the cover to <=600px q5 cover.jpg. Writes
DATA_PATH/music/cache/<rel>/. Incremental (skip albums whose `v` is unchanged),
prunes cache dirs for albums removed from the library, maintains manifest.json.
Endpoints (sidecar, auto-proxied by /api/music/*):
POST /reindex async build; GET /reindex/status polls progress
GET /manifest { version, albums: { "<rel>": { v, cover, tracks } } }
GET /meta?path=<rel> album meta.json (ETag: v, 304 on If-None-Match)
GET /cover?path=<rel> compressed cover (ETag: v, 304 on If-None-Match)
Phone resync: GET /manifest, diff `v` against last-stored → fetch only changed
albums' meta+cover; drop rels missing from the manifest. No re-download of
unchanged albums.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an officer-music sidecar that owns an audio-streaming HTTP server, and a
thin authenticating proxy on the platform. All processing (path resolution,
byte-range streaming, ffprobe duration) is in the sidecar; the platform only
authenticates and forwards.
App-facing contract (handoff):
GET /api/music/stream?path=<home-relative path>&token=<jwt>
- auth via userMiddleware (Bearer or ?token= for media elements)
- 200 full / 206 on Range, with Accept-Ranges, Content-Length,
Content-Range, Content-Type, and X-Audio-Duration (seconds, ffprobe)
- path resolved within HOME_DIR, traversal-guarded (400); 404 if missing
Purpose: stream + seek without pre-downloading the whole file — the app can
read X-Audio-Duration instead of scanning for VBR duration.
Pieces:
- sidecar/music/{index.ts,stream-audio.ts}: Bun.serve on a random port, /stream
+ /health, duration cached by path+mtime; reports its port via a new
music:server sidecar event on connect.
- api/music/{sidecar-server.ts,router.ts}: capture the port; reverse-proxy
/api/music/* → sidecar, streaming status + headers through.
- protocol.ts music:server event; hono.ts mounts /api/music; ecosystem adds
officer-music.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.
Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.
Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.
Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.
getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the
Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an
Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM
per-session path (/run/user/<uid>/gdm/Xauthority) with a ~/.Xauthority fallback.
setup.sh fixes:
- desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned
officer-vnc service, which never matched so setup-desktop re-ran every time)
- remove Pi (install, --list-models validation, verification check)
- export GOPATH before the cliamp build so `go install` lands where it's checked
even when Go was already present this run
- write PUBLIC_BUILD_ENV=production and quote all .env values
- guard the interactive .env block behind a TTY check so non-interactive runs
skip cleanly instead of aborting on read EOF under set -e
- restart systemd-logind only when a key actually changed
- sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns now run in the sidecar via `opencode run --dir <cwd> --format json
--dangerously-skip-permissions [-s <ses_>]` instead of the serve's
`POST /session/{id}/message` path. That path was unreliable at reporting
tool completion — tools finished but the turn stayed status=running,
wedging the UI at "Working…". `run` re-anchors tools to the chat cwd via
--dir, reports completion faithfully, and exits when done.
- runner.ts (new): spawn `run`, map its JSON events (text/tool_use/
step_finish) to ChatEvent, report the `ses_` id for resume, accumulate
cost; inactivity (120s) + hard-cap (10min) watchdogs kill a hung turn
and emit a clean error instead of hanging forever.
- protocol.ts: opencode:run-streaming/kill commands; opencode:spawned/
event/session events; OpenCodeRunParams.
- sidecar index.ts: wire run/kill; sweepStaleServes() on startup kills
only an `opencode serve` whose resolved /proc/<pid>/cwd == SERVE_CWD,
so an unclean prior exit can't leave two.
- sidecar-registry.ts: spawnOpenCodeStreaming/killOpenCode/onOpenCodeEvent/
onOpenCodeSession helpers.
- send-opencode.ts: rewritten to mirror send-claude-code (subscribe →
resolve resume id → spawn → kill handle).
- sidecar-server.ts: persist reported ses_ id into state for resume.
- list-models/server-manager: route to the sidecar's reported serve URL.
The serve stays up only for read-only calls that never hung (model
listing, session history).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New officer-opencode sidecar (same philosophy as officer-claude): a singleton that owns
an `opencode serve` running from DATA_PATH/opencode-sidecar (created if missing) on a
random port, registers with the API as capability 'opencode', and reports its port via a
new `opencode:server` protocol event. The API stores it (sidecar-server.ts, wired in
server.tsx via getOpenCodeServerUrl). ecosystem.config.cjs runs the sidecar instead of a
bare pm2 serve. Turn-running + API rewiring come in later steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default /chat working directory is used by both the Claude and OpenCode harnesses
now, so its Claude-specific name was misleading.
- Rename the dir + accessors: getClaudeSessionsCwd → getGeneralChatSessionsCwd,
ensureClaudeSessionsCwd → ensureGeneralChatSessionsCwd, path segment claude_sessions
→ general_chat_sessions (data-path on disk + code + UI labels/comments). No history
migration — the old Claude transcript slug is orphaned (intentionally).
- Remove the vestigial chat_sessions dir (leftover from the retired session store):
it only ever held empty claude/archived/ dirs, recreated by a signin hook. Drop that
hook (+ its dead imports) and the 4 unused data-path accessors (getUserSessionsDir,
getClaudeDir, getSessionDir, getArchivedSessionDir), and delete the dir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorganizes email storage: the DB moves from DATA_PATH/<user>/emails.db to
DATA_PATH/<user>/email_accounts/<accountEmail>/emails.db, with a shared
email_accounts/attachment_cache/ (was Gmail/emails/attachments). openEmailDb now
takes (owner, account); a new openUserEmailDb(owner, userId) resolves the user's
configured account (first enabled) for read paths. Threads the account through
email.ts, accounts, resync, queue sync, channel handlers, and the email_db MCP
tool path. Drops the dead getUserEmailDir helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-user platform: the Super Admin's Claude process now uses the real home
(HOME_DIR) instead of DATA_PATH/<email>/home, so its transcript store IS the same
~/.claude the terminal `claude` uses — platform and terminal sessions are
interchangeable (native `/resume` sees them). The session reader resolves the
same home. The generated container CLAUDE.md is no longer written for the Super
Admin (it would pollute the personal global ~/.claude/CLAUDE.md and is stale);
MCP tools still load via --mcp-config, and email/project panels inject their own
prompts. Sandboxed users keep their isolated home.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking a session in the list loads its transcript (GET /chat/sessions/:id,
parsed from Claude's JSONL into display messages) and continues the actual Claude
session: a resumeSessionId is threaded chat handler -> send-claude-code -> sidecar
-> claude-manager, which passes --resume <uuid> (in-memory session mapping still
takes precedence for live turns). Parser verified against real transcripts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-manager pinned the spawn cwd to HOST_HOME for Super Admin, ignoring the
cwd passed from the chat handler — so /chat sessions ran in /home/pastilhas
regardless. Now it uses params.cwd when provided (falling back to HOST_HOME),
so /chat actually runs from the dedicated claude_sessions directory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 1 of removing Pi (Claude-only). Cuts the non-Claude branches in the chat
WS handler, pipeline executor, and channel send-and-await; deletes the Pi
sidecar, its ecosystem entry, pi-bridge, and the Pi model-listing spawn (now a
static Claude tier list). Adds a guard coercing any legacy non-claude-code model
preference to the Claude default so old settings don't break chat or jobs.
Removes the dead no-op session-save REST route and stale Pi docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AUTOMATION_CONTEXT.md and SIDECAR.md described the multi-tenant scope
model, the seed/ tree, the marketplace, and a single sidecar owning the
queue — all superseded. Fix the stale doc pointers in opencode.json and
CLAUDE.md, and point at TODO.md as the source of truth on direction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the marketplace service dependency and the native/global/user
scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding
skills, tools, tasks, processes and extensions as plain files.
- tasks move from Postgres to TASK.md files (new file-backed task layer);
task editing now works, which the DB path never supported
- skills/tools/processes collapse into one shared file router (single dir)
- remove the marketplace client (sync-marketplace/sync-version) and the
boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store
- drop the dead tasks + vestigial skills/tools/processes/extensions +
item_chats tables (migration 0004)
- one-time migration script exports DB tasks and consolidates disk items
Migration verified: all 6 tasks round-trip through the runtime parser
identically to their DB rows (pipeline steps, triggers, script impls and
agentic bodies all intact).
NOTE: not yet functionally tested end-to-end — every item (each task mode,
tool, skill, extension) still needs to be run/exercised in the app before
this is trusted. To be done manually.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- vnc-manager now runs x11vnc against :0 rather than vncserver on a fresh display,
so the browser shows the same session as the tv instead of a parallel one
- x11vnc reads :0's cookie from the logged-in user's own .Xauthority, so no root is
needed; mirroring only works while someone is logged in (the greeter's cookie
belongs to lightdm)
- -scale 0.5 halves the 4k framebuffer to 1080p for the stream, -shared -forever
keeps it up across browser disconnects, -localhost keeps it behind the ws bridge
- readiness is now the listening port, not exit code: x11vnc stays in the
foreground where vncserver daemonized and exited
- drops findFreeDisplay and per-email session tracking; there is exactly one :0
- the parallel desktops this replaces caused real breakage: a ghost logind session
that broke lightdm login, and a brave profile lock held on :2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add getValidGoogleAccessToken helper and use it in email-cron, email account auth resolver, and the new gmail proxy
- POST /api/integrations/google/gmail-proxy forwards arbitrary gmail rest calls server-side, with auto-refreshed oauth
- pi-manager and claude user-instance inject OFFICER_API_URL + per-session JWT so tools can call back as the user
- claude anthropic proxy uses Authorization: Bearer + preserves any anthropic-beta headers (pro oauth tokens are rejected via x-api-key, and overwriting the beta header broke context_management)
- pi --list-models: fall back to stderr when stdout is empty (pi v0.73.1 writes the table to stderr)
- mcp tool server + pi tool loader: accept type: object inputs so json bodies stay structured
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sandbox now mounts global content at short /officer/* paths to avoid
bwrap intermediate directory traversal issues. Pi uses NODE_PATH for
extension dependency resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Capture HOST_HOME before user-instance overrides process.env.HOME so
Super Admin spawns claude in /home/pastilhas. Generate separate MCP
configs for sandbox (sandbox paths) and host (real filesystem paths),
pick based on role at spawn time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move claude and pi from sudo global installs to ~/.local. Claude
binary is copied to /usr/local/bin for sandbox visibility, pi runs
via node from ~/.local/lib (ro-mounted). Fix bwrap intermediate dir
traversal by setting 0755 perms on auto-created HOME dirs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- extract buildSandboxPrefix/buildRunuserSuffix into shared sandbox.ts
- super admin bypasses bwrap for full host access (claude, pi, terminal)
- member pi processes now use bwrap instead of sudo -u
- member terminals now use bwrap instead of sudo -u
- mount /run for systemd-resolved DNS inside sandbox
- pass role through claude spawn params and channel types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces prompt injection workaround with a proper MCP server that dynamically
discovers marketplace tools and exposes them as callable tools to Claude Code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add agent-agnostic tool registry (tool-registry.ts) that discovers tools from disk
- Embed tool-loader extension as platform infrastructure (ensure-tool-loader.ts)
- Inject tool context into Claude prompts on first message
- Add marketplace tool sync (sync-marketplace.ts)
- Fix model selector defaulting to claude-code when no model explicitly selected
- Exclude tool-loader-source.ts from tsconfig (Pi-specific deps)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
replaces the single hardcoded systemd VNC service with a dynamic
sidecar that manages per-user VNC sessions on demand. any authenticated
user can now access their own desktop, not just Super Admin.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flips the connection model so sidecars register themselves with the API
server via WebSocket at /api/sidecar/register, enabling dynamic discovery,
location independence, and automatic reconnection from either side.
- Add registration protocol types and PTY command/event types
- Create sidecar-registry.ts (replaces sidecar-client.ts) as passive registry
- Create sidecar connector (connect.ts) with exponential backoff reconnect
- Convert process sidecar from WS server to WS client
- Convert PTY sidecar from WS server to multiplexed WS client
- Simplify terminal bridge to thin adapter using registry
- Add PTY sidecar as PM2-managed process
- Update all consumer imports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewired EmailList sync button to call /email/accounts/:id/sync instead
of the old gmail-sync job. Shows sync button for connected and synced
accounts. Allow manual incremental sync for synced accounts.
Added duration logging to queue runner: start/complete/fail markers
with elapsed time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sidecar/queue runner no longer needs job-specific context. API server
resolves account details, IMAP auth, and user email at enqueue time —
all persisted in the job file. Handler reads directly from job meta.
Removed "Load account" step. Sync step auto-reconnects up to 10 times
when Gmail drops the connection, resuming from saved UIDs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Bun routes option matched "/" before fetch could handle the
WebSocket upgrade, so the API server could never connect. Moved
route handling into fetch after the upgrade check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces a separate Bun process (port 5100) that owns all spawned
processes and long-running work, so the API server can restart freely
without disrupting active sessions.
The sidecar owns:
- Anthropic proxy (port 5051) with persisted secret across restarts
- Claude Code process spawning and session tracking (--resume support)
- Pi agent spawning and RPC lifecycle (prompt/abort/thinking)
- Job queue engine (lane processing, retries, notifications)
The API server becomes a thin client that forwards commands over a
single WebSocket connection with auto-reconnect. send-claude-code.ts
goes from 550 lines of spawn logic to 73 lines of sidecar delegation.
State persisted to data/sidecar/state.json every 30s and on shutdown.
Lockfile prevents duplicate instances. See SIDECAR.md for full docs
and manual testing procedures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>