app store: the catalogue, the install-state table, and what phase 0 must not foreclose
First slice, on a worktree branch so none of it touches the tree the live server runs from. `sidecar_installs` — server-level, no userId, because a sidecar is one process serving the machine. That is the line that keeps the model coherent for several users: installed is server-level and owner-only, configured is per user in service_connections. A member can use Gitea without being able to install it or point it somewhere else. `installed` and `enabled` are separate because they answer different questions, which is what gives the reversible middle ground: disable stops the process and keeps container, config, schema and data. `completedSteps` makes install resumable rather than merely retryable — the failure mode being designed against is a half-installed service that neither works nor uninstalls. The catalogue is data, not code: no functions, no compile-time coupling, because the same shape has to arrive as JSON from marketplace.officer.dev later. Its test pins it to the real estate — it offers exactly the processes the light profile excludes, names processes that exist, and claims capabilities that exist. That last check earned itself immediately: it caught `vault` (no capability at all — it is EXEMPT because Bitwarden clients carry a Vaultwarden bearer, not a platform JWT) and `notify` (which does have one, where I had written null). Docker templates follow the convention already in use across 47 services in ~/dockers: a directory per service, compose inside, relative bind mounts so data sits beside it, USER_UID/USER_GID as the owner. An existing directory is evidence of an existing install and must be adopted, never overwritten. Records what Phase 0 must not foreclose: a remote marketplace, sidecars moving to their own repositories, and third-party plugins — including the note that catalogue.test.ts pins Phase 0's invariant rather than the design's, since that relationship inverts once sidecars leave this repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,8 +60,12 @@ for someone who does not. The prompt is the fork.
|
||||
|
||||
**Officer is the installer, never the owner.** Concretely:
|
||||
|
||||
- A real `docker-compose.yml` per service, written into a **user-owned directory**
|
||||
(`~/officer-services/<service>/`), from our template.
|
||||
- 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.
|
||||
- Started with `docker compose up -d` **as the owner**, not as officer's own identity.
|
||||
- Found again by **label** (`officer.sidecar=<id>`), not by holding a handle.
|
||||
|
||||
@@ -75,8 +79,16 @@ Consequences, which are the point:
|
||||
The template is what makes this non-technical-user-friendly: sensible defaults, ports, volumes and
|
||||
health checks already correct, so "install Gitea" does not become a tutorial.
|
||||
|
||||
`[open]` Rootful Docker runs container processes as root unless `user:` is set. Do we set it? And do we
|
||||
support Podman for people who want genuinely rootless?
|
||||
**`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.
|
||||
|
||||
`[open]` Podman, for anyone wanting genuinely rootless.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,6 +186,29 @@ What a plugin author is promised, and bound by. To be written properly; the shap
|
||||
|
||||
---
|
||||
|
||||
## What Phase 0 must not foreclose
|
||||
|
||||
Three things are coming, and each one constrains a decision that looks free today.
|
||||
|
||||
**1. `marketplace.officer.dev`.** Phase 1 keeps the catalogue inside this repo; later the app lists what
|
||||
is on a remote marketplace instead. So catalogue entries must stay **serialisable data** — no functions,
|
||||
no imports, nothing that only means something at compile time. They are plain objects today and must
|
||||
remain so, because the same shape has to arrive as JSON over HTTP. Compose templates travel with them.
|
||||
|
||||
**2. Every sidecar becomes its own repository.** Today `catalogue.test.ts` asserts the catalogue equals
|
||||
"everything in ecosystem.config.cjs that light excludes". That is the right check _now_, and it inverts
|
||||
later: once sidecars live elsewhere, the catalogue entry becomes the source of truth for how to run one
|
||||
(command, args, env) and the ecosystem file is generated from what is installed, not the other way
|
||||
round. **Do not treat that test as a permanent law** — it pins Phase 0's invariant, not the design's.
|
||||
|
||||
**3. Third-party plugins.** Already the reason per-sidecar schema is in scope. It is also why the
|
||||
`service_connections` ID needs namespacing before the marketplace opens, not after.
|
||||
|
||||
The through-line: **nothing in Phase 0 may assume the catalogue is compiled in, or that a sidecar's code
|
||||
is in this repository.**
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
1. `user:` in compose, and Podman support for rootless.
|
||||
|
||||
Reference in New Issue
Block a user