Skip to content

fix(provision): the credential mechanism stops naming one forge, and a gate says so - #899

Draft
wenzowski wants to merge 1 commit into
mainfrom
claude/new-session-2rp805
Draft

fix(provision): the credential mechanism stops naming one forge, and a gate says so#899
wenzowski wants to merge 1 commit into
mainfrom
claude/new-session-2rp805

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Closes CLOUD-1615.

Non-negotiable rule 1 binds mechanisms, not only string literals, and it had failed twice inside crates/batten — both in the same commit, both invisible for the same reason: each worked perfectly when tested in this repository, on this forge, in this container.

The names

CREDENTIAL_NAMES was two GitHub-shaped literals in the engine. A consumer on another forge — or on a host injecting under a third name — has neither, so every candidate is absent, credential_health answers Unusable, and every removal is skipped forever. Nothing can see that failure, because skipping a removal is also the right answer for a genuinely bad credential: the dead path and the working path are identical from outside.

The names move to [credential] names, beside the probe_url that was already config for exactly this reason. Every could-not-look now says which one it is — an undeclared table, an undeclared endpoint and an empty name list each get their own line, because a revoked token is the operator's to replace and the other three are the consumer's config to write.

The control token

MUST_FAIL was one forge's token prefix followed by a run of zeroes, which breaks the criterion its own doc states: the token must be plausible enough that the refusal is about the credential rather than about malformed input. Off that forge it is malformed input, so route_honours_credentials reports "this route honours credentials" on evidence it has not earned, and every verdict downstream rests on a rubber stamp — which is the one thing a control arm exists to prevent.

It is now derived from the credential about to be tested: structural prefix kept, body refilled at the same length and character class. Plausible on whatever forge this is, needs no config, and deliberately not a config field — a consumer who could name the known-bad could name one the route accepts.

The gate (rule 2)

the_engine_names_no_consumer_of_its_own scans every .rs under crates/batten/src. Its fragments are assembled at runtime so the scanner's own corpus never carries the shape it hunts, and the_agnostic_scan_would_find_a_violation proves the instrument is not vacuous.

It caught this PR's own first draft, where a doc comment had quoted the banned literal back into the engine while explaining why it was banned.

Withdrawn, rather than quietly dropped

config.rs's schema path was inventoried as a third violation and is not one. schema/batten.schema.json is batten's own convention, the same class as batten.toml itself, and names no consumer. The real defect there was a hand-joined second spelling of config::SCHEMA_PATH — two authorities for one location — fixed by using the constant. The scan deliberately does not ban that path: a ban on the constant would ban the convention.

The portable-skew gap that arm still has in every consumer is unchanged and remains CLOUD-1572's.

Measured

real token   proxied 200 | direct 200
junk token   proxied 200 | direct 401

The probe uses get_direct, so it takes the direct column: junk refused, real accepted, Verdict::Live. api.github.com is absent from the ambient NO_PROXY — the exemption is a [[provision.env]] row and applies to processes the launcher starts, which is why a bare curl sees the proxy's identity and is not evidence about the fence.

Note on the commit

commit-attribution refused the Claude-Session: trailer this session's harness asks for; [attribution] trailer_deny carries it with trailer_allow = []. Per AGENTS.md rule 8 the gate outranks the harness request, so the trailer is out and the durable record is the Linear row.

https://claude.ai/code/session_014kQRLAmRkQKRawJTNCBt42

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 16 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 4512e2c6-2344-456e-9234-fa9ad9def3a5

📥 Commits

Reviewing files that changed from the base of the PR and between 7fe35ef and 516072c.

📒 Files selected for processing (5)
  • batten.toml
  • crates/batten/src/config.rs
  • crates/batten/src/provision.rs
  • crates/batten/tests/it/provision.rs
  • schema/batten.schema.json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…a gate says so

Non-negotiable rule 1 binds MECHANISMS, not only string literals, and it had
failed twice in `crates/batten` — both in the same commit, both invisible for the
same reason: each worked perfectly when tested in this repository, on this forge,
in this container.

THE NAMES. `CREDENTIAL_NAMES` was two GitHub-shaped literals in the engine. A
consumer on another forge, or on a host injecting under a third name, has
neither, so every candidate is absent, `credential_health` answers `Unusable`,
and every removal is skipped FOREVER — a failure nothing can see, because
skipping a removal is also the right answer for a genuinely bad credential. The
names move to `[credential] names`, beside the `probe_url` that was already
config for exactly this reason. An empty list is could-not-look and is now SAID:
an undeclared table, an undeclared endpoint and an undeclared name list each get
their own line, because a revoked token is the operator's to replace and the
other three are the consumer's config to write.

THE CONTROL TOKEN. `MUST_FAIL` was one forge's token prefix and a run of zeroes,
which breaks the criterion its own doc states: the token must be plausible enough
that the refusal is about the CREDENTIAL rather than about malformed input. Off
that forge it IS malformed input, so `route_honours_credentials` returns "this
route honours credentials" on evidence it has not earned, and every verdict
downstream rests on a rubber stamp. It is now DERIVED from the credential about
to be tested — structural prefix kept, body refilled at the same length and
class — so it is plausible on whatever forge this is, needs no config, and stays
un-nameable by a consumer who could otherwise pick one the route accepts.
Deliberately not a config field for that reason.

THE GATE (non-negotiable rule 2). `the_engine_names_no_consumer_of_its_own`
scans every `.rs` under `crates/batten/src`; its fragments are assembled at
runtime so the scanner's own corpus never carries the shape it hunts, and
`the_agnostic_scan_would_find_a_violation` proves the instrument is not vacuous.
It caught this commit's first draft, where a doc comment had quoted the banned
literal back into the engine while explaining why it was banned.

WITHDRAWN, and recorded rather than quietly dropped: `config.rs`'s schema path
was inventoried as a third violation and is not one. `schema/batten.schema.json`
is batten's own convention, the same class as `batten.toml` itself, and names no
consumer. The real defect there was a hand-joined second spelling of
`config::SCHEMA_PATH` — two authorities for one location — fixed by using the
constant. The scan does not ban that path, because a ban on the constant would
ban the convention.

BREAKING CHANGE: `provision::CredentialProbe` gains a field. The probe now
carries the credential variable names alongside its `probe_url`, because the
names moved out of the engine and a consumer that declares an endpoint but no
names has said only half of what the probe needs. Constructing the struct
positionally no longer compiles.

Refs: CLOUD-1615
Admits: 3596b860bfa558171666073c6853e37a0583335aa0ca91fb62bf4507035a3c24
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-anchor: call:65f08f551acb6211ff2c7ffaa9fcc81a93b336d6
Admits-epoch: 1daa8dabaf28d2aa6e4474b819e192af29b36644cdbca699867b4e86de26e5f9
Admits-author: alec@wenzowski.com
Admits-prev: 2cef7da5183edf69172ef195b344e858bf73c71d6d20b86851e0bffa4b615a12
Admits-answer-lost: The engine half lands without the consumer half. CLOUD-1615 removes the hardcoded credential variable names from crates/batten and reads them from `[credential] names` instead; with the key undeclared, credential_health() reports could-not-look and returns Unusable, so every `unset` and `reject_prefix` row in this file is skipped, the proxy variables stay, and this repository's own sessions keep the host's substituted credential. That is a live regression introduced by the fix, for as long as the two halves are apart.
Admits-answer-precondition: The surface this class names is a pull request, and this IS a pull request: the change is on branch claude/new-session-2rp805 for CLOUD-1615 and lands as a reviewed diff, never as a restore. No other surface can express it — `[credential] names` is a new config key, so nothing but an edit to batten.toml can declare it, and the engine change that reads the key is in the same commit range, where a reviewer sees both halves together.
Admits-answer-rejected-route: `config read first` — rejected because it is already satisfied rather than skipped: the `[credential]` table and every surrounding `[[provision.env]]` row were read this session (the new key's doc comment cites their reasoning), and reading again cannot produce a key that does not yet exist. `patch run first` — rejected because there is no patch surface for adding a TOML key; the applying tool is the same Edit this rule refuses, so it would relabel the write rather than replace it.
@wenzowski
wenzowski force-pushed the claude/new-session-2rp805 branch from 80558fb to 516072c Compare September 8, 2026 04:43
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant