diff --git a/bridged/src/main/java/dev/ltms/bridged/member/OpenCodeLauncher.java b/bridged/src/main/java/dev/ltms/bridged/member/OpenCodeLauncher.java index b830672..8eb300e 100644 --- a/bridged/src/main/java/dev/ltms/bridged/member/OpenCodeLauncher.java +++ b/bridged/src/main/java/dev/ltms/bridged/member/OpenCodeLauncher.java @@ -7,6 +7,7 @@ import dev.ltms.bridged.herdr.Agent; import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.WorkspaceControl; import dev.ltms.bridged.peer.Capability; +import dev.ltms.bridged.peer.CharterReceipt; import dev.ltms.bridged.peer.PeerHandle; import dev.ltms.bridged.peer.SpawnRequest; @@ -406,6 +407,11 @@ public final class OpenCodeLauncher extends HerdrPeerLauncher { // appeared). return discovery.sessionIdForDirectory(cwd); } + + @Override + public CharterReceipt charterReceipt() { + return delegate.charterReceipt(); + } } // --- Agent-returning convenience spawns (used by callers/tests that want the herdr Agent) --- diff --git a/bridged/src/main/java/dev/ltms/bridged/peer/PeerHandle.java b/bridged/src/main/java/dev/ltms/bridged/peer/PeerHandle.java index 9c80c5e..8d4827c 100644 --- a/bridged/src/main/java/dev/ltms/bridged/peer/PeerHandle.java +++ b/bridged/src/main/java/dev/ltms/bridged/peer/PeerHandle.java @@ -74,9 +74,11 @@ public interface PeerHandle { * adapter, or a launcher before this field); the session registry stores it so the spawn result * and the roster row can show an operator which charter a member actually got. * + *
Deliberately not a {@code default}: a decorator that forgets to override this silently + * answers {@code null} for a question it has no basis to answer, and the gap surfaces only as + * a missing roster field, not a compile error. Every implementation must answer explicitly. + * * @return the fingerprint, or {@code null} when the launcher carries none */ - default CharterReceipt charterReceipt() { - return null; - } + CharterReceipt charterReceipt(); } diff --git a/bridged/src/test/java/dev/ltms/bridged/member/CompositePeerLauncherTest.java b/bridged/src/test/java/dev/ltms/bridged/member/CompositePeerLauncherTest.java index f73ccf8..b1f1994 100644 --- a/bridged/src/test/java/dev/ltms/bridged/member/CompositePeerLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/member/CompositePeerLauncherTest.java @@ -10,6 +10,7 @@ import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.FakeHerdr; import dev.ltms.bridged.herdr.WorkspaceControl; import dev.ltms.bridged.peer.Capability; +import dev.ltms.bridged.peer.CharterReceipt; import dev.ltms.bridged.peer.MemberRole; import dev.ltms.bridged.peer.PeerHandle; import dev.ltms.bridged.peer.PeerLauncher; @@ -93,6 +94,7 @@ class CompositePeerLauncherTest { @Override public String id() { return "pane-" + p; } @Override public String terminalId() { return "term-" + p; } @Override public String profile() { return p; } + @Override public CharterReceipt charterReceipt() { return null; } }; } diff --git a/bridged/src/test/java/dev/ltms/bridged/member/OpenCodeLauncherTest.java b/bridged/src/test/java/dev/ltms/bridged/member/OpenCodeLauncherTest.java index b4dee40..5c1600e 100644 --- a/bridged/src/test/java/dev/ltms/bridged/member/OpenCodeLauncherTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/member/OpenCodeLauncherTest.java @@ -7,6 +7,7 @@ import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.FakeHerdr; import dev.ltms.bridged.herdr.WorkspaceControl; import dev.ltms.bridged.peer.Capability; +import dev.ltms.bridged.peer.CharterReceipt; import dev.ltms.bridged.peer.PeerHandle; import dev.ltms.bridged.peer.PeerUnreachableException; import dev.ltms.bridged.peer.SpawnRequest; @@ -322,6 +323,26 @@ class OpenCodeLauncherTest { assertFalse(herdr.called("agent.get"), "no polling when the gate is disabled"); } + @Test + void handleCarriesTheRealCharterReceiptNotTheInterfaceDefault(@TempDir Path root) { + // The base's WorkerHandle computes a real CharterReceipt (CB-571), but the opencode adapter + // wraps it in SessionAwareHandle for lazy session discovery. Before this fix that decorator + // did not override charterReceipt(), so it silently inherited PeerHandle's `null` default + // and the real receipt sitting on its delegate was lost. + FakeHerdr herdr = new FakeHerdr(); + BridgedConfig.Fleet fleet = new BridgedConfig.Fleet(Map.of(), Map.of(), Map.of(), Map.of(), + Map.of("dev", "role rule"), null); + PeerHandle handle = service(herdr, root, + opencodeCfg("google/gemini-2.5-pro", "http://127.0.0.1:8765/mcp", null), () -> fleet) + .spawn(new SpawnRequest(null, null, null)); + + assertNotNull(handle.charterReceipt(), + "an opencode spawn's charterReceipt() must not silently be null"); + String composed = "role rule\n\n" + HerdrPeerLauncher.REPLY_CHARTER; + assertEquals(CharterReceipt.digestOf(composed), handle.charterReceipt().charterSha256(), + "the receipt on the wrapped handle must match the exact composed charter bytes"); + } + // --- CB-508: pinned OpenAI-compatible endpoint (e.g. a local vLLM) --------------------------- /** A profile with a baseUrl but no model provider prefix cannot be resolved — fail loudly. */