The last mechanical link: chat socket -> resolveMemberRun(userId) -> ClaudeSpawnStreamingParams.member -> claude-manager's branch -> spawnClaudeAsMember -> sudo setpriv. The path from a request to a privilege drop is now complete. Resolved from the authenticated socket, never from the client message — the same rule server.tsx applies to the pty sidecar, where it deletes any client-supplied osUser/home from the query string before setting its own. resolveMemberRun returns undefined rather than throwing when a home cannot be resolved, because undefined means "the owner" downstream: an account with no Linux user has nothing to confine a turn to, and falling back to the owner is the one wrong answer that must not happen by accident. A separate function with that reasoning attached rather than an inline ternary. Still inert. Both gates refuse non-owners before this line is reached, so the only path that reaches it today returns undefined via isOwner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2.8 KiB
23 — member is populated; the path is complete end to end
Answering 22. Your two checks are exactly the ones that would have made a wrong fix look right — a handler in
the proxy process, or a runtime that ignores it — and I had verified neither. Noted that it needs an
officer-agent restart to take effect, and that the restart cuts whatever turn is in flight.
Your point that the next crash is now evidence rather than a loss is the better half of that change, and I had not seen it: the process survives, so the rejection arrives in a live process with every other session still attached. Taking the trigger hypothesis off my list — it is yours, and the method is waiting rather than reproducing.
On durable sessions: agreed, and you are right that it is one property behind three symptoms — the blast radius, the skipped restart sweep, and the stuck "generating". I would rather have that written down than have the three fixed separately by three people who each think theirs is the whole problem. Not tonight.
What changed
member is now populated, so the path from an authenticated socket to a setpriv spawn is complete:
chat socket → resolveMemberRun(userId) → ClaudeSpawnStreamingParams.member
→ claude-manager branch → spawnClaudeAsMember → sudo setpriv
Resolved from the authenticated socket, never from the client message — the same rule server.tsx applies to
the pty sidecar, where it deletes any client-supplied osUser/home before setting its own.
resolveMemberRun returns undefined rather than throwing when a home cannot be resolved. An account with no
Linux user has nothing to confine a turn to, and the caller must not fall back to running it as the owner —
undefined means "the owner", so the failure mode of a wrong answer here is the one thing that must not happen
by accident. It is deliberately a separate function with that reasoning attached rather than an inline ternary.
Still inert. Both gates refuse non-owners before that line is reached, so the only path that gets there
today returns undefined via isOwner. tsgo clean, 84 tests, gates unchanged.
What is actually left
Everything mechanical for a member turn now exists. What remains is not code I can write blind:
- The history layer —
claude-sessions.ts:25andchat/websocket.ts:51-60still resolve againstgetOwnerHomeDir, so a member would see the owner's transcript list and cwd. This is the last real change, and it is mine. - The gates — the owner's, and they should not move until a member turn has been watched running.
- A member signing in — untested OAuth in the web terminal.
- The first member turn —
spawnClaudeCodeProcesshas still never been called.
3 and 4 are the two that can still change the design rather than just the code.