Files
platform/CLAUDE.md
T
pastilhasandClaude Opus 4.8 86930f5b17 rename officer-claude to officer-anthropic-proxy
the pm2 entry named officer-claude never ran an agent. it starts
sidecar/claude/index.ts, which registers as capabilities: ['proxy'] and only
holds the anthropic proxy secret and forwards api traffic. the process that
actually spawns claude is sidecar/claude/user-instance.ts, which had no pm2
entry at all and was spawned on demand by the main server.

that misnomer is how both CLAUDE.md files ended up claiming that restarting
officer does not disturb a running agent session. it does: the agent was a
grandchild of officer and died with it. correct the name so the next reader
starts from a true model, and fix the claim in both files.

no behaviour change — officer-agent arrives in the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 04:28:19 +00:00

222 lines
11 KiB
Markdown

# CLAUDE.md
## Project Overview
Officer is a self-hosted personal platform for one person: the server owner. It bundles an AI agent,
a terminal, a file browser, a code editor, email, chat channels, a remote desktop and customisable
dashboards behind a single web app.
**Single-user is a hard invariant, not a stage.** There is exactly one account, created once by
`POST /auth/bootstrap` while the user table is empty. There are no roles, no invitations, no
sandboxing of one user from another, and no per-user isolation anywhere in the codebase. If a change
seems to need "which user is this", the answer is always the owner.
## Architecture
One Bun process (`src/server.tsx`) serves everything:
- the React SPA, via Bun's HTML import of `src/apps/officer-web/index.html` (HMR in dev)
- the REST API, a Hono app mounted at `/api` (`src/servers/hono.ts`)
- eight WebSocket providers — terminal, chat, task-runner, pipeline, dev-server proxy, cliamp,
cliamp-audio, desktop — plus a sidecar registration socket
- a browser relay on its own port (`BROWSER_RELAY_PORT`, default 18792)
Long-running and privileged work lives in **sidecars**: separate processes that dial back in over
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
(`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`,
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
`officer-slskd`.
**`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic
credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry
named `officer-claude` until the sidecar-isolation work — which is exactly how the false claim that
"restarting officer doesn't disturb the agent" survived so long. Every sidecar is a PM2 peer of
`officer`, so **no sidecar is a child of the server and restarting the server does not kill one.**
Agents run **unsandboxed as the server owner**, with `--dangerously-skip-permissions`. This is
deliberate — it is the owner's own machine. Do not add a jail without being asked.
## Repository Layout
```
src/
├── server.tsx # the single entrypoint: routes, WS upgrades, static files
├── apps/
│ ├── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard
│ └── landing/ # marketing landing page
├── servers/
│ ├── hono.ts # router composition; everything under /api
│ ├── _middlewares/ # auth, body parsing, origin validation, rate limiting
│ ├── api/<feature>/ # one folder per feature, each exporting a router
│ ├── channels/ # Telegram / WhatsApp / Discord bridges
│ ├── queue/ # background job engine
│ └── sidecar/ # sidecar implementations + the wire protocol
├── databases/officer_db/ # the only database (Postgres + Drizzle)
├── extensions/browser-relay/
└── workspaces/ # shared packages, each a bun workspace
```
`src/workspaces/officerdev` is the biggest of these: the windowed "apps" (FileBrowser, Chat,
Terminal, CodeEditor, Desktop, Projects, Dashboards…) that the shell hosts, behind an `AppRegistry`.
`src/apps/officer-web` is only the shell — screens, routing and settings.
**Path aliases** (`tsconfig.json`): `@/*``src/apps/officer-web`, `@/components/*`
`src/workspaces/components`, `@@/*``src/servers`, `@/public/*``public`. Workspace packages are
imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helpers`, …).
## Tech Stack
- **Runtime**: Bun (Node 22 is enforced by a `preinstall` check)
- **Language**: TypeScript, strict. `bunx tsgo` is clean — keep it that way.
- **Frontend**: React 19, React Router 7, React Query, Tailwind 4, shadcn/ui + custom components
- **Backend**: Hono
- **Database**: one Postgres database via Drizzle
- **Agents**: Claude Code and opencode, driven through sidecars; tools exposed over MCP
## Data
Two stores, and the split matters:
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
projects, email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written queries in
`src/queries/`, types inferred from the schema in `src/types.ts`.
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` contains one directory
per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no database rows, no
scope tiers. `DATA_PATH/<email>/` holds the managed home, attachments and per-account email SQLite
stores. Path helpers live in `src/servers/data-path.ts`; note `getHomeDir` (the managed home under
`DATA_PATH`) versus `getOwnerHomeDir` (the owner's real login home when `HOME_DIR` is set, which is
where terminals, chats and task runs actually execute).
### Schema changes use `push`, not migrations
This database is kept in sync with `bun db:push`, which diffs the schema code against the live
database and alters it directly. **`drizzle-kit migrate` has never been run here** — there is no
`__drizzle_migrations` table. Change the schema, run `bun db:push`, done.
`bun db:gen` writes files to `officer_db/migrations/`, but nothing applies them; the old numbered
history was deleted because it had drifted from the real schema. Treat the schema code, not those
files, as the source of truth.
## Security Model
The perimeter is one credential, so the guards matter:
- `IS_DEV_BUILD` (`src/servers/build-env.ts`) is true **only** when `PUBLIC_BUILD_ENV` is explicitly
`dev`/`development`. Everything else, including unset, is hardened. Origin validation, rate
limiting and password rules all key off it — they fail closed.
- Allowed origins come from `PUBLIC_URL`. Officer always sits behind an HTTPS reverse proxy, so the
forwarded `Host` must equal `PUBLIC_URL`'s authority exactly.
- JWTs are 30-day, blacklisted on signout, and invalidated by a password change (`passwordChangedAt`).
- A panic lockdown (`src/servers/api/auth/panic.ts`) is in-memory only and refuses every
authenticated request until the server restarts.
## Commands
```bash
bun dev # the whole app — SPA + API + WebSockets, watched
bun start # production
bunx tsgo # typecheck (not tsc)
bun test # tests
bun format # prettier over every dirty file — see the note below before running it
bun db:push # apply the schema to Postgres
bun setup # guided install (writes .env, incl. PUBLIC_BUILD_ENV=production)
```
Sidecar control: `bun run start:sidecar` / `stop:sidecar` / `restart:sidecar` / `logs:sidecar`.
**Format your own files, not the whole dirty tree.** `bun format` globs `git diff --name-only HEAD`,
so it rewrites every uncommitted file — including work in progress that isn't yours, which then shows
up as unexplained whitespace churn in someone else's diff. Run `bunx prettier --write <paths>` on the
files you actually touched. `bun format` is only safe when the tree is otherwise clean.
Note: `build:editor*` in `package.json` points at `scripts/build/editor.ts`, which does not exist.
## Code Style
- **Paradigm**: functional — pure functions, immutability, composition
- **TypeScript**: strict, no `any`. Type-only imports are required (`verbatimModuleSyntax`).
- **Comments**: minimal, and about *why*. Don't narrate what the code already says.
- **Async**: always async/await
- **Exports**: named only, no defaults
- **Files**: `PascalCase.tsx` for components, `kebab-case.ts` for everything else
- **Prettier**: single quotes in JS, double in JSX, semicolons, trailing commas, 120 cols
### Imports
Order: types → external → workspace → relative.
```ts
import type { User } from 'types';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
import { formatDate } from 'helpers/formatters';
import { useWebsites } from '../useWebsites';
```
In app code import types from `'types'`, which re-exports the database types. Only server and
database code imports from `officerdb/types` directly.
### Functions
Arrow functions for one-liners, regular functions for anything with real logic. No multiline
parameter lists — extract a params type instead.
```ts
type ProcessDataParams = { body: Record<string, unknown> | undefined; query: Record<string, string> };
export function processData({ body, query }: ProcessDataParams): Result { ... }
```
### Components and hooks
```tsx
type ExperimentCardProps = { experiment: Experiment; onSelect: (id: number) => void };
export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => { ... };
```
Complex hooks return objects; simple state hooks return `as const` tuples. Event parameters are
always named `ev`, never `e`.
### API calls
`useClient()` returns typed verbs — `client.get<Foo>('/foo')` really does return `Foo`, so pass the
type parameter and let inference flow from it.
## Working With Me
- **Ask first** — confirm the approach before a significant change
- **Explore thoroughly** — read the related files before editing
- **Keep it simple** — no over-engineering, no premature abstraction
- **Be explicit** — no magic, no implicit behaviour
- **Stay focused** — note unrelated problems, don't fix them uninvited
- **Report honestly** — say what you verified and what you didn't
Commit messages: simple lowercase, no prefixes.
## Frontend route conventions (apply to EVERY new dashboard route)
Worked examples: `/soulseek`, `/music`, `/chat`.
- **Workspace/Panel framework, always — never a standalone single screen.** The screen renders
`<WorkspaceView workspace={ws} locked />` where
`ws = useDashboardState<LayoutNode>('screens/<name>', defaultLayout)`, guarded by a `normalizeLayout`
that pins `appType`s to an allow-list. Panels are windowed apps under
`src/workspaces/officerdev/src/apps/<Feature>/`, each exporting `appRegistryMetas`
(`{ key, name, icon, component, availableOnPanel: false }`) and registered in `AppRegistry.tsx`.
Panels coordinate via `usePanelChannel`.
- **Page title by route.** Add a rule to `RULES` in `src/apps/officer-web/state/usePageTitle.ts`
(`{ match: (p) => p.startsWith('/<name>'), title: '<Name>' }`, most-specific first);
`usePageTitleSync` does the rest.
## Further Reading
- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale
- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree**
- `src/apps/CLAUDE.md` — shared frontend patterns
- `src/databases/CLAUDE.md` — database patterns
The other markdown files in the repo root (`OFFICERDEV_*.md`, `MARKETING_WEBSITE.md`, `PHONE_APP.md`,
`SECURITY_AUDIT.md`, `SETUP_*.md`, …) are older design notes. Treat the code as the source of truth.