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 |
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, matchingsafe_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-xrenders 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 needsapi-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 guaranteedkey=valuestructure to mask value-by-value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
URL to scrub. |
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. |
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
|
required |
rendered
|
object
|
Whatever the package produced for display, logging, or serialisation.
Converted with |
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. |
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.