Skip to content

symfonic.capabilities.governance.stages.credentials

credentials

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

The built-in pattern and compiler come from the stdlib-only symfonic.agent.hygiene leaf. That exact import is the governance package's one containment exception: it does not construct or import the legacy engine, and both paths now use the same credential policy object.

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

Build a single alternation regex from a list of pattern fragments.

  • None -> return :data:DEFAULT_CREDENTIAL_KEY_PATTERN (v6.1.9).
  • Empty list/tuple -> return None (explicit opt-out of scrubbing).
  • Non-empty list -> compile each fragment as a sub-alternation and OR them together into one case-insensitive regex.

Invalid regex fragments raise :class:re.error at compile time, not silently at scrub time — a misconfigured pattern list should fail fast during config construction, not leak secrets at runtime.

Source code in src/symfonic/agent/hygiene.py
def compile_credential_pattern(
    patterns: Sequence[str] | None,
) -> re.Pattern[str] | None:
    """Build a single alternation regex from a list of pattern fragments.

    - ``None`` -> return :data:`DEFAULT_CREDENTIAL_KEY_PATTERN` (v6.1.9).
    - Empty list/tuple -> return ``None`` (explicit opt-out of scrubbing).
    - Non-empty list -> compile each fragment as a sub-alternation and
      OR them together into one case-insensitive regex.

    Invalid regex fragments raise :class:`re.error` at compile time, not
    silently at scrub time — a misconfigured pattern list should fail fast
    during config construction, not leak secrets at runtime.
    """
    if patterns is None:
        return DEFAULT_CREDENTIAL_KEY_PATTERN
    if len(patterns) == 0:
        return None
    # Wrap each user fragment in its own group so the outer alternation
    # preserves atomicity even if the fragment itself contains ``|``.
    joined = "|".join(f"(?:{p})" for p in patterns)
    return re.compile(r"(?i)(" + joined + r")")