The secrets boundary is a hard invariant: credentials and tokens are accessible through exactly one function and are never logged, stored inline, or passed beyond the call site.
secrets/store.py exposes exactly one function:
def get_secret(connector_id: str, key: str) -> str: ...No list. No dump. No direct DB access from outside this module. The store is injected as a SecretsStore instance at adapter construction time; adapters do not instantiate it themselves.
SecretsStore instance injected at construction — never call get_secret() at module level.get_secret(connector_id, key) inside execute(), immediately before the credential is needed.self, in a checkpoint, in a log, or in any data structure that persists beyond the call.config_ref on RegisteredTool points to a DB row with the credential reference — not the credential value. The value is fetched at execution time only.
Milestone 1 implementation: Fernet-encrypted local file, with the encryption key sourced from an environment variable or OS keyring.
Secrets are stored as: connector_id:key → encrypted_value. The file is read on startup; values are decrypted on get_secret() calls.
Nothing in this implementation should appear in audit payloads, logs, or stack traces. The redaction policy in AuditEvent and the payload_ref pattern enforce this.
SecretsStore is a protocol (interface). Callers depend only on get_secret(connector_id, key) -> str. The implementation is swappable:
keyring library. Callers do not change.SecretsStore against the Vault API. Callers do not change.Migration is a backend swap in secrets/store.py — no changes to adapter code, no changes to the audit or safety layer. This is Outstanding Decision OD-04. See dev/outstanding-decisions.
The following examples illustrate the rules from How adapters must use it. Each shows a correct pattern alongside the violation it prevents.
SecretsStore must be injected — never instantiated inside the adapter.
# Correct — store is injected by the caller
class GmailAdapter:
def __init__(self, secrets: SecretsStore):
self.secrets = secrets# Wrong — adapter creates its own store, bypassing the boundary
class GmailAdapter:
def __init__(self):
self.secrets = SecretsStore()get_secret() at execution time (Rule 2)Credentials must be fetched inside execute(), immediately before use — not at construction, not at module import.
# Correct — fetched at the moment the credential is needed
class GmailAdapter:
def execute(self, tool_input):
token = self.secrets.get_secret("gmail", "oauth_token")
return gmail_api.send(token, tool_input.payload)# Wrong — fetched at construction and held on self
class GmailAdapter:
def __init__(self, secrets: SecretsStore):
self.secrets = secrets
self.token = self.secrets.get_secret("gmail", "oauth_token") # too early
def execute(self, tool_input):
return gmail_api.send(self.token, tool_input.payload) # self.token persistsThe returned value should be consumed directly at the call site, not passed as a variable beyond it.
# Correct — used inline, never assigned to a travelling variable
def execute(self, tool_input):
return gmail_api.send(
self.secrets.get_secret("gmail", "oauth_token"),
tool_input.payload
)# Wrong — token is assigned and passed to another method, extending its lifetime
def execute(self, tool_input):
token = self.secrets.get_secret("gmail", "oauth_token")
return self._make_request(token, tool_input.payload) # token travels beyond the call siteThe credential must not be stored on self, in a checkpoint, in a log, or in any structure that outlives the call.
# Incorrect - Stored on self
self.token = self.secrets.get_secret("gmail", "oauth_token")
# Incorrect - Written to a checkpoint
checkpoint["token"] = self.secrets.get_secret("gmail", "oauth_token")
# Incorrect - Leaked into a log
token = self.secrets.get_secret("gmail", "oauth_token")
logger.info(f"Calling Gmail API with token: {token}")
# Incoorect - Embedded in an audit payload
audit.record({"credential": self.secrets.get_secret("gmail", "oauth_token")})config_ref — reference vs valueRegisteredTool.config_ref stores a pointer to the credential, not the credential itself. The value is only resolved at execution time.
# What lives in the DB (safe to store):
RegisteredTool.config_ref → { connector_id: "gmail", key: "oauth_token" }
# What never lives in the DB (only exists transiently at execution time):
get_secret("gmail", "oauth_token") → "ya29.A0ARrda..."Even a full dump of the RegisteredTool table exposes only references — never the underlying credentials.