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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
getUserExtensionsDir,
|
getUserExtensionsDir,
|
||||||
getGlobalToolsDir,
|
getGlobalToolsDir,
|
||||||
getUserToolsDir,
|
getUserToolsDir,
|
||||||
toShellUsername,
|
|
||||||
} from '../../data-path';
|
} from '../../data-path';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
@@ -84,7 +83,6 @@ async function resolveApiKeyForModel(model: string): Promise<string | null> {
|
|||||||
|
|
||||||
type SpawnPiOptions = {
|
type SpawnPiOptions = {
|
||||||
sessionFile?: string;
|
sessionFile?: string;
|
||||||
username?: string;
|
|
||||||
role?: string;
|
role?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,43 +119,27 @@ export async function spawnPi(
|
|||||||
|
|
||||||
const homeDir = getHomeDirForRole(email, options?.role ?? null);
|
const homeDir = getHomeDirForRole(email, options?.role ?? null);
|
||||||
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':');
|
const toolsDirs = [getGlobalToolsDir(), getUserToolsDir(email)].join(':');
|
||||||
const shellUsername = options?.username ?? toShellUsername('', email);
|
|
||||||
|
|
||||||
const isServiceUser = (options?.username ?? toShellUsername('', email)) === (process.env.USER ?? '');
|
|
||||||
|
|
||||||
const env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
HOME: homeDir,
|
HOME: process.env.HOME ?? '',
|
||||||
OFFICER_USER_HOME: homeDir,
|
OFFICER_USER_HOME: homeDir,
|
||||||
OFFICER_USER_ROOT: join(DATA_PATH, email),
|
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,
|
PI_TOOLS_DIRS: toolsDirs,
|
||||||
OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'),
|
OFFICER_EMAIL_DB: join(DATA_PATH, email, 'emails.db'),
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
PATH: process.env.PATH ?? '',
|
PATH: process.env.PATH ?? '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// For service user, keep real HOME so Pi finds its config
|
const proc = Bun.spawn(piArgs, {
|
||||||
if (isServiceUser) {
|
|
||||||
env.HOME = process.env.HOME ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const proc = isServiceUser
|
|
||||||
? Bun.spawn(piArgs, {
|
|
||||||
cwd,
|
cwd,
|
||||||
stdin: 'pipe',
|
stdin: 'pipe',
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'pipe',
|
stderr: 'pipe',
|
||||||
env: { ...process.env, ...env },
|
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', {
|
logger.info('Spawned Pi', {
|
||||||
username: shellUsername,
|
|
||||||
model,
|
model,
|
||||||
skills: skillFlags.filter((f) => f !== '--skill').length,
|
skills: skillFlags.filter((f) => f !== '--skill').length,
|
||||||
extensions: extensionFlags.filter((f) => f !== '--extension').length,
|
extensions: extensionFlags.filter((f) => f !== '--extension').length,
|
||||||
|
|||||||
Reference in New Issue
Block a user