docs(secrets): align provider model and add exec resolver coverage

This commit is contained in:
joshavant
2026-02-25 17:58:10 -06:00
committed by Peter Steinberger
parent 4e7a833a24
commit bde9cbb058
18 changed files with 321 additions and 135 deletions

View File

@@ -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",

View File

@@ -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.

View File

@@ -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:

View File

@@ -14,7 +14,7 @@ use the longlived 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):

View File

@@ -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.
---

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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).

View File

@@ -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`).

View File

@@ -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>

View File

@@ -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).

View File

@@ -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>

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 its 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

View File

@@ -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 its bundled/installed.
- `env`: injected **only if** the variable isnt 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).

View File

@@ -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);