From 2289e94223049b789319a7c839de02c5a64d3d00 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Mon, 5 Oct 2026 10:44:55 +0200 Subject: [PATCH] fleetd #759: fix the fleet_reply comment and the hand-copied role list Finding 4: the comment above fleet_reply's handler claimed the authz check asks whether the caller is a worker at all. It actually checks terminal ownership (Authz.java REPLY/ASK -> caller.ownsSession), which is why an observer can reply on its own pane with no role test involved. Finding 5: fleet_list's tool description hardcoded 'architect/dev/reviewer', missing hunter. Added MemberRole.wireNames() (pulled out of parse()'s error message builder, which now calls it too) and used it in the description so the list can't drift again. --- .../main/java/dev/ltms/fleet/mcp/FleetMcp.java | 9 +++++---- .../java/dev/ltms/fleet/peer/MemberRole.java | 16 ++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java index bbcb276..8e91598 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java +++ b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java @@ -529,8 +529,9 @@ public final class FleetMcp { ? sendAsync(messages, target, content, onAccepted, workers.profiles(), caller) : send(messages, target, content, timeoutMs(a), onAccepted, workers.profiles(), callerOwner); }; - // fleet_reply's identity is the CONNECTION, never an argument — so the authz check - // is "is this caller a worker at all", and it can only ever reply as itself. + // fleet_reply's identity is the CONNECTION, never an argument. The authz check is + // terminal ownership, not a role test: the caller may reply only for its own pane, + // which is why no role appears in the check at all. BiFunction replyHandler = (exchange, req) -> { String self = callerTerminal(exchange); @@ -2782,8 +2783,8 @@ public final class FleetMcp { + "orchestrators, each with its sessionId (the address to fleet_send to), " + "name, live status, and 'self': true on your own row; this is how you " + "discover a peer lead without being told its address. 'members' are the " - + "sessions delegated to — each with sessionId, paneId, role (architect/dev/" - + "reviewer), profile (the backend it runs on), state, optional " + + "sessions delegated to — each with sessionId, paneId, role (" + MemberRole.wireNames() + + "), profile (the backend it runs on), state, optional " + "worktree/branch/owner/agentSessionId, and live herdr status. agentSessionId, " + "when present, is the id to pass as fleet_spawn's resumeSessionId to relaunch " + "onto that same conversation. It is ABSENT — not a guess — for a member fleetd " diff --git a/fleetd/src/main/java/dev/ltms/fleet/peer/MemberRole.java b/fleetd/src/main/java/dev/ltms/fleet/peer/MemberRole.java index d954655..bc054e6 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/peer/MemberRole.java +++ b/fleetd/src/main/java/dev/ltms/fleet/peer/MemberRole.java @@ -1,6 +1,8 @@ package dev.ltms.fleet.peer; import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * What a member is for — the contract it runs under. @@ -100,6 +102,11 @@ public enum MemberRole { return null; } + /** The wire name of every role, joined with {@code ", "} in declaration order. */ + public static String wireNames() { + return Stream.of(values()).map(MemberRole::wireName).collect(Collectors.joining(", ")); + } + /** * Parse a config/wire spelling, case-insensitively. * @@ -118,14 +125,7 @@ public enum MemberRole { } } } - StringBuilder valid = new StringBuilder(); - for (MemberRole r : values()) { - if (!valid.isEmpty()) { - valid.append(", "); - } - valid.append(r.wireName()); - } throw new IllegalArgumentException( - "unknown member role '" + s + "'; valid roles are: " + valid); + "unknown member role '" + s + "'; valid roles are: " + wireNames()); } }