Source code for jeevesagent.security.secrets

"""Concrete :class:`~jeevesagent.core.protocols.Secrets`
implementations.

Two ship in the framework, neither requiring extra dependencies:

* :class:`EnvSecrets` — reads from ``os.environ``. Default for
  :class:`~jeevesagent.Agent` so today's behaviour is preserved
  (API keys come from environment variables) without callers
  having to wire anything.
* :class:`DictSecrets` — explicit in-memory dict, useful in tests
  and for callers who load secrets from a config file or a
  vault-fetch-once-at-startup script.

Production users running on AWS / GCP / Vault should write a
custom :class:`Secrets` adapter that calls their secret manager
inside ``resolve()`` and caches into a local dict for
``lookup_sync()``. The framework only requires
``lookup_sync()`` to return synchronously (it's called from
inside Agent / model-adapter constructors); ``resolve()`` /
``store()`` can do whatever async work you need.

A simple regex-based redaction is also provided here so callers
who don't wire a vault still get safe-by-default audit log
behaviour.
"""

from __future__ import annotations

import os
import re
from typing import Final

__all__ = [
    "DictSecrets",
    "EnvSecrets",
]


# Patterns we redact by default. Conservative — false-positives
# (real prose containing the keyword) are preferable to leaking a
# real key into an audit log. Production users override this in
# subclasses.
_REDACTION_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
    re.compile(r"sk-[A-Za-z0-9_-]{16,}"),       # OpenAI-style
    re.compile(r"sk-ant-[A-Za-z0-9_-]{16,}"),   # Anthropic-style
    re.compile(r"AKIA[0-9A-Z]{16}"),            # AWS access key id
    re.compile(r"ghp_[A-Za-z0-9]{36}"),         # GitHub PAT
)


def _apply_redaction(text: str) -> str:
    out = text
    for pat in _REDACTION_PATTERNS:
        out = pat.sub("[REDACTED]", out)
    return out


[docs] class EnvSecrets: """Reads secrets from ``os.environ``. The default :class:`Secrets` impl wired by :class:`Agent` when the caller doesn't pass an explicit one. Behaviour matches the pre-M10 framework: API keys are looked up as the corresponding environment variable name (``OPENAI_API_KEY``, ``ANTHROPIC_API_KEY``, etc.). """
[docs] async def resolve(self, ref: str) -> str: value = os.environ.get(ref) if value is None: raise KeyError(f"environment variable {ref!r} is not set") return value
[docs] async def store(self, ref: str, value: str) -> None: # We don't write to ``os.environ`` from a Secrets impl — # mutating the environment of a running process is rude # to other code running inside it. Use :class:`DictSecrets` # for an in-process writable store, or write a custom # impl that hits your real secret manager. raise NotImplementedError( "EnvSecrets is read-only; use DictSecrets or a custom " "Secrets backend for write access." )
[docs] def redact(self, text: str) -> str: return _apply_redaction(text)
[docs] def lookup_sync(self, ref: str) -> str | None: return os.environ.get(ref)
[docs] class DictSecrets: """In-process :class:`Secrets` backed by an explicit dict. Useful in tests and for callers that fetch secrets once at startup (from a config file, a one-shot Vault read, etc.) and want to make them available to the framework without leaking them into ``os.environ``. Mutable: ``store()`` updates the in-process map. Not durable across process restarts. """ def __init__(self, initial: dict[str, str] | None = None) -> None: self._values: dict[str, str] = dict(initial or {})
[docs] async def resolve(self, ref: str) -> str: try: return self._values[ref] except KeyError as exc: raise KeyError(f"secret {ref!r} not present") from exc
[docs] async def store(self, ref: str, value: str) -> None: self._values[ref] = value
[docs] def redact(self, text: str) -> str: return _apply_redaction(text)
[docs] def lookup_sync(self, ref: str) -> str | None: return self._values.get(ref)