From 1ccb21e67f77bd70ce5b4c52fa178adc22c7539b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 13 Aug 2026 00:43:16 +0000 Subject: [PATCH] CLAUDE.md: five capability kinds, not four MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file described a stricter model than the code enforces. It said terminal, chat, files, tasks, desktop and browser are all `kind: 'execution'` and therefore never shareable — but terminal, chat and files moved to `confined` on 2026-08-11 with per-user Linux accounts, and are grantable. The distinction matters and is now written down: a confined grant means nothing without a Linux user. authorize.ts:97 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. Verified in the code, not inferred from the comment. Also brings the Data section in line with today: DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are no longer environment variables, the install root is derived from cwd, and assertInstallLayout is why a wrong cwd fails instead of relocating the install. And `bun setup` runs officer-setup.sh, which is sections 1-6 of 10 — worth saying, since the entry read as though it were finished. Registry needs real work for where this is going. This is only the docs catching up to what is there now. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 58 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 18 deletions(-) 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-`.