Skip to content

feat: persist index credentials in the OS keyring (sysand auth login/logout/status/whoami)#453

Open
consideRatio wants to merge 27 commits into
sensmetry:mainfrom
consideRatio:credential-storage
Open

feat: persist index credentials in the OS keyring (sysand auth login/logout/status/whoami)#453
consideRatio wants to merge 27 commits into
sensmetry:mainfrom
consideRatio:credential-storage

Conversation

@consideRatio

@consideRatio consideRatio commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Non-AI writeup - I just wanted to see what came out of following the planned feature, and enable live testing a UX with it.

It was mostly fable 5 doing the work, fallback to opus 8 some times i figure.


Implements credential storage and the sysand auth command family, v1 of the design settled in #437 (the reviewed plan lives in this comment and is imported as design/credential-storage.md in this branch's first commit).

A note on how this PR came to be: it is a one-shot, agent-driven implementation of that plan. The thinking work happened in the plan itself (several adversarial review passes, documented in the issue); the code was then produced in one pass of small reviewed increments, each verified on the integrated branch. If the direction is wrong, discarding this costs tokens, not accumulated human design work, so please review the plan first and treat the code as its consequence. Draft until a maintainer wants to take it forward.

The server-side counterpart (the v1/whoami endpoint) is already deployed on sysand.com (a validating login against it probes v1/whoami).

What this delivers

Store an index credential once with sysand auth login, and sysand reuses it across runs on Windows, macOS, and Linux, with the secret held by the OS keyring, never a plaintext file. Bearer-only v1; SYSAND_CRED_* env vars keep working unchanged and take precedence (CI overrides an interactive login).

Command surface: auth login / logout / status
  • sysand auth login [INDEX_URL] [--token-stdin]
    • No URL resolves the default-index chain (SYSAND_DEFAULT_INDEX, config default = true, else https://sysand.com); more than one configured default errors and asks for an explicit URL. The resolved index is always echoed before any secret entry. There is deliberately no per-subcommand --default-index flag: it would only duplicate the positional argument.
    • Secret via hidden prompt (Enter token for ...:, rpassword) or --token-stdin (trims exactly one trailing newline); never an inline argument. No TTY and no --token-stdin fails fast instead of hanging. Empty token errors.
    • Re-login over an existing entry prints a replacing notice before the write.
    • On a host with no usable keyring backend, login refuses to persist and prints exact SYSAND_CRED_* lines with the secret as a literal <token> placeholder (never echoed; asserted by test).
  • sysand auth logout [INDEX_URL]: removes a stored login (same default-index resolution, echo, and clear missing-entry error).
  • sysand auth status: one unified view of everything sysand will authenticate with. Stored entries (key in the exact auth logout <key> form, covered globs, subject and token prefix when a validating login ran, expires in N days / expired, SYSAND_CRED_* shadowing) and env entries, tagged by source; each stored entry shows its persisted validation claim (validated (read/api) or a warn-styled not validated); absence negatives print only when nothing is configured at all; entries covering the resolved default index carry a dim (default index) marker. Never shows secrets. Output is styled with the CLI's existing anstyle tokens and aligned to the same 12-column gutter as the rest of the CLI (plain when piped or under NO_COLOR).
  • sysand auth whoami [INDEX_URL]: query-only live identity check against v1/whoami, using the same credential selection as the runtime (env over stored, and the output names which source was used). Exit 0 only on an accepted credential; rejected/unreachable/rate-limited get distinct nonzero messages. Never writes the store. Errors clearly when the index advertises no API.
  • URL-template index locations (for example a GitLab repository-files-API index, https://gitlab.com/api/v4/projects/<id>/repository/files/{path}/raw?ref=index) are accepted as login/logout/whoami targets: the credential is scoped to the template's literal prefix, and template keys are normalized so login/logout/status round-trip.
Storage: single keyring blob, fail-closed, locked
  • All persisted credentials live in one OS-keyring entry (service sysand, account credentials) holding a versioned JSON blob of records {key, globs, scheme, secret, expires_at?, subject?, token_name?, token_prefix?}.
  • Fail-closed codec: unknown fields round-trip (serde(flatten)), a parse failure surfaces "credential store unreadable" instead of silently clobbering stored credentials. A pre-existing older blob is proven to still parse.
  • Read-modify-write is guarded by a cross-process advisory file lock (fd-lock) at a per-user path, bounded wait; never an existence-based lock file.
  • Windows blob cap (~2.5 KB, measured in UTF-16 bytes) yields a clear "store full / token too large" error instead of truncation.
  • Error taxonomy: backend absent (env fallback allowed) vs locked/denied (surfaced with an unlock suggestion and the SYSAND_CRED_* fallback named).
  • Cargo feature layout: record types, codec, and the CredentialStore trait are unconditional core; the OS-keyring backend sits behind a new non-default keyring feature (enabled by the CLI; the js/wasm binding stays green, verified by wasm cargo check). dbus is vendored so clippy --all-features works on runners without libdbus headers.
Consumption: lazy, no extra requests, env precedence
  • New CredentialStoreAuthentication combinator wraps the existing env policy: runs it first, returns non-4xx untouched, and only on an auth-relevant 4xx reads the credential blob (once per process, cached via OnceCell; spawn_blocking for the sync keyring call) and issues a forced bearer retry when a stored glob matches.
  • Crucially it passes the initial response through: no matching record means the original response is returned with zero extra requests (404 is routine on the resolve path, so a naive retry layer would have doubled round-trips for logged-in users; asserted by request-counting tests).
  • Local/offline commands, public unauthenticated reads, and never-logged-in users never touch the keyring. Steady-state keychain reads are silent per platform.
  • Precedence is SYSAND_CRED_* > stored login > unauthenticated, end to end.
  • After a forced retry from a stored credential still fails with any 4xx and the record's expires_at is past, a hint suggests re-running sysand auth login (any 4xx, since some hosts answer 404 on bad auth); at most once per record per process.
Login validation (always on)
  • Discovery-first: the login fetches sysand-index-config.json once, both for glob scoping and to learn api_root. The API surface is probed only when discovery explicitly advertised api_root (a new api_root_advertised flag distinguishes it from the plain-URL runtime default), so static indexes are never phantom-probed.
  • Per-surface probe: unauthenticated baseline, then a forced-auth retry only if the baseline was 4xx. A surface counts as tested only when the credential was actually exercised; a public 200 proves nothing. v1/whoami is forced-only. Probes never follow redirects (a redirected probe counts as not tested, naming the target). A 429 is never a verdict (rate-limited probes count as not tested; this was a spec bug found during implementation and fixed in both code and plan).
  • Refusal rule: store if any exercised surface accepted (warning about the rest); refuse, before any write, only when at least one exercised surface rejected and none accepted; nothing exercised stores as "stored, not validated". Output always scopes the claim: validated (read), validated (api), validated (read, api), or stored, not validated.
  • A rejected login whose read surface challenged with WWW-Authenticate: Basic says the index uses username/password auth and routes to SYSAND_CRED_<X>_BASIC_USER / _BASIC_PASS.
  • A successful whoami probe persists subject, token name/prefix, and expires_at into the record for auth status.
  • There is no validation opt-out, no flag and no core parameter: offline or unreachable indexes degrade gracefully through the refusal rule (nothing exercised the credential, so it stores as "stored, not validated" with warnings), false refusals are engineered out (429 and redirects are never verdicts; acceptance by any exercised surface wins), and SYSAND_CRED_* remains the escape hatch for pathological middleboxes.
  • The login discovery fetch is an unauth baseline with a forced-bearer retry (carrying the just-entered secret) on any 4xx, including 404, distinguishing a hidden discovery document from an absent one. A fully private dynamic index therefore gets its api_root learned, disjoint roots covered, and can validate as validated (read, api); a forced 404 is treated as an authoritative "no document".
Glob scoping
  • Derived globs are globset-escaped (globset::escape(<normalized root>) + **), so URLs containing glob metacharacters, including IPv6 literals like https://[::1]:8000/, cannot silently fail to match themselves (tested).
  • Coverage is anchored on the user-supplied discovery URL and extended with the resolved index_root / api_root only when not already prefix-covered; disjoint hosts get their own escaped glob. Tests assert the discovery document URL, index.json URL, and upload URL each match the derived set.
  • Templated locations, both discovery-advertised index_roots and typed login targets, are anchored at their literal prefix (clamped to at least scheme://authority/; a template with no safe anchor is rejected with a pointer to SYSAND_CRED_*).
  • Stored globs are the login-time snapshot and are never auto-updated from live discovery (the security boundary from the plan's trust model).
Publish integration
  • Bearer selection is now source precedence: env matches first (single match within env; ambiguity errors per source without falling back), then stored logins, replacing the old flat exactly-one-match rule. Trusted publishing is unchanged and still wins in CI.
  • The selected bearer carries provenance, so an upload 401/403 says where the credential came from ("from SYSAND_CRED_<LABEL>" vs "from your stored login for <key>") with matching remediation; a 403 additionally points at sysand auth status (catches a project token scoped to a different project).
  • A stored bearer with a clearly past expires_at (60 s skew margin) stops publish before the archive upload; the server's 401 remains the authority.
  • Publish reads the keyring only when env has no matching bearer (asserted by panicking-provider tests).
Protocol spec and server counterpart
  • design/index-api-protocol.md gains a Token Identity section specifying GET v1/whoami under the resolved api_root: bearer auth, 200 body (subject.type user/project/oidc with per-type subject.name semantics, token.name null for exchanged OIDC tokens, non-secret token.prefix, expires_at with Z suffix), 401 with unspecified body, MUST NOT consume single-use credentials, MAY rate limit.
  • The endpoint itself is implemented and tested in the sysand-index repo (routed at api/v1/whoami, reusing the existing bearer resolution across all three token types). It is already deployed on sysand.com, satisfying the ordering requirement that whoami be live before this client (a validating login against an official index without it would refuse valid tokens on a 404).
Testing and verification
  • 463 sysand-core tests (with the keyring feature enabled) and 164 sysand CLI tests, all green on the final branch; cargo check -p sysand-core -F networking (keyring off) and wasm cargo check green; prek run -a clean; every commit DCO-signed.
  • CLI integration tests never touch the real OS keyring: a debug-build-only SYSAND_TEST_CREDENTIAL_STORE seam selects a file-backed or absent backend (hard-errors in release builds), enabling end-to-end login/status/logout tests including a PTY-driven hidden-prompt test and a no-secret-leak assertion on the no-keyring path.
  • Notable properties covered by tests: zero keyring reads for public/unauthenticated flows, exactly one blob read per process, original response returned with no extra request on no-match, S1-S4 validation matrix from the plan, refusal never mutates an existing record, IPv6/template glob derivation, pre-B7 blob compatibility.
Deferred and follow-ups

Deferred by design (plan section 10): auth set / auth unset, basic auth via --username, --pattern, longest-prefix glob tie-breaking, and P2 protocol enforcement (a separate breaking change). Follow-ups noted during implementation: exposing do_auth_* through the py/java bindings, templated login targets, a CI test lane with -F keyring, and cargo-deny verification of the new deps (keyring, fd-lock, believed MIT/Apache-compatible).

docs.sysand.com documentation. The user-facing docs for this feature are written and reviewed (a multi-lens review pass, fixes applied) on the docs/sysand-auth branch in the sysand-index repo, and are live on the staging docs preview. That branch is 16 pages: the client authentication explanation / how-to / reference, the auth login / logout / status / whoami command pages, publish and configuration cross-links, and the index-side API-token pages. It is deliberately gated on this PR shipping: docs.sysand.com deploys from main, and the docs policy forbids documenting functionality that has not been released, so the branch merges to main (and goes live) only once the release carrying this change is out.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN

@consideRatio

consideRatio commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

Doing some UX testing, dumping notes here. (Transcripts below refreshed to match the gutter-aligned output that landed after the original capture.)

  • sysand auth status output included no coloring, it should look nicer
  • sysand auth login <gitlab index with {path}> doesn't work yet, but plan was that it should
  • sysand auth whoami felt like a very relevant command that was missing, and there is for example kubectl auth whoami, so adding it makes sense to me
  • Fix output alignment to mirror other commands
  • It is not so obvious that indexes configured are typically not considered, its mostly the default index that is considered with commands. not sure what i suggest if anything, sysand auth status
  • (semi unrelated) sysand publish requires the flag --index which is a bit weird UX wise, while sysand auth login takes an optional positional argument. I think it smells a bit, not sure what i suggest.
  • (unrelated) no nice tab indentation etc on sysand info --iri ... output

Testing with staging.sysand.com

$ sysand auth login https://staging.sysand.com
  Logging in to index `https://staging.sysand.com/`
Enter token for `https://staging.sysand.com/`:
error: credential for `https://staging.sysand.com/` was rejected by the index API (`v1/whoami`) and accepted by no surface; nothing was stored

note: pass `-v`/`--verbose` to output additional logs

$ sysand auth login https://staging.sysand.com
  Logging in to index `https://staging.sysand.com/`
Enter token for `https://staging.sysand.com/`:
      Stored credential for `https://staging.sysand.com/` (validated (api))
      Covers https://staging.sysand.com/**

$ sysand auth login https://staging.sysand.com
  Logging in to index `https://staging.sysand.com/`
Enter token for `https://staging.sysand.com/`:
   Replacing existing credential for `https://staging.sysand.com/`
      Stored credential for `https://staging.sysand.com/` (validated (api))
      Covers https://staging.sysand.com/**

$ sysand auth status
      Stored https://staging.sysand.com/  validated (api)
             patterns: https://staging.sysand.com/**
             subject: user admin
             token prefix: sysand_u_53a8fea5
             expires: 2026-10-17 11:27:09 UTC (expires in 89 days)

$ sysand build
    Building kpar `/home/erik/dev/sensmetry/sysand-test-repo/output/proj0-4.0.21.kpar`
updating file metadata (1/1)
   Including readme from `/home/erik/dev/sensmetry/sysand-test-repo/README.md`
   Including license from `/home/erik/dev/sensmetry/sysand-test-repo/LICENSES/MIT.txt`

$ sysand publish --index https://staging.sysand.com
warning: KPAR does not contain a changelog file CHANGELOG.md;
         it is recommended to provide it to inform users of
         the changes between versions
  Publishing admin/proj0 v4.0.21 to https://staging.sysand.com/
   Published new release successfully

$ sysand auth logout https://staging.sysand.com
 Logging out from index `https://staging.sysand.com/`
     Removed stored credential for `https://staging.sysand.com/`

$ sysand auth status
No credentials configured (no stored logins, no `SYSAND_CRED_*` variables).

Testing with private index on GitHub

$ sysand auth login "https://raw.githubusercontent.com/consideratio/priv-index-example-github/refs/heads/index/"
  Logging in to index `https://raw.githubusercontent.com/consideratio/priv-index-example-github/refs/heads/index/`
Enter token for `https://raw.githubusercontent.com/consideratio/priv-index-example-github/refs/heads/index/`:
      Stored credential for `https://raw.githubusercontent.com/consideratio/priv-index-example-github/refs/heads/index/` (validated (read))
      Covers https://raw.githubusercontent.com/consideratio/priv-index-example-github/refs/heads/index/**

$ sysand info --iri "pkg:sysand/example-publisher-1/example-project-1" --index "https://raw.githubusercontent.com/consideratio/priv-index-example-github/refs/heads/index/"
warning: Direct or transitive usages of SysML v2/KerML standard library packages are
         ignored by default. If you want to process them, pass `--include-std` flag
Name: example-project-1
Publisher: Example Publisher 1
Version: 0.0.1
License: MIT
No usages.

Testing with private index on GitLab

$ sysand auth login "https://gitlab.com/api/v4/projects/84113019/repository/files/{path}/raw?ref=index"
  Logging in to index `https://gitlab.com/api/v4/projects/84113019/repository/files/{path}/raw?ref=index`
Enter token for `https://gitlab.com/api/v4/projects/84113019/repository/files/{path}/raw?ref=index`:
      Stored credential for `https://gitlab.com/api/v4/projects/84113019/repository/files/{path}/raw?ref=index` (validated (read))
      Covers https://gitlab.com/api/v4/projects/84113019/repository/files/**

$ sysand auth status
      Stored https://gitlab.com/api/v4/projects/84113019/repository/files/{path}/raw?ref=index  validated (read)
             patterns: https://gitlab.com/api/v4/projects/84113019/repository/files/**

$ sysand info --iri "pkg:sysand/example-publisher-1/example-project-1" --index "https://gitlab.com/api/v4/projects/84113019/repository/files/{path}/raw?ref=index"
warning: Direct or transitive usages of SysML v2/KerML standard library packages are
         ignored by default. If you want to process them, pass `--include-std` flag
Name: example-project-1
Publisher: Example Publisher 1
Version: 0.0.1
License: MIT
No usages.

@consideRatio
consideRatio force-pushed the credential-storage branch 2 times, most recently from a94ecaf to 99a0bd1 Compare July 20, 2026 11:35
@consideRatio consideRatio changed the title feat: persist index credentials in the OS keyring (sysand auth login/logout/status) feat: persist index credentials in the OS keyring (sysand auth login/logout/status/whoami) Jul 22, 2026
@consideRatio
consideRatio force-pushed the credential-storage branch 2 times, most recently from 18c55d5 to 3dcef4b Compare July 22, 2026 17:18
@consideRatio
consideRatio marked this pull request as ready for review July 22, 2026 17:24
Comment on lines +54 to +60
- **P2 - API presence is read from discovery (consumed, not enforced
here).** This plan treats an index as having an API iff its discovery
document advertises `api_root`, and derives globs accordingly. It does
**not** change the runtime default (today a plain-URL index still defaults
`api_root` to its root). Enforcing "`api_root` required" at the protocol
level, and dropping that default, is a **separate, decoupled change**
(§12), out of this plan's phases.

@andrius-puksta-sensmetry andrius-puksta-sensmetry Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why require api_root to be explicitly advertised to treat it as such?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Without doing so, you could end up not knowing if you failed to access an API or there was no API if the discovery/index was public but not the API, as error codes cant be trusted to distinguish that.

However, I dislike the state of things now with this PR, as we havnt forced sysand publish to require api_root to be explicitly declared.

Wdyt? Make sysand publish require it?

An index must have a file serving index root, but not an API, so it would be good to have the discovery doc (or missing of one) to indicate it - for example with an explicit api_root

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not reflected at great length, but asked ai to make such requirement #478 preliminary

consideRatio and others added 14 commits July 24, 2026 16:47
Squashed import of the reviewed plan from the design/credential-storage-plan
branch so the implementation branch is self-contained; see
sensmetry#437 for the discussion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Rename StandardHTTPAuthentication::try_into_publish_bearer_auth_map to
publish_bearer_auth_map and take &self, cloning the bearer tokens into
the returned GlobMap instead of consuming the policy to move them. The
upcoming lazy keyring auth layer is not Clone, so publish can no longer
use Arc::unwrap_or_clone to consume the policy; the secret clones are
accepted as the cost of that layer (design/credential-storage.md,
section 9). No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
…ture

Implement phase 1 storage from design/credential-storage.md (sections 9
and 14), the store only, nothing consuming it yet.

Unconditional in sysand-core:

- CredentialRecord / CredentialBlob types with a versioned JSON codec;
  unknown fields are tolerated on read and round-tripped on rewrite, and
  a parse failure or unknown version fails closed with a dedicated
  "credential store unreadable" error, never treated as empty
- normalize_index_key: index-URL key normalization via the url crate
  (trailing slash, lowercased host, http(s) only)
- the CredentialStore trait plus an in-memory implementation for tests

Behind the new non-default keyring cargo feature:

- OsKeyringBackend: one keyring v3 entry (service "sysand", account
  "credentials") holding the whole blob, with the error taxonomy split
  into backend-absent (callers may fall back to env) and backend-denied
  (callers must surface)
- LockedBlobStore: cross-process advisory file locking via fd-lock with
  a bounded wait, a per-user lock path (XDG runtime/state dir or
  %LOCALAPPDATA%, home fallback), and 0600/0700 modes on unix
- Windows CRED_MAX_CREDENTIAL_BLOB_SIZE gate measured in UTF-16 bytes,
  with keyring's TooLong mapped to the same friendly error
- deleting the entry when the last record is removed, keeping the cheap
  no-entry fast path for logged-out users

The blob handling is generic over a BlobBackend trait so the locking,
fail-closed, and size-limit logic is tested headlessly without an OS
keyring. The keyring crate uses its vendored dbus so Linux builds need
no system libdbus headers; the feature stays off for the js/wasm
binding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Restructure publish bearer selection from one flat glob map into a
source-tagged PublishBearerSources struct (env map plus keyring map),
per design/credential-storage.md sections 7 and 8. Selection tries env
first: a single env match wins, an ambiguous env match errors without
consulting the keyring, and only when env has no match at all is the
keyring source tried under the same single-match rule. No match in any
source keeps the existing NoPublishBearer error, whose SYSAND_CRED_*
hint stays truthful while `sysand auth login` does not exist yet.

The keyring map is constructed but always empty for now, so behavior is
unchanged today (env remains the only source); a later phase populates
it from stored logins. The keyring-side constructor is crate-private on
purpose so the eager two-map shape is not a public API commitment (the
stored-login source must stay lazily read). AmbiguousPublishBearer now
carries the ambiguous source and renders a per-source remediation hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Implement the deferred/cached auth policy from
design/credential-storage.md (sections 9 and 14): stored logins are now
consulted at request time, with the credential blob read at most once
per process and only when a credential might actually be needed.

New in sysand-core (behind the existing networking feature):

- CredentialStoreAuthentication<Inner, S>, a combinator generic over
  the CredentialStore trait. The inner env policy (SYSAND_CRED_*) runs
  first; only a 4xx triggers the store read. Deliberately not a stock
  SequenceAuthentication: the lower arm there cannot see the higher
  arm's response, so a routine resolve-path 404 would be re-requested
  identically on every call. This combinator passes the initial
  response down and returns it untouched when no stored record matches
  the URL, so no-match costs no extra request.
- A matching record sends a forced bearer retry (v1 stores bearer
  credentials only). Several matching patterns are a real ambiguity
  only between distinct tokens (one login stores several globs);
  genuine ambiguity warns and tries all matches in order, mirroring
  RestrictAuthentication.
- The read is cached in a tokio OnceCell so concurrent first requests
  (the resolve path fans out) share one keychain touch, and it runs
  under spawn_blocking because the keyring crate is synchronous and a
  locked Secret Service can block on an unlock prompt. A synchronous
  accessor sharing the same cache serves publish, which resolves
  credentials outside the runtime.
- Store errors degrade to no-credentials for the process: an absent
  backend is a quiet debug, a locked/denied backend warns once naming
  the unlock and SYSAND_CRED_* remediations. An invalid stored URL
  pattern skips only itself, not every login.

Publish keyring source: resolve_publish_bearer now takes the eager env
bearer map plus a lazy stored-credential provider, invoked only when no
env bearer matches the upload URL, replacing the eager two-map
PublishBearerSources from the source-precedence refactor. Env-match,
env-ambiguity, and trusted-publishing paths never read the store.

The sysand CLI enables core's keyring feature and composes the policy
as CliAuthPolicy (env policy over KeyringCredentialStore::open_default,
degrading with a warning if the store cannot be opened). Bindings are
unchanged: the layer is generic over the store trait, core still
compiles with networking on and keyring off, and the wasm/minimal
configs stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
First slice of the sysand auth command family
(design/credential-storage.md sections 4, 9, 14); login follows
separately and slots into the same subcommand tree.

Core (commands/auth.rs, gated filesystem+networking like publish):
do_auth_logout and do_auth_status orchestration, generic over
CredentialStore. Library calls never prompt and never print; env
credentials are passed in as EnvCredentialEntry values, and the status
view reports per-record key, globs, expires_at (with an expired flag
computed in core), and which SYSAND_CRED_* entries may shadow the
record. StoredCredentialStatus is the extension point for the
subject/prefix fields the validation work will add.

CLI: a `sysand auth` subcommand with `status` and
`logout [index-url]`. Bare logout resolves the target through the
default-index chain (--default-index / SYSAND_DEFAULT_INDEX, then a
default = true configured index, else the built-in default index URL;
more than one distinct default asks for an explicit URL) via a shared
resolve_default_index helper that login will reuse, and echoes the
resolved index on stdout so --quiet cannot hide it. Missing credential,
non-HTTP(S) target, and unparseable URLs are clear non-zero errors.
Status renders one unified view (stored entries with the exact logout
key form, env entries as label + pattern, never a secret), degrades to
an env-only view with a note when no keyring backend is usable, and
surfaces a locked or denied keyring with the unlock and SYSAND_CRED_*
hints. Auth commands dispatch before the eager env-policy build so
`auth status` stays usable for diagnosing malformed SYSAND_CRED_*
groups.

Tests: core coverage over InMemoryCredentialStore (logout removal,
normalization, error taxonomy, status assembly, shadow detection,
clock-based expiry) and CLI tests for parsing, the default-index
resolution branches, and status rendering, all constructed to never
require or touch a real OS keyring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Store a bearer credential for an index (design/credential-storage.md
sections 4, 8, 9, 14), without validation probes; those arrive with the
upcoming --validation flag, and do_auth_login leaves an explicit slot for
them between glob derivation and the store write.

Core (do_auth_login, generic over CredentialStore, never prompts):

- Discovery is fetched best-effort with the unauthenticated policy to
  resolve index_root/api_root for glob scoping; when it cannot be read
  (network failure, or 401 on a private index) the credential falls back
  to the URL-derived glob with a notice.
- Escaped glob derivation: the primary glob anchors on the normalized
  user-supplied URL (globset::escape(root) + "**"); the resolved
  index_root and api_root add globs only when not already covered
  (Case B), keeping the set minimal. A templated index_root anchors on
  its literal prefix cut back to the last "/" boundary and reparsed as
  a URL, and is skipped with a notice when no anchor at least as deep as
  scheme://authority/ exists (a shallower anchor could match other
  hosts). Tests pin the section 8 coverage guarantee (discovery
  document, index.json, and upload URL each match) and IPv6-literal
  escaping.
- Replacing an existing login is reported through a notice before the
  write; an absent keyring backend is an outcome carrying the derived
  globs so the CLI can print the exact SYSAND_CRED_* lines to set
  instead (secret always as a literal <token> placeholder).
- URL-template targets are rejected up front for both login and logout
  (normalization would silently mangle the braces); SYSAND_CRED_* stays
  the authentication path for templated indexes.

CLI:

- sysand auth login [INDEX_URL] [--token-stdin], with the default-index
  chain of logout and the resolved index echoed before any secret entry.
- The token comes from a hidden prompt (rpassword) or --token-stdin
  (trimming exactly one trailing newline), never an inline argument;
  a non-TTY stdin without --token-stdin fails fast, and an empty token
  is an error. Success wording says "stored, not validated".
- New debug-only test seam (SYSAND_TEST_CREDENTIAL_STORE) swapping the
  OS keyring for a file-backed or simulated-absent blob store, so CLI
  integration tests never touch a real keyring; release builds refuse
  the variable rather than silently using the keyring. The auth
  commands and the lazy auth policy now share this store construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Implement design/credential-storage.md section 5 for `auth login`:

- `--validation true|false` (default true), mapped as `Option<bool>`
  through core so bindings later get a clean optional keyword.
  `--validation false` preserves the previous store-without-probing
  behavior exactly.
- Probe mechanism, discovery-first: the read surface
  (`index_root/index.json`) gets an unauthenticated baseline and a
  forced-bearer retry only on a baseline 4xx; the API surface
  (`api_root/v1/whoami`) is forced-only and probed only when discovery
  explicitly advertised `api_root` (`ResolvedEndpoints` gains
  `api_root_advertised`, never set by the plain-URL runtime default).
  Probes use a dedicated no-redirect, timeout-bounded client; a
  redirected probe counts as not tested with a warning naming the
  target.
- Refusal rule: store when any exercised surface accepted (warning per
  rejected or unreachable surface); refuse, without touching an
  existing stored login, only when at least one exercised surface
  rejected and none accepted; store as "stored, not validated" when
  nothing exercised the credential. Output always scopes the claim
  ("validated (read)", "validated (api)", "validated (read, api)").
- A `WWW-Authenticate: Basic` challenge on a rejected read surface
  routes the user to `SYSAND_CRED_<X>_BASIC_USER` / `_BASIC_PASS`.
- `CredentialRecord` persists whoami identity (`subject`,
  `token_name`, `token_prefix`, `expires_at`; blob version stays 1,
  pre-existing blobs still parse), and `auth status` shows subject,
  token prefix, and "expires in N days" / "expired".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Task B8 of the credential-storage plan (design/credential-storage.md
sections 5, 7, 9):

- 429 is never a probe verdict: a rate-limited unauth baseline or forced
  retry during `sysand auth login` validation counts the surface as not
  tested (new ProbeRateLimited notice) instead of rejected, so throttling
  can no longer false-refuse a valid token. A 429 baseline also sends no
  forced retry. Recorded in the section 5 refusal rule.
- Reactive expiry hint: when a stored credential's forced retry still
  ends in any 4xx and the record carries a past expires_at, the lazy
  layer warns once per record per process to re-run `sysand auth login`.
- Source-named publish auth failures: an upload 401/403 now states
  whether the selected bearer came from `SYSAND_CRED_<LABEL>` (with the
  unset/rotate remediation, since env shadows stored logins) or from the
  stored login for an index key (re-login remediation); a 403 also points
  at `sysand auth status`. Provenance is threaded via SelectedPublishBearer
  and the new EnvBearerAuth/StoredBearerAuth map values.
- Fail fast on expiry: publish stops before uploading when the selected
  stored bearer's known expires_at is clearly past (60 s skew margin);
  env bearers carry no expiry and always proceed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Follow the CLI's house convention for disabling a default behavior
(--no-lock, --no-deps, ...): the login flag becomes the presence flag
--no-validation instead of the value-taking --validation true|false,
which would have been the only boolean flag in the CLI taking a value.

The flag spelling and the library parameter are independent: core's
do_auth_login keeps validation: Option<bool> (None = validate), the CLI
maps --no-validation to Some(false), and future language bindings still
get a clean optional keyword. The plan's section 5 rationale is updated
accordingly, both the house-style flag and the bindings ergonomics are
kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
A templated index location (for example the GitLab repository files API,
https://gitlab.com/api/v4/projects/<id>/repository/files/{path}/raw?ref=<branch>)
was rejected as a sysand auth target, leaving SYSAND_CRED_* environment
variables as the only authentication path for such indexes. Accept it:

- The storage key is the template text with its literal-prefix anchor
  normalized through url::Url serialization (lowercased scheme and host,
  default port stripped, IDN punycoded); the placeholder, suffix, and the
  prefix text past its last slash stay verbatim. The form is idempotent,
  so logout and status agree with login across spellings of the anchored
  part.
- The primary credential glob anchors on the template's literal prefix,
  cut back to the last slash and clamped to at least scheme://authority/,
  reusing the discovery-advertised templated index_root rules (design
  section 8). A template with no safe anchor is rejected before the
  secret prompt (AuthCommandError::TemplateWithoutAnchor), since no
  credential scope could be derived for it.
- Validation is unchanged: the read probe goes through the template-
  resolved index.json URL, and the API surface is probed only when a
  discovery document explicitly advertised api_root (never for a
  template without one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Style sysand auth status through the existing house tokens
(sysand_core::style) and anstream, which strips styling on non-terminal
stdout and under NO_COLOR: source tags in the header style, keys and env
labels as literals, (expired) in red, near expiry (7 days or less) and
shadowed-by warnings in yellow, and the backend-unavailable note with a
styled note: prefix. The layout and plain-text content are unchanged, so
piped output and the existing CLI test assertions stay identical.

Add sysand auth whoami [INDEX_URL], a query-only live identity check
against api_root/v1/whoami. The target resolves like login/logout
(explicit URL or the default-index chain) and is echoed; only an index
whose discovery configuration explicitly advertises api_root can be
asked, and the read surface is never probed. Credential selection
mirrors the runtime and publish source precedence: a SYSAND_CRED_*
bearer matching the whoami URL wins over a stored login, candidates
carrying one token collapse to one credential, and the output names the
source used, since the right remediation for a rejected credential
depends on where it came from. One forced-bearer GET is sent with the
no-redirect probe client: 200 renders the subject and token details and
exits 0, 401 reports the rejection with a source-tailored hint, and
redirects, rate limiting, other statuses, and network errors report the
API as unreachable, all nonzero. The command never writes the credential
store; cached identity fields are deliberately not refreshed. Unlike
login, the discovery fetch runs with the regular read policy (built
tolerantly, so malformed SYSAND_CRED_* groups cannot break a diagnostic
command), because a private index may gate its discovery document.

Core logic lives in do_auth_whoami, generic over CredentialStore and
never printing; the CLI wrapper renders. Covered by core unit tests for
the credential selection and seam-plus-mockito CLI tests for the 200,
401, env-precedence, no-credential, unadvertised-api, and non-HTTP
paths.

Also record sysand auth whoami in the design section 4 command table and
the target-defaulting principle in section 7: irreversible remote
effects require an explicit target (sysand publish --index stays
required), while reversible local effects may default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Restyle every user-facing line of sysand auth login, logout, status,
and whoami to the cargo-style convention used by the rest of the CLI:
a header-styled leading word right-aligned in a 12-column gutter, with
continuation sublines indented to the gutter. Status keeps its B10
hierarchy (Stored/Env tags with label: value sublines) while whoami
puts each identity field's label in the gutter, a deliberate split
recorded in the renderer's doc comment. Expiry timestamps drop
chrono's sub-second noise via a shared formatting helper.

Every line keeps its stdout/stderr channel; the login, logout, and
whoami echoes stay println so --quiet cannot hide them, and the
alignment spaces are part of plain piped output, which the CLI tests
now assert with leading-space predicates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Status output suppression: a credential source with nothing to show is
omitted instead of announced with a negative; when neither stored logins
nor SYSAND_CRED_* variables exist, a single combined line prints. The
no-usable-keyring note still prints whenever the backend is unusable.

Stored entries are separated by a blank line when there is more than
one, and the sublabels (patterns:, subject:, token prefix:, expires:)
render dim through a new house style token; values stay plain, and
shadowed by: stays warn-styled. Plain piped output is unchanged apart
from these content changes.

CredentialRecord gains an optional validated field: the surfaces that
exercised and accepted the credential at login, written by
do_auth_login from the probe outcome and serialized compactly as
["read","api"] strings (absent for --no-validation or
nothing-exercised logins; plain strings so a surface name written by a
newer sysand does not fail the whole blob closed). The blob version
stays 1 and pre-existing blobs parse with the claim absent. auth
status shows the claim on each entry's key line: validated (read) and
so on dimmed, or a warn-styled not validated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
consideRatio and others added 13 commits July 24, 2026 16:49
Annotate the auth status entries (stored and env) that apply to the
default index with a dim '(default index)' marker, answering which
credential bare commands will actually use. Resolution reuses the
login/logout default-index chain, including a new --default-index arg
on status, but is lenient: an ambiguous chain prints a note and marks
nothing, and an unusable default silently marks nothing; status never
errors over the chain. A stored entry is marked on normalized-key
equality with the default's key or when one of its globs matches the
default index root URL (a template default anchors on its literal
prefix); an env entry when its pattern matches that root. Patterns
compile leniently, like shadow detection.

The test harness now strips an ambient SYSAND_DEFAULT_INDEX so a
developer's override cannot inject markers or notes into asserted
output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
sysand auth login fetched discovery unauthenticated only, so a fully
private index (S4: private read surface and API) answered 401 (or, in
GitLab's style, 404) and login never learned api_root: no glob coverage
for a disjoint API host, no whoami probe, and at most a validated
(read) claim.

Discovery for a login is now an unauthenticated baseline retried once
with the in-hand secret as a forced bearer on any 4xx answer, mirroring
the probe mechanism: a forced 200 with a valid document is used exactly
like a public discovery success; a forced 404 is the authoritative "no
document" answer and reconstructs the flat topology without a warning;
everything else (other 4xx, 429, redirect, 5xx, network failure) falls
back to the URL-derived glob with the existing notice, and the probes
still deliver the credential verdict. The forced discovery fetch never
counts toward the validated claim: the index.json probe stays the sole
read verdict.

The validation opt-out is removed entirely (no --no-validation flag, no
validation parameter on do_auth_login): login has exactly one behavior.
Offline degrades gracefully through the refusal rule (nothing exercised
means "stored, not validated" with warnings, and an unreachable
discovery baseline gets no forced retry, so no secret is transmitted),
false refusals are engineered out (429 and redirects are never
verdicts, acceptance by any exercised surface wins), and SYSAND_CRED_*
remains the escape hatch.

The store is read before any network so an absent keyring backend is
detected before the secret could be spent on a credentialed request;
that path keeps discovery strictly unauthenticated. In discovery.rs the
fetch is restructured (fetch_index_config_strict surfaces an absent
document as its 404 status; resolve_index_config interprets a document
for the forced-retry path); public callers are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
…ands

The flag existed as the clap carrier for SYSAND_DEFAULT_INDEX (global
resolution options do not propagate to sibling subcommands), but as a
user-facing option it only duplicated the positional index argument.
resolve_default_index now reads the environment variable directly; the
chain (env var, config default = true, built-in) is otherwise unchanged,
including comma-delimited lists and the ambiguity error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
When publish finds no matching bearer for the upload URL, the error now
suggests `sysand auth login <index-url>` alongside the `SYSAND_CRED_*`
environment fallback. The message predated the `auth login` command, so
it only mentioned the environment path; the docs already describe the
login hint, so this aligns the client with the documented behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
The pre-upload expiry stop refused to even attempt an upload when a
stored token was more than 60 seconds past its `expires_at`. Real
client-clock skew is often minutes, not seconds, so a clock running a
few minutes fast would false-trip the check and refuse a token the
server would still accept.

The stop is only an optimization (avoid uploading a large archive with a
known-dead token); the server's 401 is the real authority. So the margin
should err toward attempting: widen it to one hour, past which a clock is
wrong enough that a request would likely fail regardless. Raised in the
plan review on sensmetry#437.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Address questions from the plan review (sensmetry#437): state
that the blob `version` gates the format and fails closed on an
unrecognized version (the migration hook); spell out the conflict model
(no record-level merge; the file lock serializes writers, last writer
wins, and field round-tripping preserves a concurrent newer binary's
additions); note the lock coordinates sysand processes with each other
only, not against other applications; and describe the expiry skew margin
as a generous hour that errs toward attempting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
On the platform blob cap (Windows ~2.5 KB), `sysand auth login` now
points at how to fix it: run `sysand auth status` to see stored logins,
`sysand auth logout <index>` to remove one, and it lists any
already-expired logins as the obvious drop candidates. A first login
whose single token overflows says to use a smaller token rather than
suggesting a nonexistent login to remove.

The message is built in the CLI, not core: the core store reports only
the condition (it must not name CLI commands). Raised in the plan review
on sensmetry#437.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Core (`sysand-core`) error messages described conditions but also named
`sysand auth login` / `sysand auth status`, coupling the library to the
CLI's command vocabulary (wrong for the bindings and any other frontend).
The library now states the condition (and the `SYSAND_CRED_*` fallback,
which is core's own env mechanism, not CLI vocabulary); the CLI adds the
`sysand auth` remediation from the error's fields, so user-facing output
is unchanged.

Relocated to the CLI:
- `NoWhoamiCredential` (whoami handler).
- `NoPublishBearer`, `StoredCredentialExpired`, and the provenance-aware
  `PublishAuthFailed` (publish handler via `publish_error_with_hint`): a
  stored credential is refreshed by re-login, an env credential is not,
  and a 403 points at `sysand auth status` for the subject.

Neutral-reworded in core, where relocation is impractical: the reactive
stored-bearer expiry warning is a `log::warn!` deep in the request path
the CLI never sees, so it now says "re-authenticate to store a fresh
credential" instead of naming a command.

Raised in the plan review on sensmetry#437.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Core states conditions (and may reference `SYSAND_CRED_*`, its own env
mechanism) but does not name `sysand <command>` subcommands; the frontend
adds command hints. Documents the rule the auth error-message refactor
established so it does not silently drift back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Safe fixes from a multi-lens review of the branch:

- security: give `ForceBearerAuth` a hand-written redacting `Debug`. It is
  the single secret-bearing leaf, so redacting it stops any accidental
  `{:?}` on a composed policy from leaking `SYSAND_CRED_*` bearer tokens
  (latent: no such formatting exists today).
- api hygiene: narrow the keyring service/account constants and the
  Windows blob-size helpers to `pub(crate)`, and gate the size helpers to
  `#[cfg(any(windows, test))]` since only the Windows path and the
  size-logic tests use them.
- simplify: extract the duplicated `v1/whoami` body parse into one
  `parse_whoami_identity` helper; collapse the single-match and ambiguous
  stored-bearer retry arms (the single case is the same path with an empty
  remainder loop), preserving the distinct log line.
- ux: do not render "expires in 0 days" for a token with under a day left
  ("expires in less than a day"); align the no-keyring status note with the
  login/logout phrasing.
- docs/comments: sync the design doc's reactive-expiry quote with the
  command-name-free wording the code now emits; correct the
  `stored_bearer_map_blocking` comment to state the real invariant and the
  latent multi-thread hazard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Decisions taken on the review's judgment calls:

- terminology: standardize the user-facing noun on "credential" (a
  UX-research pass found comparable CLIs, gh/docker/gcloud/npm/aws, all
  call the stored thing a credential and use "login" only as a verb). Sweep
  "stored login(s)" -> "stored credential(s)" in output, errors, and the
  design doc; the `Stored`/`Env` tags disambiguate from `SYSAND_CRED_*`.
  Verbs and command names (`sysand auth login`) are untouched.
- `auth login` output: emit the "Covers" result line via `log::info!` like
  "Stored", so `--quiet` suppresses the whole confirmation as a unit
  (matching `publish`'s "Published") instead of leaving an orphan "Covers"
  on stdout.
- security: gate the debug-only test-seam backends (the plaintext file /
  absent variants) behind `#[cfg(debug_assertions)]` so the cleartext path
  is physically absent from release binaries, not merely refused at
  runtime; the loud refusal stays for a release binary that sees the var.
- api hygiene: make the test-only `InMemoryCredentialStore` `#[cfg(test)]`
  so the double never ships in the public API (kept in place, shared by
  several core test modules).
- ux: a whoami failure that got a server response (403, redirect, 429) no
  longer reads "could not reach the index API"; it now says "could not get
  an identity from the index API", which fits both transport failures and
  server-answered non-identities.
- testing: the Windows blob-size cap enforcement (`store_blob` -> `upsert`)
  is Windows-only in production, so give the store an injectable size limit
  and add a test that drives the `BlobTooLarge` wiring on any platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
- Align the glob-list label: `auth status` now says "covers:" like
  `auth login`'s "Covers", instead of "patterns:".
- The non-HTTP index error now names a next step: "(use an https:// index
  URL)".
- Finish the "credential" terminology sweep in internal doc-comments (the
  earlier sweep covered user-facing output and the design doc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Making `InMemoryCredentialStore` `#[cfg(test)]` removed the only
non-keyring caller of `upsert_record` / `remove_record`, so the
`sysand-core --no-default-features --features std` clippy lane (no
keyring, no test) saw them as dead code and failed under `--deny
warnings`. Gate both to `#[cfg(any(feature = "keyring", test))]`, the
configs where the keyring store or the in-memory test store actually
call them. Local prek runs one clippy config; CI runs the full feature
matrix, which is what caught this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfyrkJkeo3mRngVoppJjUN
Signed-off-by: Erik Sundell <erik.sundell+2025@sensmetry.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants