Skip to content

plex: a stored server becomes current only if its origin may carry the credential - #183

Merged
GLinnik21 merged 2 commits into
mainfrom
fix/credential-eligible-registration
Sep 20, 2026
Merged

GLinnik21 merged 2 commits into
mainfrom
fix/credential-eligible-registration

Conversation

@GLinnik21

Copy link
Copy Markdown
Owner

Closes part of #107.

The defect

Discovery already refused to activate a plaintext candidate. Restoring a stored session did not.

Installation gated on SourceRef::dialable, which is well-formedness — the port fits an i32, the address parses — and says nothing about whether a token may ride that scheme. A legacy entry holding only address + port has its origin synthesised as http, and servers::activate published it as the current credentialed client without a second look.

The transport then did its job and refused every token-bearing request. So the session was not merely artwork-less: hubs, metadata and library browsing all failed together, and the only trace was one refused plaintext PMS credentials; HTTPS required line. That is exactly the sequence in the log attached to #107.

This is reachable in a shipping build. CredentialPolicy::build() is HttpsOnly there — but that only made the transport refuse; it never stopped such a client from becoming active.

The change

CredentialPolicy is unchanged and remains the single cfg! for this rule. What changes is where it is asked.

A CREDENTIAL_ELIGIBLE bitmask now sits beside ACTIVE and is written in activate() — the one point every registration path reaches: stored primary, roster, controlled bootstrap, discovery activation and endpoint repair alike. set_current refuses a slot that is not in it.

An ineligible origin is not dropped. A user whose only stored entry is plaintext would otherwise lose their server with no way back, so the entry stays active as recovery metadata with a blank token, visible to Sources and to the rediscovery loop; a later eligible HTTPS registration re-points the same slot, clears the verdict and restores ordinary activation. Developer builds under AllowPlaintext behave exactly as before.

client()'s doc no longer claims an unchanged contract: its precondition genuinely moved from "something installed" to "something credential-eligible installed". No reachable panic was found — production paths use client_for/client_opt, and direct client() calls are test support — but the stale claim was worth correcting before someone relies on it.

Verification

Gates on the committed tree, working tree empty before and after:

test result: ok. 3999 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 27.49s
test result: ok. 4046 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 27.98s
test result: ok. 85 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s

--no-default-features (the shipping set, and the feature set this defect lives in): clean. ci/check-deps.sh: all gates green.

New tests cover the plaintext stored source under HttpsOnly, the legacy address+port synthesised-HTTP path, the unchanged AllowPlaintext behaviour, a cold boot carrying one HTTP-only stored source through install plus boot activation, and recovery of retained insecure metadata to HTTPS.

Relationship to #182

Independent. #107 has two separate causes and this is the other one; neither PR depends on the other, and the files do not overlap.

🤖 Generated with Claude Code

…e credential

Discovery already refused to activate a plaintext candidate, but restoring a
stored session did not: installation gated on SourceRef::dialable, which is
well-formedness — the port fits an i32, the address parses — and says nothing
about whether a token may ride that scheme. A legacy entry holding only address
and port has its origin synthesised as http, and servers::activate published it
as the current credentialed client without a second look.

The transport then did its job and refused every token-bearing request, so the
session was not merely artwork-less: hubs, metadata and library browsing all
failed together, and the only trace was one `refused plaintext PMS credentials;
HTTPS required` line. That is the sequence in the log attached to #107.

CredentialPolicy is unchanged and remains the single cfg! for this rule; what
changes is where it is asked. A CREDENTIAL_ELIGIBLE bitmask now sits beside
ACTIVE and is written in activate(), the one point every registration path
reaches — stored primary, roster, controlled bootstrap, discovery activation and
endpoint repair alike — and set_current refuses a slot that is not in it.

An ineligible origin is NOT dropped. A user whose only stored entry is plaintext
would otherwise lose their server with no way back, so the entry stays active as
recovery metadata with a blank token, visible to Sources and to the rediscovery
loop; a later eligible HTTPS registration re-points the same slot, clears the
verdict and restores ordinary activation. Developer builds under AllowPlaintext
behave exactly as before.

client()'s doc no longer claims an unchanged contract: its precondition moved
from "something installed" to "something credential-eligible installed".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 20, 2026 21:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-20T23:24:35.931903Z eab7e57 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@GLinnik21

Copy link
Copy Markdown
Owner Author

@codex review

Two things worth attacking directly:

  1. CREDENTIAL_ELIGIBLE and ACTIVE are separate atomics written in activate(). Is every read of the pair consistent, or is there an interleaving where a slot is ACTIVE-and-not-yet-eligible (or the reverse) that a concurrent current()/client_for could observe?
  2. The recovery story is the part with no device coverage: an ineligible origin stays active with a blank token. Is there any path that can still attach a token to it, or that treats the blank token as a valid grant rather than as 'not usable yet'?

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c09fc23ea4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pub(crate) fn register_captured_origin_with_connection(machine_id: &str, origin: &Origin,
token: &str, pin: Option<&ResolvePin>, client_id: &str, connection: ConnectionFacts) -> ServerId {
let id = register_lazy(machine_id, origin, token, pin, connection, &|| client_id.to_owned());
let policy = CredentialPolicy::build();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the stale CredentialPolicy caller list

Update rust-modules/src/plex/origin.rs:112-114 alongside this new call: that documentation still says only the two auth live edges invoke CredentialPolicy::build() themselves, but this commit adds several registry entry points that do so. This leaves the credential-policy architecture documentation factually incorrect and can mislead future security-policy audits.

AGENTS.md reference: AGENTS.md:L104-L105

Useful? React with 👍 / 👎.

origin.rs said only the two discovery live edges call CredentialPolicy::build()
themselves. #183 added three more callers inside servers.rs's registry
(register_origin, register_captured_origin_with_connection,
register_pinned_with_client_id), so a stored origin is graded by the same
policy a discovered one is — the doc now says so, and names the transport
boundary (http::credential_transport_allowed) as the third, per-request ask.

session.rs's SourceRef::dialable() doc said a plaintext credential is refused
"solely... at the point of the actual probe or request — never here [at
registration]". #183 is exactly the fix that makes that false: registration
now asks CredentialPolicy too, before a stored origin can become the current
client. Updated to describe both decision points.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eab7e57bab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +848 to 849
if credential_eligible && !current().is_set() {
CURRENT.store(id.0 as u32, Ordering::Release);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the intended primary after HTTPS repair

When a stored primary is rejected as plaintext but another stored source is credential-eligible, that fallback source becomes CURRENT; a later endpoint repair that re-points the intended primary to HTTPS reaches this branch with current().is_set() == true, so it never restores the primary. The RegistryPlan::Endpoint handler in auth.rs only registers, describes, and publishes the probe result, without calling set_current, leaving current-based artwork, server-info, and route fallbacks pointed at the other authority until an explicit switch or restart.

Useful? React with 👍 / 👎.

@GLinnik21
GLinnik21 merged commit ece326c into main Sep 20, 2026
7 of 8 checks passed
@GLinnik21
GLinnik21 deleted the fix/credential-eligible-registration branch September 20, 2026 23:34
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.

2 participants