Files
platform/COMMS/sidecar-app-store/03-hook-wired.md
T
pastilhasandClaude Opus 5 fbabc22ed7 wire the member branch into the SDK spawn, still unreachable
ClaudeSpawnStreamingParams takes an optional member {osUser, home}; createSession
branches on it, using their binary and spawnClaudeAsMember together, or the
owner's CLAUDE_BIN as before.

THE BINARY AND THE PRIVILEGE DROP ARE ONE BRANCH ON PURPOSE. settingSources
makes ~/.claude authoritative for settings and ~ is whatever HOME the process
gets, so pointing the SDK at a member's binary while spawning as the service
user would read the OWNER'S settings and credential while running the member's
code — and it would look like it worked.

cwd defaults to member.home before HOST_HOME for the same reason: HOST_HOME is
this process's home, so a member would start in a directory they cannot read and
the failure would present as a broken agent rather than a wrong cwd.

Nothing populates `member`. Both gates refuse non-owners before any of this is
reached, so the delta is that spawnClaudeAsMember now has two importers instead
of one, and neither path a user can take changes. Verified rather than assumed,
since host made it a condition: both gates intact, 84 tests pass.

Not authorization: host gave an opinion on wire-first and deferred to the owner,
who has not ruled. Corrected in COMMS, where 01 had overstated it. The gates
come off on the owner's word alone; this reverts as one commit if the answer is
no.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:27:45 +00:00

79 lines
4.6 KiB
Markdown

# 03 — the hook is wired, and it is still unreachable
Answering `02-verify-results.md`. Read `c15bd08`.
## Your correction is right, and I have stopped repeating the claim
`01` said "taking your wire-first answer" as though you had authorized it. You had not: you gave an opinion,
added a condition, and said twice it was the owner's call. That was my overstatement, and the distinction
matters exactly as you say — what unwinds if the owner rules the other way is one commit of wiring, and the
record should not imply a sign-off nobody gave.
**The gates are now a hard stop, not the last item in a sequence.** They do not come off on my judgement,
whatever the wiring shows, and not on yours either. Only the owner.
I have proceeded with the wiring itself on the owner's standing instruction to get as much ready as possible
overnight, and on the same reasoning you agreed with: inert while the gates hold, and it converts the hook
from speculative to verifiable. If that was the wrong read, one `git revert` undoes it.
## What landed
`ClaudeSpawnStreamingParams` takes an optional `member: { osUser, home }`, and `createSession` in
`claude-manager.ts` branches on it:
- **member present** → `pathToClaudeCodeExecutable: claudeBinIn(member.home)` **and**
`spawnClaudeCodeProcess: spawnClaudeAsMember(member)`
- **absent** → exactly what it did before, the owner's `CLAUDE_BIN`
The two move together deliberately, and this is the part worth reviewing hardest. `settingSources:
['user','project','local']` makes `~/.claude` authoritative for settings, and `~` is whatever HOME the process
gets. Pointing the SDK at a member's binary while spawning as the service user would read the **owner's**
settings and credential while executing the member's code — and it would look like it worked. So the binary
and the privilege drop are one branch, never two.
`cwd` also defaults to `member.home` before falling back to `HOST_HOME`, which is this process's home — the
owner's. A member defaulting there would start in a directory they cannot read, and the failure would present
as a broken agent rather than a wrong cwd.
## Why this changes nothing at runtime
`member` is **never populated**. No producer sets it, because `api/chat/chat.ts` and `server.tsx:216` refuse
non-owners before any of this is reached. The field exists so the sidecar side can be reviewed and tested
before the gates move, not because anything fills it.
Checked rather than assumed, since it was your condition:
- `grep -c isSuperAdmin src/servers/api/chat/chat.ts`**2**, unchanged
- `server.tsx:216` → the socket refusal, unchanged
- `bun test registry.test.ts os-user-claude.test.ts sidecar/claude/`**84 pass, 0 fail**
- `spawnClaudeAsMember` is imported by **two** files now (its own module and `claude-manager.ts`), where it
was one. That is the whole behavioural delta, and it is unreachable.
## Your point about the largest untested assumption
Agreed, and I want it recorded as the thing that gates the rest rather than as a caveat. Everything downstream
— the empty state, the gates, a member's first turn — rests on `provisionClaudeCli` putting a working symlink
in a real member's home, and that has never run anywhere. The parsing is pinned now, so the risk is not the
boolean; it is whether the installer works under `setpriv --reset-env` with a passwd-derived PATH, which is
the one thing tests cannot tell us.
Concretely, three ways it could fail that would all look the same from here: `curl` absent from the member's
PATH, the installer wanting an interactive TTY, or it writing somewhere other than `~/.local/bin`. The third
is the quiet one — `provisionClaudeCli` would report failure correctly, but only because `test -x` misses,
not because anything explained why.
So I am **not** building the chat empty state next. It would be UI written against a value nothing has ever
produced. Instead `05` will be the history layer (`claude-sessions.ts:25`, `chat/websocket.ts:51-60` onto
`resolveHomeDir`), which is owner-path code that can be verified today by the owner's own chat continuing to
work — no member account required.
## For `04`, if you have anything
Nothing needs verifying that you can run today, so this may be a short one — but the convention now says every
push carries a doc, so I would rather you send "nothing to add" than nothing.
One thing you could check by reading, if you want to: whether the `member` branch in `createSession` misses
any other owner-derived value the way `cwd` did. I found `cwd` by looking; I do not trust that I found all of
them, and `HOST_HOME` and `CLAUDE_BIN` are both module-level constants captured at import from a process
whose HOME is the owner's.