CB-610: subscription: is the knob that spends the operator's Claude plan, and bridged.example.yaml never mentions it #112

Closed
opened 2026-08-17 14:24:16 +02:00 by ltms · 1 comment
Owner

Found in a pre-tag scan on 2026-08-17, by auditing every config key the code reads against bridged/bridged.example.yaml.

The finding

profiles.<name>.subscription is read by the code:

  • BridgedConfig.java:290 — the Profile record field
  • BridgedConfig.java:413 — isSubscription()
  • BridgedConfig.java:1719-1737 — validateSubscriptionProfiles
  • ClaudeCodeLauncher.java:191 — the launcher branch it selects
  • Bridged.java:581 — excluded from the required-secrets check

It is not in bridged.example.yaml as a settable key. The word "subscription" appears there six times, all in prose or inside an unrelated comment example (profile: opus). There is no subscription: line at any level.

It is in active use: the live config sets subscription: true on two profiles.

Why this one matters more than a normal doc gap

This is the switch that decides who pays. With it set, a member runs on the operator's own Claude plan instead of the metered gateway; CB-539 added it deliberately, and CLAUDE.md warns elsewhere that spawning such a profile bills the operator.

And bridged.yaml is gitignored. The example file is the only place an operator can learn a key exists. So today: a fresh host has no way to discover the one knob that moves cost onto the operator's subscription, and no way to discover that two of the shipped profiles have it switched on.

This is the [gitignored-config] shape we have hit repeatedly — CB-597, CB-589's workaround, CB-595. The difference here is the blast radius: the others cost behaviour, this one costs money.

Acceptance criteria

  1. subscription: appears in bridged.example.yaml as a real, settable key on a profile, with a comment saying plainly what it does: the member runs on the operator's Claude subscription and bills it.
  2. The comment says what it is mutually exclusive with (baseUrl — see BridgedConfig.java:256) and what the launcher does differently (ClaudeCodeLauncher.java:191: no ANTHROPIC_BASE_URL, no guard vetting, no token required).
  3. It also says the non-obvious consequence already recorded in Bridged.java:581: a subscription: true profile is skipped by the startup required-secrets check, so a missing token for it is never reported.
  4. maxLoad on such a profile is described as what it actually is — the only throttle against the operator's own plan (BridgedConfig.java:340).
  5. Do not change any behaviour. This is a documentation fix; the code is correct.

Milestone

1.1. I am putting this on the closing milestone rather than 2.0, against my own admission rule, and the reason should be explicit: the rule is "if it would still be broken with exactly one host, it is 1.1." An operator on one host, standing up a fresh bridged, cannot discover the knob that spends their money. That is broken on one host, and it is broken worse on a fresh host than on this one, because this host's bridged.yaml at least has the setting visible.

If that reading is rejected, this moves to 2.0 and the tag goes ahead — it is a documentation defect, not a runtime one, and nothing misbehaves today.

Related

Found alongside CB-611, which is why the guard test did not catch this.

Found in a pre-tag scan on 2026-08-17, by auditing every config key the code reads against `bridged/bridged.example.yaml`. ## The finding `profiles.<name>.subscription` is read by the code: - `BridgedConfig.java:290` — the `Profile` record field - `BridgedConfig.java:413` — `isSubscription()` - `BridgedConfig.java:1719-1737` — `validateSubscriptionProfiles` - `ClaudeCodeLauncher.java:191` — the launcher branch it selects - `Bridged.java:581` — excluded from the required-secrets check It is **not in `bridged.example.yaml`** as a settable key. The word "subscription" appears there six times, all in prose or inside an unrelated comment example (`profile: opus`). There is no `subscription:` line at any level. It is in active use: the live config sets `subscription: true` on **two** profiles. ## Why this one matters more than a normal doc gap This is the switch that decides **who pays**. With it set, a member runs on the operator's own Claude plan instead of the metered gateway; CB-539 added it deliberately, and `CLAUDE.md` warns elsewhere that spawning such a profile bills the operator. And `bridged.yaml` is **gitignored**. The example file is the only place an operator can learn a key exists. So today: a fresh host has no way to discover the one knob that moves cost onto the operator's subscription, and no way to discover that two of the shipped profiles have it switched on. This is the [gitignored-config] shape we have hit repeatedly — CB-597, CB-589's workaround, CB-595. The difference here is the blast radius: the others cost behaviour, this one costs money. ## Acceptance criteria 1. `subscription:` appears in `bridged.example.yaml` as a real, settable key on a profile, with a comment saying plainly what it does: the member runs on the operator's Claude subscription and bills it. 2. The comment says what it is mutually exclusive with (`baseUrl` — see `BridgedConfig.java:256`) and what the launcher does differently (`ClaudeCodeLauncher.java:191`: no `ANTHROPIC_BASE_URL`, no guard vetting, no token required). 3. It also says the non-obvious consequence already recorded in `Bridged.java:581`: a `subscription: true` profile is skipped by the startup required-secrets check, so a missing token for it is never reported. 4. `maxLoad` on such a profile is described as what it actually is — the only throttle against the operator's own plan (`BridgedConfig.java:340`). 5. Do **not** change any behaviour. This is a documentation fix; the code is correct. ## Milestone **1.1.** I am putting this on the closing milestone rather than 2.0, against my own admission rule, and the reason should be explicit: the rule is *"if it would still be broken with exactly one host, it is 1.1."* An operator on one host, standing up a fresh `bridged`, cannot discover the knob that spends their money. That is broken on one host, and it is broken **worse** on a fresh host than on this one, because this host's `bridged.yaml` at least has the setting visible. If that reading is rejected, this moves to 2.0 and the tag goes ahead — it is a documentation defect, not a runtime one, and nothing misbehaves today. ## Related Found alongside **CB-611**, which is why the guard test did not catch this.
ltms added this to the 1.1 — single-host close-out milestone 2026-08-17 14:24:16 +02:00
Author
Owner

Fixed in bf616e1. Kept on 1.1 — the operator confirmed the reading that a fresh single host cannot discover the knob that spends their money, so it belongs in the close-out.

bridged.example.yaml now carries subscription: as a documented, settable profile key. What it says, against the criteria:

  1. What it does — the member runs on the operator's own Claude subscription; every spawn bills the plan and eats the usage limit. Framed as a deliberate exception, since off-subscription is the point of the daemon.
  2. Mutually exclusive with baseUrl, refused at config load (CB-542), with the reason: on the subscription path no guard vets the URL, so allowing both would be a way around SubscriptionGuard rather than a configuration.
  3. What the launcher does differently — no ANTHROPIC_BASE_URL, no ANTHROPIC_AUTH_TOKEN, the member inherits the operator's own Claude Code auth (which is exactly why it bills the plan), no guard vetting, no token required so tokenEnv is irrelevant.
  4. The invisible-to-the-secret-check gotcha — reportRequiredSecrets skips subscription profiles on purpose, so a boot log reporting every secret as fine says nothing about them.
  5. maxLoad is the only throttle. Written plainly: there is no metering, no budget, and no refusal on cost. The cap on live members is the single thing between a fan-out and the monthly limit.

No behaviour changed — comments only. BridgedConfigTest: 94 tests, 0 failures, BUILD SUCCESS; the example still loads.

Note for CB-611 (#113): this fix does not make the guard test catch the class of defect. subscription: is a nested key and the guard matches only column-0 names, so the same gap remains open for every other nested key. The guard passed before this fix and passes after it, which is the point of that ticket.

Fixed in `bf616e1`. Kept on 1.1 — the operator confirmed the reading that a fresh single host cannot discover the knob that spends their money, so it belongs in the close-out. `bridged.example.yaml` now carries `subscription:` as a documented, settable profile key. What it says, against the criteria: 1. **What it does** — the member runs on the operator's own Claude subscription; every spawn bills the plan and eats the usage limit. Framed as a deliberate exception, since off-subscription is the point of the daemon. 2. **Mutually exclusive with `baseUrl`**, refused at config load (CB-542), with the reason: on the subscription path no guard vets the URL, so allowing both would be a way *around* `SubscriptionGuard` rather than a configuration. 3. **What the launcher does differently** — no `ANTHROPIC_BASE_URL`, no `ANTHROPIC_AUTH_TOKEN`, the member inherits the operator's own Claude Code auth (which is exactly why it bills the plan), no guard vetting, no token required so `tokenEnv` is irrelevant. 4. **The invisible-to-the-secret-check gotcha** — `reportRequiredSecrets` skips subscription profiles on purpose, so a boot log reporting every secret as fine says nothing about them. 5. **`maxLoad` is the only throttle.** Written plainly: there is no metering, no budget, and no refusal on cost. The cap on live members is the single thing between a fan-out and the monthly limit. No behaviour changed — comments only. `BridgedConfigTest`: 94 tests, 0 failures, BUILD SUCCESS; the example still loads. **Note for CB-611 (#113):** this fix does *not* make the guard test catch the class of defect. `subscription:` is a nested key and the guard matches only column-0 names, so the same gap remains open for every other nested key. The guard passed before this fix and passes after it, which is the point of that ticket.
ltms closed this issue 2026-08-17 16:08:39 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#112