Skip to main content

Credentials resolver

AlphaSwarm collapses every "where does this service's credential come from?" question into a single :class:alphaswarm.credentials.CredentialResolver.

The resolver walks an ordered chain of :class:alphaswarm.credentials.SecretStore instances and returns the first non-empty hit, falling back to a caller-supplied default. The chain order means a fresh M2M token wins over a bootstrap-minted file payload, which wins over a static settings seed.

Why

The motivating bug: iceberg_bootstrap mints a runtime principal (alphaswarm_runtime) and persists it to data/bootstrap/polaris-principal.json, but polaris_client and iceberg_catalog._build_properties historically read settings.polaris_client_* / settings.iceberg_rest_credential — the static root / s3cr3t seed — so Polaris kept rejecting the API container's writes with CREATE_TABLE_DIRECT_WITH_WRITE_DELEGATION 403s.

The resolver closes that loop without forking the credential paths.

Architecture

The resolver is a process-wide singleton built lazily by :func:alphaswarm.credentials.get_resolver. The default chain is Env + File; M2M plugs in front when :func:alphaswarm.auth.m2m.install_m2m_store runs (controlled by ALPHASWARM_AUTH_M2M_ENABLED).

Usage

from alphaswarm.credentials import CredentialKey, get_resolver

cred = get_resolver().resolve(
CredentialKey("polaris", "oauth"),
default={"client_id": "root", "client_secret": "s3cr3t"},
)
client_id = cred.get("client_id")
client_secret = cred.get("client_secret")

Credential.source is "file" / "env" / "m2m" / "default", useful for diagnostics.

Field maps

Per (service, purpose), here is what consumers expect:

  • polaris:oauthclient_id, client_secret, principal
  • polaris:rest / iceberg:restcredential (<id>:<secret>), token, oauth2_server_uri, scope
  • trino:basicuser, source, optional token / access_token
  • minio:staticaccess_key, secret_key, endpoint_url, region
  • minio:stssession_token (M2M-issued)
  • neo4j:basicuser, password, uri

Add new entries to alphaswarm/credentials/stores/env_store.py when you wire a new service to the resolver.

Bootstrap → resolver

Bootstrap workflows call :func:alphaswarm.services.iceberg_bootstrap.persist_principal_credentials (and similar) to write JSON under settings.bootstrap_state_dir. FileSecretStore reads those files; the bootstrap also resets any caches that depend on the credentials (e.g. iceberg_catalog.reset_catalog_cache()).

When you add a new bootstrap step:

  1. Add the file name to alphaswarm/credentials/stores/file_store.py::_FILE_MAP.
  2. Persist a JSON payload with at least client_id / client_secret.
  3. Reset any consumer caches in your bootstrap writer.

Diagnostics

get_resolver().describe() returns the active store chain and priorities — wire it into a debug endpoint when you need to inspect the resolution order from outside the process.

Testing

tests/credentials/ contains the canonical test patterns:

  • Test the resolver chain priority order with pytest.
  • Test new env store branches with a _StubSettings shim.
  • Test new file store keys by writing the JSON to a tmp_path.

The reset_resolver fixture re-builds the singleton between tests so you don't have to track down stale state.