CB-517: add bridge_whoami and make the bridge prompt a portable charter
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:
Dai Ha
2026-08-04 16:01:16 +02:00
parent cf4ad186ab
commit 979b2b5632
6 changed files with 355 additions and 113 deletions
+37 -54
View File
@@ -1,24 +1,21 @@
---
name: implementer
description: Implementer-role playbook for a bridged worker — you are in an isolated git worktree on a dedicated branch; implement the assigned task, commit, push, open your own PR to main, and hand off the PR URL via bridge_reply. You never merge. Load this when you have been delegated an implementation task over bridged.
description: Implementer-role procedure for a bridged worker — verify your worktree, implement the scope, commit, push, open your own PR, and hand off the PR URL. Load this when the lead delegates you an implementation task over bridged.
---
# Implementer worker
# Implementer worker — procedure
You are an **implementer** in the claude-bridge fleet. The lead delegated you one scoped task,
and you are running in an **isolated git worktree on your own branch** — a full peer of the
primary (same `CLAUDE.md`, skills, memory, MCP), differing only in the model behind you and the
branch you sit on. Your job for this turn: **implement the task, then hand off a PR the lead can
review and merge.** You do the work; the lead (or human) is the merge gate — you never merge.
The turn contract (one `bridge_reply`, `bridge_ask` for the lead's decisions, honest reporting,
never merge, never commit `.mcp.json` or `wiki/`) is in **`CLAUDE.md` → Bridge communication →
Worker** and already applies. This skill is only the *implement-and-hand-off procedure*.
Delivery mechanics (how the task reached you, how your reply resolves the lead's blocked send)
are in [`docs/MCP-Contract.md`](../../../docs/MCP-Contract.md); the worktree/PR model is in
[`docs/Worker-Git-Workflow.md`](../../../docs/Worker-Git-Workflow.md). You only need the steps
below.
You run in an **isolated git worktree on your own branch** — a full peer of the primary (same
repo, `CLAUDE.md`, skills, MCP), differing in the model behind you and the branch you sit on.
The worktree model is documented in [`docs/Worker-Git-Workflow.md`](../../../docs/Worker-Git-Workflow.md).
## 1. Confirm where you are — a worktree on a dedicated branch
## 1. Confirm where you are
Before touching anything, verify your ground truth:
Before touching anything:
```bash
git rev-parse --show-toplevel # your worktree root — NOT the primary's main tree
@@ -26,46 +23,40 @@ git branch --show-current # your dedicated branch: worker/<ticket>-<nonce>
git status # should be clean at the start
```
Do **all** work here, on this branch. **Never** switch to `main`, never `git checkout main`,
never rebase onto or push to `main` directly. The branch is your isolation — respect it.
Do **all** work here, on this branch. Never `git checkout main`, never rebase onto or push to
`main`. The branch is your isolation — respect it.
## 2. Implement the task
## 2. Implement
- Implement exactly the scope the lead named. Keep changes focused; if you notice something out
of scope, note it in your reply rather than expanding the diff.
- Match the surrounding code's style, naming, and idioms. Follow project `CLAUDE.md`.
- **You cannot run the IDE MCP tools** (intellij-index / jetbrains are the primary's, not yours).
So **never claim a file is "IDE-clean" or "diagnostics-clean"** — you cannot verify that. State
only what you actually ran (e.g. `mvn`, a test) and its real output. A fabricated clean claim is
worse than an honest "I could not verify inspections here."
- Run whatever build/test you can and **report the true result** — including failures.
- Implement exactly the scope the lead named. Keep the diff focused; note anything out of scope
in your reply instead of widening it.
- Match the surrounding code's style, naming, and idioms.
- Run whatever build/test you can — `mvn clean install` from the module root. Read its **full**
output; a piped `mvn ... | tail` hides failures.
## 3. Commit — focused, and never the excluded files
## 3. Commit
```bash
git add <the files you changed>
git add <the files you changed> # explicitly — never `git add -A` / `git add .`
git commit -m "<ticket>: <clear one-line summary>"
```
**Excluded from every commit, always:** `.mcp.json` (the primary's local, session-modified copy —
present only for parity) and `wiki/` (a separate submodule). Stage files explicitly; do **not**
`git add -A` / `git add .` blindly, or you risk staging them. If `.mcp.json` shows as modified,
leave it — it is flagged `--skip-worktree` and is not yours to commit.
`.mcp.json` will show as modified. Leave it — it is `--skip-worktree` and not yours to commit.
## 4. Push your branch
## 4. Push
```bash
git push -u origin HEAD
```
Push is over SSH as the same user — no extra credential needed. Push the branch as-is; do not
force-push over anything you did not create.
Push is over SSH as the same user — no extra credential needed. Never force-push over anything
you did not create.
## 5. Open your own PR to `main`
Open the PR via the gitea REST API. The daemon injected a **repo-scoped token** (`GITEA_TOKEN`)
and the forge host (`GITEA_HOST`) into your env for exactly this — the token can create a PR but
**cannot merge** (that stays the lead/human gate).
Via the gitea REST API. The daemon injected a **repo-scoped token** (`GITEA_TOKEN`) and the forge
host (`GITEA_HOST`) into your env for exactly this — the token can create a PR but **cannot
merge**.
```bash
API="${GITEA_HOST%/}/api/v1/repos/lms/claude-bridge/pulls"
@@ -81,16 +72,14 @@ JSON
)"
```
The response JSON includes `"html_url"` — that is your PR URL. If the call fails (non-2xx), read
the error body, fix the cause if it is yours (e.g. branch not pushed yet), and report the failure
honestly in your reply rather than inventing a URL. If `GITEA_TOKEN` is unset, your profile was
not granted PR-create — push the branch (step 4) and report the branch name so the lead opens the
PR.
The response JSON carries `"html_url"` — that is your PR URL. On a non-2xx, read the error body,
fix it if the cause is yours (e.g. branch not pushed yet), and report the failure rather than
inventing a URL. If `GITEA_TOKEN` is unset your profile was not granted PR-create: push the branch
and report its name so the lead opens the PR.
## 6. Reply via `bridge_reply` — the PR is the handoff
## 6. Hand off — what goes in `bridge_reply`
End your turn with **exactly one** `bridge_reply`. That reply is the entire handoff — the lead
cannot see your terminal. Include:
The reply is the entire handoff; the lead cannot see your terminal.
```
PR: <html_url from step 5, or "not created: <reason>" + branch name>
@@ -100,27 +89,21 @@ tests: <what you ran and its REAL result — or "not run: <why>">
summary: <2-3 lines: what you implemented and any caveat the reviewer needs>
```
Then stop. **Do not merge. Do not touch `.mcp.json` or `wiki/`.** One reply closes the turn.
```mermaid
sequenceDiagram
autonumber
participant L as Lead
participant B as bridged
participant I as Implementer (you)
participant G as git / gitea
L->>B: bridge_send(task) — blocks
B-->>I: your assignment (in a worktree on your branch)
L->>I: delegated task (you are in a worktree on your branch)
I->>I: implement + build/test here
I->>G: git commit (never .mcp.json / wiki)
I->>G: git push -u origin HEAD
I->>G: POST /pulls (GITEA_TOKEN) — open PR to main
G-->>I: html_url
I->>B: bridge_reply(PR url, branch, files, tests)
B-->>L: { outcome:"reply", text }
Note over L,G: lead reviews the PR, merges on green — you never merge
I->>L: bridge_reply(PR url, branch, files, tests)
Note over L,G: the lead reviews the PR and merges on green — you never merge
```
*The implement turn: work in the worktree, commit → push → open the PR, hand off the URL. The
lead is the merge gate.*
*The implement turn: work in the worktree, commit → push → open the PR, hand off the URL.*
+25 -58
View File
@@ -1,69 +1,39 @@
---
name: reviewer
description: Reviewer-role playbook for a bridged worker — read the assigned scope, find the real issues, ask the lead via bridge_ask when a decision is genuinely theirs, and report the finding via bridge_reply. Load this when you have been delegated a code review over bridged.
description: Reviewer-role procedure for a bridged worker — how to work a review scope and the exact shape of the finding to report. Load this when the lead delegates you a code review over bridged.
---
# Reviewer worker
# Reviewer worker — procedure
You are a **reviewer** in the claude-bridge fleet. The lead delegated you one scoped review
over `bridged`, and your whole job is **this single turn**: examine the scope it named, and
report back. You are not the owner of the code and you do not merge anything — you surface
what the owner needs to know, then hand the turn back.
Delivery mechanics (how the task reached you, how your reply resolves the lead's blocked
send) are in [`docs/MCP-Contract.md`](../../../docs/MCP-Contract.md); you only need the three
rules below.
The turn contract (one `bridge_reply`, `bridge_ask` for the lead's decisions, honest reporting,
never merge) is in **`CLAUDE.md` → Bridge communication → Worker** and already applies. This
skill is only the *review procedure*: how to work the scope, and the exact shape of what you
send back.
## 1. Read the whole scope before you judge
The delegation names your scope — a file, a diff, a PR, a function. **Read all of it first.**
A review that fires on a snippet misses the caller that makes it safe (or the one that makes
it a bug). Reviewing only part of the scope and guessing the rest is the most common way a
reviewer worker is wrong.
A review that fires on a snippet misses the caller that makes it safe (or the one that makes it
a bug). Reviewing part of the scope and guessing the rest is the most common way a reviewer is
wrong.
## 2. Stay in your lane
## 2. Stay in the scope
- Review **only** the assigned scope. If you notice something elsewhere, mention it in one
line — do **not** go hunt it. Wandering is how two workers end up reporting the same thing
and neither covers what it was given.
- Do **not** edit files, run the build, or spawn other workers. You review; the owner acts.
- You never set `ANTHROPIC_BASE_URL` and never touch herdr — you are a Claude Code process,
not part of the transport.
- Review **only** what you were assigned. Something elsewhere looks wrong? One line in your
reply — do not go hunt it. Wandering is how two reviewers report the same thing and neither
covers what it was given.
- Do **not** edit files or run the build. You review; the owner acts.
## 3. When the decision is the lead's — ask, don't guess
## 3. Reach for `bridge_ask` only for a genuine fork
Some things you cannot resolve from the code: an ambiguous requirement, a missing acceptance
criterion, "is this behavior intended or a bug?", or a choice between two defensible fixes.
Guessing there produces a confident-but-wrong finding. Instead **pause and ask the lead** with
`bridge_ask` — a single crisp question. The call blocks; when the lead answers you **resume
the same turn** with the answer and finish. Ask only when the answer changes your finding;
don't narrate options you could decide yourself.
Ambiguous requirement, a missing acceptance criterion, "intended or a bug?", or two defensible
fixes with different consequences — those are the lead's call, and guessing produces a
confident-but-wrong finding. Anything you could settle by reading more code is yours to settle.
```mermaid
sequenceDiagram
participant L as Lead
participant B as bridged
participant R as Reviewer (you)
## 4. The finding — what goes in `bridge_reply`
L->>B: bridge_send(review scope) — blocks
B-->>R: your assignment
R->>R: read the full scope
opt a decision only the lead can make
R->>B: bridge_ask("intended, or a bug?") — you block
B-->>L: { outcome:"question", turn_id }
L->>B: bridge_send(answer, turn_id)
B-->>R: { answer } — you resume the SAME turn
end
R->>B: bridge_reply(structured finding) — ends your turn
B-->>L: { outcome:"reply", text }
```
*The review turn, with the optional `bridge_ask` detour when the call is the lead's to make.*
## 4. Report with `bridge_reply` — one structured finding
End your turn with **exactly one** `bridge_reply`. Report the **single most important** real
issue in the scope, in these four lines, under ~90 words:
Report the **single most important** real issue in the scope, in these four lines, under
~90 words:
```
1. <path>:<line>
@@ -72,13 +42,10 @@ issue in the scope, in these four lines, under ~90 words:
4. severity: high | medium | low
```
- Found nothing real after reading? Reply `NO ISSUE` and one line saying why — a clean review
is a valid result, and a fabricated issue is worse than none.
- **Nothing real after reading?** Reply `NO ISSUE` and one line saying why. A clean review is a
valid result; a fabricated issue is worse than none.
- **Severity:** `high` = wrong result, data loss, security, or a hang/crash on a real path ·
`medium` = a real bug on an edge path, or a correctness risk under load/concurrency ·
`low` = clarity, a latent foot-gun, or a smell with no current failure.
- Be specific and verifiable: a line number and a one-line repro beat an adjective. If you
can't point to where it goes wrong, you haven't found it yet.
One reply closes the turn. If you asked mid-turn, the answer you got is already folded into
this finding — you do not ask again after replying.
- Be specific and verifiable: a line number and a one-line repro beat an adjective. If you can't
point at where it goes wrong, you haven't found it yet.
+177
View File
@@ -1,7 +1,184 @@
# claude-bridge — project instructions
## Bridge communication (enforced — read this first)
> **Canonical block.** Everything down to §Layering is the portable bridge charter, copied verbatim
> into every project that mounts the bridge MCP. Keep it byte-identical with the template in the
> wiki ([Use Cases](https://git.ltms.dev/lms/claude-bridge/wiki/7-Use-Cases) → *The portable
> CLAUDE.md block*); improvements go to the template first, then out to each project. Anything
> specific to *this* repo lives under §Project addendum below, never inline above it.
If no `bridge_*` MCP tools are mounted in this session, this section does not apply — skip it.
`bridged` is the **sole communication gateway** between agents here. The orchestrating session (the
**primary**) and every delegated peer (a **worker**) mount the *same* MCP server and talk only
through its `bridge_*` tools. No session addresses a peer, a broker, or the network directly.
### Which role am I? — settle this before acting
**Both roles read this file.** A worker runs in a git worktree of this same repo, so it inherits
this `CLAUDE.md` verbatim, and every rule below is role-conditional.
**Call `bridge_whoami`.** It returns `{"role":"primary"}` or `{"role":"worker","sessionId":…,
"profile":…,"worktree":…,"branch":…}`, resolved by the daemon from your connection — unforgeable,
and the same resolution its authorization gate uses. Don't infer what you can ask.
Only if that call is unavailable, fall back to these — each is one-way, so keep reading until one
fires: the reply charter in your system prompt (*"You are an off-subscription worker in the
claude-bridge fleet"*) ⇒ **worker**; bridge tools prefixed `mcp__bridge__*` ⇒ **worker** (the
launcher fixes that mount name; a primary's mount is named by whoever wrote its `.mcp.json`, so it
varies); `ANTHROPIC_BASE_URL` set ⇒ **worker** (Claude-model workers run on a clean env, so its
*absence* proves nothing). **Still unsure ⇒ act as a worker.** The two mistakes are not symmetric: a
primary acting as a worker is refused by the authorization gate — loud and self-correcting — while a
worker acting as the primary ends its turn with no `bridge_reply`, and the sender silently receives
nothing. Fail toward the recoverable error.
### Invariants — both roles, no exceptions
1. **Never set, export, or forward `ANTHROPIC_BASE_URL`** (or `ANTHROPIC_AUTH_TOKEN`). The primary
stays on subscription; only the bridge puts a worker off it, at spawn. Mounting the bridge must
never move a session across that boundary.
2. **The bridge is the only channel.** Text you print in your terminal reaches nobody — the other
side cannot see your screen. An answer that isn't in a `bridge_*` call is silently discarded.
3. **Identity comes from the connection, never an argument.** Workers never pass a target; you
cannot act as another session. Spawn/stop/send/drain are primary-only; reply/ask are
worker-only-and-only-as-itself. A call outside your role is refused, not queued.
4. **Delivery is status-gated: one message per turn.** Don't busy-poll a peer's terminal and don't
re-send because a call looks slow — the bridge delivers when the peer is `idle`/`blocked`.
5. **Never drive the terminal multiplexer directly** (no `herdr` CLI, no socket). The bridge owns
policy; the multiplexer owns PTYs. Going around the bridge bypasses every rule above.
### Primary (lead) — orchestration
**Delegate by default — that is the job.** With the bridge mounted you are an orchestrator on a
metered subscription, and workers are cheap, parallel, and disposable. The default answer to "who
does this?" is **a worker**, not you. Reach for `bridge_send` before you reach for `Edit`.
- **Delegate**: implementation behind a clear spec, test writing, per-file or wide-area review, log
and failure triage, mechanical refactors, doc passes, and any investigation with a stated
question. If the unit of work is independent, fan out — one worker per file, area, or dimension —
and reduce the replies yourself.
- **Keep**: the conversation with the user, decomposition and planning, the final judgment call,
merges, and anything that depends on context only you hold.
- **The bar is not "could I do this faster myself?"** — usually you could. It is **"can I write a
brief good enough for a worker to succeed?"** If yes, write the brief and send it. A wasted worker
turn costs a worker turn; doing it yourself costs your context and your subscription.
- **Parallelize instead of serializing.** For independent units, spawn one worktree worker each
(`bridge_spawn{worktree:true, ticket:…}`), dispatch every one with `wait:false`, then poll the
tickets. Waiting for worker A before briefing worker B is the most common way this layer is wasted.
- **Delegating does not delegate responsibility.** You still review, verify, and merge.
| Intent | Tool |
|---|---|
| Confirm your own role | `bridge_whoami` |
| See backends available | `bridge_profiles` |
| Start a worker | `bridge_spawn{profile?, cwd?, worktree?, ticket?}` → `sessionId` + `paneId` |
| See the fleet | `bridge_list` · one worker's state: `bridge_status{sessionId}` |
| Delegate (blocking) | `bridge_send{sessionId, content}` |
| Delegate (long task) | `bridge_send{sessionId, content, wait:false}` → ticket → `bridge_poll{ticket}` |
| Answer a worker's `bridge_ask` | `bridge_send{turnId, content}` — **not** `sessionId` |
| Collect a held reply | `bridge_poll{target}` · then `bridge_ack{target, msgId}` |
| Tear down | `bridge_stop{paneId}` |
- **Pass `profile:` explicitly.** Profiles differ in model and cost, not in tier — don't assume the
default is what you want.
- **Prefer `wait:false` + `bridge_poll` for anything non-trivial.** A blocking `bridge_send` is
capped by *your own* MCP client call timeout (~60s), well below the task's real runtime; the ticket
path is what survives a long task.
- **Every delegation names the worker's playbook.** If this project ships role skills, make the
first line of `content` `Load the <name> skill.` — those skills are opt-in, and that line is what
makes them reliable. With no such skill, spell the procedure out in the brief instead.
- **A delegation must be self-contained**: scope, the files or PR in question, acceptance criteria,
and exactly what to report back. The worker sees your message and the repo — nothing of your
context, your plan, or your screen.
- **You are the gate.** Workers open PRs; you review and merge. Never delegate the merge.
- **Verify what a worker claims.** A worker mounts only the bridge MCP and cannot run your other
tooling, and a piped build command (`… | tail`) hides failures behind a zero exit — re-run the
build and the checks yourself before you believe "clean".
### Worker — the turn contract
1. **Load the playbook skill the lead named** before doing anything else.
2. **Do the assigned scope only.** Note anything you spot outside it in one line; don't go hunt it.
3. **`bridge_ask{question}`** when a decision is genuinely the lead's (ambiguous requirement, two
defensible fixes, "bug or intended?"). It blocks and you resume the *same* turn with the answer.
Don't ask what you could decide yourself.
4. **End the turn with exactly one `bridge_reply{content}`**, carrying your complete answer. This is
the whole handoff. No `bridge_reply` ⇒ the sender gets nothing and the exchange stalls.
5. **Report honestly.** State only what you actually ran and its real output, including failures.
You mount **only** the bridge MCP — the primary's other servers (IDE, forge, docs) are not yours,
so never claim the result of a check you had no way to run.
6. **Never merge.** Stage files explicitly — never `git add -A` — and leave alone anything the
project marks as not-yours-to-commit.
### Where each rule lives (don't duplicate — extend the right layer)
| Layer | Scope | Reaches |
|---|---|---|
| the launcher's reply charter | the one rule that must survive with no repo: *end every turn with `bridge_reply`* | every worker, at launch, every peer kind |
| **this section** | protocol + orchestration policy | primary **and** every Claude worker — tracked in git, so worktrees inherit it |
| role playbook skills | per-job procedure (commit/PR recipe, finding format) | a worker told to load one |
| the bridge's own docs | design detail, flows, error model | on demand |
A rule belongs in **exactly one** layer — the outermost one that must obey it. Peers that don't read
`CLAUDE.md` (non-Claude adapters) get the charter only, so any rule *they* must obey belongs in the
charter, not here.
## Project addendum — claude-bridge (not part of the canonical block)
- **This repo is the bridge.** The daemon is `bridged`, its MCP mount is `http://127.0.0.1:8765/mcp`,
and the code behind the rules above is `mcp/BridgeMcp` (tools), `auth/Authz` (the role table),
`mcp/ConnectionIdentity` (connection→role), and `worker/*Launcher` (`REPLY_CHARTER`).
- **Skills available to delegate:** `implementer` (worktree → commit → push → own PR) and
`reviewer` (scoped review → one structured finding). Name one in every delegation.
- **Never commit** `.mcp.json` (the primary's local copy, flagged `--skip-worktree`) or `wiki/`
(a submodule with its own remote).
- **Flows and the error model** — rendezvous, `bridge_ask`, detached delivery, turn-done fallback —
are diagrammed in `docs/MCP-Contract.md` §6, kept out of this file because it loads into every
session's context.
### The prompt is part of the product — update it with the code (mandatory)
This repo *is* the bridge, so the canonical block above is not documentation about someone else's
system: it is the instruction surface this codebase ships. **Every change here must end by asking
whether the block still tells the truth.** A code change that silently invalidates it is an
incomplete change — the agents reading it have no other source.
Before you call any work done, check the row that matches what you touched:
| You changed… | Re-read and update… |
|---|---|
| a `bridge_*` tool — added, removed, renamed, or its params/semantics | the primary's intent→tool table; any rule that names that tool |
| `Authz` / the role table | invariant 3, and the primary-only vs worker-only claims |
| `ConnectionIdentity` / how a caller is resolved | the `bridge_whoami` paragraph and the fallback ladder |
| `REPLY_CHARTER`, or a launcher's mount/flags | the fallback ladder (`mcp__bridge__*`), and the layering table's top row |
| the injector / status gating | invariant 4 |
| worktree provisioning or the parity overlay | the "both roles read this file" premise — it rests on the worker's worktree being a checkout of this repo |
| `.claude/skills/**` | the addendum's skill list, and the "name the playbook" rule |
| a new peer kind (non-Claude adapter) | what that peer can read — anything it must obey belongs in its charter, not in the block |
Then **propagate**: the block in this file and the template in the wiki
([Use Cases](https://git.ltms.dev/lms/claude-bridge/wiki/7-Use-Cases) → *The portable `CLAUDE.md`
block*) must stay byte-identical, and other projects carrying the block need the same edit. Verify
rather than trust:
```bash
python3 - <<'PY'
import pathlib
c = pathlib.Path("CLAUDE.md").read_text()
w = pathlib.Path("wiki/7-Use-Cases.md").read_text()
S, E = "## Bridge communication (enforced", "## Project addendum — claude-bridge"
block = c[c.index(S):c.index(E)].rstrip() + "\n"
i = w.index("```markdown\n") + len("```markdown\n")
print("in sync:", w[i:w.index("\n```\n", i) + 1] == block)
PY
```
## IDE MCP tools & validation workflow (enforced)
> **Primary only.** Workers have no IDE MCP mount — if you are a worker, skip this section and
> report the build/test output you actually ran (see §Bridge communication → Worker).
Two IDE MCP servers are connected: **intellij-index** (semantic code intelligence) and
**jetbrains** (file problems, reformat, debugger). IntelliJ has multiple projects open; our
module is **`bridged`**. Always pass these to IDE MCP tools:
@@ -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.
@@ -1,5 +1,6 @@
package dev.ltms.bridged.mcp;
import dev.ltms.bridged.auth.Principal;
import dev.ltms.bridged.config.BridgedConfig;
import dev.ltms.bridged.guard.SubscriptionGuard;
import dev.ltms.bridged.herdr.AgentControl;
@@ -343,4 +344,58 @@ class BridgeMcpTest {
assertNotEquals(Boolean.TRUE, res.isError());
assertEquals("blocked", textOf(res));
}
// --- bridge_whoami: the caller's own identity, so an agent never has to guess its role -------
@Test
void whoamiReportsThePrimaryAsPrimaryAndNothingElse() {
FakeHerdr h = new FakeHerdr();
McpSchema.CallToolResult res = BridgeMcp.whoami(
Principal.primary(100), sessionManager(h, "http://gx00.gw:8000", Set.of("gx00.gw")));
assertNotEquals(Boolean.TRUE, res.isError());
String out = textOf(res);
assertTrue(out.contains("\"role\":\"primary\""), out);
// The primary owns no session — leaking a sessionId here would invite it to reply as one.
assertFalse(out.contains("sessionId"), out);
}
@Test
void whoamiReportsAWorkerWithItsRegisteredSession() {
FakeHerdr h = new FakeHerdr();
FakeWorktrees worktrees = new FakeWorktrees().withRepoRoot("/repo").withPrefix("/wt");
SessionManager sessions = new SessionManager(
workerService(h, "http://gx00.gw:8000", Set.of("gx00.gw")), worktrees);
WorkerSession s = sessions.acquire("ltms-local", null, "/caller/proj", "term_primary",
new WorktreeRequest("cb-517", null));
McpSchema.CallToolResult res = BridgeMcp.whoami(Principal.worker(s.terminalId(), 200), sessions);
assertNotEquals(Boolean.TRUE, res.isError());
String out = textOf(res);
assertTrue(out.contains("\"role\":\"worker\""), out);
assertTrue(out.contains("\"sessionId\":\"" + s.terminalId() + "\""), out);
assertTrue(out.contains("\"profile\":\"ltms-local\""), out);
assertTrue(out.contains("\"worktree\":\"" + s.worktree() + "\""), out);
assertTrue(out.contains("\"branch\":\"" + s.branch() + "\""), out);
assertTrue(out.contains("\"owner\":\"term_primary\""), out);
}
/**
* A worker the registry has no record of — it outlived a daemon restart — must still learn the
* load-bearing fact. Degrading to "I don't know who you are" would put it back to guessing,
* which is the failure this tool exists to remove.
*/
@Test
void whoamiStillReportsWorkerRoleWhenTheSessionIsUnregistered() {
FakeHerdr h = new FakeHerdr();
McpSchema.CallToolResult res = BridgeMcp.whoami(Principal.worker("term_orphan", 200),
sessionManager(h, "http://gx00.gw:8000", Set.of("gx00.gw")));
assertNotEquals(Boolean.TRUE, res.isError());
String out = textOf(res);
assertTrue(out.contains("\"role\":\"worker\""), out);
assertTrue(out.contains("\"sessionId\":\"term_orphan\""), out);
assertFalse(out.contains("profile"), out); // nothing invented for a session we don't track
}
}
+1 -1
Submodule wiki updated: f4af2a1c22...da015defc4