Files
platform/src/servers/app-store/paths.ts
T
pastilhas 2e8ec845c8 step 1/4: the permission engine is called permissions, not capabilities
The word meant four different things in this repo, not the three the offscale
doc records:

  1. the permission registry              → RENAMED here
  2. $OFFICER_ROOT/capabilities/ items    → kept; this is what capabilities are
  3. sidecar routing keys                 → step 3, becoming `handles`
  4. Lightning wallet features            → kept; a domain term, and on the wire
                                            to the mobile apps

The fourth was not in the doc and a global find-and-replace would have broken
the mobile wallet, which reads `{ kind, capabilities: Capability[] }` from the
wallet sidecar. So this renamed against an explicit file allowlist rather than
by sweeping the tree, and `CapabilityPage.tsx` — the UI for the item store, and
correctly named already — was left alone.

Moved: servers/capabilities/ → servers/permissions/, capability-gate.ts →
permission-gate.ts, users/capabilities-routes.ts → permissions-routes.ts,
hooks/useCapabilities.ts → usePermissions.ts. Identifiers follow.

Three breaks the typechecker could not see, all found by exercising it live.

The route paths moved with the prose sweep, so the server served
/user/permissions while the frontend still called /user/capabilities. A 404 on
every page load, and tsgo clean throughout.

The response FIELD moved too. `client.get<SelfPermissions>()` is an unchecked
cast, so `data.capabilities` became `undefined` at runtime with no compile
error — `can()` would have answered "no" to everything and the dock would have
emptied itself.

And the grants list was passed straight out of the database, so it arrived as
`{ role, capability, level }` while the screen read `grant.permission`. Every
role would have rendered as holding nothing. It is now mapped in the route:
the wire says `permission`, the column still says `capability`, and step 2
therefore changes nothing any client can see.

The stale react-query keys were the quiet one: two files still invalidated
['self-capabilities'] after the hook moved to ['self-permissions'], so
installing a plugin would have silently stopped refreshing the dock.

The database is untouched — `role_capabilities` and its `capability` column are
step 2, and the two call sites that cross that boundary say so in a comment.
Round-tripped the 9 live grants through the admin endpoint to prove the PUT
contract survived: 9 before, 9 after, Member's three intact.

Also reverted prettier churn on five landing-page files that a broad --write
picked up. Second time today; the lesson is not sticking.

tsgo clean. 797 tests, 787 pass, same 7 pre-existing failures — two of which
now read "path → permission" rather than "path → capability".
2026-08-15 16:03:22 +00:00

58 lines
3.0 KiB
TypeScript

import { join } from 'node:path';
import { OFFICER_ROOT } from '../data-path';
// Where the app store puts the containers it provisions.
//
// ── The install layout ──
//
// A machine that runs Officer is meant to look like this, whether the owner is seasoned or not:
//
// ~/officerdev/
// platform/ the app
// data/ DATA_PATH — managed homes, attachments, job logs
// dockers/ services the app store provisioned <- this file
// permissions/ the file-based item store
//
// One root, everything under it, nothing scattered. `OFFICER_ROOT` is derived from `DATA_PATH` rather
// than configured separately, because a second environment variable that must agree with the first is a
// second thing to get wrong — and on a correct install `data/` is always a direct child of the root.
//
// ── Why this is not `~/dockers` ──
//
// That is where a seasoned user already keeps their own estate — 47 services on this machine alone. Two
// reasons to stay out of it:
//
// 1. **Isolation.** Containers the app store created and containers the user manages must be
// distinguishable without inspecting them. A separate root makes that structural rather than a
// naming convention we would have to enforce and they could break.
// 2. **We never reason about someone else's compose files.** The app store does not scan, adopt or
// modify anything outside its own directory. "I already have one of these" is answered by the user
// giving a URL (`mode: 'existing'`), never by us finding a directory and guessing it is theirs.
//
// So this directory is exclusively ours to write, and everything in it was put there by an install.
/**
* The install root — on a conventional install, `~/officerdev`.
*
* Derived, not configured: see above. It moved to `../data-path` on 2026-08-12, when the direction
* inverted — it used to be `dirname(DATA_PATH)`, back when DATA_PATH was the environment variable that
* anchored everything. Re-exported here because this file is where callers expect to find it.
*/
export { OFFICER_ROOT };
/** Where provisioned services live, one directory each. Created on first install, not at boot. */
export const DOCKERS_DIR = join(OFFICER_ROOT, 'dockers');
/**
* This service's own directory: `<root>/dockers/<id>/`, holding `docker-compose.yaml` and — because the
* templates use relative bind mounts rather than named volumes — its data and configuration too.
*
* That is the convention the owner already uses everywhere: `./data`, `./database`, `./storage` beside
* the compose file, so both the app and a human can see exactly what a service is keeping and where.
* A named volume hides it behind `docker volume inspect`, which is the opposite of the point.
*/
export const serviceDir = (sidecarId: string): string => join(DOCKERS_DIR, sidecarId);
/** The compose file the installer renders and `docker compose` is run against. */
export const composeFile = (sidecarId: string): string => join(serviceDir(sidecarId), 'docker-compose.yaml');