Vault & Secret Broker
UGENT Vault is a local-first encrypted secret store and provider egress gateway. API keys live in your OS keychain or an encrypted file vault — never in config files, agent context, or logs.
Overview
The Vault solves a simple problem: your API keys should never appear in plaintext. Not in config files, not in environment variables that any process can read, not in agent prompts, not in logs.
Two layers work together:
- Secret Store — encrypted storage for API keys and tokens
- Secret Broker — a local daemon that injects credentials before forwarding API requests
CLI Commands
The ugent secret CLI path boots only the vault — no agent, LLM, tools, MCP, or plugins are initialized.
Initialize
ugent secret initCreates the metadata database and (for file-vault) the encrypted store directory.
Add a Secret
echo "sk-..." | ugent secret add @openai_api_key --provider openai --kind api_key --stdinValues are only accepted via --stdin. There is no --value flag — this prevents shell history leaks.
List and Inspect
ugent secret list
ugent secret inspect @openai_api_keyThese commands never print the secret value — only metadata (provider, kind, fingerprint, timestamps).
Rotate
echo "sk-new..." | ugent secret rotate @openai_api_key --stdinAudit
ugent secret audit
ugent secret audit @openai_api_keyShows the full audit trail — every add, rotate, lease issuance, and denial.
Storage Backends
OS Keychain (Default)
Uses the keyring crate:
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service (e.g. GNOME Keyring)
No passphrase needed — the OS manages encryption.
Encrypted File Vault
UGENT_SECRETS_BACKEND=file-vault ugent secret initUses XChaCha20-Poly1305 authenticated encryption with an Argon2id-derived master key. Set the passphrase via UGENT_SECRETS_PASSPHRASE.
Referencing Secrets in Config
Instead of plaintext values, use @secret_ref handles:
[llm.instances.openai]
type = "openai"
api_key_ref = "@openai_api_key"
default_model = "gpt-5.4-mini"When api_key_ref is set, it takes priority over api_key or $ENV_VAR. The decrypted value is resolved only at the moment of use and never written to any file.
Environment Projection
Run any command with secrets injected only into that process's environment:
ugent secret exec \
--env SERVICE_TOKENS=@context_engine_service_tokens \
--env OPENAI_API_KEY=@openai_api_key \
-- ugent-context-engine run --config ~/.ugent/context-engine/server.tomlWorks for any language, any tool — zero code changes needed.
Secret Broker
Start the broker daemon:
ugent secret broker startOr use the standalone binary:
secret-brokerTransports
- Unix socket:
~/.ugent/instances/<workspace_hash>/secret-broker.sock(no auth needed — protected by directory permissions) - Loopback HTTP:
127.0.0.1:18443(requires bootstrap bearer token from~/.ugent/secrets/broker.token)
Provider Egress Gateway
Call provider APIs without ever holding the provider key:
TOKEN=$(cat ~/.ugent/secrets/broker.token)
curl -N http://127.0.0.1:18443/proxy/openai/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-5.4-mini", "stream": true, "messages": [{"role": "user", "content": "hello"}]}'Supported proxy routes:
| Provider | Routes |
|---|---|
| OpenAI | /proxy/openai/v1/chat/completions, /proxy/openai/v1/responses, /proxy/openai/v1/embeddings |
/proxy/google/v1beta/models/{model}:generateContent, :streamGenerateContent | |
| Jina | /proxy/jina/v1/embeddings, /proxy/jina/v1/rerank |
| Voyage | /proxy/voyage/v1/embeddings, /proxy/voyage/v1/rerank |
Streaming responses pass through without buffering. Request and response bodies are never logged.
Any OpenAI-Compatible SDK
Point any SDK's base URL at the broker and use the bootstrap token as the API key:
import openai
client = openai.OpenAI(
base_url="http://127.0.0.1:18443/proxy/openai/v1",
api_key=open(Path.home() / ".ugent/secrets/broker.token").read_text().strip(),
)The broker validates the token and swaps in the real credential. Your code never sees the provider key.
Grants and Leases
Access is deny by default. Configure grants in ~/.ugent/secrets.grants.toml:
[[grants]]
secret_ref = "openai_api_key"
consumer = "ugent-core"
purposes = ["llm.chat", "llm.embeddings"]
delivery = ["native", "http_header_inject"]
allowed_hosts = ["api.openai.com"]
ttl_secs = 300A missing file denies everything. Every issue and denial is audited.
Sibling Service Integration
Context Engine
Point the context engine's embedder at the broker gateway:
# server.toml
openai_compatible_endpoint = "http://127.0.0.1:18443/proxy/openai/v1"
openai_compatible_api_key = "$BROKER_TOKEN"The provider key never enters the engine's process.
Plugin Secret Injection
Map environment variables to secret handles in plugins.toml:
[[plugins]]
id = "my-plugin"
secret_env_refs = { "MY_API_TOKEN" = "@my_service_token" }The supervisor resolves and injects the value only into the plugin child process at spawn time.
Threat Model
Protected against:
- Provider keys in config files, agent context, or logs
- Prompt injection revealing keys
- Other local processes calling the loopback API without the token
Not protected against:
- Root access
- Same-user malware that can read the bootstrap token or OS keychain
- Kernel-level attacks