Tutorials¶
Adding a new secret¶
You want to add a new credential — say, a Slack webhook URL or a Linear API token — so other Constellation code can ask secrets.slack() for it instead of reading from a .env file.
This is a worked example using a hypothetical slack secret. The pattern is identical for any other credential.
Step 1 — Put the credential in 1Password¶
1Password → Engineering vault → New Item → API Credential.
Name:
Slack Bot Token(something descriptive — this is what you’ll reference later)Fields: add whatever fields the credential has. For a Slack bot you might add
bot_tokenandsigning_secret.
Reference URIs (use these in the next step):
op://Engineering/Slack Bot Token/bot_tokenop://Engineering/Slack Bot Token/signing_secret
Sanity-check from your laptop:
op read 'op://Engineering/Slack Bot Token/bot_token'
If that prints the value, you’re good.
Step 2 — Add a Pydantic model¶
In src/constellation_utils/secrets/models.py, add a new class mirroring the existing models:
class SlackSecrets(BaseModel):
"""Slack bot credentials."""
model_config = ConfigDict(frozen=True, extra="forbid")
bot_token: str = Field(..., description="Slack bot user OAuth token (xoxb-...).")
signing_secret: str = Field(..., description="Slack signing secret for request verification.")
frozen=True means consumers can’t mutate the credential. extra="forbid" means an unexpected field in YAML triggers a validation error instead of being silently ignored.
Step 3 — Add the YAML mapping¶
Edit both src/constellation_utils/config/secrets.testing.yaml and secrets.production.yaml (or just production, if there’s only one Slack workspace):
slack:
bot_token: op://Engineering/Slack Bot Token/bot_token
signing_secret: op://Engineering/Slack Bot Token/signing_secret
Step 4 — Add backend methods¶
In src/constellation_utils/secrets/_backends.py, the _Backend Protocol gets a new method, and both real backends implement it.
# Protocol
class _Backend(Protocol):
def read_r2(self, uri_map: dict[str, str]) -> R2Secrets: ...
def read_slack(self, uri_map: dict[str, str]) -> SlackSecrets: ...
# OpCLIBackend
def read_slack(self, uri_map: dict[str, str]) -> SlackSecrets:
resolved = {field: self.read_one(uri) for field, uri in uri_map.items()}
return SlackSecrets.model_validate(resolved)
# EnvBackend
SLACK_ENV_MAP = {
"bot_token": "SLACK_BOT_TOKEN",
"signing_secret": "SLACK_SIGNING_SECRET",
}
def read_slack(self, uri_map: dict[str, str]) -> SlackSecrets:
del uri_map # env-var reads use the fixed map above
values: dict[str, str] = {}
missing: list[str] = []
for field, env_name in self.SLACK_ENV_MAP.items():
val = os.environ.get(env_name)
if val is None:
missing.append(env_name)
else:
values[field] = val
if missing:
raise ConstellationAuthError(
"EnvBackend selected but required Slack env vars are missing.\n"
f" missing : {', '.join(missing)}"
)
return SlackSecrets.model_validate(values)
If you also want EnvBackend to be selected automatically when SLACK_BOT_TOKEN is set (so that tests for Slack-using code don’t need op installed), extend the env-trigger check inside select_backend():
# in _backends.py select_backend()
if "R2_ACCESS_KEY_ID" in os.environ or "SLACK_BOT_TOKEN" in os.environ:
return EnvBackend()
This is optional — leave it out if Slack is only ever read on real laptops/rigs.
Step 5 — Expose the public function¶
In src/constellation_utils/secrets/__init__.py:
@lru_cache(maxsize=1)
def slack() -> SlackSecrets:
"""Return the Slack credentials for the current profile."""
cfg = load_profile()
if "slack" not in cfg:
raise ConstellationAuthError(
"the active profile is missing a `slack:` block."
)
backend = select_backend()
return backend.read_slack(cfg["slack"])
Don’t forget to add it to __all__ and import the model.
Step 6 — Tests¶
Add coverage in three places:
tests/test_models.py— required-fields, frozen, extra-rejectedtests/test_backends.py— env backend success / missing-var, op CLI happy path, public function cachingtests/test_config.py— both profile YAMLs have aslack:block with the expected fields andop://Engineering/URIs
Run the full suite:
uv run pytest
uv run ruff check
uv run mypy src
Step 7 — Documentation¶
Update README.md’s “Available secrets” table and the package’s docs (this site) to mention the new function. Bump the version in pyproject.toml.
Step 8 — Open the PR¶
Track the change under the Secrets Manager Linear project. Reviewers will look at:
Does the 1Password item exist and have the right fields?
Does the YAML map every field the model declares?
Is
EnvBackend.read_<name>consistent with how the other secrets handle missing-var errors?Are tests covering the same shape as the existing R2 tests?
Once merged, consumers can do:
from constellation_utils import secrets
slack = secrets.slack()
# SlackSecrets(bot_token=..., signing_secret=...)
And constellation doctor will (if extended) include a green ✓ for Slack reachability.