Merge remote-tracking branch 'origin/worker/756-758-observer-pane-discovery-7e6ffd-1' into vfy/762
CI / shell-tests (push) Failing after 7s
CI / contract (push) Successful in 54s
CI / build (push) Failing after 1m56s

This commit is contained in:
Dai Ha
2026-10-05 10:29:49 +02:00
4 changed files with 218 additions and 36 deletions
@@ -284,8 +284,13 @@ public final class CallerResolver {
&& !collaboratorTerminals.get().containsKey(target);
}
/** Whether {@code terminal} is bound to a configured slot the live roster still confirms as an architect. */
private boolean boundToArchitectSlot(String terminal) {
/**
* Whether {@code terminal} is bound to a configured slot the live roster still confirms as an
* architect — the one classifier {@link #sendableObserverTarget()} and {@code FleetMcp}'s
* {@code panes} row both read, so a pane's reported role and its {@code SEND} reachability can
* never drift apart.
*/
public boolean boundToArchitectSlot(String terminal) {
String slot = architectTerminals.get().get(terminal);
return slot != null && memberSlotRoles.apply(slot) == MemberRole.ARCHITECT;
}
@@ -308,17 +308,25 @@ public final class FleetMcp {
/**
* Pane-discovery facts for {@code fleet_list}'s {@code panes} row — every herdr tab's display
* label, and the one deliverability gate the status-gated injector itself reads.
* label, the one deliverability gate the status-gated injector itself reads, whether a
* terminal is bound to a configured architect slot, and whether an observer caller may
* {@code SEND} to it.
*
* @param tabLabels tab id → its display label, read lazily (only once the row is actually
* assembled) since it costs a herdr {@code workspace.list}/{@code tab.list}
* scan; a tab herdr reports with no label maps to a {@code null} value
* @param deliverable the same gate {@link dev.ltms.fleet.Fleetd#deliverableTo} builds for the
* injector, keyed by terminal id — never a second, separately-derived check
* @param architectSlot the same classifier {@link CallerResolver#boundToArchitectSlot} resolves
* a caller against — never a second, separately-derived check
* @param sendableToObserver the same predicate {@link CallerResolver#sendableObserverTarget}
* builds for the {@code SEND} gate — never a second, separately-derived
* check
*/
public record PaneSource(Supplier<Map<String, String>> tabLabels, Predicate<String> deliverable) {
/** Inert source — no labels, and every target reports non-deliverable. */
public static PaneSource none() { return new PaneSource(Map::of, _ -> false); }
public record PaneSource(Supplier<Map<String, String>> tabLabels, Predicate<String> deliverable,
Predicate<String> architectSlot, Predicate<String> sendableToObserver) {
/** Inert source — no labels, no deliverable targets, no architect slots, nothing sendable. */
public static PaneSource none() { return new PaneSource(Map::of, _ -> false, _ -> false, _ -> false); }
}
/**
@@ -589,7 +597,8 @@ public final class FleetMcp {
// A Supplier: the label lookup costs a herdr scan, and must stay behind
// panesVisible so it only runs for a caller that receives the row at all.
PaneSource panes = new PaneSource(() -> identity.panes().tabLabelsByTabId(),
Fleetd.deliverableTo(presence, callers::leads, callers::collaborators));
Fleetd.deliverableTo(presence, callers::leads, callers::collaborators),
callers::boundToArchitectSlot, callers.sendableObserverTarget());
return listFleet(workers, sessions, messages, capacity, healthCoverage, loopHealth, quarantine, outage,
leadSeats, leadContextGauge, leadConfigDirs, callers.leads(),
callerTerminal(exchange),
@@ -597,7 +606,7 @@ public final class FleetMcp {
new CoordinationSource(leadChannel, peers),
coordinatorVisibleTo(principal(exchange)),
leadsVisibleTo(principal(exchange)), membersVisibleTo(principal(exchange)),
panes, panesVisibleTo(principal(exchange)));
panes, panesVisibleTo(principal(exchange)), principal(exchange).isObserver());
};
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> stopHandler =
(exchange, req) -> {
@@ -824,13 +833,14 @@ public final class FleetMcp {
}
/**
* Who may see {@code fleet_list}'s {@code panes} array — every herdr-tracked agent pane on the
* host, carrying a tab label and a member's {@code cwd}. Visible to exactly the roles that may
* {@link Authz.Action#SEND} to a named peer; a plain worker or an observer holds {@code READ}
* but never {@code SEND}, so it does not see this array.
* Who may see {@code fleet_list}'s {@code panes} array — every role that may
* {@link Authz.Action#SEND} to some other pane. A plain worker holds {@code READ} but never
* {@code SEND}, so it still does not see this array. An observer does hold {@code SEND}, to
* another observer pane only, so it sees the array too — but {@code listFleet} filters its rows
* to {@link CallerResolver#sendableObserverTarget} and reduces each one; see {@code paneRows}.
*/
static boolean panesVisibleTo(Principal caller) {
return caller.isPrimary() || caller.isArchitect() || caller.isCollaborator();
return caller.isPrimary() || caller.isArchitect() || caller.isCollaborator() || caller.isObserver();
}
/**
@@ -2069,6 +2079,32 @@ public final class FleetMcp {
CoordinationSource coordination, boolean callerIsPrimary,
boolean leadsVisible, boolean membersVisible,
PaneSource panes, boolean panesVisible) {
return listFleet(workers, sessions, messages, capacity, healthCoverage, loopHealth, quarantine, outage,
leadSeats, contextGauge, leadConfigDirs, leads, selfTerm, collaborators, collaboratorsVisible,
coordination, callerIsPrimary, leadsVisible, membersVisible, panes, panesVisible, false);
}
/**
* As above, plus fleetd #758: an observer sees the {@code panes} array too, but filtered to
* {@link CallerResolver#sendableObserverTarget} and each row reduced to the five fields an
* observer may learn — see {@code paneRows}/{@code paneRow}.
*
* @param callerIsObserver whether the {@code fleet_list} caller is an observer; every wrapper
* overload above passes {@code false}, so a test that wants the
* filtered, reduced view must call this overload with an explicit
* {@code true}
*/
static McpSchema.CallToolResult listFleet(PeerLauncher workers, SessionManager sessions, MessageService messages,
CapacitySource capacity, HealthCoverageSource healthCoverage,
LoopHealthSource loopHealth,
QuarantineSource quarantine, OutageSource outage,
LeadSeatSource leadSeats, LeadContextGauge contextGauge,
LeadConfigDirSource leadConfigDirs,
Map<String, String> leads, String selfTerm,
Map<String, String> collaborators, boolean collaboratorsVisible,
CoordinationSource coordination, boolean callerIsPrimary,
boolean leadsVisible, boolean membersVisible,
PaneSource panes, boolean panesVisible, boolean callerIsObserver) {
try {
// Neither row's assembly (leadView/memberCapacityView probing herdr for live status)
// runs unless at least one of them needs the live-agent lookup backing it.
@@ -2118,7 +2154,7 @@ public final class FleetMcp {
}
// gate BEFORE assembling the row, so the key is absent rather than present-and-empty.
if (panesVisible) {
result.put("panes", paneRows(live, roster, leads, collaborators, panes));
result.put("panes", paneRows(live, roster, leads, collaborators, panes, callerIsObserver));
}
// fleetd #439: coordinator/coordinatorView is lead-to-lead coordination state and must
// never reach a worker or an architect -- gate BEFORE assembling it, not after, so the
@@ -2463,16 +2499,23 @@ public final class FleetMcp {
* is the same terminal-keyed {@link Agent} map {@code leadView}/{@code memberCapacityView}
* already read, so a pane neither configured as a lead nor spawned as a member — a hand-opened
* tab — still gets a row here.
*
* <p>fleetd #758: for an observer caller ({@code observerView}), the rows are filtered to
* {@link PaneSource#sendableToObserver} before being built, and each row is reduced — see
* {@code paneRow}.
*/
private static List<Map<String, Object>> paneRows(Map<String, Agent> live, List<MemberSession> roster,
Map<String, String> leads, Map<String, String> collaborators, PaneSource panes) {
Map<String, String> leads, Map<String, String> collaborators, PaneSource panes,
boolean observerView) {
final Map<String, String> tabLabels = tabLabelsOrEmpty(panes);
Map<String, MemberSession> byTerminal = roster.stream()
.filter(s -> s.terminalId() != null)
.collect(Collectors.toMap(MemberSession::terminalId, Function.identity(), (_, b) -> b));
return live.values().stream()
.filter(a -> !observerView || panes.sendableToObserver().test(a.terminalId()))
.sorted(Comparator.comparing(Agent::terminalId))
.map(a -> paneRow(a, byTerminal.get(a.terminalId()), leads, collaborators, tabLabels, panes))
.map(a -> paneRow(a, byTerminal.get(a.terminalId()), leads, collaborators, tabLabels, panes,
observerView))
.toList();
}
@@ -2481,38 +2524,53 @@ public final class FleetMcp {
* daemon never spawned as a member (a hand-opened tab, or a configured lead)
* @param tabLabels tab id → its herdr display label; a tab absent here, or carrying a
* {@code null} label itself, projects as a {@code null} "label"
* @param observerView fleetd #758: an observer's row carries only {@code sessionId}, {@code
* label}, {@code status}, {@code role}, {@code deliverable} — never {@code
* paneId} (the {@code fleet_stop} handle), {@code workspaceId}, {@code tabId},
* {@code agentType}, or {@code cwd} (a member's worktree path is the lead's
* business)
*/
private static Map<String, Object> paneRow(Agent a, MemberSession session, Map<String, String> leads,
Map<String, String> collaborators, Map<String, String> tabLabels, PaneSource panes) {
Map<String, String> collaborators, Map<String, String> tabLabels, PaneSource panes,
boolean observerView) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("sessionId", a.terminalId());
m.put("paneId", a.paneId());
m.put("workspaceId", a.workspaceId());
m.put("tabId", a.tabId());
if (!observerView) {
m.put("paneId", a.paneId());
m.put("workspaceId", a.workspaceId());
m.put("tabId", a.tabId());
}
m.put("label", a.tabId() == null ? null : tabLabels.get(a.tabId()));
m.put("agentType", a.agentType());
if (!observerView) {
m.put("agentType", a.agentType());
}
m.put("status", a.status() == null ? "unknown" : a.status().name().toLowerCase());
m.put("role", paneRole(a.terminalId(), session, leads, collaborators));
m.put("role", paneRole(a.terminalId(), session, leads, collaborators, panes));
m.put("deliverable", panes.deliverable().test(a.terminalId()));
if (session != null && session.cwd() != null) {
if (!observerView && session != null && session.cwd() != null) {
m.put("cwd", session.cwd());
}
return m;
}
/**
* The role this pane resolves as: a spawned member's own {@link MemberRole}, else "lead" or
* "collaborator" for a configured but currently-unoccupied slot, else "observer" for a pane
* this daemon neither spawned nor configured.
* The role this pane resolves as: a spawned member's own {@link MemberRole}, else "lead" for a
* configured but currently-unoccupied lead pane, else "architect" for a pane bound to a
* configured architect slot with no live member session, else "collaborator" for a configured
* but currently-unoccupied collaborator tab, else "observer" for a pane this daemon neither
* spawned nor configured.
*/
private static String paneRole(String terminal, MemberSession session, Map<String, String> leads,
Map<String, String> collaborators) {
Map<String, String> collaborators, PaneSource panes) {
if (session != null) {
return session.role().wireName();
}
if (leads.containsKey(terminal)) {
return "lead";
}
if (panes.architectSlot().test(terminal)) {
return "architect";
}
if (collaborators.containsKey(terminal)) {
return "collaborator";
}
@@ -512,19 +512,21 @@ class FleetMcpAuthzTest {
/**
* {@link FleetMcp#panesVisibleTo} is the whole policy decision for {@code fleet_list}'s
* {@code panes} array: visible to exactly the roles that may {@code SEND} to a named peer --
* the primary, an architect, and a collaborator -- never a worker, never an unconfigured
* observer pane, and never an anonymous caller.
* {@code panes} array: visible to every role that may {@code SEND} to some other pane -- the
* primary, an architect, a collaborator, and an observer (to another observer pane only, with
* its row filtered and reduced -- see {@code listFleet}) -- never a worker, never an
* anonymous caller.
*/
@Test
void onlyPrimaryArchitectAndCollaboratorMaySeeThePanesArray() {
void onlyPrimaryArchitectCollaboratorAndObserverMaySeeThePanesArray() {
assertTrue(FleetMcp.panesVisibleTo(PRIMARY), "the primary must see the panes array");
assertTrue(FleetMcp.panesVisibleTo(ARCH_DESIGN), "an architect must see the panes array");
assertTrue(FleetMcp.panesVisibleTo(COLLABORATOR), "a collaborator must see its own peer roster");
assertFalse(FleetMcp.panesVisibleTo(WORKER_A),
"a worker holds READ but can never SEND, so it must not see the panes array");
assertFalse(FleetMcp.panesVisibleTo(Principal.observer("term_obs", 700)),
"an unconfigured observer pane must not see every other pane's label and cwd");
assertTrue(FleetMcp.panesVisibleTo(Principal.observer("term_obs", 700)),
"an observer holds SEND to another observer pane, so it must see the (filtered, "
+ "reduced) panes array");
assertFalse(FleetMcp.panesVisibleTo(ANON), "authenticated as nothing must not see it either");
}
@@ -1138,7 +1138,7 @@ class FleetMcpTest {
presence.markPresent("term_a");
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(
() -> new PaneLocator(h).tabLabelsByTabId(),
Fleetd.deliverableTo(presence, Map::of, Map::of));
Fleetd.deliverableTo(presence, Map::of, Map::of), _ -> false, _ -> false);
String out = textOf(listFleetWithPanes(h, panes, true));
@@ -1159,7 +1159,7 @@ class FleetMcpTest {
FakeHerdr h = new FakeHerdr();
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(
() -> new PaneLocator(h).tabLabelsByTabId(),
Fleetd.deliverableTo(new MemberPresence(), Map::of, Map::of));
Fleetd.deliverableTo(new MemberPresence(), Map::of, Map::of), _ -> false, _ -> false);
String out = textOf(listFleetWithPanes(h, panes, true));
@@ -1190,7 +1190,7 @@ class FleetMcpTest {
FakeHerdr h = new FakeHerdr().workspaceListFailsWith("unavailable");
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(
() -> new PaneLocator(h).tabLabelsByTabId(),
Fleetd.deliverableTo(new MemberPresence(), Map::of, Map::of));
Fleetd.deliverableTo(new MemberPresence(), Map::of, Map::of), _ -> false, _ -> false);
McpSchema.CallToolResult res = listFleetWithPanes(h, panes, true);
@@ -1203,6 +1203,123 @@ class FleetMcpTest {
assertTrue(out.contains("\"members\":[]"), "a label-scan failure must not cost the members array: " + out);
}
/**
* fleetd #756: a pane bound to a configured architect slot with no live member session must
* report {@code role: "architect"}, read from {@link CallerResolver#boundToArchitectSlot} —
* the same classifier {@link CallerResolver#sendableObserverTarget} refuses as a {@code SEND}
* target — rather than falling through to {@code "observer"}.
*/
@Test
void listReportsArchitectForASlotBoundPaneWithNoLiveMember() {
FakeHerdr h = new FakeHerdr()
.withAgent("claude-arch", "term_unoccupied_architect", "w2:pArch", "w2:tArch");
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(
Map::of, _ -> false, "term_unoccupied_architect"::equals, _ -> false);
String out = textOf(listFleetWithPanes(h, panes, true));
assertTrue(out.contains("\"sessionId\":\"term_unoccupied_architect\""), out);
assertTrue(out.contains("\"role\":\"architect\""),
"a slot-bound pane with no live session must read \"architect\", not the generic "
+ "\"observer\" fallback: " + out);
}
/**
* Calls the canonical {@code listFleet} overload directly with an explicit {@code leads}/
* {@code collaborators} payload and {@code callerIsObserver}, mirroring exactly what the real
* {@code fleet_list} handler computes for an observer caller: {@code panesVisible} true,
* {@code leadsVisible}/{@code membersVisible}/{@code collaboratorsVisible} false.
*/
private static McpSchema.CallToolResult listFleetAsObserver(FakeHerdr h, Map<String, String> leads,
Map<String, String> collaborators, FleetMcp.PaneSource panes) {
SessionManager sessions = new SessionManager(workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")));
return FleetMcp.listFleet(
workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")), sessions, null,
FleetMcp.CapacitySource.none(), new FleetMcp.HealthCoverageSource(() -> "off"),
FleetMcp.LoopHealthSource.none(), FleetMcp.QuarantineSource.none(), FleetMcp.OutageSource.none(),
FleetMcp.LeadSeatSource.none(), new LeadContextGauge(), FleetMcp.LeadConfigDirSource.none(),
leads, "", collaborators, false,
FleetMcp.CoordinationSource.none(), false, false, false, panes, true, true);
}
/**
* fleetd #758: an observer's {@code fleet_list} now carries a {@code panes} key, filtered to
* {@link CallerResolver#sendableObserverTarget} (so a lead's pane, a spawned member's pane, a
* collaborator's pane, and an unoccupied architect-slot pane are all absent) and every
* surviving row reduced to exactly {@code sessionId}, {@code label}, {@code status},
* {@code role}, {@code deliverable} — never {@code paneId}, {@code workspaceId}, {@code tabId},
* {@code agentType}, or {@code cwd}.
*/
@Test
void listFiltersAndReducesThePanesArrayForAnObserver() {
MemberRegistry members = new MemberRegistry(new FleetConfig.Fleet(Map.of(),
Map.of("lead-designer", new FleetConfig.Slot("sonnet")), Map.of(), Map.of(), null));
assertTrue(members.bind("architect:lead-designer", "term_architect_pane"));
CallerResolver callers = CallerResolver.withLeadsAndMembers(null, false, null,
() -> Map.of("term_lead_pane", "fleet01-lead"), members,
t -> "term_member_pane".equals(t) ? MemberRole.DEV : null,
() -> Map.of("term_collab_pane", "ops"));
FakeHerdr h = new FakeHerdr()
.withAgent("claude-sendable", "term_sendable", "w2:pS", "w2:tS")
.withAgent("claude-lead", "term_lead_pane", "w2:pL", "w2:tL")
.withAgent("claude-member", "term_member_pane", "w2:pM", "w2:tM")
.withAgent("claude-collab", "term_collab_pane", "w2:pC", "w2:tC")
.withAgent("claude-arch", "term_architect_pane", "w2:pA", "w2:tA")
.withTab("w2", "w2:tS", "trinotes");
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(
() -> new PaneLocator(h).tabLabelsByTabId(), _ -> true,
callers::boundToArchitectSlot, callers.sendableObserverTarget());
String out = textOf(listFleetAsObserver(h, callers.leads(),
Map.of("term_collab_pane", "ops"), panes));
assertTrue(out.contains("\"panes\":["), out);
assertTrue(out.contains("\"sessionId\":\"term_sendable\""),
"an ordinary unclassified pane must still be sendable and visible: " + out);
assertFalse(out.contains("term_lead_pane"), "a lead's pane must not be enumerated: " + out);
assertFalse(out.contains("term_member_pane"), "a spawned member's pane must not be enumerated: " + out);
assertFalse(out.contains("term_collab_pane"), "a collaborator's pane must not be enumerated: " + out);
assertFalse(out.contains("term_architect_pane"),
"an unoccupied architect-slot pane must not be enumerated: " + out);
assertFalse(out.contains("\"paneId\""), "an observer's row must never carry paneId: " + out);
assertFalse(out.contains("\"workspaceId\""), "an observer's row must never carry workspaceId: " + out);
assertFalse(out.contains("\"tabId\""), "an observer's row must never carry tabId: " + out);
assertFalse(out.contains("\"agentType\""), "an observer's row must never carry agentType: " + out);
assertFalse(out.contains("\"cwd\""), "an observer's row must never carry cwd: " + out);
}
/**
* Control for the test above: a primary's {@code panes} row is unchanged by fleetd #758 —
* {@code callerIsObserver} false keeps every field, including {@code paneId} and a spawned
* member's {@code cwd}.
*/
@Test
void listKeepsTheFullPaneRowForAPrimaryIncludingCwdAndPaneId() {
FakeHerdr h = new FakeHerdr();
SessionManager sessions = new SessionManager(workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")));
MemberSession spawned = sessions.acquire("ltms-local", "/worktree/member-1", null, null);
h.withAgent("claude-member", spawned.terminalId(), "w9:pMember", "w9:tMember");
FleetMcp.PaneSource panes = new FleetMcp.PaneSource(Map::of, _ -> true, _ -> false, _ -> false);
McpSchema.CallToolResult res = FleetMcp.listFleet(
workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")), sessions, null,
FleetMcp.CapacitySource.none(), new FleetMcp.HealthCoverageSource(() -> "off"),
FleetMcp.LoopHealthSource.none(), FleetMcp.QuarantineSource.none(), FleetMcp.OutageSource.none(),
FleetMcp.LeadSeatSource.none(), new LeadContextGauge(), FleetMcp.LeadConfigDirSource.none(),
Map.of(), "", Map.of(), false,
FleetMcp.CoordinationSource.none(), true, true, true, panes, true, false);
String out = textOf(res);
assertTrue(out.contains("\"sessionId\":\"" + spawned.terminalId() + "\""), out);
assertTrue(out.contains("\"paneId\":\"w9:pMember\""),
"a primary must still see the fleet_stop handle: " + out);
assertTrue(out.contains("\"cwd\":\"/worktree/member-1\""),
"a primary must still see a spawned member's worktree path: " + out);
assertTrue(out.contains("\"role\":\"dev\""), out);
}
/**
* fleetd #421: {@code mailbox.pending} counts only broker-ready messages, so a blocked lead's
* normal, healthy state is {@code "pending": 0} next to a non-empty {@code held[]} — which