CB-539 + CB-542: subscription-profile opt-in, with the env: bypass closed #14

Merged
ltms merged 2 commits from worker/cb-542-subscription-env-bypass-dd2a67-6 into main 2026-08-13 15:31:51 +02:00
Owner

Opened by the lead on the worker's behalf: the gx10 profile has no gitTokenEnv: set, so the worker had no GITEA_TOKEN and could not create this itself. That gap is being addressed separately — it is not a defect in this change.

What this is

Two commits. 73f6b12 is CB-539 rebased onto current main (its original base ef1e014 is now far behind). 5afe8e1 is CB-542, which closes a hole I found while reviewing CB-539 and which blocked it from merging.

CB-539 — subscription: true

Lets a worker profile deliberately run on the operator's Claude subscription instead of an off-subscription endpoint. Mutually exclusive with baseUrl; setting both is a configuration error, and spawning one logs a warning that it will bill the subscription.

CB-542 — the hole that had to be closed first

subscription: true skips guard.assertWorker and stops the adapter writing ANTHROPIC_BASE_URL. But a profile's env: block is layered into the worker environment separately by baseEnv. On the old path the adapter's value overwrote anything env: set and the guard vetted it; on the subscription path the adapter writes nothing, so an ANTHROPIC_BASE_URL sitting in env: survived into the worker having passed no guard at all:

myprofile:
  subscription: true
  env:
    ANTHROPIC_BASE_URL: http://anything-not-on-the-allowlist/

offSubscriptionHosts never saw it. This also falsified a published claim in wiki chapter 11, that "an env: entry cannot repoint a worker past the SubscriptionGuard".

Closed in two layers, deliberately:

  1. Loud refusal at config load — validateSubscriptionProfiles(), called from Bridged beside the other fatal validations. It names the profile and the offending keys. Fatal rather than sanitised: subscription: true plus an env: repoint is a contradiction in the same way subscription: true plus a baseUrl is, and the operator should be told, not silently cleaned up after.
  2. Belt-and-braces strip at the launcher — so the invariant holds even for a profile constructed in code that never passed through config validation.

The invariant landed: there is no configuration in which a worker reaches an Anthropic endpoint that no guard vetted.

Verification

I built and tested this myself in a clean worktree at 5afe8e1, rather than relying on the worker's report:

Tests run: 474, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

main is at 464, so this adds 10 — including a launcher test that fails without the strip and config-load tests that fail without the refusal.

Review notes

  • The rebase had exactly one conflict, in ClaudeCodeLauncherTest.java, against CB-533's --model work. Both sides' tests were kept; I checked the region directly and confirmed theModelFlagComesLastSoItOutranksTheOperatorsOwnArgv and aProfileWithNoModelGetsNoModelFlag both survive intact.
  • The docs half is a separate branch on the wiki's own remote (cb-542-subscription-env-doc), since wiki/ is a submodule. The submodule pointer is deliberately not bumped here; I do that on merge.
Opened by the lead on the worker's behalf: the `gx10` profile has no `gitTokenEnv:` set, so the worker had no `GITEA_TOKEN` and could not create this itself. That gap is being addressed separately — it is not a defect in this change. ## What this is Two commits. `73f6b12` is CB-539 rebased onto current `main` (its original base `ef1e014` is now far behind). `5afe8e1` is CB-542, which closes a hole I found while reviewing CB-539 and which blocked it from merging. ## CB-539 — `subscription: true` Lets a worker profile deliberately run on the operator's Claude subscription instead of an off-subscription endpoint. Mutually exclusive with `baseUrl`; setting both is a configuration error, and spawning one logs a warning that it will bill the subscription. ## CB-542 — the hole that had to be closed first `subscription: true` skips `guard.assertWorker` **and** stops the adapter writing `ANTHROPIC_BASE_URL`. But a profile's `env:` block is layered into the worker environment separately by `baseEnv`. On the old path the adapter's value overwrote anything `env:` set and the guard vetted it; on the subscription path the adapter writes nothing, so an `ANTHROPIC_BASE_URL` sitting in `env:` **survived into the worker having passed no guard at all**: ```yaml myprofile: subscription: true env: ANTHROPIC_BASE_URL: http://anything-not-on-the-allowlist/ ``` `offSubscriptionHosts` never saw it. This also falsified a published claim in wiki chapter 11, that "an `env:` entry cannot repoint a worker past the `SubscriptionGuard`". Closed in two layers, deliberately: 1. **Loud refusal at config load** — `validateSubscriptionProfiles()`, called from `Bridged` beside the other fatal validations. It names the profile and the offending keys. Fatal rather than sanitised: `subscription: true` plus an `env:` repoint is a contradiction in the same way `subscription: true` plus a `baseUrl` is, and the operator should be told, not silently cleaned up after. 2. **Belt-and-braces strip at the launcher** — so the invariant holds even for a profile constructed in code that never passed through config validation. The invariant landed: **there is no configuration in which a worker reaches an Anthropic endpoint that no guard vetted.** ## Verification I built and tested this myself in a clean worktree at `5afe8e1`, rather than relying on the worker's report: ``` Tests run: 474, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` `main` is at 464, so this adds 10 — including a launcher test that fails without the strip and config-load tests that fail without the refusal. ## Review notes - The rebase had exactly one conflict, in `ClaudeCodeLauncherTest.java`, against CB-533's `--model` work. Both sides' tests were kept; I checked the region directly and confirmed `theModelFlagComesLastSoItOutranksTheOperatorsOwnArgv` and `aProfileWithNoModelGetsNoModelFlag` both survive intact. - The docs half is a separate branch on the wiki's own remote (`cb-542-subscription-env-doc`), since `wiki/` is a submodule. The submodule pointer is deliberately **not** bumped here; I do that on merge.
ltms added 2 commits 2026-08-13 15:30:23 +02:00
Add a per-profile subscription: true opt-in that lets a claude-code worker run on
the operator's Claude subscription when there is no off-subscription endpoint for it
(e.g. sonnet on ccs). When set, the launcher injects neither ANTHROPIC_BASE_URL nor
ANTHROPIC_AUTH_TOKEN and skips SubscriptionGuard's base_url requirement for that
profile only, logging a WARN naming the profile. subscription: true alongside a
baseUrl is refused as contradictory. The default (absent/false) keeps today's hard
refusal unchanged; every other profile stays allowlist-checked and SubscriptionGuard
is untouched.
CB-542: close the subscription env: bypass, and fix the env: env-carries-anthropic docs
CI / contract (pull_request) Successful in 44s
CI / build (pull_request) Successful in 51s
5afe8e14d9
ltms merged commit 244fbd98a5 into main 2026-08-13 15:31:51 +02:00
Sign in to join this conversation.