diff --git a/CLAUDE.md b/CLAUDE.md index ccce1bca..c009646d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,13 +16,23 @@ written: `users` holds six rows. The accurate statement is narrower and more use - **Other accounts get only what their ROLE is granted.** Roles are `Admin`, `Member`, `Developer`; grants live in `role_capabilities`, keyed on role, never on user. Absence denies — there is no row meaning "no", so an empty table is a server where members reach nothing but their own profile. -- **Some things can never be shared, structurally.** Terminal, chat, tasks, files, desktop and browser - are `kind: 'execution'` in the capability registry: they run as the owner's OS user in the owner's - home, so there is no level of "read" that makes them safe. They have no level at all and the grants - API refuses to store one. +- **Some things can never be shared, structurally.** Tasks, items, desktop and browser are + `kind: 'execution'`: they run as the owner's OS user in the owner's home, so there is no level of + "read" that makes them safe. They have no level at all and the grants API refuses to store one. +- **And some are shared only because the kernel enforces it.** Terminal, chat and files are + `kind: 'confined'`, added 2026-08-11 with per-user Linux accounts. They still touch the filesystem + and still run processes — but not the *owner's*, because the account has its own Linux user, its own + home, and the kernel refusing everything above it. -So "which user is this" now has a real answer for the **app** surface (gitea, music, photos, email, -calendar…), and is still always "the owner" for anything that executes code or touches the disk. + The distinction earns its keep in one place: **a confined grant means nothing without that Linux + user.** `authorize.ts` drops it for an account whose `osUser` is null, so "granted but unconfined" + resolves to no access rather than to the owner's home — which is what it would otherwise resolve to, + since `getOwnerHomeDir` ignores the email it is passed. That rule lives there once and covers the + HTTP routes, the websocket doors and the dock together. + +So "which user is this" has a real answer for the **app** surface (gitea, music, photos, email, +calendar…) and for the **confined** one (terminal, chat, files), and is still always "the owner" for +anything under `execution`. `src/servers/capabilities/registry.ts` is the authority and reads as the design document for this. **Mounting a router without a registry entry makes the server refuse to boot** — see "Capabilities" @@ -116,13 +126,23 @@ Two stores, and the split matters: 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//` holds the managed home, attachments and the per-account email SQLite -stores — those are the **email sidecar's**, and nothing in the platform opens them. 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). +**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`) +contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no +database rows, no scope tiers. `DATA_PATH//` holds the managed home, attachments and the +per-account email SQLite stores — those are the **email sidecar's**, and nothing in the platform opens +them. + +**None of those paths is configured.** Since 2026-08-13 `src/servers/data-path.ts` derives the install +root as `resolve(process.cwd(), '..')` and hangs `data/`, `capabilities/` and `dockers/` off it. That +replaced `DATA_PATH`, `OFFICER_ITEMS_DIR` and `HOME_DIR` in `.env` — three values that had to agree with +each other and with the tree on disk. `assertInstallLayout` refuses to boot when the working directory +is not the repo, because otherwise a wrong `cwd` relocates the whole install silently rather than +failing. + +Note `getHomeDir` (the managed home under `DATA_PATH`, now used only for NON-owner accounts and by +pipeline-executor) versus `getOwnerHomeDir` (the owner's real login home, where terminals, chats and +task runs execute — captured from `homedir()` once at module load, and it ignores the email it is +passed). ### Schema changes use `push`, not migrations @@ -164,11 +184,13 @@ valid"). It is `_middlewares/capability-gate.ts` → `capabilities/authorize.ts` ahead of everything, and it re-verifies the token itself so it covers routes that never mount `userMiddleware`. -- `capabilities/registry.ts` — the single enumeration of what the platform can do, in four kinds: - `core` (every account, not deniable), `app` (**the grantable surface**), `execution` and `admin` - (owner only, and `execution` is never grantable at any level). +- `capabilities/registry.ts` — the single enumeration of what the platform can do, in five kinds: + `core` (every account, not deniable), `app` (**the grantable surface**), `confined` (grantable, but + only to an account that has a Linux user), `execution` and `admin` (owner only, and `execution` is + never grantable at any level). 27 entries as of 2026-08-13. - `capabilities/authorize.ts` — resolves "may this account do this". Owner short-circuits first; every - other answer is role grants plus core, with `execution`/`admin` stripped even if a row grants them. + other answer is role grants plus core, with `execution`/`admin` stripped even if a row grants them, + and `confined` stripped for an account with no `osUser`. **Every catch returns deny.** Grants are cached by role and the cache's whole invalidation contract is `invalidateRoleGrants`, called by the one writer in `api/users/capabilities-routes.ts`. - `capabilities/totality.ts` — `assertCapabilityTotality` runs in `server.tsx` **before `serve()` and @@ -195,7 +217,7 @@ 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) +bun setup # runs scripts/setup/officer-setup.sh — IN PROGRESS, sections 1-6 of 10 ``` Sidecar control is PM2, not npm scripts: `pm2 restart officer-`, `pm2 logs officer-`.