diff --git a/fleetd/src/test/java/dev/ltms/fleet/FleetdAssemblyAuthorizationModeTest.java b/fleetd/src/test/java/dev/ltms/fleet/FleetdAssemblyAuthorizationModeTest.java
new file mode 100644
index 0000000..3c3f569
--- /dev/null
+++ b/fleetd/src/test/java/dev/ltms/fleet/FleetdAssemblyAuthorizationModeTest.java
@@ -0,0 +1,185 @@
+package dev.ltms.fleet;
+
+import dev.ltms.fleet.auth.Authz;
+import dev.ltms.fleet.auth.Principal;
+import dev.ltms.fleet.config.ConfigRef;
+import dev.ltms.fleet.config.FleetConfig;
+import dev.ltms.fleet.guard.SubscriptionGuard;
+import dev.ltms.fleet.herdr.FakeHerdr;
+import dev.ltms.fleet.herdr.HerdrClient;
+import dev.ltms.fleet.mcp.FleetMcp;
+import dev.ltms.fleet.msg.ReplyInbox;
+import io.javalin.Javalin;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.LongSupplier;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * fleetd #672 — pins the {@link FleetMcp.AuthorizationMode} that {@link FleetdAssembly}'s
+ * production boot path passes to {@link FleetMcp} at {@code FleetdAssembly.java:481}
+ * ({@code AuthorizationMode.ENFORCED}).
+ *
+ *
{@code FleetMcpAuthzTest} already exercises {@code FleetMcp#denyFor} against the CB-505
+ * table, but it constructs its own {@link FleetMcp} and chooses its own {@code AuthorizationMode}
+ * — it tests the seam, not the producer. This test instead reaches the exact {@link FleetMcp}
+ * {@link FleetdAssembly#assembleAndStart} builds (via {@code FleetdRuntime#mcp()}, the same
+ * accessor {@code FleetdAssemblyConnectionIdentityTest} uses for {@code identity()}) and asserts
+ * the consequence rather than reading the enum back: an unauthorized caller must actually be
+ * refused through it, and the primary must still be allowed, so the test cannot pass with the
+ * gate wired backwards.
+ *
+ *
{@code FleetMcp#denyFor} is package-private to {@code dev.ltms.fleet.mcp}; this test lives in
+ * {@code dev.ltms.fleet}, where {@link FleetdAssembly} and {@code FleetdRuntime#mcp()} live, so it
+ * cannot call {@code denyFor} directly. Reflection bridges that package boundary the same way
+ * {@code getDeclaredField} does in {@link FleetdAssemblyLeadTabScannerExclusionTest} — the
+ * assertion itself still exercises the real policy decision ({@code denyFor} calling
+ * {@code Authz.permits}), not a field read.
+ */
+class FleetdAssemblyAuthorizationModeTest {
+
+ private static final class TestResourcePorts implements ResourcePorts {
+ final FakeHerdr herdr = new FakeHerdr();
+ Runnable shutdownHook;
+
+ @Override
+ public Map environment() {
+ return Map.of();
+ }
+
+ @Override
+ public HerdrClient connectHerdr(Path socketPath) {
+ return herdr;
+ }
+
+ @Override
+ public Fleetd.AmqpOpener replyInboxOpener() {
+ return (uri, prefetch) -> new ReplyInbox() {
+ @Override public void own(String target) { }
+ @Override public void release(String target) { }
+ @Override public void publish(String target, String msgId, String content) { }
+ @Override public List peek(String target) { return List.of(); }
+ @Override public boolean ack(String target, String msgId) { return false; }
+ };
+ }
+
+ @Override
+ public Fleetd.LeadMailboxOpener leadMailboxOpener() {
+ return (uri, selfCoordId, prefetch) -> {
+ throw new UnsupportedOperationException(
+ "leadMailboxOpener must not be called — no coordinator: block is configured");
+ };
+ }
+
+ @Override
+ public LongSupplier nanoClock() {
+ return System::nanoTime;
+ }
+
+ @Override
+ public LongSupplier wallClockNanos() {
+ return System::nanoTime;
+ }
+
+ @Override
+ public ScheduledExecutorService newScheduler(String purpose) {
+ return Executors.newSingleThreadScheduledExecutor();
+ }
+
+ @Override
+ public void addShutdownHook(Runnable hook) {
+ shutdownHook = hook;
+ }
+
+ @Override
+ public void startHttp(Javalin app, String host, int port) {
+ // Do not bind a real port in this assembly test — see FleetdAssemblyLeadTabScannerExclusionTest.
+ }
+
+ @Override
+ public Runnable herdrPollWait() {
+ return () -> {
+ throw new UnsupportedOperationException("FakeHerdr is healthy; no poll wait is expected");
+ };
+ }
+ }
+
+ private TestResourcePorts ports;
+
+ @AfterEach
+ void tearDown() {
+ if (ports != null && ports.shutdownHook != null) {
+ ports.shutdownHook.run();
+ }
+ }
+
+ private static FleetConfig writeConfig(Path dir) throws Exception {
+ Path file = dir.resolve("fleetd.yaml");
+ Files.writeString(file, """
+ bind:
+ host: 127.0.0.1
+ port: 8765
+ idleSleepGuard:
+ enabled: false
+ health:
+ enabled: false
+ broker:
+ uri: "amqp://fake-test-broker/vh"
+ """);
+ return FleetConfig.load(file);
+ }
+
+ private FleetMcp assemble(Path dir) throws Exception {
+ FleetConfig cfg = writeConfig(dir);
+ ports = new TestResourcePorts();
+ FleetdRuntime runtime = FleetdAssembly.assembleAndStart(new AssemblyInputs(cfg,
+ new ConfigRef(dir.resolve("fleetd.yaml"), cfg), new SubscriptionGuard(cfg.guard().hostSet())), ports);
+ return runtime.mcp();
+ }
+
+ /**
+ * Invokes the real production {@code FleetMcp#denyFor} by reflection. The method is
+ * package-private to {@code dev.ltms.fleet.mcp}; this is the only seam available to this test
+ * without a full HTTP/servlet round trip (see this class's javadoc). No {@code catch} here can
+ * turn a missing method into a pass — a {@code NoSuchMethodException} propagates out of the
+ * test and fails it loudly if {@code denyFor} is ever renamed or removed.
+ */
+ private static McpSchema.CallToolResult denyFor(FleetMcp mcp, Principal caller, Authz.Action action,
+ String target) throws Exception {
+ Method m = FleetMcp.class.getDeclaredMethod("denyFor", Principal.class, Authz.Action.class, String.class);
+ m.setAccessible(true);
+ return (McpSchema.CallToolResult) m.invoke(mcp, caller, action, target);
+ }
+
+ @Test
+ void productionBootPathRefusesAnUnauthorizedCallerThroughTheAssembledFleetMcp(@TempDir Path dir)
+ throws Exception {
+ FleetMcp mcp = assemble(dir);
+
+ McpSchema.CallToolResult deniedForWorker = denyFor(mcp, Principal.worker("term_a", 200),
+ Authz.Action.SPAWN, "term_a");
+ assertNotNull(deniedForWorker,
+ "FleetdAssembly.java:481 must pass AuthorizationMode.ENFORCED to FleetMcp — a worker "
+ + "must not be able to fleet_spawn through the assembled production object");
+ assertTrue(deniedForWorker.isError(), "a refusal is returned as an MCP tool error");
+
+ McpSchema.CallToolResult allowedForPrimary = denyFor(mcp, Principal.primary(100),
+ Authz.Action.SPAWN, "term_a");
+ assertNull(allowedForPrimary,
+ "control: the primary must still be allowed to fleet_spawn — otherwise the worker "
+ + "refusal above would pass even with the gate wired backwards");
+ }
+}