diff --git a/docs/sidecar-app-store.md b/docs/sidecar-app-store.md index 07e150ab..42877d61 100644 --- a/docs/sidecar-app-store.md +++ b/docs/sidecar-app-store.md @@ -60,12 +60,11 @@ for someone who does not. The prompt is the fork. **Officer is the installer, never the owner.** Concretely: -- A real compose file per service, written into a **user-owned directory**, from our template — - following the convention the owner already uses for 47 services in `~/dockers/`: - one directory per service, `docker-compose.yaml` inside, and **relative bind mounts** - (`./data`, `./database`, `./storage`) so configuration and data sit beside the compose file where - both we and the user can find them. Named volumes are used by 3 of those 47 and are the exception; - templates use bind mounts, always. +- A real compose file per service, written into **`/dockers//`**, from our template — using + the convention the owner already applies to 47 services: one directory per service, + `docker-compose.yaml` inside, and **relative bind mounts** (`./data`, `./database`, `./storage`) so + configuration and data sit beside the compose file where both we and a human can see them. Named + volumes are used by 3 of those 47 and are the exception; templates use bind mounts, always. - Started with `docker compose up -d` **as the owner**, not as officer's own identity. - Found again by **label** (`officer.sidecar=`), not by holding a handle. @@ -82,11 +81,27 @@ health checks already correct, so "install Gitea" does not become a tutorial. **`USER_UID` / `USER_GID` are set to the owner**, as the existing services already do. That answers the "do containers run as root" question: no, and this is not a new convention — it is the one in use. -**An existing directory is evidence, not an obstacle.** `~/dockers/` already holds `gitea`, `memos`, -`immich`, `jellyfin` and `invoice_shelf`. The installer must never write into a directory that exists; -finding one is the strongest possible signal that this is the "you already have one" case, and the store -should offer to ADOPT it — read its ports out of the compose file and write the connection — rather than -provision a second copy or overwrite a running service's data. +**The app store's containers are isolated from the user's own**, and that is the point of the layout: + +``` +~/officerdev/ + platform/ the app + data/ DATA_PATH + dockers/ services the app store provisioned <- exclusively ours + capabilities/ the file-based item store +``` + +`OFFICER_ROOT` is derived as the parent of `DATA_PATH` rather than configured separately — a second +variable that must agree with the first is a second thing to get wrong. + +Deliberately **not** `~/dockers`, which is where a seasoned user already keeps their estate. Two +consequences, both wanted: + +1. Containers the app store created are distinguishable from the user's own **structurally**, not by a + naming convention we would have to enforce and they could break. +2. **We never reason about someone else's compose files.** The 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 whose it is. `[open]` Podman, for anyone wanting genuinely rootless. diff --git a/src/servers/app-store/paths.ts b/src/servers/app-store/paths.ts new file mode 100644 index 00000000..0e70d2f5 --- /dev/null +++ b/src/servers/app-store/paths.ts @@ -0,0 +1,60 @@ +import { dirname, join } from 'node:path'; +import { DATA_PATH } 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 +// capabilities/ 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. +// +// (This development machine predates the convention and has it inverted: the whole project sits inside +// `~/dockers/officer.dev/`, so the root derives to `officer.dev` and the app store's directory would be +// `~/dockers/officer.dev/dockers`. Which is ugly, and correct — it is still isolated, still under one +// root, and still not mixed in with anything else. New installs get the clean shape.) +// +// ── 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 — the parent of `data/`. On a conventional install, `~/officerdev`. + * + * Derived, not configured: see above. + */ +export const OFFICER_ROOT = dirname(DATA_PATH); + +/** 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: `/dockers//`, 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');