Skip to content

Secrets API

Everything the stack knows about which values are secret comes from the Sensitive() marker on a field declaration.

Declaring a secret

Use Secret for the ordinary case — an optional string that holds a credential:

from oa_configurator import Secret
from pydantic import BaseModel, Field

class MyToolConfig(BaseModel):
    endpoint: str
    api_key: Secret = Field(default=None, description="Token for the upstream API.")

Shorthand for an optional secret string field: api_key: Secret = None.

The whole design rests on implementers declaring their secrets. Prefer this over the equivalent Annotated[str | None, Sensitive()]

Marks a string field as holding a secret: masked when interactively prompted, excluded from anything this stack renders for display, and a future anchor for secret_source (env:/file:) resolution.

Spell out Annotated[..., Sensitive()] directly where the field is not an optional string — a required secret, or one that is not a str.

Reading the declaration

Whether a field carries the :class:Sensitive marker.

The stack's only runtime sensitivity predicate: masking a field in anything, rendering configuration, and :class:SecretSafeBaseModel's repr all consult this.

What it cannot reach is free text, which has no field to look up.

:class:~oa_configurator.logging_config.RedactingFormatter is scoped to URLs written by other libraries rather than trying to guess.

Parameters:

Name Type Description Default
info FieldInfo

A field from SomeModel.model_fields.

required

Returns:

Type Description
bool

Whether the field is declared sensitive.

is_sensitive() takes a FieldInfo, so it works off any model:

from oa_configurator import ConnectionConfig, is_sensitive

for name, info in ConnectionConfig.model_fields.items():
    if is_sensitive(info):
        print(name)   # -> password

Scrubbing a URL for display

Return url with every value that could be a credential masked.

For arbitrary endpoint URLs -- anything rendering :attr:~oa_configurator.domains.llm.schema.ProviderConfig.base_url, most often. :meth:~oa_configurator.domains.resources.schema.ResolvedConnection.safe_url is the SQLAlchemy-specific equivalent and covers only the password.

  • Userinfo (https://user:pw@host): the password is masked and the username kept, matching safe_url. The username answers "which account is this connecting as?", which an operator reading a redacted URL needs.
  • Query string: every value is masked and every key kept, so ?api-version=2024-02-01&api_key=sk-x renders as ?api-version=***&api_key=***. The operator still sees which parameters are set without any value being shown. Dropping the query entirely is not an option (Azure OpenAI needs api-version), and masking only the keys that look like secrets would be the guess this module exists to avoid.
  • Fragment (https://host/v1#access_token=abc): masked whole, to #***. A fragment is never sent to the server, so nothing in an endpoint's fragment is operationally meaningful, but the OAuth implicit flow delivers access tokens like this, and a fragment has no guaranteed key=value structure to mask value-by-value.

Parameters:

Name Type Description Default
url str

URL to scrub. None passes through, so callers can hand this an optional config field directly.

required

Returns:

Type Description
(str, optional)

The scrubbed URL, or the bare mask if url could not be parsed -- an unparseable string is scrubbed by refusing to show it at all rather than by echoing it back.

Rendered in place of a secret value. Shared so displays match each other.

Making your own models safe to render

PackageConfigBase already inherits SecretSafeBaseModel, so a consuming package's config section is masked in repr/str without doing anything. Subclass it directly only for a nested model of your own that is not a PackageConfigBase.

Bases: BaseModel

Base for config models: Sensitive() fields are masked in repr and str.

All config base classes must subclass this base, so that a PackageConfigBase subclass declaring its own Secret field inherits safe rendering automatically.

masked_json

masked_json(
    *, exclude_none: bool = True, indent: int = 2
) -> str

Serialize to JSON for display, with every secret replaced by MASK.

model_dump_json deliberately emits plaintext, because saving the config depends on it. That makes it the wrong call for anything shown to a person: omop-config show printed every password and API key in the stack straight to the terminal, and into scrollback, screen shares and CI logs with it.

Masking the rendered structure rather than the model keeps the two concerns apart -- serialization stays lossless, display stays safe -- and walking the model alongside its dump means the decision still comes from :func:is_sensitive rather than from key names.

Serialize to JSON for display, with every secret replaced by MASK.

model_dump_json deliberately emits plaintext, because saving the config depends on it. That makes it the wrong call for anything shown to a person: omop-config show printed every password and API key in the stack straight to the terminal, and into scrollback, screen shares and CI logs with it.

Masking the rendered structure rather than the model keeps the two concerns apart -- serialization stays lossless, display stays safe -- and walking the model alongside its dump means the decision still comes from :func:is_sensitive rather than from key names.

Use safe_endpoint() for any URL-shaped value, ProviderConfig.base_url above all. For a database connection, ConnectionConfig.safe_url() is the SQLAlchemy-specific equivalent.

from oa_configurator import safe_endpoint

safe_endpoint("https://svc:hunter2@api.example.org/v1?api-version=2024-02-01&api_key=sk-x")
# 'https://svc:***@api.example.org/v1?api-version=***&api_key=***'

safe_endpoint("https://api.example.org/v1#access_token=sk-x")
# 'https://api.example.org/v1#***'

Proving a package does not leak

Assert that no Sensitive() value of instance appears in rendered.

Give the secret fields a distinctive canary value before rendering. The check is a substring search, so a one-character password matches almost any output and a password of "postgres" matches the dialect.

Parameters:

Name Type Description Default
instance BaseModel

The configuration the rendering was produced from. Usually a StackConfig, but any model works.

required
rendered object

Whatever the package produced for display, logging, or serialisation. Converted with str(), so a mapping, dataclass, list of view rows, or already-formatted string are all acceptable.

required

Raises:

Type Description
SensitiveValueLeak

If any non-empty sensitive value appears in the rendered output. The message names the field's path within instance; it never repeats the leaked value, since assertion messages end up in CI logs.

Bases: AssertionError

A value declared Sensitive() appeared in rendered output.

Point it at a config object and whatever your package renders from it. Give the secrets a distinctive canary value first — the check is a substring search, so a password of "x" matches almost any output:

from oa_configurator import ConnectionConfig, StackConfig, assert_no_sensitive_values_leak

CANARY = "canary-8f21c0-do-not-render"

def test_snapshot_redacts_secrets():
    stack = StackConfig.for_session(
        connections={"cdm": ConnectionConfig(dialect="sqlite", database_name=":memory:", password=CANARY)},
    )
    assert_no_sensitive_values_leak(stack, my_package.snapshot(stack))

Run it over every surface that renders configuration — a TUI snapshot, a --describe payload, an MCP tool response, a CLI listing. It walks nested models and models held in lists and dicts, so a whole StackConfig can be passed in one call.