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:
@@ -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.
|
||||
Reference in New Issue
Block a user