openclaw - ✅(Solved) Fix [Docs]: `secrets apply --from <plan.json>` accepts `providerUpserts` and `providerDeletes`, but `docs/gateway/secrets-plan-contract.md` only documents `targets` [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78538Fetched 2026-05-07 03:35:42
View on GitHub
Comments
1
Participants
2
Timeline
11
Reactions
2
Author
Timeline (top)
mentioned ×3referenced ×3subscribed ×3commented ×1

docs/gateway/secrets-plan-contract.md describes the targets array exclusively. The dist code (secrets-cli-Bk2EgRx_.js) also accepts and uses providerUpserts (object) and providerDeletes (array) at the top level of the plan file, but those fields are never mentioned in the docs.

Targets that reference an exec or file provider that doesn't already exist in openclaw.json will fail with provider "<alias>" is not configured unless providerUpserts defines it in the same plan. New users can't author working multi-provider plans without reading the dist source.

Root Cause

Without providerUpserts, the target fails to apply because provider: "onepassword_anthropic" doesn't exist in secrets.providers yet. With it, the plan is self-contained and atomic.

PR fix notes

PR #78720: docs: add providerUpserts and providerDeletes to secrets plan contract

Description (problem / solution / changelog)

OpenClaw PR #78538 - Secrets Provider Migration Documentation

Summary

Addresses documentation gap for providerUpserts and providerDeletes in openclaw secrets apply plans. Users couldn't author working multi-provider migration plans without reading the dist source code.

Changes:

  • Added comprehensive providerUpserts section with validation rules and multi-provider examples
  • Added providerDeletes section explaining deletion semantics and validation
  • Documented execution order: upserts → targets → deletes (enabling atomic operations)
  • Added apply-time summary showing operation counts
  • Included real-world end-to-end 1Password provider migration use case
  • Explained dry-run workflow for plan validation

Why it matters: Users attempting atomic provider migrations (e.g., plaintext → 1Password) can now discover the required fields from documentation instead of reverse-engineering dist source code. Unblocks common multi-provider setups.

Change Type

  • Docs
  • Bug fix
  • Feature
  • Refactor
  • Security hardening
  • Chore/infra

Scope

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra (docs)

Linked Issue

  • Fixes #78538

Root Cause

Documentation gap - the contract accepted providerUpserts and providerDeletes but never documented them. Users discovered the fields only by reading dist source code (secrets-cli-*.js). This blocked atomic multi-provider migrations.

Regression Test Plan

Coverage level:

  • Documentation review
  • Unit test
  • Integration test
  • End-to-end test

Target: docs/gateway/secrets-plan-contract.md

Scenario: User reads secrets plan documentation → learns about providerUpserts and providerDeletes → can author atomic multi-provider plans → migration succeeds without dist source reference

Why minimal: Documentation changes require content validation, not automated tests.

User-visible Changes

✅ Users now see providerUpserts and providerDeletes documented
✅ Real-world 1Password migration example provided
✅ Validation rules and execution order clearly explained
✅ Apply-time CLI output explained

Scope: Secrets plan contract documentation
Impact: Enables atomic multi-provider migrations (common workflow)

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Diagram

N/A (documentation only)

Repro + Verification

Before

User tries to write end-to-end provider migration plan:

  1. Defines new provider with providerUpserts
  2. References it in targets
  3. Plan fails → user has no guidance
  4. Searches docs, finds nothing → must read dist source

After

User reads plan documentation:

  1. Finds providerUpserts section with examples
  2. Sees real-world 1Password use case
  3. Understands execution order (upserts before targets)
  4. Successfully authors atomic migration plan

Verification Steps

  1. Read updated docs/gateway/secrets-plan-contract.md
  2. Verify providerUpserts section is clear and complete
  3. Verify providerDeletes section explains deletion semantics
  4. Verify end-to-end example covers real-world workflow
  5. Verify code examples are syntactically correct
  6. Check that documentation cross-references work

Files Changed

 docs/gateway/secrets-plan-contract.md | 137 ++++++++++++++++++++++++++-
 1 file changed, 137 insertions(+), 1 deletion(-)

Quality Checklist

  • Documentation is clear, comprehensive, and accurate
  • Examples are syntactically correct and realistic
  • Formatting is consistent with existing docs style
  • No grammar/spelling errors
  • Code blocks properly formatted
  • Cross-references work (if any)
  • Validation rules explicitly documented
  • Execution order clearly explained
  • Real-world use case included
  • Commit message is clear and references issue

Testing Done

✅ Documentation reviewed for accuracy and clarity
✅ JSON5 examples validated for syntax correctness
✅ 1Password use case verified as realistic
✅ Formatting verified for consistency
✅ Cross-references checked


Notes

  • This is a straightforward documentation expansion with no code changes
  • The providerUpserts and providerDeletes contract is validated and active in dist code
  • The 1Password migration example is based on actual user patterns
  • The documentation unblocks a common workflow without any code changes needed

Fixes #78538

Changed files

  • docs/gateway/config-agents.md (modified, +1/-1)
  • docs/gateway/secrets-plan-contract.md (modified, +151/-3)

Code Example

// Around line 193
if (typed.providerUpserts !== void 0) {
  if (!isObjectRecord(typed.providerUpserts)) return false;
  for (const [providerAlias, providerValue] of Object.entries(typed.providerUpserts)) {
    // validates providerAlias matches isValidSecretProviderAlias and providerValue shape
  }
}
if (typed.providerDeletes !== void 0) {
  if (!Array.isArray(typed.providerDeletes) || typed.providerDeletes.some((providerAlias) =>
    typeof providerAlias !== "string" || !isValidSecretProviderAlias(providerAlias))) return false;
}

---

// Around line 380
{ upserts: params.plan.providerUpserts, deletes: params.plan.providerDeletes }

---

const providerUpserts = Object.keys(configured.plan.providerUpserts ?? {}).length;
const providerDeletes = configured.plan.providerDeletes?.length ?? 0;
defaultRuntime.log(`Plan: targets=${configured.plan.targets.length}, providerUpserts=${providerUpserts}, providerDeletes=${providerDeletes}.`);

---

{
  version: 1,
  protocolVersion: 1,
  targets: [ ... ]
}

---

{
  version: 1,
  protocolVersion: 1,
  // 1. Define the provider FIRST
  providerUpserts: {
    onepassword_anthropic: {
      source: "exec",
      command: "/usr/bin/op",
      args: ["read", "op://Vault/Anthropic/credential"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
      allowInsecurePath: true
    }
  },
  // 2. THEN reference it from targets
  targets: [
    {
      type: "models.providers.apiKey",
      path: "models.providers.anthropic.apiKey",
      pathSegments: ["models", "providers", "anthropic", "apiKey"],
      providerId: "anthropic",
      ref: { source: "exec", provider: "onepassword_anthropic", id: "value" }
    },
    {
      type: "auth-profiles.api_key.key",
      path: "profiles.anthropic:default.key",
      pathSegments: ["profiles", "anthropic:default", "key"],
      agentId: "main",
      authProfileProvider: "anthropic",
      ref: { source: "exec", provider: "onepassword_anthropic", id: "value" }
    }
  ]
}

---

## Provider upserts and deletes

A plan can also include `providerUpserts` (object) and `providerDeletes` (array) at the top level. These let a single plan define new exec/file/env providers in `secrets.providers` and remove existing ones, in addition to writing targets.

### `providerUpserts`

Object keyed by provider alias. Each value is the full provider config (same shape accepted by `secrets.providers.<alias>` in `openclaw.json`):

\`\`\`json5
{
  providerUpserts: {
    onepassword_anthropic: {
      source: "exec",
      command: "/usr/bin/op",
      args: ["read", "op://Vault/Anthropic/credential"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
      allowInsecurePath: true,
    },
  },
}
\`\`\`

Aliases must match `isValidSecretProviderAlias` (alphanumeric, underscore, hyphen).

Provider definitions are written **before** targets resolve, so a single plan can both define a provider and reference it.

### `providerDeletes`

Array of provider aliases to remove from `secrets.providers`:

\`\`\`json5
{
  providerDeletes: ["legacy_provider_1", "legacy_provider_2"],
}
\`\`\`

Deletes run **after** targets and after upserts. A plan that deletes a provider whose alias is still referenced by a target will refuse to apply.

### Apply-time signal

The CLI logs all three counts at apply time:

\`\`\`text
Plan: targets=4, providerUpserts=2, providerDeletes=0.
\`\`\`

Use `--validate` first to dry-run without writing.
RAW_BUFFERClick to expand / collapse

Summary

docs/gateway/secrets-plan-contract.md describes the targets array exclusively. The dist code (secrets-cli-Bk2EgRx_.js) also accepts and uses providerUpserts (object) and providerDeletes (array) at the top level of the plan file, but those fields are never mentioned in the docs.

Targets that reference an exec or file provider that doesn't already exist in openclaw.json will fail with provider "<alias>" is not configured unless providerUpserts defines it in the same plan. New users can't author working multi-provider plans without reading the dist source.

Source-level evidence

dist/secrets-cli-Bk2EgRx_.js parses both fields:

// Around line 193
if (typed.providerUpserts !== void 0) {
  if (!isObjectRecord(typed.providerUpserts)) return false;
  for (const [providerAlias, providerValue] of Object.entries(typed.providerUpserts)) {
    // validates providerAlias matches isValidSecretProviderAlias and providerValue shape
  }
}
if (typed.providerDeletes !== void 0) {
  if (!Array.isArray(typed.providerDeletes) || typed.providerDeletes.some((providerAlias) =>
    typeof providerAlias !== "string" || !isValidSecretProviderAlias(providerAlias))) return false;
}

And uses them when applying:

// Around line 380
{ upserts: params.plan.providerUpserts, deletes: params.plan.providerDeletes }

…and logs them at apply time (line 2046):

const providerUpserts = Object.keys(configured.plan.providerUpserts ?? {}).length;
const providerDeletes = configured.plan.providerDeletes?.length ?? 0;
defaultRuntime.log(`Plan: targets=${configured.plan.targets.length}, providerUpserts=${providerUpserts}, providerDeletes=${providerDeletes}.`);

So the contract is real, validated, and active — just undocumented.

What docs say today

docs/gateway/secrets-plan-contract.md currently documents only:

{
  version: 1,
  protocolVersion: 1,
  targets: [ ... ]
}

…with no mention of providerUpserts or providerDeletes.

What new users actually need to write

A plan that performs an end-to-end migration from plaintext to a 1Password exec provider needs both fields:

{
  version: 1,
  protocolVersion: 1,
  // 1. Define the provider FIRST
  providerUpserts: {
    onepassword_anthropic: {
      source: "exec",
      command: "/usr/bin/op",
      args: ["read", "op://Vault/Anthropic/credential"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
      allowInsecurePath: true
    }
  },
  // 2. THEN reference it from targets
  targets: [
    {
      type: "models.providers.apiKey",
      path: "models.providers.anthropic.apiKey",
      pathSegments: ["models", "providers", "anthropic", "apiKey"],
      providerId: "anthropic",
      ref: { source: "exec", provider: "onepassword_anthropic", id: "value" }
    },
    {
      type: "auth-profiles.api_key.key",
      path: "profiles.anthropic:default.key",
      pathSegments: ["profiles", "anthropic:default", "key"],
      agentId: "main",
      authProfileProvider: "anthropic",
      ref: { source: "exec", provider: "onepassword_anthropic", id: "value" }
    }
  ]
}

Without providerUpserts, the target fails to apply because provider: "onepassword_anthropic" doesn't exist in secrets.providers yet. With it, the plan is self-contained and atomic.

Suggested fix

Add a new section to docs/gateway/secrets-plan-contract.md after ## Plan file shape:

## Provider upserts and deletes

A plan can also include `providerUpserts` (object) and `providerDeletes` (array) at the top level. These let a single plan define new exec/file/env providers in `secrets.providers` and remove existing ones, in addition to writing targets.

### `providerUpserts`

Object keyed by provider alias. Each value is the full provider config (same shape accepted by `secrets.providers.<alias>` in `openclaw.json`):

\`\`\`json5
{
  providerUpserts: {
    onepassword_anthropic: {
      source: "exec",
      command: "/usr/bin/op",
      args: ["read", "op://Vault/Anthropic/credential"],
      passEnv: ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
      jsonOnly: false,
      allowInsecurePath: true,
    },
  },
}
\`\`\`

Aliases must match `isValidSecretProviderAlias` (alphanumeric, underscore, hyphen).

Provider definitions are written **before** targets resolve, so a single plan can both define a provider and reference it.

### `providerDeletes`

Array of provider aliases to remove from `secrets.providers`:

\`\`\`json5
{
  providerDeletes: ["legacy_provider_1", "legacy_provider_2"],
}
\`\`\`

Deletes run **after** targets and after upserts. A plan that deletes a provider whose alias is still referenced by a target will refuse to apply.

### Apply-time signal

The CLI logs all three counts at apply time:

\`\`\`text
Plan: targets=4, providerUpserts=2, providerDeletes=0.
\`\`\`

Use `--validate` first to dry-run without writing.

Also worth adding an "End-to-end migration plan" example near the top, since the typical use case for this contract is exactly the multi-provider 1Password migration that needs both fields.

Why it matters

I found this by reading the dist code while debugging a multi-provider migration. Doing it from docs alone is impossible — the only mention of providerUpserts in any user-facing surface is the apply-time log line, which a user only sees AFTER they've already written a working plan.

Environment

  • OpenClaw 2026.5.4 (325df3e)
  • Linux x64

Related

  • The targets doc that does exist is otherwise solid; this is purely an additive doc gap, not a correction.
  • Adjacent: a related issue (filed separately) requests Linux uid-check documentation for allowInsecurePath — many real-world providerUpserts need that flag for system-package binaries like /usr/bin/op.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Docs]: `secrets apply --from <plan.json>` accepts `providerUpserts` and `providerDeletes`, but `docs/gateway/secrets-plan-contract.md` only documents `targets` [1 pull requests, 1 comments, 2 participants]