CB-593: finish CB-592's two documentation criteria — inherited tokens decision, and the "only the bridge MCP" claim #79

Closed
opened 2026-08-15 18:49:32 +02:00 by ltms · 3 comments
Owner

Follow-up to #77, which is closed because the leak itself is closed and verified on a live pane.
Two of its seven acceptance criteria were documentation and decision work, not code, and are not
done. Splitting them out so closing #77 does not bury them.

1. Decide what happens to the other inherited credentials (#77 criterion 5)

Every member still inherits CONTEXT7_TOKEN and AI_GATEWAY_TOKEN from herdr's environment, by the
exact mechanism CB-592 fixed for the admin forge token.

They are far less dangerous than an admin forge token. AI_GATEWAY_TOKEN is the key members are
meant to use once CB-591 lands, and CONTEXT7_TOKEN backs an MCP mount members are meant to have.
So "leave them" is very likely the right answer.

The point is that "we chose to allow it" and "we never noticed" must not look the same. #77 was
found by accident; the same accident should not have to happen again for these two.

Now that BRIDGED_MEMBER exists, splitting either one per-role is a one-line guard in
secrets.sh — the option is cheap and available, which is why the decision should be explicit.

Done when: a short recorded decision says, for each of CONTEXT7_TOKEN and AI_GATEWAY_TOKEN,
whether a member may hold it and why. Wiki Features is the right home — the
why line is the part that stops this being re-litigated in a month.

2. Correct CLAUDE.md's "only the bridge MCP" claim (#77 criterion 7)

CLAUDE.md tells members:

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.

That is already false for opencode members. The tracked opencode.json ships in every worktree
and mounts three servers: bridged, context7, and gitea.

Two problems, and the second is the one that matters:

  1. It is simply inaccurate about what a member has.
  2. The reasoning attached to it is still right, and must survive the correction. A member must
    never claim the result of a check it had no way to run. The fix is to make the sentence describe
    the real mount set per peer kind, without weakening that rule.

Note the claim is in the canonical block, so the edit must be propagated byte-identically to the
wiki template (Use Cases → The portable
CLAUDE.md block
) and to other projects carrying it. Use the sync check at the end of CLAUDE.md
to verify rather than trust.

Done when: the block describes the real mount set, keeps the "never claim an unrun check" rule,
and the sync check prints in sync: True.

Not in scope

The leak, the launcher sentinel, the BRIDGED_MEMBER marker and the secrets.sh guard are all done
and verified — see #77.

Follow-up to #77, which is closed because the **leak itself** is closed and verified on a live pane. Two of its seven acceptance criteria were documentation and decision work, not code, and are **not** done. Splitting them out so closing #77 does not bury them. ## 1. Decide what happens to the other inherited credentials (#77 criterion 5) Every member still inherits `CONTEXT7_TOKEN` and `AI_GATEWAY_TOKEN` from herdr's environment, by the exact mechanism CB-592 fixed for the admin forge token. They are far less dangerous than an admin forge token. `AI_GATEWAY_TOKEN` is the key members are *meant* to use once CB-591 lands, and `CONTEXT7_TOKEN` backs an MCP mount members are meant to have. So "leave them" is very likely the right answer. The point is that **"we chose to allow it" and "we never noticed" must not look the same**. #77 was found by accident; the same accident should not have to happen again for these two. Now that `BRIDGED_MEMBER` exists, splitting either one per-role is a one-line guard in `secrets.sh` — the option is cheap and available, which is why the decision should be explicit. **Done when:** a short recorded decision says, for each of `CONTEXT7_TOKEN` and `AI_GATEWAY_TOKEN`, whether a member may hold it and why. Wiki [Features](wiki/11-Features.md) is the right home — the *why* line is the part that stops this being re-litigated in a month. ## 2. Correct `CLAUDE.md`'s "only the bridge MCP" claim (#77 criterion 7) `CLAUDE.md` tells members: > 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. That is **already false for opencode members**. The tracked `opencode.json` ships in every worktree and mounts three servers: `bridged`, `context7`, and `gitea`. Two problems, and the second is the one that matters: 1. It is simply inaccurate about what a member has. 2. **The reasoning attached to it is still right, and must survive the correction.** A member must never claim the result of a check it had no way to run. The fix is to make the sentence describe the real mount set per peer kind, without weakening that rule. Note the claim is in the **canonical block**, so the edit must be propagated byte-identically to the wiki template ([Use Cases](https://git.ltms.dev/lms/claude-bridge/wiki/7-Use-Cases) → *The portable `CLAUDE.md` block*) and to other projects carrying it. Use the sync check at the end of `CLAUDE.md` to verify rather than trust. **Done when:** the block describes the real mount set, keeps the "never claim an unrun check" rule, and the sync check prints `in sync: True`. ## Not in scope The leak, the launcher sentinel, the `BRIDGED_MEMBER` marker and the `secrets.sh` guard are all done and verified — see #77.
Author
Owner

Criterion 2 is DONE — and the premise in the issue body was backwards

Correcting my own text above before recording the fix. The issue says the claim is "already false
for opencode members"
, reasoning from the tracked opencode.json. That was an assumption and it
is wrong.
I measured it instead of reasoning about it: one member spawned per backend, each asked
what MCP tools it actually holds.

member what it really mounts is the claim true?
gx (opencode) 11 bridge tools, nothing else TRUE
local (claude-code) 11 bridge + 45 mcp__gitea__* + 2 mcp__context7__* FALSE

So it was false for exactly the backend the issue said was fine, and true for the one it said was
broken. The lesson is the ordinary one: the config file is not the mount set.

Why the tracked opencode.json was a red herring

The worktree parity overlay does its job. Both members reported their worktree copies neutralised —
.mcp.json is {"mcpServers": {}} and opencode.json is {}. Nothing the repo ships reaches a
member.

The real source is outside anything this repo controls: ~/.claude.json declares user-scope
mcpServers (gitea, context7, ccs-image-analysis, ccs-websearch), and Claude Code's
--mcp-config adds to that scope rather than replacing it. The overlay cannot see it, so no
change on our side would have caught this. opencode is unaffected only because its home config
declares no MCP block.

The forge tools are mounted but cannot authenticate — CB-592 is holding

This was the part worth checking, since 45 gitea tools include delete_branch, delete_file,
create_repo and wiki_write. Measured on the live member:

GITEA_ACCESS_TOKEN   len=43  prefix=blocke     <- CB-592 sentinel
BRIDGED_MEMBER       len=1   value=1           <- CB-592 marker
WORKER_GITEA_TOKEN   len=40  prefix=7c682f     <- real, but gitea does not read it

get_me       -> MCP -32603: invalid username, password or token
list_issues  -> MCP -32603: invalid username, password or token

Both calls fail at client creation, not at scope. The gitea entry in ~/.claude.json reads
GITEA_ACCESS_TOKEN, CB-592 shadows that name, so the server starts and every call fails. This is
defence in depth working in a path it was never designed for.
Worth stating plainly, because it is
also the reason nobody noticed: the tools are present and useless, which looks like absence.

Residual risk, not fixed here

The tools fail only because the credential is blocked. If the user-scope gitea entry were ever
pointed at WORKER_GITEA_TOKEN — which is real and valid — a member would gain 45 working forge
tools including destructive ones. Nothing in this repo prevents that, because the config is outside
it. Recording it rather than fixing it: the fix is a launcher change (--strict-mcp-config), which
is a behaviour change for every Claude Code member and deserves its own ticket and decision.

What changed

  • CLAUDE.md — member rule 5 and lead step 6 both rewritten. The block now says what a member really
    mounts per backend, keeps the "never claim the result of a check you had no way to run" rule
    intact, and adds the thing this investigation actually taught: a mounted tool is not a working
    tool.
  • Propagated byte-identically to the wiki template; the sync check prints in sync: True.
  • main 2124e04, wiki 1d95e3f.

Criterion 2 done. Criterion 1 (the recorded decision on CONTEXT7_TOKEN / AI_GATEWAY_TOKEN, and
its Features entry) is still open, so this issue stays open.

## Criterion 2 is DONE — and the premise in the issue body was backwards Correcting my own text above before recording the fix. The issue says the claim is *"already false for opencode members"*, reasoning from the tracked `opencode.json`. **That was an assumption and it is wrong.** I measured it instead of reasoning about it: one member spawned per backend, each asked what MCP tools it actually holds. | member | what it really mounts | is the claim true? | |---|---|---| | `gx` (opencode) | 11 bridge tools, nothing else | **TRUE** | | `local` (claude-code) | 11 bridge + **45 `mcp__gitea__*`** + 2 `mcp__context7__*` | **FALSE** | So it was false for exactly the backend the issue said was fine, and true for the one it said was broken. The lesson is the ordinary one: the config file is not the mount set. ### Why the tracked `opencode.json` was a red herring The worktree parity overlay does its job. Both members reported their worktree copies neutralised — `.mcp.json` is `{"mcpServers": {}}` and `opencode.json` is `{}`. Nothing the repo ships reaches a member. The real source is **outside anything this repo controls**: `~/.claude.json` declares user-scope `mcpServers` (`gitea`, `context7`, `ccs-image-analysis`, `ccs-websearch`), and Claude Code's `--mcp-config` **adds to** that scope rather than replacing it. The overlay cannot see it, so no change on our side would have caught this. opencode is unaffected only because its home config declares no MCP block. ### The forge tools are mounted but cannot authenticate — CB-592 is holding This was the part worth checking, since 45 gitea tools include `delete_branch`, `delete_file`, `create_repo` and `wiki_write`. Measured on the live member: ``` GITEA_ACCESS_TOKEN len=43 prefix=blocke <- CB-592 sentinel BRIDGED_MEMBER len=1 value=1 <- CB-592 marker WORKER_GITEA_TOKEN len=40 prefix=7c682f <- real, but gitea does not read it get_me -> MCP -32603: invalid username, password or token list_issues -> MCP -32603: invalid username, password or token ``` Both calls fail at client creation, not at scope. The `gitea` entry in `~/.claude.json` reads `GITEA_ACCESS_TOKEN`, CB-592 shadows that name, so the server starts and every call fails. **This is defence in depth working in a path it was never designed for.** Worth stating plainly, because it is also the reason nobody noticed: the tools are present and useless, which looks like absence. ### Residual risk, not fixed here The tools fail *only* because the credential is blocked. If the user-scope `gitea` entry were ever pointed at `WORKER_GITEA_TOKEN` — which is real and valid — a member would gain 45 working forge tools including destructive ones. Nothing in this repo prevents that, because the config is outside it. Recording it rather than fixing it: the fix is a launcher change (`--strict-mcp-config`), which is a behaviour change for every Claude Code member and deserves its own ticket and decision. ### What changed - `CLAUDE.md` — member rule 5 and lead step 6 both rewritten. The block now says what a member really mounts per backend, keeps the "never claim the result of a check you had no way to run" rule intact, and adds the thing this investigation actually taught: **a mounted tool is not a working tool.** - Propagated byte-identically to the wiki template; the sync check prints `in sync: True`. - main `2124e04`, wiki `1d95e3f`. **Criterion 2 done. Criterion 1 (the recorded decision on `CONTEXT7_TOKEN` / `AI_GATEWAY_TOKEN`, and its Features entry) is still open, so this issue stays open.**
ltms added this to the 1.1 — single-host close-out milestone 2026-08-16 16:49:37 +02:00
Author
Owner

Criterion 1 turned out to be a corner of something larger, now filed as #82 (CB-596).

Working the "may a member hold CONTEXT7_TOKEN and AI_GATEWAY_TOKEN" question meant asking how they get there. The answer is the member's pane login shell sourcing ${SHARED_ENV}/tools/secrets.sh — the same mechanism CB-592 had to fight with the BRIDGED_MEMBER marker. That file exports about thirty names. CB-592 blocks one.

So the two tokens in this ticket are not a special case; they are two rows in a table nobody has filled in. GITLAB_PERSONAL_ACCESS_TOKEN — a second forge — sits in that same table with nothing blocking it.

One of the two answers is already clear and I have recorded it on #82: AI_GATEWAY_TOKEN is required, not leaked. bridged.yaml names it in tokenEnv: for the local and gx profiles, so a member reaching the gateway is by design. That is criterion 1's first half, decided.

CONTEXT7_TOKEN I have deliberately not decided. It should be answered together with the other twenty-eight rather than on its own, which is what #82 is for.

Also worth recording here: I attempted to measure the rest by briefing a member to report which names are set — names, lengths and 6-character prefixes only, never values — and the command classifier refused it. That refusal is right, and I did not route around it. Authorising that measurement is step 1 of #82.

This issue stays open for criterion 1's remaining half and for the --strict-mcp-config decision. Both are now in the 1.1 — single-host close-out milestone.

Criterion 1 turned out to be a corner of something larger, now filed as **#82 (CB-596)**. Working the "may a member hold `CONTEXT7_TOKEN` and `AI_GATEWAY_TOKEN`" question meant asking *how* they get there. The answer is the member's pane login shell sourcing `${SHARED_ENV}/tools/secrets.sh` — the same mechanism CB-592 had to fight with the `BRIDGED_MEMBER` marker. That file exports about **thirty** names. CB-592 blocks **one**. So the two tokens in this ticket are not a special case; they are two rows in a table nobody has filled in. `GITLAB_PERSONAL_ACCESS_TOKEN` — a second forge — sits in that same table with nothing blocking it. One of the two answers is already clear and I have recorded it on #82: **`AI_GATEWAY_TOKEN` is required, not leaked.** `bridged.yaml` names it in `tokenEnv:` for the `local` and `gx` profiles, so a member reaching the gateway is by design. That is criterion 1's first half, decided. `CONTEXT7_TOKEN` I have deliberately **not** decided. It should be answered together with the other twenty-eight rather than on its own, which is what #82 is for. Also worth recording here: I attempted to measure the rest by briefing a member to report which names are set — names, lengths and 6-character prefixes only, never values — and the command classifier refused it. That refusal is right, and I did not route around it. Authorising that measurement is step 1 of #82. This issue stays open for criterion 1's remaining half and for the `--strict-mcp-config` decision. Both are now in the **1.1 — single-host close-out** milestone.
Author
Owner

Both criteria are now met.

1. The inherited-credential decision — recorded

New Features entry, Which inherited credentials a member may keep (wiki commit b1d13d7). It states, per credential, whether a member may hold it and why:

Credential May a member hold it Why
AI_GATEWAY_TOKEN yes It is the key a member is meant to use. Paid-backend members reach their model through llm.ltms.dev and that token is the single front-door key. Blocking it stops those members working at all.
CONTEXT7_TOKEN yes It backs the context7 docs MCP, a read-only lookup members are meant to have. Worst case is documentation reads on the operator quota.
GITEA_ACCESS_TOKEN no Admin scope. Blocked at every launch; members push with the repo-scoped WORKER_GITEA_TOKEN.

The entry says why it is written down at all: BRIDGED_MEMBER now exists, so splitting either one per-role is a one-line guard. The option being cheap is exactly why the decision has to be explicit rather than implied by nobody having done it.

Its gotcha line keeps the scope honest — this decides two names out of about thirty. It is not a statement that the rest were checked. #82 still tracks enumerating them and still needs the operator.

2. The "only the bridge MCP" claim — already corrected, now verified

This was fixed in the canonical block before this ticket was picked up. The block now says what a member actually mounts, per backend:

Measure your own tools; do not assume them. What you mount depends on your backend: an opencode member gets the bridge and nothing else, while a Claude Code member also inherits the operator\u2019s user-scope MCP servers, which the bridge never chose for you.

The rule the old sentence carried survived the correction, which was the part that mattered: a member must never claim the result of a check it had no way to run. It is still there, and the follow-on point — a mounted tool is not a working tool, because the forge server holds a deliberately blocked credential — is stated with it.

I ran the sync check from CLAUDE.md rather than trusting it:

in sync: True

Also fixed while in there

The backfill note still described the reply-push nudge budget as per source. CB-598 merged today and moved it to per pending item, so the note was already stale. Corrected, including the consequence worth writing up later: the cap bounds nudges about one item, so a lead with a steady arrival of new work keeps being nudged. That is correct behaviour but it is not what the knob name suggests.

Both criteria are now met. ## 1. The inherited-credential decision — recorded New Features entry, *Which inherited credentials a member may keep* (wiki commit `b1d13d7`). It states, per credential, whether a member may hold it and why: | Credential | May a member hold it | Why | |---|---|---| | `AI_GATEWAY_TOKEN` | **yes** | It is the key a member is *meant* to use. Paid-backend members reach their model through `llm.ltms.dev` and that token is the single front-door key. Blocking it stops those members working at all. | | `CONTEXT7_TOKEN` | **yes** | It backs the `context7` docs MCP, a read-only lookup members are meant to have. Worst case is documentation reads on the operator quota. | | `GITEA_ACCESS_TOKEN` | **no** | Admin scope. Blocked at every launch; members push with the repo-scoped `WORKER_GITEA_TOKEN`. | The entry says why it is written down at all: `BRIDGED_MEMBER` now exists, so splitting either one per-role is a one-line guard. The option being cheap is exactly why the decision has to be explicit rather than implied by nobody having done it. Its gotcha line keeps the scope honest — this decides **two names out of about thirty**. It is not a statement that the rest were checked. #82 still tracks enumerating them and still needs the operator. ## 2. The \"only the bridge MCP\" claim — already corrected, now verified This was fixed in the canonical block before this ticket was picked up. The block now says what a member actually mounts, per backend: > **Measure your own tools; do not assume them.** What you mount depends on your backend: an opencode member gets the bridge and nothing else, while a Claude Code member also inherits the operator\u2019s user-scope MCP servers, which the bridge never chose for you. The rule the old sentence carried survived the correction, which was the part that mattered: a member must never claim the result of a check it had no way to run. It is still there, and the follow-on point — a mounted tool is not a working tool, because the forge server holds a deliberately blocked credential — is stated with it. I ran the sync check from `CLAUDE.md` rather than trusting it: ``` in sync: True ``` ## Also fixed while in there The backfill note still described the reply-push nudge budget as **per source**. CB-598 merged today and moved it to **per pending item**, so the note was already stale. Corrected, including the consequence worth writing up later: the cap bounds nudges about *one item*, so a lead with a steady arrival of new work keeps being nudged. That is correct behaviour but it is not what the knob name suggests.
ltms closed this issue 2026-08-16 18:22:30 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#79