Credentials
Credentials are secrets stored encrypted at rest. The agent injects only the environment variables a worker needs and keeps secrets out of prompts and step state. Infra credentials (GitHub, Grafana, Kubernetes) never reach a coding runtime; a model provider key does, because the worker calls its provider itself.
Encryption is AES-256-GCM with a key derived from the master key. The master key is resolved from the config table first and falls back to STEELFRAME_MASTER_KEY. “Master key not configured” means no key exists anywhere; a failure reading the key is a database error, surfaced as one.
Manage credentials under Config → Credentials or the JSON API:
| Method | Path | Purpose |
|---|---|---|
GET | /api/credentials | List stored credentials (secrets never returned) |
GET | /api/credentials/catalog | Well-known services plus master-key status |
PUT | /api/credentials | Create or replace a credential |
DELETE | /api/credentials/{id} | Delete a credential |
Services and shapes
Section titled “Services and shapes”The catalog exposes well-known services. Steelframe only looks up recognized names; unknown services are rejected.
| Service | Environment-scoped | Shape | Used for |
|---|---|---|---|
grafana | staging + production | URL + secret | gcx metrics/logs/alerts during investigation |
kubectl | staging + production | URL + secret | Cluster correlation via a written kubeconfig |
github_app_private_key | global | secret only (PEM) | GitHub App installation tokens |
discord_webhook | global | URL only | Failure and investigation notifications |
discord_bot | global | secret only | @mention Chat and 📥 Intake |
cloudflare_ai | global | URL + secret | Clef decisions + Clairic extraction (account ID + Workers AI token) |
memory_s3 | global | URL + secret | Session-memory WAL access key + secret |
model_anthropic | global | secret only | Pi’s anthropic model provider |
model_openai | global | secret only | Pi’s openai model provider |
model_deepseek | global | secret only | Pi’s deepseek model provider |
model_xai | global | secret only | Pi’s xai model provider |
model_openrouter | global | secret only | Pi’s openrouter model provider |
model_google | global | secret only | Pi’s google model provider |
Shapes:
- url_and_secret —
target_urlandsecretare both required. - url_only — the webhook URL is the whole credential. Accept it in either field; it is stored as an encrypted secret with kind
webhook. - secret_only —
secretis required.github_app_private_keydefaultstarget_urltohttps://api.github.com.
kind is one of token, pem, or webhook; it defaults per service.
Model providers
Section titled “Model providers”coding_runtime_model runs any Pi catalog model (<provider>/<id>). Provider auth resolves in order:
- the encrypted
model_<provider>credential, - host Pi auth —
pi auth login --provider <id>on the agent host (the subscription path; Steelframe resolves a token withpi auth print-api-key/print-bearer-token), - the provider’s ambient environment variable.
The resolved value is injected into the worker as the provider’s env var (DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, or ANTHROPIC_OAUTH_TOKEN for an Anthropic subscription) and is never logged or written to a prompt. A catalog model with none of the three fails closed before a worker spawns, with an error naming the service to add.
Environment-scoped services require environment to be exactly staging or production. Global services must omit it. Lookups prefer the exact environment and fall back to a legacy global row.
Minting a read-only kubectl credential
Section titled “Minting a read-only kubectl credential”The kubectl credential is a bearer token plus the API server URL; the agent renders a minimal kubeconfig (token + insecure-skip-tls-verify) in the worker workspace. Mint one ServiceAccount per cluster:
apiVersion: v1kind: ServiceAccountmetadata: name: steelframe-readonly namespace: kube-system---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: steelframe-readonly-viewroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: viewsubjects: - kind: ServiceAccount name: steelframe-readonly namespace: kube-system---apiVersion: v1kind: Secretmetadata: name: steelframe-readonly-token namespace: kube-system annotations: kubernetes.io/service-account.name: steelframe-readonlytype: kubernetes.io/service-account-tokenThe built-in view role excludes Secrets and Nodes. When metrics-server is installed it labels system:aggregated-metrics-reader with rbac.authorization.k8s.io/aggregate-to-view: "true", so view already includes metrics.k8s.io pods and nodes and kubectl top needs no second binding. Node status is not part of view. Grant it with a role and bind that role to steelframe-readonly:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: steelframe-readonly-nodesrules: - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: steelframe-readonly-nodesroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: steelframe-readonly-nodessubjects: - kind: ServiceAccount name: steelframe-readonly namespace: kube-systemThen store the token (it is never returned by the API):
TOKEN=$(kubectl -n kube-system get secret steelframe-readonly-token -o jsonpath='{.data.token}' | base64 -d)curl -sS -X PUT "$AGENT/api/credentials" \ -H "Authorization: Bearer $OPERATOR_TOKEN" -H 'Content-Type: application/json' \ --data "$(jq -n --arg t "$TOKEN" '{service:"kubectl",environment:"production", target_url:"https://<api-server>:6443",kind:"token",secret:$t}')"target_url must be reachable from the agent host (public API endpoint, in-cluster address, or a tunnel).
Injection into workers
Section titled “Injection into workers”Workers receive env at spawn. Secrets are never put in LLM prompts:
- Grafana:
GRAFANA_SERVERandGRAFANA_TOKEN. - Kubernetes:
KUBECONFIGpointing at a written kubeconfig. - GitHub: a nine-variable bundle (
GH_TOKEN,GIT_AUTHOR_*,GIT_COMMITTER_*, and git config headers) so workerghandgit pushrun under the bot identity. - Model provider: the provider’s env var (
DEEPSEEK_API_KEY,ANTHROPIC_API_KEY,ANTHROPIC_OAUTH_TOKEN, …) for a catalog model.
Gotchas
Section titled “Gotchas”PUT /api/credentialswithout a configured master key returns412and writes nothing.- A failed master-key read is
500, not “not configured”. - Upsert replaces in place, which is how you rotate the GitHub App private key.
- Discord webhook URLs are redacted when returned (
…/api/webhooks/••••); the stored secret still holds the full value. - Listing never returns secret material — only
target_url(redacted where applicable), kind, and service.