CB-1xx: worker launch via ccs + inline bridge MCP mount (step 4) + port 8765

Spawn a real worker with 'ccs ltms-local' (profile sets CLAUDE_CONFIG_DIR + off-sub base_url; auto mode preconfigured as defaultMode:auto). bridged appends the bridge MCP mount (--mcp-config, inline JSON) and the reply charter (--append-system-prompt) as launch FLAGS — non-invasive, nothing written to the worker's profile (safer than provisioning its config dir, which would clobber it). Identity is connection-based so the mount is shared. config: worker.mcpUrl. Default bind port 8080 -> 8765. The reply charter is guidance; the send timeout catches a non-cooperative worker. 60 tests green, IDE-clean.
This commit is contained in:
Dai Ha
2026-07-14 16:40:25 +02:00
parent fa570ab32a
commit 07722a6007
5 changed files with 114 additions and 12 deletions
+7 -1
View File
@@ -6,7 +6,7 @@
# REST + MCP listen address. Keep it on loopback — bridged is same-host in Stage-1.
bind:
host: 127.0.0.1
port: 8080
port: 8765
# herdr Unix socket. Omit to use the client default
# (${HERDR_SOCKET_PATH:-~/.config/herdr/herdr.sock}).
@@ -24,6 +24,12 @@ worker:
placement: tab # tab | pane
workspace: bridged-workers # the dedicated worker space (found-or-created, shared)
tabLabel: "worker: {profile} #{n}" # {profile}/{model}/{n} substituted; {n} keeps sibling tabs distinct
# Step 4 — real worker: launch via ccs (profile sets CLAUDE_CONFIG_DIR + off-sub base_url; put
# `defaultMode: "auto"` in the profile so it runs autonomously). bridged appends the bridge MCP
# (--mcp-config, inline) + reply charter (--append-system-prompt) as flags — nothing is written
# to the profile. Omit mcpUrl (and use a bash argv) for a mechanics-only probe that never replies.
mcpUrl: http://127.0.0.1:8765/mcp
argv: ["ccs", "ltms-local"]
# Subscription boundary. A worker's base_url host MUST be one of these; the primary
# must carry none. Grounded in ltms-local's real endpoints.
@@ -32,7 +32,7 @@ public record BridgedConfig(
public record Bind(String host, int port) {
public Bind {
if (host == null || host.isBlank()) host = "127.0.0.1";
if (port <= 0) port = 8080;
if (port <= 0) port = 8765;
}
}
@@ -53,11 +53,14 @@ public record BridgedConfig(
* @param tabLabel template for a worker tab's label; {@code {profile}}/{@code {model}}
* and {@code {n}} (per-worker number, to keep sibling tabs distinct)
* are substituted (default {@code "worker: {profile} #{n}"})
* @param mcpUrl bridge MCP URL to provision into the worker's {@code configDir} so it
* can call {@code bridge_reply} ({@code null}/blank → no provisioning; the
* worker won't reply, only the fallback/timeout resolves the send)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record Worker(String profile, String baseUrl, String model,
String configDir, String tokenEnv, List<String> argv,
String placement, String workspace, String tabLabel) {
String placement, String workspace, String tabLabel, String mcpUrl) {
public Worker {
argv = (argv == null || argv.isEmpty()) ? List.of("claude") : List.copyOf(argv);
tokenEnv = (tokenEnv == null || tokenEnv.isBlank()) ? "BRIDGED_WORKER_TOKEN" : tokenEnv;
@@ -71,6 +74,11 @@ public record BridgedConfig(
return "tab".equals(placement);
}
/** True when the bridge MCP should be mounted into a spawned worker (via launch flags). */
public boolean hasMcp() {
return mcpUrl != null && !mcpUrl.isBlank();
}
/**
* Render {@link #tabLabel} for the {@code n}-th worker (substitutes
* {@code {profile}}/{@code {model}}/{@code {n}}), so sibling worker tabs are distinct.
@@ -12,6 +12,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -46,6 +47,18 @@ public final class WorkerService {
private final BridgedConfig.Worker cfg;
private final Function<String, String> env; // host env lookup (injectable for tests)
private final AtomicLong nameSeq = new AtomicLong(); // per-worker counter (also the tab #)
/**
* Standing instruction appended to the worker's system prompt so it returns its result via
* {@code bridge_reply}. Injected as a launch flag, so nothing is written to the worker's
* profile — it is guidance, and a worker that never replies is caught by the send's timeout.
*/
static final String REPLY_CHARTER =
"You are a worker in the claude-bridge fleet. You receive one delegated task at a time "
+ "as a prompt. When you finish the task, immediately call the bridge_reply MCP tool "
+ "with `content` set to your answer — do not ask for confirmation; that reply is how "
+ "the delegator receives your result.";
// Per-process token mixed into each worker name so a fresh process (nameSeq back at 0)
// cannot collide with same-profile workers that outlived a restart. See startUniquelyNamed.
private final String nameNonce = String.format("%06x", new SecureRandom().nextInt(1 << 24));
@@ -71,11 +84,34 @@ public final class WorkerService {
String token = env.apply(cfg.tokenEnv());
putIfPresent(workerEnv, "ANTHROPIC_AUTH_TOKEN", token);
return cfg.tabPlacement() ? spawnInTab(workerEnv) : spawnAsPane(workerEnv);
// Mount the bridge MCP + reply charter as launch FLAGS (non-invasive: nothing written to
// the worker's profile/config dir). Identity is connection-based, so the mount is shared.
List<String> argv = argvWithBridge();
return cfg.tabPlacement() ? spawnInTab(workerEnv, argv) : spawnAsPane(workerEnv, argv);
}
/**
* The launch argv, plus — when {@code worker.mcpUrl} is set — inline {@code --mcp-config} for
* the bridge server and {@code --append-system-prompt} for the {@link #REPLY_CHARTER}. Neither
* touches the profile's config; both are pure command-line flags.
*/
private List<String> argvWithBridge() {
if (!cfg.hasMcp()) {
return cfg.argv();
}
String mcpJson = "{\"mcpServers\":{\"bridge\":{\"type\":\"http\",\"url\":\""
+ cfg.mcpUrl() + "\"}}}";
List<String> argv = new ArrayList<>(cfg.argv());
argv.add("--mcp-config");
argv.add(mcpJson);
argv.add("--append-system-prompt");
argv.add(REPLY_CHARTER);
return argv;
}
/** Dedicated worker space → own tab → drop the placeholder shell so only the worker remains. */
private Agent spawnInTab(Map<String, String> workerEnv) {
private Agent spawnInTab(Map<String, String> workerEnv, List<String> argv) {
Workspace space = spaces.ensureWorkspace(cfg.workspace());
Tab.Created tab = spaces.createTab(space.workspaceId());
log.info("spawning worker profile={} base_url={} space={} tab={}",
@@ -83,7 +119,7 @@ public final class WorkerService {
Started started;
try {
started = startUniquelyNamed(workerEnv, tab.tab().tabId());
started = startUniquelyNamed(workerEnv, argv, tab.tab().tabId());
} catch (RuntimeException e) {
// The worker never started — don't leave the tab we just created orphaned.
// Best-effort cleanup; never let it mask the real spawn failure.
@@ -123,10 +159,10 @@ public final class WorkerService {
}
/** Legacy placement: herdr splits the currently-focused tab. */
private Agent spawnAsPane(Map<String, String> workerEnv) {
private Agent spawnAsPane(Map<String, String> workerEnv, List<String> argv) {
log.info("spawning worker (pane placement) profile={} base_url={} argv={}",
cfg.profile(), cfg.baseUrl(), cfg.argv());
Agent worker = startUniquelyNamed(workerEnv, null).agent();
cfg.profile(), cfg.baseUrl(), argv);
Agent worker = startUniquelyNamed(workerEnv, argv, null).agent();
log.info("worker started pane={} terminal={}", worker.paneId(), worker.terminalId());
return worker;
}
@@ -145,13 +181,13 @@ public final class WorkerService {
* retry is a belt-and-braces backstop for the astronomically unlikely nonce+seq clash;
* the name is a label only — herdr detects kind and status from terminal output, not it.
*/
private Started startUniquelyNamed(Map<String, String> workerEnv, String tabId) {
private Started startUniquelyNamed(Map<String, String> workerEnv, List<String> argv, String tabId) {
HerdrException last = null;
for (int attempt = 0; attempt < NAME_RETRIES; attempt++) {
long seq = nameSeq.incrementAndGet();
String name = "claude-" + cfg.profile() + "-" + nameNonce + "-" + seq;
try {
return new Started(agents.start(name, cfg.argv(), workerEnv, tabId), seq);
return new Started(agents.start(name, argv, workerEnv, tabId), seq);
} catch (HerdrException e) {
if (!"agent_name_taken".equals(e.code())) throw e;
log.debug("worker name '{}' taken, retrying", name);
@@ -52,7 +52,7 @@ class BridgedAppTest {
private int start(FakeHerdr herdr, String workerBaseUrl, Set<String> allow, String placement) {
BridgedConfig.Worker wcfg = new BridgedConfig.Worker(
"ltms-local", workerBaseUrl, "coder", null, "BRIDGED_WORKER_TOKEN", null,
placement, "bridged-workers", "worker: {profile} #{n}");
placement, "bridged-workers", "worker: {profile} #{n}", null);
AgentControl agents = new AgentControl(herdr);
WorkerService workers = new WorkerService(
agents, new WorkspaceControl(herdr), new SubscriptionGuard(allow), wcfg,
@@ -0,0 +1,52 @@
package dev.ltms.bridged.worker;
import dev.ltms.bridged.config.BridgedConfig;
import dev.ltms.bridged.guard.SubscriptionGuard;
import dev.ltms.bridged.herdr.AgentControl;
import dev.ltms.bridged.herdr.FakeHerdr;
import dev.ltms.bridged.herdr.WorkspaceControl;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
/** The step-4 launch-flag injection: the bridge MCP + reply charter are appended to the argv. */
class WorkerServiceTest {
private WorkerService service(FakeHerdr herdr, List<String> argv, String mcpUrl) {
BridgedConfig.Worker cfg = new BridgedConfig.Worker(
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
argv, "tab", "bridged-workers", "worker: {profile} #{n}", mcpUrl);
return new WorkerService(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), cfg, _ -> null);
}
@SuppressWarnings("unchecked")
private List<String> spawnedArgv(FakeHerdr herdr) {
return (List<String>) ((Map<String, Object>) herdr.lastCall("agent.start").params()).get("argv");
}
@Test
void appendsBridgeMcpAndReplyCharterWhenMcpUrlSet() {
FakeHerdr herdr = new FakeHerdr();
service(herdr, List.of("ccs", "ltms-local"), "http://127.0.0.1:8765/mcp").spawn();
List<String> argv = spawnedArgv(herdr);
assertEquals(List.of("ccs", "ltms-local"), argv.subList(0, 2), "base command preserved first");
assertTrue(argv.contains("--mcp-config"));
assertTrue(argv.stream().anyMatch(a -> a.contains("\"bridge\"") && a.contains("http://127.0.0.1:8765/mcp")),
"inline bridge MCP config present");
assertTrue(argv.contains("--append-system-prompt"));
assertTrue(argv.stream().anyMatch(a -> a.contains("bridge_reply")), "reply charter present");
}
@Test
void noBridgeFlagsWhenMcpUrlAbsent() {
FakeHerdr herdr = new FakeHerdr();
service(herdr, List.of("bash", "-c", "sleep 1"), null).spawn();
assertEquals(List.of("bash", "-c", "sleep 1"), spawnedArgv(herdr), "argv untouched without mcpUrl");
}
}