diff --git a/bridged/src/main/java/dev/ltms/bridged/auth/CallerResolver.java b/bridged/src/main/java/dev/ltms/bridged/auth/CallerResolver.java index 833151a..d3806bf 100644 --- a/bridged/src/main/java/dev/ltms/bridged/auth/CallerResolver.java +++ b/bridged/src/main/java/dev/ltms/bridged/auth/CallerResolver.java @@ -65,13 +65,13 @@ public final class CallerResolver { private final Function memberSlotRoles; private final Function memberSlotNames; - /** Loopback-trust resolver: no token required, historical behaviour. */ - public CallerResolver(ConnectionIdentity identity) { + /** Loopback-trust resolver: no token required, historical behaviour. Test-only. */ + CallerResolver(ConnectionIdentity identity) { this(identity, false, null, Map.of()); } - /** As {@link #CallerResolver(ConnectionIdentity, boolean, String, Map)} with no leads pinned. */ - public CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token) { + /** As {@link #CallerResolver(ConnectionIdentity, boolean, String, Map)} with no leads pinned. Test-only. */ + CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token) { this(identity, tokenMode, token, Map.of()); } @@ -86,8 +86,8 @@ public final class CallerResolver { * @param pinnedPrimaryTerminal the primary's own herdr {@code terminal_id} * ({@code null}/blank = unpinned) */ - public static CallerResolver pinnedTo(ConnectionIdentity identity, boolean tokenMode, - String token, String pinnedPrimaryTerminal) { + static CallerResolver pinnedTo(ConnectionIdentity identity, boolean tokenMode, + String token, String pinnedPrimaryTerminal) { return new CallerResolver(identity, tokenMode, token, pinnedPrimaryTerminal == null || pinnedPrimaryTerminal.isBlank() ? Map.of() : Map.of(pinnedPrimaryTerminal, "primary")); @@ -102,21 +102,11 @@ public final class CallerResolver { * {@link Role#PRIMARY} — rather than a worker. Empty = nothing pinned, * so every pane resolves as a worker. */ - public CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token, - Map leadTerminals) { + CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token, + Map leadTerminals) { this(identity, tokenMode, token, fixed(leadTerminals), null); } - /** - * Map-form of both registries (CB-548): lead terminals and the initial architect terminal - * bindings, each snapshotted at construction (a handed-over map is not offered as live state). - */ - public CallerResolver(ConnectionIdentity identity, boolean tokenMode, String token, - Map leadTerminals, - Map architectTerminals) { - this(identity, tokenMode, token, fixed(leadTerminals), fixed(architectTerminals)); - } - /** * Live-registry form: {@code leadTerminals} is consulted on every resolve, so leads discovered * after startup (CB-531's tab scan) take effect without a restart. @@ -125,29 +115,18 @@ public final class CallerResolver { * {@link #pinnedTo}: {@code Map} and {@code Supplier} overloads are ambiguous for a literal * {@code null}. */ - public static CallerResolver withLeads(ConnectionIdentity identity, boolean tokenMode, - String token, - Supplier> leadTerminals) { + static CallerResolver withLeads(ConnectionIdentity identity, boolean tokenMode, + String token, + Supplier> leadTerminals) { return new CallerResolver(identity, tokenMode, token, leadTerminals, null); } /** - * Live-registry form for both {@code leadTerminals} and the CB-548 architect registry: both - * are consulted on every resolve, so a slot binding injected after startup takes effect - * without a restart. + * Live registry form that can confirm a bound slot is an architect slot. * - *

A static factory rather than a constructor overload, for the same reason as - * {@link #pinnedTo}: too many {@code Map}/{@code Supplier} combinations to make {@code null} - * unambiguous. + *

This is the only public construction path. It keeps terminal bindings and slot roles in + * the same {@link MemberRegistry}, so a configured architect can resolve as an architect. */ - public static CallerResolver withLeadsAndMembers(ConnectionIdentity identity, - boolean tokenMode, String token, - Supplier> leadTerminals, - Supplier> architectTerminals) { - return new CallerResolver(identity, tokenMode, token, leadTerminals, architectTerminals); - } - - /** Live registry form that can confirm a bound slot is an architect slot. */ public static CallerResolver withLeadsAndMembers(ConnectionIdentity identity, boolean tokenMode, String token, Supplier> leadTerminals, diff --git a/bridged/src/test/java/dev/ltms/bridged/auth/CallerResolverTest.java b/bridged/src/test/java/dev/ltms/bridged/auth/CallerResolverTest.java index 6597548..9c2a757 100644 --- a/bridged/src/test/java/dev/ltms/bridged/auth/CallerResolverTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/auth/CallerResolverTest.java @@ -315,6 +315,7 @@ class CallerResolverTest { @Test void aBoundArchitectPaneResolvesToArchitectBeforeTheWorkerFallback() { + // This is the production construction path used by Bridged. Principal p = CallerResolver.withLeadsAndMembers(workerIdentity(), false, null, Map::of, boundMembers("architect:lead-designer", MemberRole.ARCHITECT)) .resolve("127.0.0.1", 42, null); @@ -338,9 +339,10 @@ class CallerResolverTest { @Test void anUnboundPaneStillResolvesAsAWorker() { - Map arch = Map.of("term_elsewhere", "reviewer"); + MemberRegistry members = new MemberRegistry(new BridgedConfig.Fleet(Map.of(), + Map.of("lead-designer", new BridgedConfig.Slot("sonnet")), Map.of(), Map.of(), null)); Principal p = CallerResolver.withLeadsAndMembers(workerIdentity(), false, null, - Map::of, () -> arch).resolve("127.0.0.1", 42, null); + Map::of, members).resolve("127.0.0.1", 42, null); assertEquals(Role.WORKER, p.role()); assertNull(p.name()); @@ -376,16 +378,6 @@ class CallerResolverTest { assertEquals("architect:lead-designer", r.members().get("term_a")); } - @Test - void theArchitectMapFormIsCopiedSoLaterMutationCannotGrantArchitect() { - Map mutable = new java.util.LinkedHashMap<>(); - CallerResolver r = new CallerResolver(workerIdentity(), false, null, Map.of(), mutable); - - mutable.put("term_a", "sneaky"); - - assertEquals(Role.WORKER, r.resolve("127.0.0.1", 42, null).role()); - } - @Test void describeNamesTheArchitectSlot() { assertEquals("architect:lead-designer", diff --git a/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpAuthzTest.java b/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpAuthzTest.java index 4cbc114..96923cf 100644 --- a/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpAuthzTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/mcp/BridgeMcpAuthzTest.java @@ -2,6 +2,7 @@ package dev.ltms.bridged.mcp; import dev.ltms.bridged.auth.Authz; import dev.ltms.bridged.auth.CallerResolver; +import dev.ltms.bridged.auth.MemberRegistry; import dev.ltms.bridged.auth.Principal; import dev.ltms.bridged.auth.Role; import dev.ltms.bridged.config.BridgedConfig; @@ -69,7 +70,8 @@ class BridgeMcpAuthzTest { mcp = new BridgeMcp(messages, workers, sessions, identity, sessions.asPresence(), new PrimaryRegistry(null), - enforce ? new CallerResolver(identity) : null, + enforce ? CallerResolver.withLeadsAndMembers(identity, false, null, + Map::of, new MemberRegistry(null)) : null, metrics); return mcp; } diff --git a/bridged/src/test/java/dev/ltms/bridged/rest/BridgedAppAuthTest.java b/bridged/src/test/java/dev/ltms/bridged/rest/BridgedAppAuthTest.java index 56f1d70..91ca023 100644 --- a/bridged/src/test/java/dev/ltms/bridged/rest/BridgedAppAuthTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/rest/BridgedAppAuthTest.java @@ -1,6 +1,7 @@ package dev.ltms.bridged.rest; import dev.ltms.bridged.auth.CallerResolver; +import dev.ltms.bridged.auth.MemberRegistry; import dev.ltms.bridged.config.BridgedConfig; import dev.ltms.bridged.guard.SubscriptionGuard; import dev.ltms.bridged.herdr.AgentControl; @@ -65,9 +66,8 @@ class BridgedAppAuthTest { MessageService messages = new MessageService(agents, injector, new Rendezvous()); ConnectionIdentity identity = new ConnectionIdentity(new PaneLocator(herdr), _ -> pid); - CallerResolver callers = tokenMode - ? new CallerResolver(identity, true, token) - : new CallerResolver(identity); + CallerResolver callers = CallerResolver.withLeadsAndMembers(identity, tokenMode, token, + Map::of, new MemberRegistry(null)); metrics = BridgedMetrics.create(sessions, new dev.ltms.bridged.msg.InMemoryReplyInbox()); app = new BridgedApp(herdr, workers, sessions, messages, sessions.asPresence(), null,