Skip to content

symfonic.capabilities.governance.stages.credentials

credentials

Credential hygiene: the first stage, and one of the two fail-closed ones.

The pattern set is a re-declaration of the legacy default rather than an import of it — this package imports nothing outside itself — and a contract test pins the two lists byte-for-byte in both directions, so a divergence is a failing test rather than a silent policy drift.

Shallow by design (the legacy invariant): nested dicts are not walked, because graph properties are persisted flat and value-scanning carries a false-positive tax that belongs to its own design track.

One scoped exception, added for #27. ToolCall.result is free text, and the stage never read it — so a tool returning a credential in its output (an API client echoing an auth header, a shell tool printing an env dump, a DB tool returning a connection string) passed a fail-closed scrubber untouched. Keys cannot be dropped from a string, so the result is redacted whole when the pattern matches anywhere in it.

That is blunt, and deliberately so. Partial redaction of free text means guessing where a value begins, and a guess inside a fail-closed stage is the worst place for one. The cost is real — a result that merely discusses authorization is redacted too — and it is pinned by a test rather than discovered, so a deployment that finds it unacceptable narrows patterns knowing exactly what it is trading.

CredentialHygieneStage

CredentialHygieneStage(patterns: Sequence[str] | None | object = USE_DEFAULT_PATTERNS)

Drop credential-shaped keys before any other stage observes them.

Source code in src/symfonic/capabilities/governance/stages/credentials.py
def __init__(
    self, patterns: Sequence[str] | None | object = USE_DEFAULT_PATTERNS,
) -> None:
    supplied = None if patterns is USE_DEFAULT_PATTERNS else patterns
    self._pattern = compile_credential_pattern(supplied)  # type: ignore[arg-type]
    self._disabled = self._pattern is None

compile_credential_pattern

compile_credential_pattern(patterns: Sequence[str] | None) -> re.Pattern[str] | None

None -> the default set; [] -> disabled; otherwise a replacement.

An invalid fragment raises re.error here, at construction, rather than at scrub time: a misconfigured pattern list must fail while the deployment is being built, not while a secret is passing through it.

Source code in src/symfonic/capabilities/governance/stages/credentials.py
def compile_credential_pattern(
    patterns: Sequence[str] | None,
) -> re.Pattern[str] | None:
    """``None`` -> the default set; ``[]`` -> disabled; otherwise a replacement.

    An invalid fragment raises ``re.error`` here, at construction, rather
    than at scrub time: a misconfigured pattern list must fail while the
    deployment is being built, not while a secret is passing through it.
    """
    if patterns is None:
        return _DEFAULT_PATTERN
    if len(patterns) == 0:
        return None
    joined = "|".join(f"(?:{p})" for p in patterns)
    return re.compile(r"(?i)(" + joined + r")")