todo: one file per item, grouped by theme, with a generated index

TODO.md had reached 1050 lines and 72 open items across 28 sections, read mostly
on a phone. It is now a directory: todo/<theme>/<item>.md, one card per file, with
frontmatter carrying id, status, created date and order, and the prose body copied
across verbatim. TODO.md becomes a generated index (bun run todo:index).

Dates: where an item's prose stated one it is used; otherwise its old section's;
otherwise 2026-08-20, floored at the day before the split rather than invented.
date_source records which.

Shaped for a two-way Kanban board, filed as todo/tooling — a card is a file, id is
the identity, frontmatter is the machine's half and the body is the owner's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 05:42:39 +00:00
co-authored by Claude Fable 5
parent 8fa560df1b
commit 6071ded175
124 changed files with 2240 additions and 1043 deletions
+5
View File
@@ -0,0 +1,5 @@
---
theme: multi-user
heading: "Multi-user"
kind: theme
---
@@ -0,0 +1,17 @@
---
id: multi-user/a-plugin-s-settings-routes-are-reachable-by
title: "A plugin's settings routes are reachable by anyone the plugin is granted to"
theme: multi-user
status: open
created: 2026-08-21
date_source: stated
order: 1
---
**A plugin's settings routes are reachable by anyone the plugin is granted to.** Jellyfin's
`/_config` stores a server's admin token and drives provisioning, and a Member granted the plugin
can reach it. Agreed 2026-08-21 that the plugin should enforce this from facts the platform passes
(`isOwner` plus the effective level, never the role name), with a proposal to declare owner-only
path prefixes in the manifest so forgetting the check is not a silent hole. **Blocked on credential
resolution:** connections are per-user, so locking `/_config` today leaves a Member with a plugin
that only shows a connect form they may not submit. See `docs/plugin-route-authorization.md`.
@@ -0,0 +1,17 @@
---
id: multi-user/a-second-super-admin-was-storable-and-made
title: "A second Super Admin was storable, and made the owner nondeterministic"
theme: multi-user
status: done
created: 2026-08-11
date_source: stated
order: 9
---
**A second Super Admin was storable, and made the owner nondeterministic.** Fixed 2026-08-11.
`ck_users_owner_is_super_admin` pins user 1's role but a row-level CHECK cannot see other rows, and
`updateUserRoleHandler` happily promoted anyone — while `getOwnerUser()` was
`WHERE role='Super Admin' LIMIT 1` with no ORDER BY. Two holders would have made "who owns this
server" a question the query plan answered, and that answer feeds the agent sidecar's identity,
vault access and origin scoping. Both write paths now refuse the role, the list endpoint offers
`assignableRoles` without it, and `getOwnerUser()` orders by id.
@@ -0,0 +1,15 @@
---
id: multi-user/agent-sessions-are-not-durable-and-it-is-one
title: "Agent sessions are not durable, and it is one property behind three symptoms"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 5
---
**Agent sessions are not durable, and it is one property behind three symptoms.** A sidecar restart
loses session identity, which is why `endTurnIfAgentIsGone` must skip sessions with no recorded
`userId`, why a stuck "generating" spinner survives until a reconnect, and why any crash in that process
is destructive rather than merely inconvenient. Fixing the three separately would miss that they are one
missing property. `docs/open-threads-after-per-user-claude.md` §2.
@@ -0,0 +1,19 @@
---
id: multi-user/assertpermissiontotality-checks-the-wrong
title: "`assertPermissionTotality` checks the wrong list, and `registry.test.ts` has been red since 2026-08-13"
theme: multi-user
status: open
created: 2026-08-13
date_source: stated
order: 12
---
**`assertPermissionTotality` checks the wrong list, and `registry.test.ts` has been red since
2026-08-13.** It is fed `Object.keys(handlers)` from `server.tsx`, but Bun serves the _route table_.
Those diverged when the cliamp/desktop/vault plugins were switched off: `/api/cliamp/ws` and
`/api/cliamp/audio/ws` are still live routes with their handlers and registry claims commented out.
Not exploitable — `isWsProviderAllowed` finds no permission and 403s a member; the owner upgrades onto
a dead socket. But the boot check that exists to stop exactly this cannot see it. Two fixes: point
totality at the route table, and either delete the dead routes or restore their claims. The 8 failing
tests in `registry.test.ts` are the same drift — `REAL_WS` still lists all nine providers as served,
which is why nobody noticed. Found 2026-08-14.
@@ -0,0 +1,16 @@
---
id: multi-user/cross-user-writes-in-the-notify-sidecar
title: "Cross-user writes in the notify sidecar"
theme: multi-user
status: done
created: 2026-08-07
date_source: stated
order: 19
---
**Cross-user writes in the notify sidecar** (fixed 2026-08-07, this session).
`DELETE /_officer/devices/:token` deleted by token with no user predicate, so any account with the
`notify` permission could deregister another's device; and `POST /_officer/notify` let a request
body's `userId` override the proxy-injected `X-Officer-User`, so the same account could push to
another's devices. `deletePushDevice` now takes an optional `userId` (the route passes it, the
APNs/FCM dead-token paths deliberately do not) and the header now wins over the body.
@@ -0,0 +1,16 @@
---
id: multi-user/dashboards-id-is-a-global-primary-key-and-ids
title: "`dashboards.id` is a global primary key, and ids are `slugify(name)`"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 10
---
**`dashboards.id` is a global primary key, and ids are `slugify(name)`.** Two accounts cannot
both have a dashboard named "Home". Reachable today: six accounts exist. The recommendation on
the table is a composite PK `(user_id, id)` — it matches the `uq_dashboards_user_id` index
already there and keeps every stored `ws-layout-<id>` address valid, which uuid ids would not.
Note the drizzle composite-PK re-diff quirk in `databases/CLAUDE.md`. Full analysis in
`docs/workspace-panel-todo.md` §3.
@@ -0,0 +1,18 @@
---
id: multi-user/deprovisionosaccount-is-written-but-has-never
title: "`deprovisionOsAccount` is written but has never run against a real account"
theme: multi-user
status: open
created: 2026-08-12
date_source: stated
order: 2
---
**`deprovisionOsAccount` is written but has never run against a real account.** Landed 2026-08-12 in
`os-user-deprovision.ts` and wired into `deleteUserHandler`, which now refuses to delete the row when
the Linux teardown fails — so a failure is retryable instead of forgotten. Only the pure guards
(`guardDeletable`, `guardMemberTree`, `parseSubUidEntry`) have tests; the reap loop, the `chown -R`
sever and `userdel` have been exercised by nobody. `docs/deprovision-os-account.md` → "What is still
unproven" has the five-step validation, and it has to happen on the production host with a throwaway
account that has **a shell left open** and **a container writing as a non-root user** — those are the
two cases the quiet path passes vacuously.
@@ -0,0 +1,14 @@
---
id: multi-user/getownerhomediremail-ignores-its-argument
title: "`getOwnerHomeDir(email)` ignores its argument"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 14
---
**`getOwnerHomeDir(email)` ignores its argument** whenever `HOME_DIR` is set, which it is here —
every caller resolves to the owner's real login home. Safe only because all seven callers sit
behind `execution` permissions. If per-user home confinement is ever attempted, this is the
function to start from.
@@ -0,0 +1,14 @@
---
id: multi-user/markinterruptedjobs-and-getoldestpendingjob
title: "`markInterruptedJobs()` and `getOldestPendingJob()` are platform-wide"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 18
---
**`markInterruptedJobs()` and `getOldestPendingJob()` are platform-wide.** The pipeline queue is a
single global lane; ownership is enforced one layer up, in `pipeline-jobs-routes.ts`, by an
explicit `job.userId !== user.id → 404` on every by-id route. Correct today, but the queue itself
has no notion of whose work it is running.
@@ -0,0 +1,12 @@
---
id: multi-user/no-empty-state-for-a-denied-screen
title: "No empty state for a denied screen"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 13
---
**No empty state for a denied screen.** A member who reaches a route their role lacks gets a
broken panel or an endless spinner rather than a clean refusal.
@@ -0,0 +1,17 @@
---
id: multi-user/no-way-to-create-a-second-account
title: "No way to create a second account"
theme: multi-user
status: done
created: 2026-08-11
date_source: stated
order: 7
---
**No way to create a second account.** Fixed 2026-08-11 on `sidecar-app-store`: `POST /api/users`
(`api/users/create-user.ts`, owner-gated) plus an Add-account form in
Settings → User management. Created accounts are `status: 'Active'` — the column defaults to
`'Unverified'` and `signin.ts` refuses anything else with a bare UNAUTHORIZED, which is the trap
the hand-INSERT route fell into. The owner sets the password and reads it out; `passwordChangedAt`
stays null. Directories come from the shared `provisionUserDirs`/`USER_DIRS` in `data-path.ts`,
which `scripts/provision-user-dirs.ts` now imports rather than restating.
@@ -0,0 +1,14 @@
---
id: multi-user/permissions-authorize-ts-has-no-automated
title: "`permissions/authorize.ts` has no automated tests"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 11
---
**`permissions/authorize.ts` has no automated tests.** `registry.test.ts` covers the pure
registry functions and the totality check; the resolver that does the owner bypass, the grant
lookup, the role cache and the fail-closed catches is exercised only by hand. It is the file
standing between a Member and a shell.
@@ -0,0 +1,16 @@
---
id: multi-user/processtransport-is-not-ready-for-writing
title: "`ProcessTransport is not ready for writing` — survivable since `8c4f150`, still unexplained"
theme: multi-user
status: open
created: 2026-08-11
date_source: stated
order: 6
---
**`ProcessTransport is not ready for writing` — survivable since `8c4f150`, still unexplained.** A
floating rejection inside the SDK's own input pump, with no frames from our code, so no `await` of ours
can catch it. It crashed `officer-agent` four times on 2026-08-11, once truncating a turn mid-sentence;
the `unhandledRejection` backstop has caught it once since. Best hypothesis is the `claude` CLI exiting
while `streamInput` is still pumping. It needs looking at after the next occurrence, not catching in the
act — markers to grep in `docs/open-threads-after-per-user-claude.md` §3.
@@ -0,0 +1,16 @@
---
id: multi-user/pty-vault-and-opencode-receive-no-identity-at
title: "`pty`, `vault` and `opencode` receive no identity at all"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 15
---
**`pty`, `vault` and `opencode` receive no identity at all.** Every other sidecar validates
`X-Officer-User`. The pty sidecar keys purely on a `sessionId` from the query string and its
`/_officer/sessions` endpoints list and kill _every_ session on the box; vault and opencode take
no user argument. All three are covered today only because `terminal`, `vault` and the agent are
owner-only permissions — that is a correct outcome resting on the wrong layer, and it is the
thing to fix first if any of them is ever granted.
@@ -0,0 +1,13 @@
---
id: multi-user/radicale-is-configured-type-owneronly
title: "Radicale is configured `type = owner_only`"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 16
---
**Radicale is configured `type = owner_only`** (`sidecar/caldav/radicale.ts:54`) while the caldav
sidecar itself is fully per-user and confines every JSON read to `/dav/<userId>/`. The platform
side is ready for members; the CalDAV server underneath is not.
@@ -0,0 +1,14 @@
---
id: multi-user/remove-dead-username-plumbing
title: "Remove dead `username` plumbing"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 20
---
**Remove dead `username` plumbing.** `send-claude-code.ts` declares `username` in two types
without using it. (`toShellUsername` is NOT dead — `server.tsx:190` and
`pipeline-job-manager.ts:267` both call it. The `provision.ts` caller this item used to name no
longer exists.)
@@ -0,0 +1,14 @@
---
id: multi-user/still-no-invite-flow-and-no-password-reset
title: "Still no invite flow, and no password reset for a member"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 8
---
**Still no invite flow, and no password reset for a member.** The owner types the password and
tells the person, which means the owner knows it and the member cannot change it back if they
forget theirs — recovery today is delete-and-recreate. An invite (token, expiry, member sets
their own) needs a mail path. This is the next piece, not a nice-to-have.
@@ -0,0 +1,13 @@
---
id: multi-user/the-music-library-is-one-global-index
title: "The music library is one global index"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 17
---
**The music library is one global index.** `sidecar/music/indexer.ts` reads `HOME_DIR` and serves
every account from it. Favourites, playlists and now-playing _are_ per-user. Deliberate for now
(one household, one library) but worth stating rather than discovering.
@@ -0,0 +1,14 @@
---
id: multi-user/the-terminal-replays-terminal-queries-which
title: "The terminal replays terminal QUERIES, which get typed into the shell"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 3
---
**The terminal replays terminal QUERIES, which get typed into the shell.** `sidecar/pty/sessions.mjs`
replays the whole scrollback on attach; query sequences in the buffer get re-asked, xterm.js answers,
and the answers arrive as keystrokes. Visible to a member daily. Fix is to strip query sequences in
`appendBuffer`, so a replay reproduces output and never re-issues requests.
@@ -0,0 +1,15 @@
---
id: multi-user/the-web-terminal-renders-a-long-url-as
title: "The web terminal renders a long URL as unreadable fragments"
theme: multi-user
status: open
created: 2026-08-20
date_source: assumed
order: 4
---
**The web terminal renders a long URL as unreadable fragments.** Claude Code's first-run login prints
a ~400-character OAuth URL; the web terminal shows scattered characters with large gaps, nothing
selectable. Half worked around by `2a8f004` (OSC 52, so "press c to copy" reaches the clipboard) — the
rendering itself is undiagnosed. This is every new member's first five minutes.
`docs/open-threads-after-per-user-claude.md` §1 has what is known and where to start.