From 35ade146300eb54e12f3e92c1ca8f8c20815684c Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 15 Aug 2026 09:55:52 +0200 Subject: [PATCH] CB-571: make PeerHandle.charterReceipt() abstract, fix OpenCode adapter's silent null SessionAwareHandle wrapped the base's WorkerHandle but never overrode charterReceipt(), so it silently inherited the interface default (null) while the real receipt sat on its delegate. sol/terra never got a charterSource/charterSha256 roster row. Deletes the default so every PeerHandle must answer explicitly; the compiler now catches this class of gap instead of a roster field quietly going missing. --- .../ltms/bridged/member/OpenCodeLauncher.java | 6 ++++++ .../dev/ltms/bridged/peer/PeerHandle.java | 8 ++++--- .../member/CompositePeerLauncherTest.java | 2 ++ .../bridged/member/OpenCodeLauncherTest.java | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) 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. */