Skip to content

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, not in shell history.

Two layers work together:

  1. Secret Store — encrypted storage for API keys and tokens
  2. Secret Broker — an optional local daemon that injects credentials before forwarding API requests

Two Deployment Modes

UGENT supports two distinct deployment models. Choose based on your security posture and operational needs.

Mode 1: Native Resolution (Default)

Each UGENT instance resolves keys directly from the shared vault (~/.ugent/secrets/) via api_key_ref in its ugent.toml. This is the simplest mode — no broker daemon required. The decrypted key lives in each instance process memory at call time and is never written to disk or logs.

toml
[llm.instances.openai]
type = "openai"
api_key_ref = "@openai_api_key"
default_model = "gpt-5.4-mini"

When to use: Single-machine setups, development workstations, or any case where you want zero extra infrastructure.

Mode 2: Shared Broker (Hardened)

One machine-wide broker daemon holds all credentials. Every UGENT instance on that machine reaches the broker instead of resolving keys locally. This is the most credential-isolating mode: the decrypted key lives in exactly one process — the broker — and never enters any agent process, config file, or log.

Enable shared mode in each instance's ugent.toml:

toml
[secrets.broker]
shared = true
transparent_proxy = true          # route LLM traffic through the gateway
broker_fallback = "fail"          # fail-closed if broker unreachable

When shared = true, the broker:

  • Binds to the machine-wide socket ~/.ugent/secret-broker.sock (not a per-workspace path)
  • Reads its provider profiles and lease policy from the canonical ~/.ugent/broker.toml (not any workspace's ugent.toml)
  • Uses the loopback port (127.0.0.1:18443) as a single-broker arbiter: if a second broker starts while one is running, it detects the bound port and exits cleanly

When to use: Production servers, multi-workspace machines, shared development servers, or any environment where credential isolation across instances matters.

Comparison

Native ModeShared Broker Mode
Decrypted key lives inEach instance processBroker daemon only
Extra infrastructureNoneOne daemon process
Config sourceugent.toml per workspace~/.ugent/broker.toml (canonical)
Socket pathPer-workspaceMachine-wide ~/.ugent/secret-broker.sock
Best forDevelopment, single-userProduction, multi-workspace

CLI Commands

The ugent secret CLI path boots only the vault — no agent, LLM, tools, MCP, or plugins are initialized. This means it works even when the rest of your workspace config is broken.

All secret values are accepted only via stdin. There is no --value flag — this prevents shell history and process listing leaks. Use clipboard piping to keep secrets out of your terminal history entirely.

Initialize

bash
ugent secret init

Creates the metadata database and (for file-vault) the encrypted store directory.

Add a Secret

Pipe the value from your clipboard so it never appears in shell history:

bash
pbpaste | ugent secret add @openai_api_key --provider openai --kind api_key --stdin
bash
xclip -selection clipboard -o | ugent secret add @openai_api_key --provider openai --kind api_key --stdin
bash
wl-paste | ugent secret add @openai_api_key --provider openai --kind api_key --stdin
bash
powershell.exe -Command Get-Clipboard | ugent secret add @openai_api_key --provider openai --kind api_key --stdin

Copy your API key to the clipboard first, then run the command. The key travels through the pipe directly into encrypted storage — it never touches disk in plaintext, never appears in ~/.bash_history, and never shows up in ps output.

If add finds the ref already exists, it refuses with an error. Use rotate to overwrite.

List and Inspect

bash
ugent secret list
ugent secret inspect @openai_api_key

These commands never print the secret value — only metadata (provider, kind, fingerprint, timestamps, value length).

Rotate

bash
# Copy the new key to your clipboard, then:
pbpaste | ugent secret rotate @openai_api_key --stdin

Delete

bash
ugent secret delete @openai_api_key

Audit

bash
ugent secret audit @openai_api_key

Shows the full audit trail — every add, rotate, lease issuance, and denial — from ~/.ugent/secrets/audit.jsonl.

Storage Backends

OS Keychain (Default)

Uses the OS-native secret service:

  • macOS: Keychain
  • Windows: Credential Manager
  • Linux: Secret Service (GNOME Keyring, KWallet)

No passphrase needed — the OS manages encryption.

Encrypted File Vault

For headless servers or environments without an OS keychain:

bash
UGENT_SECRETS_BACKEND=file-vault ugent secret init

Uses XChaCha20-Poly1305 authenticated encryption with an Argon2id-derived master key. Set the passphrase via environment variable:

bash
export UGENT_SECRETS_PASSPHRASE="your-strong-passphrase"

WARNING

For system services (systemd), the OS keychain is usually unavailable. Use file-vault with a passphrase stored in a mode-0600 environment file.

Referencing Secrets in Config

Instead of plaintext values, use @secret_ref handles:

toml
[llm.instances.openai]
type = "openai"
api_key_ref = "@openai_api_key"
default_model = "gpt-5.4-mini"

[llm.instances.anthropic]
type = "anthropic"
api_key_ref = "@anthropic_api_key"
default_model = "claude-opus-4-8"

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.

Linux Daemon Mode

For production Linux servers, run the broker as a systemd service so it starts automatically on boot and restarts on failure. Two unit files ship in packaging/systemd/.

System Service (Shared Broker)

The standalone secret-broker binary defaults to shared mode: it serves the machine-wide socket and reads config from ~/.ugent/broker.toml.

bash
# Install the binary
install -m0755 target/release/secret-broker /usr/local/bin/secret-broker

# Install the service unit
install -m0644 packaging/systemd/ugent-secret-broker.service /etc/systemd/system/

# Create the environment file (for file-vault passphrase)
cat > /etc/ugent/secret-broker.env << 'EOF'
UGENT_SECRETS_BACKEND=file-vault
UGENT_SECRETS_PASSPHRASE=your-strong-passphrase
EOF
chmod 0600 /etc/ugent/secret-broker.env

# Enable and start
systemctl daemon-reload
systemctl enable --now ugent-secret-broker.service

The service is ordered Before=ugent.service so the credential proxy is up before UGENT makes its first brokered call. It runs with hardened systemd protections: NoNewPrivileges, PrivateTmp, ProtectSystem=full, and UMask=0077.

Per-User Service

For desktop Linux where the OS keychain (Secret Service) is available, use the user-level unit instead:

bash
install -m0755 target/release/secret-broker ~/.local/bin/secret-broker
install -m0644 packaging/systemd/ugent-secret-broker-user.service ~/.config/systemd/user/
systemctl --user enable --now ugent-secret-broker.service

This runs under your login session so it can reach GNOME Keyring or KWallet.

Verify the Daemon

bash
systemctl status ugent-secret-broker.service
# Or check the socket directly:
ls -la ~/.ugent/secret-broker.sock

Starting the Broker Manually

Without systemd, start the broker in the foreground:

bash
# Per-workspace broker (native mode companion):
ugent secret broker start

# Shared broker (reads ~/.ugent/broker.toml):
ugent secret broker start --config ~/.ugent/ugent.toml   # with [secrets.broker] shared = true

# Or use the standalone binary directly:
secret-broker

The standalone secret-broker binary exists for process supervisors that want their own PID. It defaults to shared mode.

Provider Egress Gateway

The gateway lets any client call a provider API without ever holding the provider key. The client sends the request to the broker (authenticating with the bootstrap token), and the broker injects the real credential before forwarding upstream.

Transports

  1. Unix domain socket: ~/.ugent/instances/<workspace_hash>/secret-broker.sock (per-workspace), or ~/.ugent/secret-broker.sock (shared mode). No authentication needed — protected by directory permissions.
  2. Loopback HTTP: 127.0.0.1:18443 (configurable via loopback_bind). Requires a bootstrap bearer token from ~/.ugent/secrets/broker.token (mode 0600) on every request.

Calling the Gateway

bash
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"}]}'

The broker validates the token, injects the real API key, forwards to the upstream provider, and streams the response back. Request and response bodies are never logged.

Built-in Providers

11 providers ship out of the box, covering every LLM UGENT itself supports:

ProviderAuth InjectionSecret RefKey Routes
OpenAIAuthorization: Bearer@openai_api_keychat, responses, embeddings, images, audio, files
Anthropicx-api-key header@anthropic_api_keymessages, count_tokens
Googlex-goog-api-key@google_gemini_api_keygenerateContent, streamGenerateContent, embedContent
DeepSeekAuthorization: Bearer@deepseek_api_keychat/completions
DashScope (Alibaba)Authorization: Bearer@dashscope_api_keycompatible-mode chat, embeddings
MoonshotAuthorization: Bearer@moonshot_api_keychat/completions
GLM (Zhipu)Authorization: Bearer@glm_api_keyapi/paas/v4 chat/completions
MiniMaxAuthorization: Bearer@minimax_api_keytext/chatcompletion, chat/completions
OpenRouterAuthorization: Bearer@openrouter_api_keyapi/v1 chat/completions
JinaAuthorization: Bearer@jina_api_keyembeddings, rerank
VoyageAuthorization: Bearer@voyage_api_keyembeddings, rerank

Provider-specific headers are forwarded automatically (e.g. anthropic-version, anthropic-beta for Anthropic; openai-beta, openai-organization for OpenAI). Streaming responses pass through without buffering.

Custom Providers

Add any provider in ugent.toml (native mode) or ~/.ugent/broker.toml (shared mode) — no code changes needed:

toml
[secrets.broker.providers.customai]
upstream_base_url = "https://api.customai.example"
secret_ref = "@customai_api_key"
auth = { type = "header", name = "x-api-key" }   # or { type = "bearer" } | { type = "query", name = "key" }
forward_request_headers = ["x-customai-version"]
routes = [
  { method = "POST", path = "/api/v2/generate", streaming = true },
  { method = "GET", path = "/api/v2/jobs/{id}" },
]

An entry whose name matches a built-in replaces it wholesale. Disable a built-in entirely with enabled = false.

Transparent Routing (UGENT's Own Traffic)

Enable UGENT to route its own LLM calls through the broker automatically, so provider keys never enter the agent process:

toml
[secrets.broker]
transparent_proxy = true
broker_fallback = "fail"          # "fail" (default) | "native"

At client build time, UGENT resolves the broker socket, probes it, and rewrites the provider base URL to the gateway. The real key lives only in the broker. If the broker is unreachable after connect-retry, broker_fallback decides: fail (fail-closed) or native (fall back to local key resolution).

Transparent routing covers: OpenAI, Anthropic, DashScope, OpenRouter (when using their default base URLs). Instances with custom base URLs (e.g. Azure/Foundry) fall through to native calls.

3rd-Party Application Support

The broker is not limited to UGENT. Any application that can speak HTTP can use it as a credential-injecting proxy. Three integration patterns cover virtually any use case.

Pattern 1: Any OpenAI-Compatible SDK

Point any SDK's base URL at the broker gateway and use the bootstrap token as the API key. The broker validates the token and swaps in the real credential. Your code never sees the provider key.

python
import openai

client = openai.OpenAI(
    base_url="http://127.0.0.1:18443/proxy/openai/v1",
    api_key=open("/home/user/.ugent/secrets/broker.token").read().strip(),
)

response = client.chat.completions.create(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "hello"}],
)
javascript
import OpenAI from "openai";
import { readFileSync } from "fs";

const token = readFileSync("/home/user/.ugent/secrets/broker.token", "utf-8").trim();

const client = new OpenAI({
  baseURL: "http://127.0.0.1:18443/proxy/openai/v1",
  apiKey: token,
});

const response = await client.chat.completions.create({
  model: "gpt-5.4-mini",
  messages: [{ role: "user", content: "hello" }],
});
bash
TOKEN=$(cat ~/.ugent/secrets/broker.token)
curl 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","messages":[{"role":"user","content":"hi"}]}'

Pattern 2: Environment Projection

Run any command with secrets injected only into that child process's environment — never the current shell, never logged. Works for any language, any tool, zero code changes.

bash
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.toml

The --env VAR=@secret_ref form is the only supported syntax. Values are resolved from the vault at spawn time and injected into the child process environment only.

Pattern 3: Sibling Service Configuration

Point a sibling service's embedder or API client at the broker gateway in its own config file. The provider key never enters that service's process.

Context Engine:

toml
# ~/.ugent/context-engine/server.toml
[embedder]
openai_compatible_endpoint = "http://127.0.0.1:18443/proxy/openai/v1"
openai_compatible_api_key = "$BROKER_TOKEN"

Any application reading from environment:

bash
# Launch your app with secrets projected from the vault:
ugent secret exec \
  --env OPENAI_API_KEY=@openai_api_key \
  --env ANTHROPIC_API_KEY=@anthropic_api_key \
  --env GOOGLE_API_KEY=@google_gemini_api_key \
  -- your-application --config /etc/yourapp/config.yaml

Pattern 4: HTTP API (Any Language)

The management and lease API speaks plain HTTP/JSON over the Unix socket or loopback HTTP. Any language that can make HTTP requests can manage secrets programmatically.

text
GET    /v1/secrets                       -> list metadata (never values)
POST   /v1/secrets                       -> create (value in body, stored encrypted)
GET    /v1/secrets/{ref}                 -> metadata for one ref
DELETE /v1/secrets/{ref}                 -> delete
POST   /v1/secrets/{ref}/rotate          -> overwrite value
GET    /v1/secrets/{ref}/audit           -> audit trail
POST   /v1/leases                        -> request a lease (deny-by-default)

All responses contain metadata only. Decrypted secret values are never returned through any endpoint.

Grants and Leases

Access is deny by default. Configure grants in ~/.ugent/secrets.grants.toml:

toml
# UGENT core resolving the OpenAI key natively for chat and embeddings.
[[grants]]
secret_ref = "@openai_api_key"
consumer = "ugent-core"
purposes = ["llm.chat", "llm.embeddings"]
delivery = ["native"]
allowed_hosts = ["api.openai.com"]
ttl_secs = 300

# The broker's provider egress gateway forwarding /proxy/openai/... requests.
[[grants]]
secret_ref = "@openai_api_key"
consumer = "secret-broker.proxy"
purposes = ["llm.chat", "llm.embeddings"]
delivery = ["http_header_inject"]
allowed_hosts = ["api.openai.com"]
ttl_secs = 300

A missing file denies everything. Every issue and denial is audited to ~/.ugent/secrets/audit.jsonl. Edits to the grants file take effect on the next lease request — no broker restart needed.

Fields:

  • secret_ref — vault handle (e.g. @openai_api_key)
  • consumer — identity of the requester (e.g. ugent-core, secret-broker.proxy, or your own name)
  • purposes — optional allowlist of free-form purpose strings
  • delivery — optional allowlist: native, http_header_inject, query_param_inject, child_process_env, temp_file, human_reveal
  • allowed_hosts — optional upstream host allowlist
  • ttl_secs — per-grant TTL cap (clamped to policy max, default 3600)

Shared Broker Configuration

In shared mode, the broker reads from ~/.ugent/broker.toml instead of any workspace's ugent.toml. The schema mirrors [secrets.broker] / [secrets.policy] minus the secrets. prefix:

toml
# ~/.ugent/broker.toml -- canonical config for the shared broker.

[broker]
loopback_bind = "127.0.0.1:18443"
allow_loopback_http = true

# Optional: override or add provider profiles.
[broker.providers.foundry-claude]
upstream_base_url = "https://YOUR-RESOURCE.services.ai.azure.com"
secret_ref = "@foundry_claude_api_key"
auth = { type = "header", name = "api-key" }
forward_request_headers = ["anthropic-version", "anthropic-beta"]
routes = [{ method = "POST", path = "/anthropic/v1/messages", streaming = true }]

[policy]
lease_max_ttl_secs = 3600
allow_human_reveal = false
require_approval_for_new_grant = true

An absent broker.toml means embedded provider defaults plus default policy — the broker works out of the box.

Trust Domain

All UGENT instances of the same OS user authenticate to the shared broker with the same bootstrap token and can read the same vault, forming a single trust domain. The grants system gates which secrets may be leased, not which instance is asking. Per-instance identity is a future extension.

Plugin Secret Injection

Map environment variables to secret handles in your plugin configuration. The supervisor resolves and injects values only into the plugin child process at spawn time:

toml
[[plugins]]
id = "my-plugin"
secret_env_refs = { "MY_API_TOKEN" = "@my_service_token" }

Threat Model

Protected against:

  • Provider keys in config files, agent context, or logs
  • Prompt injection revealing keys (the LLM never sees decrypted values)
  • Other local processes calling the loopback API without the bootstrap token
  • Shell history and process listing leaks (secrets enter only via stdin/pipe)
  • Upstream credential redirect attacks (the broker never follows redirects)

Not protected against:

  • Root access
  • Same-user malware that can read the bootstrap token file or OS keychain
  • Kernel-level attacks

Released under the Private Beta License.