Features: credential probe reads the policy, allow-list refuses a non-zsh spawn, free counts the lead's seat
Three operator-visible changes shipped and live on 2026-09-03: - fleetd #111 — the probe fetches GET /member-credentials instead of carrying its own 31-name copy. Verified live in a member: 34 checked, 29 blocked, 5 present. - fleetd #155 — policy: allow-list now refuses a spawn under a non-zsh login shell instead of degrading to the weaker overlay. Includes the memberLoginShell/memberHerdrSocket trap that refuses every spawn. - fleetd #176 — fleet_list's free subtracts the lead's subscription seat and the row carries leadSeats. Records that free and the spawn gate now disagree by one on purpose, so nobody raises maxLoad to compensate.
+85
@@ -3039,6 +3039,91 @@ is data, so copying it can only move values; `.envrc` is executable shell that `
|
||||
`cd`, so copying it moves behaviour. An operator who wants it can still list it explicitly, and then
|
||||
owns that choice.
|
||||
|
||||
## The credential probe asks the daemon what the policy is
|
||||
|
||||
**What.** `scripts/probe-member-credentials.sh` reads the policy's name list from the daemon over a
|
||||
new read-only endpoint, `GET /member-credentials`, instead of carrying its own copy of the names. The
|
||||
endpoint returns the policy mode, the known and allowed **names**, and the counts. Never a value —
|
||||
the daemon does not hold the values, and `MemberCredentialPolicyView` reads no environment at all, so
|
||||
there is nothing to redact by construction.
|
||||
|
||||
**On.** Automatic. The probe needs no flag; it refuses to run outside a member shell unless you pass
|
||||
`--allow-outside-member`, which labels the reading as a comparison rather than a finding.
|
||||
|
||||
**Why it exists.** The probe used to hold a hardcoded array of 31 names. The live policy had 34. It
|
||||
reported **26 blocked** against a policy that blocks **29**, exited 0, and printed a table that
|
||||
looked complete. Both numbers were right; they were counting different sets, and nothing said so.
|
||||
|
||||
That is the worst direction for a verification tool to fail in. Its clean output is taken as
|
||||
evidence, so a silent gap stops anyone looking. The same "second hand-maintained copy" defect had
|
||||
already been fixed twice elsewhere, and the fix here is the same one: **delete the copy** rather than
|
||||
correct it.
|
||||
|
||||
**The gotcha: it refuses rather than degrades.** An unreachable daemon, an absent or empty policy, or
|
||||
a `knownCount` that disagrees with the length of `known[]` all exit non-zero. There is deliberately
|
||||
no local fallback — a checker that quietly drops to a weaker check is the thing this replaced.
|
||||
|
||||
It also prints its own denominator: *"policy contains 34 name(s); this run checked 34 — they match."*
|
||||
Two numbers a reader can compare beat one number they have to trust.
|
||||
|
||||
The startup log line and the endpoint now share one class, so the counting exists in one place. Watch
|
||||
one subtlety if you ever recompute it: **blocked is not `known - allowed`.** Live, `known` is 34 and
|
||||
`allow` is 7, but only 5 of those 7 appear in `known`, so blocked is 29 and the naive subtraction
|
||||
gives 27.
|
||||
|
||||
## An allow-list policy refuses a spawn it cannot enforce
|
||||
|
||||
**What.** `memberCredentials.policy: allow-list` is enforced by a generated `.zlogin` under a
|
||||
per-member `ZDOTDIR`. A non-zsh login shell ignores `ZDOTDIR` entirely, so no scrub runs. Under that
|
||||
policy the launcher now **refuses the spawn**, naming the shell it actually found and offering three
|
||||
ways out. Under `policy: deny-by-default` nothing changed — that overlay is applied to the pane
|
||||
before any shell runs, so it does not depend on the shell.
|
||||
|
||||
**On.** Automatic whenever `policy: allow-list` is set.
|
||||
|
||||
**Why it exists.** The launcher already *detected* a non-zsh shell and logged a warning — then
|
||||
degraded to the weaker overlay and spawned anyway. The operator had explicitly asked for the blocking
|
||||
control and silently received the weaker one. Detection existed; refusal did not, and a control that
|
||||
silently does nothing is worse than no control, because the config still says it is on.
|
||||
|
||||
**The gotcha: `memberLoginShell:` and `memberHerdrSocket:` must be set together.** Which shell is
|
||||
checked depends on the routing. With `memberHerdrSocket` absent — today's normal mode — fleetd's own
|
||||
`$SHELL` decides. With it set, member panes run as a **different OS user**, so fleetd's `$SHELL`
|
||||
describes the wrong account and only the configured `memberLoginShell:` is read. If you set
|
||||
`memberHerdrSocket` and leave `memberLoginShell` unset, the shell reads as `<unset>`, counts as
|
||||
non-zsh, and **every spawn is refused**. The refusal message says so, but it is cheaper to know first.
|
||||
|
||||
## `free` counts the seat the lead itself holds
|
||||
|
||||
**What.** For a `subscription: true` profile, `fleet_list`'s `free` now subtracts the seat the lead's
|
||||
own session occupies on that same account, and the row carries a `leadSeats` field when the count is
|
||||
positive. Profiles are grouped by **account**, not by profile name: a profile with no explicit
|
||||
`credentialId` that is `subscription: true` joins a shared `<subscription>` group. An explicit
|
||||
`credentialId` still wins, so two genuinely separate Claude logins on one host stay apart.
|
||||
|
||||
**On.** Automatic for `subscription: true` profiles, once `fleet.leaders.<name>.profile` is set.
|
||||
|
||||
**Why it exists.** The lead is always a live `claude` session on the operator's subscription, and
|
||||
`maxLoad` only ever counted members. So a fan-out that filled every member slot still left the lead's
|
||||
seat unaccounted for, and the daemon advertised a slot that the account was already using.
|
||||
|
||||
The same grouping fixes a second, wider bug: quarantining one subscription profile now also refuses
|
||||
spawns on the other. One subscription hitting a usage limit really does take out every profile
|
||||
running on it.
|
||||
|
||||
**The gotcha: `free` and the spawn gate now disagree by one, deliberately.** The subtraction is
|
||||
**reporting only**. The placement gate uses `maxLoad` and the live count and never consults the lead
|
||||
seat, so with `maxLoad: 3` and two members up, `fleet_list` says `free: 0` while a third spawn still
|
||||
succeeds. That is the opposite of the overstatement this was filed to fix, and which of the two
|
||||
numbers is "right" is an open question — see the follow-up ticket before changing `maxLoad` to
|
||||
compensate. Raising `maxLoad` to win the slot back grants a real extra member; the slot was never
|
||||
taken.
|
||||
|
||||
The first attempt at this shipped **inert** and every test passed: the matcher grouped by
|
||||
`effectiveCredentialId()`, which fell back to the profile's own *name*, so a lead on `opus` and
|
||||
members on `sonnet` never matched and zero seats were charged. Every test in that change put the lead
|
||||
on the same profile name as the target — the one shape the live config does not have.
|
||||
|
||||
### A note for anyone briefing a worker to read this page
|
||||
|
||||
**A worker cannot see the current version of this file.** `wiki/` is a submodule, and the parent
|
||||
|
||||
Reference in New Issue
Block a user