mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
docs(secrets): align provider model and add exec resolver coverage
This commit is contained in:
committed by
Peter Steinberger
parent
4e7a833a24
commit
bde9cbb058
@@ -166,7 +166,7 @@ Use these identifiers for delivery and allowlists:
|
||||
googlechat: {
|
||||
enabled: true,
|
||||
serviceAccountFile: "/path/to/service-account.json",
|
||||
// or serviceAccountRef: { source: "file", id: "/channels/googlechat/serviceAccount" }
|
||||
// or serviceAccountRef: { source: "file", provider: "filemain", id: "/channels/googlechat/serviceAccount" }
|
||||
audienceType: "app-url",
|
||||
audience: "https://gateway.example.com/googlechat",
|
||||
webhookPath: "/googlechat",
|
||||
|
||||
@@ -50,7 +50,7 @@ openclaw onboard --non-interactive \
|
||||
```
|
||||
|
||||
With `--secret-input-mode ref`, onboarding writes env-backed refs instead of plaintext key values.
|
||||
For auth-profile backed providers this writes `keyRef` entries; for custom providers this writes `models.providers.<id>.apiKey` as an env ref (for example `{ source: "env", id: "CUSTOM_API_KEY" }`).
|
||||
For auth-profile backed providers this writes `keyRef` entries; for custom providers this writes `models.providers.<id>.apiKey` as an env ref (for example `{ source: "env", provider: "default", id: "CUSTOM_API_KEY" }`).
|
||||
|
||||
Non-interactive `ref` mode contract:
|
||||
|
||||
@@ -63,7 +63,7 @@ Interactive onboarding behavior with reference mode:
|
||||
- Choose **Use secret reference** when prompted.
|
||||
- Then choose either:
|
||||
- Environment variable
|
||||
- Encrypted `sops` file (JSON pointer)
|
||||
- Configured secret provider (`file` or `exec`)
|
||||
- Onboarding performs a fast preflight validation before saving the ref.
|
||||
- If validation fails, onboarding shows the error and lets you retry.
|
||||
|
||||
|
||||
@@ -57,17 +57,7 @@ openclaw secrets migrate --write --no-scrub-env
|
||||
- Scrub target is `<config-dir>/.env`.
|
||||
- Only known secret env keys are considered.
|
||||
- Entries are removed only when the value exactly matches a migrated plaintext secret.
|
||||
- If `<config-dir>/.sops.yaml` or `<config-dir>/.sops.yml` exists, migrate passes it explicitly to `sops`, runs `sops` with `cwd=<config-dir>`, and sets `--filename-override` to the absolute target secrets path (for example `/home/user/.openclaw/secrets.enc.json`) so strict `creation_rules` continue to match when OpenClaw encrypts through a temp file.
|
||||
|
||||
Common migrate write failure:
|
||||
|
||||
- `config file not found, or has no creation rules, and no keys provided through command line options`
|
||||
|
||||
If you hit this:
|
||||
|
||||
- Add or fix `<config-dir>/.sops.yaml` / `.sops.yml` with valid `creation_rules`.
|
||||
- Ensure key access is available in the command environment (for example `SOPS_AGE_KEY_FILE`).
|
||||
- Re-run `openclaw secrets migrate --write`.
|
||||
- Migration writes to the configured default `file` provider path when present; otherwise `<state-dir>/secrets.json`.
|
||||
|
||||
Rollback a previous migration:
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use the long‑lived token created by `claude setup-token`.
|
||||
|
||||
See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage
|
||||
layout.
|
||||
For SecretRef-based auth (env/sops-backed refs), see [Secrets Management](/gateway/secrets).
|
||||
For SecretRef-based auth (`env`/`file`/`exec` providers), see [Secrets Management](/gateway/secrets).
|
||||
|
||||
## Recommended Anthropic setup (API key)
|
||||
|
||||
@@ -88,8 +88,8 @@ openclaw models auth paste-token --provider openrouter
|
||||
|
||||
Auth profile refs are also supported for static credentials:
|
||||
|
||||
- `api_key` credentials can use `keyRef: { source, id }`
|
||||
- `token` credentials can use `tokenRef: { source, id }`
|
||||
- `api_key` credentials can use `keyRef: { source, provider, id }`
|
||||
- `token` credentials can use `tokenRef: { source, provider, id }`
|
||||
|
||||
Automation-friendly check (exit `1` when expired/missing, `2` when expiring):
|
||||
|
||||
|
||||
@@ -1987,7 +1987,7 @@ See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.1 via LM Studio
|
||||
},
|
||||
entries: {
|
||||
"nano-banana-pro": {
|
||||
apiKey: { source: "env", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },
|
||||
},
|
||||
peekaboo: { enabled: true },
|
||||
@@ -2394,13 +2394,15 @@ Secret refs are additive: plaintext values still work.
|
||||
Use one object shape:
|
||||
|
||||
```json5
|
||||
{ source: "env" | "file", id: "..." }
|
||||
{ source: "env" | "file" | "exec", provider: "default", id: "..." }
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
- `provider` pattern: `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `source: "env"` id pattern: `^[A-Z][A-Z0-9_]{0,127}$`
|
||||
- `source: "file"` id: absolute JSON pointer (for example `"/providers/openai/apiKey"`)
|
||||
- `source: "exec"` id pattern: `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
|
||||
|
||||
### Supported fields in config
|
||||
|
||||
@@ -2411,18 +2413,29 @@ Validation:
|
||||
- `channels.googlechat.accounts.<accountId>.serviceAccount`
|
||||
- `channels.googlechat.accounts.<accountId>.serviceAccountRef`
|
||||
|
||||
### Secret sources config
|
||||
### Secret providers config
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
sources: {
|
||||
env: { type: "env" }, // optional
|
||||
file: {
|
||||
type: "sops",
|
||||
path: "~/.openclaw/secrets.enc.json",
|
||||
providers: {
|
||||
default: { source: "env" }, // optional explicit env provider
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: "~/.openclaw/secrets.json",
|
||||
mode: "jsonPointer",
|
||||
timeoutMs: 5000,
|
||||
},
|
||||
vault: {
|
||||
source: "exec",
|
||||
command: "/usr/local/bin/openclaw-vault-resolver",
|
||||
passEnv: ["PATH", "VAULT_ADDR"],
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
env: "default",
|
||||
file: "filemain",
|
||||
exec: "vault",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2430,9 +2443,9 @@ Validation:
|
||||
|
||||
Notes:
|
||||
|
||||
- `file` source requires `sops` in `PATH` (`sops >= 3.9.0`).
|
||||
- `timeoutMs` defaults to `5000`.
|
||||
- File payload must decrypt to a JSON object; `id` is resolved via JSON pointer.
|
||||
- `file` provider supports `mode: "jsonPointer"` and `mode: "raw"` (`id` must be `"value"` in raw mode).
|
||||
- `exec` provider requires an absolute `command` path and uses protocol payloads on stdin/stdout.
|
||||
- Secret refs are resolved at activation time into an in-memory snapshot, then request paths read the snapshot only.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -492,32 +492,40 @@ Rules:
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Secret refs (env and encrypted file)">
|
||||
<Accordion title="Secret refs (env, file, exec)">
|
||||
For fields that support SecretRef objects, you can use:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
openai: { apiKey: { source: "env", id: "OPENAI_API_KEY" } },
|
||||
openai: { apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" } },
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
entries: {
|
||||
"nano-banana-pro": {
|
||||
apiKey: { source: "file", id: "/skills/entries/nano-banana-pro/apiKey" },
|
||||
apiKey: {
|
||||
source: "file",
|
||||
provider: "filemain",
|
||||
id: "/skills/entries/nano-banana-pro/apiKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
googlechat: {
|
||||
serviceAccountRef: { source: "file", id: "/channels/googlechat/serviceAccount" },
|
||||
serviceAccountRef: {
|
||||
source: "exec",
|
||||
provider: "vault",
|
||||
id: "channels/googlechat/serviceAccount",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
SecretRef details (including `secrets.sources.file` for `sops`) are in [Secrets Management](/gateway/secrets).
|
||||
SecretRef details (including `secrets.providers` for `env`/`file`/`exec`) are in [Secrets Management](/gateway/secrets).
|
||||
</Accordion>
|
||||
|
||||
See [Environment](/help/environment) for full precedence and sources.
|
||||
|
||||
@@ -9,7 +9,7 @@ title: "Secrets Management"
|
||||
|
||||
# Secrets management
|
||||
|
||||
OpenClaw supports additive secret references so credentials do not need to be stored in plaintext config files.
|
||||
OpenClaw supports additive secret references so credentials do not need to be stored as plaintext in config files.
|
||||
|
||||
Plaintext still works. Secret refs are optional.
|
||||
|
||||
@@ -17,107 +17,137 @@ Plaintext still works. Secret refs are optional.
|
||||
|
||||
Secrets are resolved into an in-memory runtime snapshot.
|
||||
|
||||
- Resolution is eager during activation (not lazy on request paths).
|
||||
- Startup fails fast if any required ref cannot be resolved.
|
||||
- Resolution is eager during activation, not lazy on request paths.
|
||||
- Startup fails fast if any referenced credential cannot be resolved.
|
||||
- Reload uses atomic swap: full success or keep last-known-good.
|
||||
- Runtime requests use the active in-memory snapshot.
|
||||
- Runtime requests read from the active in-memory snapshot.
|
||||
|
||||
This keeps external secret source outages off the hot request path.
|
||||
This keeps secret-provider outages off the hot request path.
|
||||
|
||||
## Onboarding reference preflight
|
||||
|
||||
When onboarding runs in interactive mode and you choose secret reference storage, OpenClaw performs a fast preflight check before saving:
|
||||
|
||||
- Env refs: validates env var name and confirms a non-empty value is visible during onboarding.
|
||||
- File refs (`sops`): validates `secrets.sources.file`, decrypts, and resolves the JSON pointer.
|
||||
- Provider refs (`file` or `exec`): validates the selected provider, resolves the provided `id`, and checks value type.
|
||||
|
||||
If validation fails, onboarding shows the error and lets you retry with a different ref/source.
|
||||
If validation fails, onboarding shows the error and lets you retry.
|
||||
|
||||
## SecretRef contract
|
||||
|
||||
Use one object shape everywhere:
|
||||
|
||||
```json5
|
||||
{ source: "env" | "file", id: "..." }
|
||||
{ source: "env" | "file" | "exec", provider: "default", id: "..." }
|
||||
```
|
||||
|
||||
### `source: "env"`
|
||||
|
||||
```json5
|
||||
{ source: "env", id: "OPENAI_API_KEY" }
|
||||
{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `id` must match `^[A-Z][A-Z0-9_]{0,127}$`
|
||||
- Example error: `Env secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: "OPENAI_API_KEY").`
|
||||
|
||||
### `source: "file"`
|
||||
|
||||
```json5
|
||||
{ source: "file", id: "/providers/openai/apiKey" }
|
||||
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" }
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `id` must be an absolute JSON pointer (`/...`)
|
||||
- Use RFC6901 token escaping in segments: `~` => `~0`, `/` => `~1`
|
||||
- Example error: `File secret reference id must be an absolute JSON pointer (example: "/providers/openai/apiKey").`
|
||||
- RFC6901 escaping in segments: `~` => `~0`, `/` => `~1`
|
||||
|
||||
## v1 secret sources
|
||||
|
||||
### Environment source
|
||||
|
||||
No extra config required for resolution.
|
||||
|
||||
Optional explicit config:
|
||||
### `source: "exec"`
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
sources: {
|
||||
env: { type: "env" },
|
||||
},
|
||||
},
|
||||
}
|
||||
{ source: "exec", provider: "vault", id: "providers/openai/apiKey" }
|
||||
```
|
||||
|
||||
### Encrypted file source (`sops`)
|
||||
Validation:
|
||||
|
||||
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
|
||||
- `id` must match `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
|
||||
|
||||
## Provider config
|
||||
|
||||
Define providers under `secrets.providers`:
|
||||
|
||||
```json5
|
||||
{
|
||||
secrets: {
|
||||
sources: {
|
||||
file: {
|
||||
type: "sops",
|
||||
path: "~/.openclaw/secrets.enc.json",
|
||||
timeoutMs: 5000,
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: "~/.openclaw/secrets.json",
|
||||
mode: "jsonPointer", // or "raw"
|
||||
},
|
||||
vault: {
|
||||
source: "exec",
|
||||
command: "/usr/local/bin/openclaw-vault-resolver",
|
||||
args: ["--profile", "prod"],
|
||||
passEnv: ["PATH", "VAULT_ADDR"],
|
||||
jsonOnly: true,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
env: "default",
|
||||
file: "filemain",
|
||||
exec: "vault",
|
||||
},
|
||||
resolution: {
|
||||
maxProviderConcurrency: 4,
|
||||
maxRefsPerProvider: 512,
|
||||
maxBatchBytes: 262144,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Contract:
|
||||
### Env provider
|
||||
|
||||
- OpenClaw shells out to `sops` for decrypt/encrypt.
|
||||
- Minimum supported version: `sops >= 3.9.0`.
|
||||
- For migration, OpenClaw explicitly passes `--config <config-dir>/.sops.yaml` (or `.sops.yml`), runs `sops` with `cwd=<config-dir>`, and sets `--filename-override` to the absolute target secrets path (for example `/home/user/.openclaw/secrets.enc.json`) so strict `creation_rules` still match even though encryption uses a temp input file.
|
||||
- Decrypted payload must be a JSON object.
|
||||
- `id` is resolved as JSON pointer into decrypted payload.
|
||||
- Default timeout is `5000ms`.
|
||||
- Optional allowlist via `allowlist`.
|
||||
- Missing/empty env values fail resolution.
|
||||
|
||||
Common errors:
|
||||
### File provider
|
||||
|
||||
- Missing binary: `sops binary not found in PATH. Install sops >= 3.9.0 or disable secrets.sources.file.`
|
||||
- Timeout: `sops decrypt timed out after <n>ms for <path>.`
|
||||
- Missing creation rules/key access (common during migrate write): `config file not found, or has no creation rules, and no keys provided through command line options`
|
||||
- Reads local file from `path`.
|
||||
- `mode: "jsonPointer"` expects JSON object payload and resolves `id` as pointer.
|
||||
- `mode: "raw"` expects ref id `"value"` and returns file contents.
|
||||
- Path must pass ownership/permission checks.
|
||||
|
||||
Fix for creation-rules/key-access errors:
|
||||
### Exec provider
|
||||
|
||||
- Ensure `<config-dir>/.sops.yaml` or `<config-dir>/.sops.yml` contains a valid `creation_rules` entry for your secrets file.
|
||||
- Ensure the runtime environment for `openclaw secrets migrate --write` can access decryption/encryption keys (for example `SOPS_AGE_KEY_FILE` for age keys).
|
||||
- Re-run migration after confirming both config and key access.
|
||||
- Runs configured absolute binary path, no shell.
|
||||
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
|
||||
- Request payload (stdin):
|
||||
|
||||
```json
|
||||
{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }
|
||||
```
|
||||
|
||||
- Response payload (stdout):
|
||||
|
||||
```json
|
||||
{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "sk-..." } }
|
||||
```
|
||||
|
||||
Optional per-id errors:
|
||||
|
||||
```json
|
||||
{
|
||||
"protocolVersion": 1,
|
||||
"values": {},
|
||||
"errors": { "providers/openai/apiKey": { "message": "not found" } }
|
||||
}
|
||||
```
|
||||
|
||||
## In-scope fields (v1)
|
||||
|
||||
@@ -135,15 +165,15 @@ Fix for creation-rules/key-access errors:
|
||||
- `profiles.<profileId>.keyRef` for `type: "api_key"`
|
||||
- `profiles.<profileId>.tokenRef` for `type: "token"`
|
||||
|
||||
OAuth credential storage changes are out of scope for this sprint.
|
||||
OAuth credential storage changes are out of scope.
|
||||
|
||||
## Required vs optional behavior
|
||||
## Required behavior and precedence
|
||||
|
||||
- Optional field with no ref: ignored.
|
||||
- Field with a ref: required at activation time.
|
||||
- If both plaintext and ref exist, ref wins at runtime and plaintext is ignored.
|
||||
- Field without ref: unchanged.
|
||||
- Field with ref: required at activation time.
|
||||
- If plaintext and ref both exist, ref wins at runtime and plaintext is ignored.
|
||||
|
||||
Warning code used for that override:
|
||||
Warning code:
|
||||
|
||||
- `SECRETS_REF_OVERRIDES_PLAINTEXT`
|
||||
|
||||
@@ -151,16 +181,16 @@ Warning code used for that override:
|
||||
|
||||
Secret activation is attempted on:
|
||||
|
||||
- Startup (preflight + final activation)
|
||||
- Startup (preflight plus final activation)
|
||||
- Config reload hot-apply path
|
||||
- Config reload restart-check path
|
||||
- Manual reload via `secrets.reload`
|
||||
|
||||
Activation contract:
|
||||
|
||||
- If activation succeeds, snapshot swaps atomically.
|
||||
- If activation fails on startup, gateway startup fails.
|
||||
- If activation fails during runtime reload, active snapshot remains last-known-good.
|
||||
- Success swaps the snapshot atomically.
|
||||
- Startup failure aborts gateway startup.
|
||||
- Runtime reload failure keeps last-known-good snapshot.
|
||||
|
||||
## Degraded and recovered operator signals
|
||||
|
||||
@@ -174,21 +204,21 @@ One-shot system event and log codes:
|
||||
Behavior:
|
||||
|
||||
- Degraded: runtime keeps last-known-good snapshot.
|
||||
- Recovered: emitted once after successful activation.
|
||||
- Repeated failures while already degraded only log warnings (no repeated system events).
|
||||
- Startup fail-fast does not emit degraded events because no runtime snapshot is active yet.
|
||||
- Recovered: emitted once after a successful activation.
|
||||
- Repeated failures while already degraded log warnings but do not spam events.
|
||||
- Startup fail-fast does not emit degraded events because no runtime snapshot exists yet.
|
||||
|
||||
## Migration command
|
||||
|
||||
Use `openclaw secrets migrate` to move plaintext static secrets into file-backed refs.
|
||||
|
||||
Default is dry-run:
|
||||
Dry-run (default):
|
||||
|
||||
```bash
|
||||
openclaw secrets migrate
|
||||
```
|
||||
|
||||
Apply changes:
|
||||
Apply:
|
||||
|
||||
```bash
|
||||
openclaw secrets migrate --write
|
||||
@@ -203,22 +233,26 @@ openclaw secrets migrate --rollback 20260224T193000Z
|
||||
What migration covers:
|
||||
|
||||
- `openclaw.json` fields listed above
|
||||
- `auth-profiles.json` API key and token plaintext fields
|
||||
- optional scrub of matching plaintext values from config-dir `.env` (default on)
|
||||
- if `<config-dir>/.sops.yaml` or `<config-dir>/.sops.yml` exists, migration uses it explicitly for sops decrypt/encrypt
|
||||
- `auth-profiles.json` plaintext API key/token fields
|
||||
- optional scrub of matching plaintext values from `<config-dir>/.env` (default on)
|
||||
|
||||
Migration writes secrets to:
|
||||
|
||||
- configured default `file` provider path when present
|
||||
- otherwise `<state-dir>/secrets.json`
|
||||
|
||||
`.env` scrub semantics:
|
||||
|
||||
- Scrub target path is `<config-dir>/.env` (`resolveConfigDir(...)`), not `OPENCLAW_HOME/.env`.
|
||||
- Only known secret env keys are eligible (for example `OPENAI_API_KEY`).
|
||||
- A line is removed only when its parsed value exactly matches a migrated plaintext value.
|
||||
- Non-secret keys, comments, and unmatched values are preserved.
|
||||
- target path is `<config-dir>/.env`
|
||||
- only known secret env keys are eligible
|
||||
- a line is removed only when value exactly matches a migrated plaintext value
|
||||
- comments/non-secret keys/unmatched values are preserved
|
||||
|
||||
Backups:
|
||||
|
||||
- Path: `~/.openclaw/backups/secrets-migrate/<backupId>/`
|
||||
- Manifest: `manifest.json`
|
||||
- Retention: 20 backups
|
||||
- path: `~/.openclaw/backups/secrets-migrate/<backupId>/`
|
||||
- manifest: `manifest.json`
|
||||
- retention: 20 backups
|
||||
|
||||
## `auth.json` compatibility notes
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ Use this when auditing access or deciding what to back up:
|
||||
- `~/.openclaw/credentials/<channel>-allowFrom.json` (default account)
|
||||
- `~/.openclaw/credentials/<channel>-<accountId>-allowFrom.json` (non-default accounts)
|
||||
- **Model auth profiles**: `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`
|
||||
- **Encrypted secrets payload (optional)**: `~/.openclaw/secrets.enc.json`
|
||||
- **File-backed secrets payload (optional)**: `~/.openclaw/secrets.json`
|
||||
- **Secrets migration backups (optional)**: `~/.openclaw/backups/secrets-migrate/<backupId>/`
|
||||
- **Legacy OAuth import**: `~/.openclaw/credentials/oauth.json`
|
||||
|
||||
@@ -763,7 +763,7 @@ Assume anything under `~/.openclaw/` (or `$OPENCLAW_STATE_DIR/`) may contain sec
|
||||
- `openclaw.json`: config may include tokens (gateway, remote gateway), provider settings, and allowlists.
|
||||
- `credentials/**`: channel credentials (example: WhatsApp creds), pairing allowlists, legacy OAuth imports.
|
||||
- `agents/<agentId>/agent/auth-profiles.json`: API keys, token profiles, OAuth tokens, and optional `keyRef`/`tokenRef`.
|
||||
- `secrets.enc.json` (optional): encrypted file-backed secret payload used by SecretRefs (`secrets.sources.file`).
|
||||
- `secrets.json` (optional): file-backed secret payload used by `file` SecretRef providers (`secrets.providers`).
|
||||
- `backups/secrets-migrate/**` (optional): migration rollback backups + manifests.
|
||||
- `agents/<agentId>/agent/auth.json`: legacy compatibility file. Static `api_key` entries are scrubbed when discovered.
|
||||
- `agents/<agentId>/sessions/**`: session transcripts (`*.jsonl`) + routing metadata (`sessions.json`) that can contain private messages and tool output.
|
||||
|
||||
@@ -79,7 +79,7 @@ See [Configuration: Env var substitution](/gateway/configuration#env-var-substit
|
||||
OpenClaw supports two env-driven patterns:
|
||||
|
||||
- `${VAR}` string substitution in config values.
|
||||
- SecretRef objects (`{ source: "env", id: "VAR" }`) for fields that support secrets references.
|
||||
- SecretRef objects (`{ source: "env", provider: "default", id: "VAR" }`) for fields that support secrets references.
|
||||
|
||||
Both resolve from process env at activation time. SecretRef details are documented in [Secrets Management](/gateway/secrets).
|
||||
|
||||
|
||||
@@ -1291,18 +1291,18 @@ Related: [Agent workspace](/concepts/agent-workspace), [Memory](/concepts/memory
|
||||
|
||||
Everything lives under `$OPENCLAW_STATE_DIR` (default: `~/.openclaw`):
|
||||
|
||||
| Path | Purpose |
|
||||
| --------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `$OPENCLAW_STATE_DIR/openclaw.json` | Main config (JSON5) |
|
||||
| `$OPENCLAW_STATE_DIR/credentials/oauth.json` | Legacy OAuth import (copied into auth profiles on first use) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/agent/auth-profiles.json` | Auth profiles (OAuth, API keys, and optional `keyRef`/`tokenRef`) |
|
||||
| `$OPENCLAW_STATE_DIR/secrets.enc.json` | Optional encrypted file-backed secret payload (`sops`) |
|
||||
| `$OPENCLAW_STATE_DIR/backups/secrets-migrate/` | Optional migration rollback backups + manifests |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/agent/auth.json` | Legacy compatibility file (static `api_key` entries scrubbed) |
|
||||
| `$OPENCLAW_STATE_DIR/credentials/` | Provider state (e.g. `whatsapp/<accountId>/creds.json`) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/` | Per-agent state (agentDir + sessions) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/sessions/` | Conversation history & state (per agent) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/sessions/sessions.json` | Session metadata (per agent) |
|
||||
| Path | Purpose |
|
||||
| --------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `$OPENCLAW_STATE_DIR/openclaw.json` | Main config (JSON5) |
|
||||
| `$OPENCLAW_STATE_DIR/credentials/oauth.json` | Legacy OAuth import (copied into auth profiles on first use) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/agent/auth-profiles.json` | Auth profiles (OAuth, API keys, and optional `keyRef`/`tokenRef`) |
|
||||
| `$OPENCLAW_STATE_DIR/secrets.json` | Optional file-backed secret payload for `file` SecretRef providers |
|
||||
| `$OPENCLAW_STATE_DIR/backups/secrets-migrate/` | Optional migration rollback backups + manifests |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/agent/auth.json` | Legacy compatibility file (static `api_key` entries scrubbed) |
|
||||
| `$OPENCLAW_STATE_DIR/credentials/` | Provider state (e.g. `whatsapp/<accountId>/creds.json`) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/` | Per-agent state (agentDir + sessions) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/sessions/` | Conversation history & state (per agent) |
|
||||
| `$OPENCLAW_STATE_DIR/agents/<agentId>/sessions/sessions.json` | Session metadata (per agent) |
|
||||
|
||||
Legacy single-agent path: `~/.openclaw/agent/*` (migrated by `openclaw doctor`).
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ For a high-level overview, see [Onboarding Wizard](/start/wizard).
|
||||
- **Skip**: no auth configured yet.
|
||||
- Pick a default model from detected options (or enter provider/model manually).
|
||||
- Wizard runs a model check and warns if the configured model is unknown or missing auth.
|
||||
- API key storage mode defaults to plaintext auth-profile values. Use `--secret-input-mode ref` to store env-backed refs instead (for example `keyRef: { source: "env", id: "OPENAI_API_KEY" }`).
|
||||
- API key storage mode defaults to plaintext auth-profile values. Use `--secret-input-mode ref` to store env-backed refs instead (for example `keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" }`).
|
||||
- OAuth credentials live in `~/.openclaw/credentials/oauth.json`; auth profiles live in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` (API keys + OAuth).
|
||||
- More detail: [/concepts/oauth](/concepts/oauth)
|
||||
<Note>
|
||||
|
||||
@@ -134,7 +134,7 @@ Use this when debugging auth or deciding what to back up:
|
||||
- `~/.openclaw/credentials/<channel>-allowFrom.json` (default account)
|
||||
- `~/.openclaw/credentials/<channel>-<accountId>-allowFrom.json` (non-default accounts)
|
||||
- **Model auth profiles**: `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`
|
||||
- **Encrypted secrets payload (optional)**: `~/.openclaw/secrets.enc.json`
|
||||
- **File-backed secrets payload (optional)**: `~/.openclaw/secrets.json`
|
||||
- **Secrets migration backups (optional)**: `~/.openclaw/backups/secrets-migrate/<backupId>/`
|
||||
- **Legacy OAuth import**: `~/.openclaw/credentials/oauth.json`
|
||||
More detail: [Security](/gateway/security#credential-storage-map).
|
||||
|
||||
@@ -33,7 +33,7 @@ openclaw onboard --non-interactive \
|
||||
Add `--json` for a machine-readable summary.
|
||||
|
||||
Use `--secret-input-mode ref` to store env-backed refs in auth profiles instead of plaintext values.
|
||||
Interactive selection between env refs and encrypted file refs (`sops`) is available in the onboarding wizard flow.
|
||||
Interactive selection between env refs and configured provider refs (`file` or `exec`) is available in the onboarding wizard flow.
|
||||
|
||||
In non-interactive `ref` mode, provider env vars must be set in the process environment.
|
||||
Passing inline key flags without the matching env var now fails fast.
|
||||
@@ -165,7 +165,7 @@ openclaw onboard --non-interactive \
|
||||
--gateway-bind loopback
|
||||
```
|
||||
|
||||
In this mode, onboarding stores `apiKey` as `{ source: "env", id: "CUSTOM_API_KEY" }`.
|
||||
In this mode, onboarding stores `apiKey` as `{ source: "env", provider: "default", id: "CUSTOM_API_KEY" }`.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -179,7 +179,7 @@ What you set:
|
||||
|
||||
Interactive onboarding supports the same API key storage choices as other provider API key flows:
|
||||
- **Paste API key now** (plaintext)
|
||||
- **Use secret reference** (env or encrypted `sops` file pointer, with preflight validation)
|
||||
- **Use secret reference** (env ref or configured provider ref, with preflight validation)
|
||||
|
||||
Non-interactive flags:
|
||||
- `--auth-choice custom-api-key`
|
||||
@@ -210,16 +210,16 @@ API key storage mode:
|
||||
- Default onboarding behavior persists API keys as plaintext values in auth profiles.
|
||||
- `--secret-input-mode ref` enables reference mode instead of plaintext key storage.
|
||||
In interactive onboarding, you can choose either:
|
||||
- environment variable ref (for example `keyRef: { source: "env", id: "OPENAI_API_KEY" }`)
|
||||
- encrypted file ref via `sops` JSON pointer (for example `keyRef: { source: "file", id: "/providers/openai/apiKey" }`)
|
||||
- environment variable ref (for example `keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" }`)
|
||||
- configured provider ref (`file` or `exec`) with provider alias + id
|
||||
- Interactive reference mode runs a fast preflight validation before saving.
|
||||
- Env refs: validates variable name + non-empty value in the current onboarding environment.
|
||||
- File refs: validates `secrets.sources.file` + `sops` decrypt + JSON pointer resolution.
|
||||
- Provider refs: validates provider config and resolves the requested id.
|
||||
- If preflight fails, onboarding shows the error and lets you retry.
|
||||
- In non-interactive mode, `--secret-input-mode ref` is env-backed only.
|
||||
- Set the provider env var in the onboarding process environment.
|
||||
- Inline key flags (for example `--openai-api-key`) require that env var to be set; otherwise onboarding fails fast.
|
||||
- For custom providers, non-interactive `ref` mode stores `models.providers.<id>.apiKey` as `{ source: "env", id: "CUSTOM_API_KEY" }`.
|
||||
- For custom providers, non-interactive `ref` mode stores `models.providers.<id>.apiKey` as `{ source: "env", provider: "default", id: "CUSTOM_API_KEY" }`.
|
||||
- In that custom-provider case, `--custom-api-key` requires `CUSTOM_API_KEY` to be set; otherwise onboarding fails fast.
|
||||
- Existing plaintext setups continue to work unchanged.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ The wizard starts with **QuickStart** (defaults) vs **Advanced** (full control).
|
||||
(OpenAI-compatible, Anthropic-compatible, or Unknown auto-detect). Pick a default model.
|
||||
For non-interactive runs, `--secret-input-mode ref` stores env-backed refs in auth profiles instead of plaintext API key values.
|
||||
In non-interactive `ref` mode, the provider env var must be set; passing inline key flags without that env var fails fast.
|
||||
In interactive runs, choosing secret reference mode lets you point at either an environment variable or an encrypted `sops` file pointer, with a fast preflight validation before saving.
|
||||
In interactive runs, choosing secret reference mode lets you point at either an environment variable or a configured provider ref (`file` or `exec`), with a fast preflight validation before saving.
|
||||
2. **Workspace** — Location for agent files (default `~/.openclaw/workspace`). Seeds bootstrap files.
|
||||
3. **Gateway** — Port, bind address, auth mode, Tailscale exposure.
|
||||
4. **Channels** — WhatsApp, Telegram, Discord, Google Chat, Mattermost, Signal, BlueBubbles, or iMessage.
|
||||
|
||||
@@ -26,7 +26,7 @@ All skills-related configuration lives under `skills` in `~/.openclaw/openclaw.j
|
||||
entries: {
|
||||
"nano-banana-pro": {
|
||||
enabled: true,
|
||||
apiKey: { source: "env", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
env: {
|
||||
GEMINI_API_KEY: "GEMINI_KEY_HERE",
|
||||
},
|
||||
@@ -56,7 +56,7 @@ Per-skill fields:
|
||||
- `enabled`: set `false` to disable a skill even if it’s bundled/installed.
|
||||
- `env`: environment variables injected for the agent run (only if not already set).
|
||||
- `apiKey`: optional convenience for skills that declare a primary env var.
|
||||
Supports plaintext string or SecretRef object (`{ source, id }`).
|
||||
Supports plaintext string or SecretRef object (`{ source, provider, id }`).
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ Bundled/managed skills can be toggled and supplied with env values:
|
||||
entries: {
|
||||
"nano-banana-pro": {
|
||||
enabled: true,
|
||||
apiKey: { source: "env", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
|
||||
env: {
|
||||
GEMINI_API_KEY: "GEMINI_KEY_HERE",
|
||||
},
|
||||
@@ -221,7 +221,7 @@ Rules:
|
||||
- `enabled: false` disables the skill even if it’s bundled/installed.
|
||||
- `env`: injected **only if** the variable isn’t already set in the process.
|
||||
- `apiKey`: convenience for skills that declare `metadata.openclaw.primaryEnv`.
|
||||
Supports plaintext string or SecretRef object (`{ source, id }`).
|
||||
Supports plaintext string or SecretRef object (`{ source, provider, id }`).
|
||||
- `config`: optional bag for custom per-skill fields; custom keys must live here.
|
||||
- `allowBundled`: optional allowlist for **bundled** skills only. If set, only
|
||||
bundled skills in the list are eligible (managed/workspace skills unaffected).
|
||||
|
||||
@@ -108,6 +108,147 @@ describe("secret ref resolver", () => {
|
||||
expect(value).toBe("value:openai/api-key");
|
||||
});
|
||||
|
||||
it("supports non-JSON single-value exec output when jsonOnly is false", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-plain-"));
|
||||
cleanupRoots.push(root);
|
||||
const scriptPath = path.join(root, "resolver-plain.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
["#!/usr/bin/env node", "process.stdout.write('plain-secret');"].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(value).toBe("plain-secret");
|
||||
});
|
||||
|
||||
it("rejects exec refs when protocolVersion is not 1", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-protocol-"),
|
||||
);
|
||||
cleanupRoots.push(root);
|
||||
const scriptPath = path.join(root, "resolver-protocol.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 2, values: { 'openai/api-key': 'x' } }));",
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("protocolVersion must be 1");
|
||||
});
|
||||
|
||||
it("rejects exec refs when response omits requested id", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-id-"));
|
||||
cleanupRoots.push(root);
|
||||
const scriptPath = path.join(root, "resolver-missing-id.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 1, values: {} }));",
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow('response missing id "openai/api-key"');
|
||||
});
|
||||
|
||||
it("rejects exec refs with invalid JSON when jsonOnly is true", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-exec-json-"));
|
||||
cleanupRoots.push(root);
|
||||
const scriptPath = path.join(root, "resolver-invalid-json.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
["#!/usr/bin/env node", "process.stdout.write('not-json');"].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("returned invalid JSON");
|
||||
});
|
||||
|
||||
it("supports file raw mode with id=value", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-raw-"));
|
||||
cleanupRoots.push(root);
|
||||
|
||||
Reference in New Issue
Block a user