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>
79 lines
4.6 KiB
Markdown
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.
|