CB-517: add bridge_whoami and make the bridge prompt a portable charter
CI / build (push) Successful in 1m25s
CI / build (push) Successful in 1m25s
The communication rules lived only in two opt-in skills, so nothing always-on told the primary how to orchestrate and nothing guaranteed a worker loaded its playbook. Move protocol and policy into CLAUDE.md, which a worker inherits for free (its worktree is a checkout of this repo), and leave the skills as pure per-job procedure. bridge_whoami closes the load-bearing gap: every tool already consumed the caller identity ConnectionIdentity resolves from the connection, but none reported it, so an agent had to infer its own role from side channels the daemon does not control. Guessing fails asymmetrically — a primary acting as a worker is refused by the authz gate and learns at once, while a worker acting as the primary ends its turn without bridge_reply and the sender silently receives nothing. The tool reuses the same Principal the gate is built on, so the two cannot disagree; the primary gets role only (handing it a sessionId it does not own would invite the forged reply Authz refuses), and a worker missing from the registry still gets role + sessionId rather than 'unknown'. The CLAUDE.md block is written to be copied as-is into any project that mounts the bridge: repo-local details (Authz paths, the .mcp.json/wiki exclusions, the skill names) moved below it into a project addendum, and every role-inference fallback is stated one-way — the mount-name signal only holds for mcp__bridge__* (the launcher fixes it), not for the primary's mount, which each project names itself. The wiki carries the block verbatim as the template, with a sync check. Because this repo IS the bridge, that block is shipped surface, not documentation: the addendum adds a mandatory checklist mapping each part of the code to the part of the prompt it can invalidate. Also: delegate-by-default policy for the primary — the test is not 'could I do this faster myself' but 'can I write a brief good enough for a worker'. mvn clean install: 356 tests green (353 + 3 for whoami); ide_diagnostics clean on both changed files.
This commit is contained in:
@@ -198,6 +198,11 @@ public final class BridgeMcp {
|
||||
if (denied != null) return denied;
|
||||
return profiles(workers);
|
||||
})
|
||||
.toolCall(whoamiTool(), (exchange, _) -> {
|
||||
McpSchema.CallToolResult denied = deny(exchange, Authz.Action.READ, null);
|
||||
if (denied != null) return denied;
|
||||
return whoami(principal(exchange), sessions);
|
||||
})
|
||||
.build();
|
||||
this.authz = callers;
|
||||
this.metrics = metrics;
|
||||
@@ -460,6 +465,49 @@ public final class BridgeMcp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code bridge_whoami}: the caller's own identity, as the daemon already resolved it.
|
||||
*
|
||||
* <p>Every other tool <em>consumes</em> this identity — the authorization gate, the reply
|
||||
* rendezvous, the cwd inherit — but none reported it, so an agent had to infer its own role
|
||||
* from side channels the daemon does not control: a charter string in its system prompt, the
|
||||
* name its MCP mount happens to carry, or {@code ANTHROPIC_BASE_URL} (which Claude-model
|
||||
* workers do not set). The failure mode of guessing is asymmetric and silent: a primary that
|
||||
* mistakes itself for a worker is refused by {@link Authz} and learns immediately, while a
|
||||
* worker that mistakes itself for the primary ends its turn without {@code bridge_reply} and
|
||||
* the sender simply receives nothing. This tool removes the guess.
|
||||
*
|
||||
* <p>For a worker the session registry adds what it knows about that session. A worker the
|
||||
* registry has no record of — one that outlived a daemon restart — still gets its role and
|
||||
* {@code sessionId}, which is the load-bearing part.
|
||||
*/
|
||||
static McpSchema.CallToolResult whoami(Principal caller, SessionManager sessions) {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("role", caller.role().name().toLowerCase());
|
||||
if (!caller.isWorker()) {
|
||||
return text(json(m));
|
||||
}
|
||||
m.put("sessionId", caller.terminal());
|
||||
sessions.roster().stream()
|
||||
.filter(s -> caller.terminal().equals(s.terminalId()))
|
||||
.findFirst()
|
||||
.ifPresent(s -> {
|
||||
m.put("paneId", s.paneId());
|
||||
m.put("profile", s.profile());
|
||||
m.put("state", s.state().name().toLowerCase());
|
||||
if (s.worktree() != null) {
|
||||
m.put("worktree", s.worktree());
|
||||
}
|
||||
if (s.branch() != null) {
|
||||
m.put("branch", s.branch());
|
||||
}
|
||||
if (s.ownerTerminal() != null) {
|
||||
m.put("owner", s.ownerTerminal());
|
||||
}
|
||||
});
|
||||
return text(json(m));
|
||||
}
|
||||
|
||||
// --- fleet management logic (CB-108 / CB-301) --------------------------------------------
|
||||
|
||||
/** {@code bridge_spawn} without cwd/caller context (default resolution). */
|
||||
@@ -689,6 +737,18 @@ public final class BridgeMcp {
|
||||
List.of("sessionId")));
|
||||
}
|
||||
|
||||
private static McpSchema.Tool whoamiTool() {
|
||||
return tool("bridge_whoami",
|
||||
"Report who YOU are on the bridge — your role is resolved from your connection "
|
||||
+ "(unforgeable), never from anything you claim. Returns role 'primary' (you "
|
||||
+ "orchestrate: spawn/send/stop, and you must never call bridge_reply) or "
|
||||
+ "'worker' (you were delegated to: you must end every turn with exactly one "
|
||||
+ "bridge_reply, and cannot spawn or send), plus your own sessionId, profile, "
|
||||
+ "worktree and branch when you are a worker. Call this first when following "
|
||||
+ "role-conditional instructions rather than guessing your role.",
|
||||
objectSchema(Map.of(), List.of()));
|
||||
}
|
||||
|
||||
// --- small helpers -------------------------------------------------------------------------
|
||||
|
||||
// The SDK 2.0.0 deprecates its own Tool builders without a stable replacement — isolate it here.
|
||||
|
||||
Reference in New Issue
Block a user