retire the single-user claim from the docs it outlived
CLAUDE.md asserted "single-user is a hard invariant, not a stage" while users held six rows and role_capabilities held grants. Every doc that repeated it is corrected here, in prose and in the code comments that carried the same claim. The accurate statement is narrower: one owner who bypasses every check, other accounts holding only what their role is granted, and a set of capabilities — terminal, chat, files, tasks, items, desktop, browser — that are structurally ungrantable because they execute as the owner's OS user. TODO.md gains a Multi-user section for what the read turned up: no way to create a second account, dashboards.id colliding across users, authorize.ts untested, pty/vault/opencode taking no identity, Radicale still owner_only. claude-sidecar-isolation.md's open question is answered rather than left open — the per-email spawn model is dead weight, because chat is an execution capability and no second account can ever reach it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,9 @@ speaks DAV.
|
||||
|
||||
### 1.2 The credential model
|
||||
|
||||
- **Username** = the account's email address. (Officer is single-user; there is exactly one.)
|
||||
- **Username** = the account's email address — the signed-in account's own, not a constant. (This said
|
||||
"Officer is single-user; there is exactly one" until 2026-08-07. `calendar` is now a grantable
|
||||
capability, so a member can hold their own app passwords and their own collections.)
|
||||
- **Password** = a **DAV app password**, not the login password.
|
||||
|
||||
DAV app passwords are argon2-hashed at rest, scoped to `/dav` and nothing else, and **the plaintext is
|
||||
@@ -100,9 +102,12 @@ A collection https://<host>/dav/<userId>/<collection>/
|
||||
`<userId>` **is the platform user id, by construction** — the same integer `/auth/me` returns. They
|
||||
cannot diverge: `sync-router.ts` sets `X-Officer-User: String(userId)` straight from the app-password
|
||||
row, the sidecar forwards it to Radicale as `X-Remote-User`, and Radicale's storage tree is literally
|
||||
`/<that value>/`. There is no mapping table to get out of step. On a single-user instance — which every
|
||||
Officer instance is — that is `1`. Deriving it from `/auth/me` is safe; so is deriving it from the
|
||||
collection paths, which is why both work today.
|
||||
`/<that value>/`. There is no mapping table to get out of step.
|
||||
|
||||
**Do not hardcode `1`.** This passage used to say that on a single-user instance — "which every Officer
|
||||
instance is" — the value is always `1`. That stopped being true on 2026-08-07: members can hold the
|
||||
`calendar` capability, and a member's id is not 1. Derive it from `/auth/me` or from the collection
|
||||
paths; both work, and both stay correct when the caller is not the owner.
|
||||
|
||||
**A collection cannot live outside `/dav/<userId>/`.** Two independent guards: the sidecar rejects any
|
||||
`collection` outside that prefix, and Radicale runs `rights type = owner_only`.
|
||||
|
||||
Reference in New Issue
Block a user