Commit Graph
5 Commits
Author SHA1 Message Date
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
pastilhasandClaude Opus 5 3f071c0b24 the install root is derived, not configured
Seven variables out of .env. DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are gone
from the code entirely; PUBLIC_URL, PUBLIC_BUILD_ENV, JWT_SECRET and
VAULT_STORE_KEY are no longer written by the setup script.

data-path.ts now derives OFFICER_ROOT as dirname(process.cwd()), with data/,
capabilities/ and dockers/ as fixed names under it. The direction used to run the
other way — DATA_PATH from env, then OFFICER_ROOT = dirname(DATA_PATH) in
app-store/paths.ts — which meant three environment variables that had to agree
with each other and with the tree on disk.

Eight files re-read process.env.DATA_PATH independently, each with its own
`?? cwd()/data` fallback. They import the one value now, which is what made
removing it safe: otherwise each would have derived its own and drifted.

Three things this turned up.

The cwd pin in ecosystem.profile.cjs was broken. It set `cwd: __dirname` under a
comment asserting "__dirname is the repo root — this file sits beside
ecosystem.config.cjs", which stopped being true when these files moved into
ecosystem-files/. It walks up to the platform's package.json now, which holds
wherever the file lives. That was a live bug before this change and a load-bearing
one after it, since cwd now decides where the install is.

assertInstallLayout joins the other two boot assertions. A wrong cwd does not
error — it computes a plausible root somewhere else and writes managed homes and
agent runs into it, so the install looks empty and the data looks lost with
nothing naming the cause. It throws before serve(), first of the three, because a
wrong answer there makes the other two check the wrong files.

getOwnerHomeDir captures homedir() once at module load rather than per call.
Measured on bun 1.3.10: both os.homedir() and os.userInfo().homedir return $HOME
when set rather than reading passwd, and user-instance.ts assigns process.env.HOME
on its way to spawning an agent. A lazy read would have returned the owner's home
on the first call and a member's afterwards. data-path.ts imports only node
builtins, so it is evaluated before any of that runs.

JWT_SECRET and VAULT_STORE_KEY leaving .env means an install made by this script
does not boot — jwt.ts throws at module load without one. That is the agreed
sequencing: they move to the SQLite store (docs/secret-store.md), and writing them
here meanwhile would create a second origin for a secret the store then has to be
reconciled with. Said plainly in .env.example and in lib/env.sh rather than left
to be discovered.

Not typechecked: node_modules is empty here and installs are frozen. Every edited
file parses under `bun build --no-bundle`; the profile loads and pins the right
cwd; assertInstallLayout was exercised from both the repo and /tmp; the setup
section was run and writes five variables. Prettier was NOT run — 3.9.6 via bunx
is not the pinned resolution and reformatted unrelated unions and line wraps in
six files, so those were reverted and the edits re-applied by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 23:18:48 +00:00
pastilhasandClaude Opus 5 a64d5610e6 officer-setup section 5: Postgres, and only Postgres
The original offered five containers. Of those:

  Postgres    the only database Officer has — account, passkeys, settings,
              dashboards, email accounts, the queue. Required.
  Redis       not referenced anywhere in the platform. No import of the client,
              no environment variable, no mention; the only "redis" string in
              src/ is the word "rediscover" in a comment. Dropped. (It is still
              in package.json and comes out in the dependency pass.)
  SearXNG     zero references anywhere. Dropped. If it ever arrives it brings
              its own compose file and its own Redis with it.
  Mailhog     a development convenience, offered separately rather than here.
  Nginx PM    a deployment choice — Caddy, Traefik, nginx or the tailnet — and
              not something a setup script should pick.

Provisioned into $OFFICER_ROOT/dockers/postgres/, the same convention the app
store uses: one directory per service, the compose file in it, relative bind
mounts so the data sits beside the compose file.

Bound to 127.0.0.1, deliberately and with the reason in the compose file itself.
Docker publishes ports by writing iptables rules underneath ufw, so "5432:5432"
is reachable from the internet whatever the firewall reports — the same mechanism
the machine-setup firewall section exists to close. The platform runs on this
machine, so loopback is all it needs.

The password lives in a 0600 .env beside the compose file rather than inside it,
so the compose file can be read or copied without carrying a credential. A second
run reuses it rather than minting a new one, which would leave the container and
the URL disagreeing.

Readiness is waited for rather than assumed: Postgres initialises its data
directory on first start, and db:push against a database that is still starting
fails in a way that reads as a schema problem.

Choosing an existing database checks the URL but does not insist on it — the URL
may be right and the database not yet started, and refusing to continue over that
would be worse than saying so.

Also carries the whitespace fix for the comment removed in the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:12:10 +00:00
pastilhasandClaude Opus 5 911b79b6a2 drop a comment describing a machine that no longer exists
paths.ts carried a parenthetical explaining that the development machine had the
layout inverted — the project inside ~/dockers/officer.dev/, so the root derived
to officer.dev and the app store's directory came out as a dockers inside a
dockers.

That machine is gone. The project sits at ~/officerdev/platform, which is the
clean shape the comment said new installs would get. Anyone reading it now goes
looking for a directory that is not there and comes away unsure whether the
derivation can be trusted.

The rule above it is unchanged and is the whole contract: data/ is a direct child
of the root, and OFFICER_ROOT is dirname(DATA_PATH).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:07:19 +00:00
pastilhasandClaude Opus 5 654cb10711 app store: put provisioned containers under the officer root, not the user's
The install layout a machine should have, seasoned owner or not:

  ~/officerdev/
    platform/      the app
    data/          DATA_PATH
    dockers/       services the app store provisioned
    capabilities/  the file-based item store

One root, everything under it. OFFICER_ROOT derives from DATA_PATH rather than being a second variable
that has to agree with the first.

Deliberately not `~/dockers`, where a seasoned user already keeps their own estate — 47 services on this
machine. That separation buys two things. Containers the app store created are distinguishable from the
user's own structurally, rather than by a naming convention we would have to enforce and they could
break. And we never reason about someone else's compose files: the store does not scan, adopt or modify
anything outside its own directory.

That also simplifies "I already have one of these" — it is answered by the user giving a URL, never by
us finding a directory and guessing whose it is. An earlier draft had the installer adopting existing
directories, which meant reading, and potentially writing over, services Officer did not create.

This development machine predates the convention and derives an ugly-but-correct path, since the project
sits inside ~/dockers/officer.dev. Still isolated, still one root. New installs get the clean shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:44:30 +00:00