From bfddb43631537465df2c147e708ff4e5aeea84ac Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Thu, 3 Sep 2026 13:37:34 +0700 Subject: [PATCH] Features: credential probe reads the policy, allow-list refuses a non-zsh spawn, free counts the lead's seat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- 11-Features.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/11-Features.md b/11-Features.md index 8258dab..7393253 100644 --- a/11-Features.md +++ b/11-Features.md @@ -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 ``, 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 `` 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..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