Secrets

One Protocol for credentials. Venues never touch os.environ directly.

horizon.secrets lets venues read broker credentials without hard-coding where credentials live. In dev, environment variables. In production, Vault, AWS Secrets Manager, GCP Secret Manager, or 1Password Connect. Same venue code, different backend.

Protocol

python
class Secrets(Protocol):
    def get(self, key: str, default: str | None = None) -> str | None: ...
    def has(self, key: str) -> bool: ...

Two methods is enough for venues. SecretsProvider adds set and rotate for ops tooling. Venue code needs read access only.

Key naming

Keep stable. Do not rename without a migration plan.

<venue>.<field>      "alpaca.key", "alpaca.secret"
                     "ibkr.username", "ibkr.password"
                     "polygon.api_key"
                     "polymarket.private_key"
<service>.<field>    "slack.webhook", "pagerduty.routing_key"

Keys are case-insensitive (providers normalize via .lower()).

Backends

EnvSecrets (default)

Reads process environment variables. Maps alpaca.key to ALPACA_KEY by default (uppercase, dots to underscores).

python
from horizon.secrets import EnvSecrets

secrets = EnvSecrets()
secrets.get("alpaca.key")                 # ALPACA_KEY
secrets.get("alpaca.secret")              # ALPACA_SECRET
secrets.get("polygon.api_key")            # POLYGON_API_KEY

Prefix

python
secrets = EnvSecrets(prefix="HORIZON_")
secrets.get("alpaca.key")                 # HORIZON_ALPACA_KEY

Useful when multiple agents share a host and need per-agent env namespaces.

Explicit mapping

python
secrets = EnvSecrets(mapping={
    "alpaca.key": "MY_LEGACY_ALPACA_KEY_NAME",
})

require()

Returns the value or raises SecretsError on missing or empty. Use for required credentials so startup fails fast instead of failing on the first order.

python
from horizon.secrets import EnvSecrets, SecretsError

try:
    api_key = secrets.require("alpaca.key")
except SecretsError as e:
    print(f"Missing credential: {e}")
    exit(1)

DictSecrets (tests)

python
from horizon.secrets import DictSecrets

fake = DictSecrets({
    "alpaca.key": "test_key",
    "alpaca.secret": "test_secret",
})

venue = Alpaca(secrets=fake, paper=True)

Not production. Isolates tests from environment state.

L1 and L2

  • VaultSecrets. HashiCorp Vault.
  • AwsSecretsManagerSecrets. AWS SM.
  • GcpSecretManagerSecrets. GCP SM.
  • OnePasswordConnectSecrets. Smaller shops.

Each implements the Secrets / SecretsProvider Protocol. Venues do not change.

Venue integration

Shipped: Alpaca reads via Secrets:

python
def __init__(self, *, secrets: Secrets | None = None, paper: bool = True, ...):
    self._secrets = secrets
    # ...

# Resolution: explicit api_key/api_secret -> Secrets -> EnvSecrets fallback

Pass secrets= at construction to plug Vault, AWS SM, or DictSecrets in without changing venue logic.

Auditing secret access

L1 will optionally emit AuditCategory.SecretAccess on every secrets.get(...). The event records the key name; the value is never logged. See Audit trail.

python
# L1
from horizon.audit import AuditLog
from horizon.secrets import EnvSecrets

audit_log = AuditLog(sink=SQLiteSink("audit.db"))
raw_secrets = EnvSecrets()
secrets = AuditingSecrets(raw_secrets, audit_log)

secrets.get("alpaca.key")
# AuditEvent: category=SecretAccess, actor="system", payload={"key": "alpaca.key"}

Rotation

SecretsProvider.rotate(key) targets backends that support it (Vault, AWS SM). Typical pattern: a scheduled job calls rotate and triggers a venue reconnect.

python
# Ops-side
new_value = secrets.rotate("alpaca.key")
venue.reconnect()

Today

  • Use EnvSecrets() in dev. Route reads through the Protocol.
  • Plan a production backend before staging. Switch is a one-line change in the venue construction site.
  • Do not hard-code credentials. Do not log them. Do not include them in exception messages.