diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index ff3fa812a4d..8281d0fb0d2 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -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", diff --git a/docs/cli/onboard.md b/docs/cli/onboard.md index 9e9bf585a0d..7485499d1ea 100644 --- a/docs/cli/onboard.md +++ b/docs/cli/onboard.md @@ -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..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..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. diff --git a/docs/cli/secrets.md b/docs/cli/secrets.md index e46c24f6ce4..5eeb820ff7d 100644 --- a/docs/cli/secrets.md +++ b/docs/cli/secrets.md @@ -57,17 +57,7 @@ openclaw secrets migrate --write --no-scrub-env - Scrub target is `/.env`. - Only known secret env keys are considered. - Entries are removed only when the value exactly matches a migrated plaintext secret. -- If `/.sops.yaml` or `/.sops.yml` exists, migrate passes it explicitly to `sops`, runs `sops` with `cwd=`, 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 `/.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 `/secrets.json`. Rollback a previous migration: diff --git a/docs/gateway/authentication.md b/docs/gateway/authentication.md index 3b68633730c..448789c9a6c 100644 --- a/docs/gateway/authentication.md +++ b/docs/gateway/authentication.md @@ -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): diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 9f18bf8d48b..9b8eff2cbc2 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -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..serviceAccount` - `channels.googlechat.accounts..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. --- diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 5e319cb18bd..46756dbc01a 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -492,32 +492,40 @@ Rules: - + 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). See [Environment](/help/environment) for full precedence and sources. diff --git a/docs/gateway/secrets.md b/docs/gateway/secrets.md index 91dbda364a8..23e6f7edb69 100644 --- a/docs/gateway/secrets.md +++ b/docs/gateway/secrets.md @@ -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 /.sops.yaml` (or `.sops.yml`), runs `sops` with `cwd=`, 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 ms for .` -- 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 `/.sops.yaml` or `/.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..keyRef` for `type: "api_key"` - `profiles..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 `/.sops.yaml` or `/.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 `/.env` (default on) + +Migration writes secrets to: + +- configured default `file` provider path when present +- otherwise `/secrets.json` `.env` scrub semantics: -- Scrub target path is `/.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 `/.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//` -- Manifest: `manifest.json` -- Retention: 20 backups +- path: `~/.openclaw/backups/secrets-migrate//` +- manifest: `manifest.json` +- retention: 20 backups ## `auth.json` compatibility notes diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index f335d7424ba..3e5dec6f664 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -206,7 +206,7 @@ Use this when auditing access or deciding what to back up: - `~/.openclaw/credentials/-allowFrom.json` (default account) - `~/.openclaw/credentials/--allowFrom.json` (non-default accounts) - **Model auth profiles**: `~/.openclaw/agents//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//` - **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//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//agent/auth.json`: legacy compatibility file. Static `api_key` entries are scrubbed when discovered. - `agents//sessions/**`: session transcripts (`*.jsonl`) + routing metadata (`sessions.json`) that can contain private messages and tool output. diff --git a/docs/help/environment.md b/docs/help/environment.md index a404c9cb030..d261faeaa07 100644 --- a/docs/help/environment.md +++ b/docs/help/environment.md @@ -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). diff --git a/docs/help/faq.md b/docs/help/faq.md index 3becbb62e49..3e48a7b39ca 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -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//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//agent/auth.json` | Legacy compatibility file (static `api_key` entries scrubbed) | -| `$OPENCLAW_STATE_DIR/credentials/` | Provider state (e.g. `whatsapp//creds.json`) | -| `$OPENCLAW_STATE_DIR/agents/` | Per-agent state (agentDir + sessions) | -| `$OPENCLAW_STATE_DIR/agents//sessions/` | Conversation history & state (per agent) | -| `$OPENCLAW_STATE_DIR/agents//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//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//agent/auth.json` | Legacy compatibility file (static `api_key` entries scrubbed) | +| `$OPENCLAW_STATE_DIR/credentials/` | Provider state (e.g. `whatsapp//creds.json`) | +| `$OPENCLAW_STATE_DIR/agents/` | Per-agent state (agentDir + sessions) | +| `$OPENCLAW_STATE_DIR/agents//sessions/` | Conversation history & state (per agent) | +| `$OPENCLAW_STATE_DIR/agents//sessions/sessions.json` | Session metadata (per agent) | Legacy single-agent path: `~/.openclaw/agent/*` (migrated by `openclaw doctor`). diff --git a/docs/reference/wizard.md b/docs/reference/wizard.md index 8321f2d683b..6cc8a83b927 100644 --- a/docs/reference/wizard.md +++ b/docs/reference/wizard.md @@ -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//agent/auth-profiles.json` (API keys + OAuth). - More detail: [/concepts/oauth](/concepts/oauth) diff --git a/docs/start/setup.md b/docs/start/setup.md index d0185c4b133..7d25fe8776a 100644 --- a/docs/start/setup.md +++ b/docs/start/setup.md @@ -134,7 +134,7 @@ Use this when debugging auth or deciding what to back up: - `~/.openclaw/credentials/-allowFrom.json` (default account) - `~/.openclaw/credentials/--allowFrom.json` (non-default accounts) - **Model auth profiles**: `~/.openclaw/agents//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//` - **Legacy OAuth import**: `~/.openclaw/credentials/oauth.json` More detail: [Security](/gateway/security#credential-storage-map). diff --git a/docs/start/wizard-cli-automation.md b/docs/start/wizard-cli-automation.md index a81cecbcee3..14f4a9d5d32 100644 --- a/docs/start/wizard-cli-automation.md +++ b/docs/start/wizard-cli-automation.md @@ -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" }`. diff --git a/docs/start/wizard-cli-reference.md b/docs/start/wizard-cli-reference.md index 354fb491d49..1790020c852 100644 --- a/docs/start/wizard-cli-reference.md +++ b/docs/start/wizard-cli-reference.md @@ -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..apiKey` as `{ source: "env", id: "CUSTOM_API_KEY" }`. + - For custom providers, non-interactive `ref` mode stores `models.providers..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. diff --git a/docs/start/wizard.md b/docs/start/wizard.md index 240d02818bd..6cdb2e8fa95 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -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. diff --git a/docs/tools/skills-config.md b/docs/tools/skills-config.md index 0bd5362ebd3..589d464bb13 100644 --- a/docs/tools/skills-config.md +++ b/docs/tools/skills-config.md @@ -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 diff --git a/docs/tools/skills.md b/docs/tools/skills.md index fd437b3649c..de3fe807ed2 100644 --- a/docs/tools/skills.md +++ b/docs/tools/skills.md @@ -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). diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index 411b91ddb3d..641d7119054 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -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);