Per-client isolated vaults
Each onboarded client can be given its own Agent Vault identity —
client-{slug} with agent {slug}-steelmoth-runtime and a durable token —
instead of sharing a pre-provisioned client-pool-NN slot. A client gateway is
pinned to its own vault (AGENT_VAULT_VAULT=client-{slug}) and reads its system
secrets from that vault using that token. The isolation boundary is the
prontera per-token ACL: the gateway’s token can read only its own vault, so a
client can never reach another client’s — or the operator’s client-mark —
secrets. This is the steelmoth side only; the broker edit and the prontera
endpoint deploy are a separate, operator-gated prontera step.
Everything is default-OFF and inert until that gated step plus a flag flip.
The fail-closed read-binding
Section titled “The fail-closed read-binding”On the broker GET path (credentials._brokered_secret), a SYSTEM-alias read
(memlink-scope is the load-bearing one; also the shared OAuth-app id/secret and
the model-provider keys) now carries the header
X-Agent-Vault-Client-Vault: <the gateway's own vault> — only when the
gateway is vault-pinned to a client-{slug} / client-pool-NN vault. The
operator (client-mark) and any unpinned process send no header, so their GET is
byte-identical to today. Both bound and unbound reads prefer the
auto-renewing 24h session (AGENT_VAULT_SESSION_TOKEN): the prontera broker
validates that session against the declared X-Agent-Vault-Client-Vault (its
non-reveal ACL check) and performs the reveal itself via its own writer grant, so
a session bearer resolves per-client aliases (the broker patch that added this is
a separate, operator-gated prontera deploy). Preferring the rotating session
keeps a leaked bearer’s exposure ≤24h rather than the long-lived identity token’s
until-re-mint life; the gateway keeps that session fresh in-process
(AGENT_VAULT_INPROCESS_REFRESH=1 on the owlet template — the same
VaultSessionRefresher daemon the operator API runs). The long-lived identity
token (AGENT_VAULT_TOKEN_FILE) stays the one-shot fallback: any 401/404 on the
env-session bearer gets exactly one identity-token retry (stale vault session detected warning), for bound and unbound reads alike (the earlier
identity-token-first bound-read path — the 2026-07-02/03 garcia Telegram
incident — is superseded). This mirrors the read-binding memlink_scope_sign
already sends on the /sign path, so the broker applies the identical binding it
enforces on signing. The in-process result cache is keyed by (alias, binding)
so a client gateway’s binding can never serve the operator’s cached value. It is
inert today: with the broker’s read allowlist empty (until the gated step) the
broker returns the same result with or without the header.
Per-client end-user OAuth refresh handles (oauth-*,
resolve_opaque_credential_ref) are deliberately not system aliases — they
resolve to per-client secrets the broker already scopes by the handle’s own owner
segment, never by vault.
Provision / teardown lifecycle (flag on)
Section titled “Provision / teardown lifecycle (flag on)”The provisioner gains a default-OFF flag
STEELMOTH_PROVISIONER_DEDICATED_CLIENT_VAULT_ENABLED. When set (and a
client-vault-admin client is wired):
- provision —
provision_oncecalls the prontera client-vault-adminprovision-clientendpoint to mint the client’s vault before spawning the gateway (the gateway binds the vault’s token), then records the host port viapool_repository.create_or_get_client_vault(reusing the pool’s free-port allocator). Any failure fails the job rather than spawning against a missing vault. When the flag is off, it claims a pre-provisioned pool slot exactly as today. - teardown —
teardown_oncedestroys the client’s vault (revoke token + vault delete + drop signing keys + remove host token) in place of the pool-slot release.
Guardian invariant: erase BEFORE destroy
Section titled “Guardian invariant: erase BEFORE destroy”On teardown the vault is destroyed only after owner-data erasure
(erase_owner_data) succeeds. If erasure fails, the job finishes FAILED and
returns before any prontera delete — the vault identity is what authenticates
the idempotent erase retry, so destroying it on a failed wipe would strand
un-erased owner PII with no way back in. The teardown step order on the dedicated
path is therefore: container → erase → destroy vault → drop port row → delete
registry row.
Three fail-closed slug guards
Section titled “Three fail-closed slug guards”A slug that resolves to the operator vault (mark → client-mark) — or is not a
vault/container-safe slug — is refused before any vault action, in three
independent places: pool_repository.validate_client_slug,
ClientVaultAdminClient.{provision,delete}_client, and
scripts/provision-pool.sh::validate_client_slug. So a hostile slug can never
point a client at the operator’s vault.
Authority: a dedicated bearer, not bare reachability
Section titled “Authority: a dedicated bearer, not bare reachability”ClientVaultAdminClient authenticates its provision/delete calls with a
DEDICATED admin bearer (broker alias client-vault-admin-token, held broker-only
in memory per INV-024 / HARD RULE 10). Network reachability of the endpoint is
never sufficient authority — an empty/missing bearer is refused on both ends. The
endpoint host/scheme is allowlisted to prontera (HTTPS) or loopback.
Reuse + the gated prontera step
Section titled “Reuse + the gated prontera step”scripts/provision-pool.sh exposes a reusable provision-client <slug>
codepath (the SAME vault/agent/token/signing-key provisioning the pool loop uses,
keyed by the client slug); the prontera endpoint wraps it. The
prontera-assets/client-vault-admin/ artefacts are documentation-only
(HR10) — they deploy to the vault host in the gated step and are never imported
as steelmoth runtime code. New per-client vaults require that prontera deploy,
not a compose change in this repo.
Source Map
Section titled “Source Map”| Claim Area | Source |
|---|---|
| GET read-binding header + system-alias set + admin-token alias | steelmoth-runtime/src/steelmoth_runtime/credentials.py |
create_or_get_client_vault, identity, slug guard, admin client |
steelmoth-runtime/src/steelmoth_runtime/client_provisioning/pool_repository.py |
| Flag, provision-before-spawn, erase-before-destroy teardown | steelmoth-runtime/src/steelmoth_runtime/client_provisioning/provisioner.py |
Reusable provision-client codepath + slug refusal |
scripts/provision-pool.sh |
| Prontera endpoint contract (doc-only) | prontera-assets/client-vault-admin/README.md |
Related Pages
Section titled “Related Pages”security-and-secrets.md— the broker-only secret model and the alias map.access-request-flow.md— client onboarding that feeds the provisioner.data-and-scope.md— per-tenant scope GUCs the erase runs under.
Maintenance Notes
Section titled “Maintenance Notes”- Update this page when the credentials read-binding, the provisioner flag, or the slug-guard logic changes.
- The broker allowlist + the prontera client-vault-admin endpoint are external to this repo (gated prontera step); this page describes only the steelmoth side.
Known Unknowns
Section titled “Known Unknowns”- The exact prontera-side delete-client agent-vault verbs are wired in the gated deploy; the in-repo artefact specifies the contract, not the final implementation.

