From 3fc7bcb32faac6a6738e4bf917fa25d9ea23450a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 15 Jul 2026 21:57:49 +0000 Subject: [PATCH] always run pi as the service user, drop the sudo -u branch - the sandbox already drops privileges to the OS user (id -un), so pi-bridge was the only code path that switched to a per-user unix account - the four accounts it targeted (andrepadez, john-wick, fedra, miguelbenoliel) are vestigial: created by scripts/provision-existing-users.sh, with no home dirs, no files, no processes. the only live account is pastilhas@officer.dev, which maps to the service user, so isServiceUser was always true and the sudo -u branch could never fire - add TODO.md tracking the leftover username plumbing, the useradd script, known bugs (task-executor relative cwd, bootstrap pi EEXIST), and the ufw 9010 rule Co-Authored-By: Claude Opus 4.8 --- TODO.md | 53 +++++++++++++++++++++++++++++++++ src/servers/api/pi/pi-bridge.ts | 38 +++++++---------------- 2 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..de61383a --- /dev/null +++ b/TODO.md @@ -0,0 +1,53 @@ +# TODO + +Deferred work. Context: Officer is collapsing from multi-tenant / open-source-ready to a +**single-user platform**. Treat multi-tenant indirection as accidental complexity, not a requirement. + +## Single-user cleanup + +- [ ] **Remove dead `username` plumbing.** `pi-bridge.spawnPi` no longer uses `username` — it always + runs as the service user. But the handlers still compute `toShellUsername(...)` and thread it + through `sendAndAwait` → `spawnOptions`, where nothing reads it. `send-claude-code` declares + `username` (lines 8, 36) without using it. Typechecks clean today (excess-property checks don't + fire on variables), so it silently *looks* load-bearing. + Touches: `channels/{whatsapp,telegram,discord}/handler.ts`, `channels/send-and-await.ts`, + `channels/send-claude-code.ts`. After this, `toShellUsername` has one caller left + (`provision.ts` → `generateClaudeSettings`) and may be inlinable. + +- [ ] **Delete or gut `scripts/provision-existing-users.sh`.** Line ~100 runs + `sudo useradd -d "$HOME_DIR" -s /bin/zsh -M "$shell_user"`. This is what created the four + vestigial Unix accounts (`andrepadez`, `john-wick`, `fedra`, `miguelbenoliel`) that cluttered + the lightdm greeter. Nothing in the server creates Unix users; if this script runs again they + come back. + +- [ ] **Collapse the rest of the multi-tenant machinery.** Candidates, in rough order of payoff: + roles (`Super Admin`/`Member`), the sandboxed-vs-unsandboxed path split, per-email home dirs + under `dev-data/{email}/home`, per-user server state (dock, user apps, AppRegistry keyed by + email), and auth (passkeys-per-origin, JWT signin, unmounted `PasskeyGate`). + +## Known bugs + +- [ ] **Convert Audio task fails with `ENOENT: posix_spawn 'bash'`.** Misleading error — the real + cause is a *relative* `cwd` passed to `Bun.spawn` (ENOENT fires when the child's cwd doesn't + exist, not when the binary is missing). Fix is in `api/tasks/task-executor.ts`: resolve + `msg.cwd` against the user's home when it isn't absolute + (`isAbsolute(msg.cwd) ? msg.cwd : join(homeDir, msg.cwd)`). Was written and verified once, then + discarded in a working-tree cleanup. + +- [ ] **`bootstrap.ts` runs `npm install -g` for Pi on every boot.** `findPiPackageDir` checks stale + paths and an outdated package name, so `installPi` always fires and floods the logs with + `EEXIST` noise. Should detect `@earendil-works/pi-coding-agent` at the real npm prefix. + +## Infra (alpha) + +- [ ] **`ufw` blocks port 9010 on the LAN.** Default deny incoming; only `22/tcp`, `80/tcp`, + `443/tcp`, and everything on `tailscale0` are allowed — so `http://192.168.47.196:9010` is + unreachable from the LAN while localhost and Tailscale work. + Fix: `sudo ufw allow from 192.168.47.0/24 to any port 9010 proto tcp` + +## Backburner + +- [ ] **Music tagging feature** (`/music`, Mp3tag-inspired). v1 was scoped as a two-panel workspace — + file browser left, table right, single "Load" context-menu item flattening audio files into the + table. Discarded before completion; would need `GET /file-browser/flatten-audio`, a + `useFilesAPI.flattenAudio` method, and the Music app panel rebuilt from scratch. diff --git a/src/servers/api/pi/pi-bridge.ts b/src/servers/api/pi/pi-bridge.ts index 4f884a90..5a163d88 100644 --- a/src/servers/api/pi/pi-bridge.ts +++ b/src/servers/api/pi/pi-bridge.ts @@ -12,7 +12,6 @@ import { getUserExtensionsDir, getGlobalToolsDir, getUserToolsDir, - toShellUsername, } from '../../data-path'; import { logger } from './logger'; @@ -84,7 +83,6 @@ async function resolveApiKeyForModel(model: string): Promise { type SpawnPiOptions = { sessionFile?: string; - username?: string; role?: string; }; @@ -121,43 +119,27 @@ export async function spawnPi( const homeDir = getHomeDirForRole(email, options?.role ?? null); const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':'); - const shellUsername = options?.username ?? toShellUsername('', email); - - const isServiceUser = (options?.username ?? toShellUsername('', email)) === (process.env.USER ?? ''); const env: Record = { - HOME: homeDir, + HOME: process.env.HOME ?? '', OFFICER_USER_HOME: homeDir, OFFICER_USER_ROOT: join(DATA_PATH, email), - PI_CODING_AGENT_DIR: isServiceUser ? PI_CONFIG_DIR : join(homeDir, '.pi', 'agent'), + PI_CODING_AGENT_DIR: PI_CONFIG_DIR, PI_TOOLS_DIRS: toolsDirs, OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'), TERM: 'xterm-256color', PATH: process.env.PATH ?? '', }; - // For service user, keep real HOME so Pi finds its config - if (isServiceUser) { - env.HOME = process.env.HOME ?? ''; - } + const proc = Bun.spawn(piArgs, { + cwd, + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe', + env: { ...process.env, ...env }, + }); - const proc = isServiceUser - ? Bun.spawn(piArgs, { - cwd, - stdin: 'pipe', - stdout: 'pipe', - stderr: 'pipe', - env: { ...process.env, ...env }, - }) - : Bun.spawn(['sudo', '-u', shellUsername, 'env', ...Object.entries(env).map(([k, v]) => `${k}=${v}`), ...piArgs], { - cwd, - stdin: 'pipe', - stdout: 'pipe', - stderr: 'pipe', - }); - - logger.info('Spawned Pi as user', { - username: shellUsername, + logger.info('Spawned Pi', { model, skills: skillFlags.filter((f) => f !== '--skill').length, extensions: extensionFlags.filter((f) => f !== '--extension').length,