Skip to content

feat(secrets): add Azure Key Vault secret provider#43644

Closed
akoscz wants to merge 2 commits into
openclaw:mainfrom
akoscz:feature/secrets-azure-provider
Closed

feat(secrets): add Azure Key Vault secret provider#43644
akoscz wants to merge 2 commits into
openclaw:mainfrom
akoscz:feature/secrets-azure-provider

Conversation

@akoscz

@akoscz akoscz commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds AzureSecretProvider for resolving secrets from Azure Key Vault.

Azure SDK packages (@azure/keyvault-secrets + @azure/identity) are lazy-loaded as optional peer dependencies — no startup cost unless the provider is configured.

Authentication

DefaultAzureCredential chain:

  • Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)
  • Managed Identity (Azure VMs, App Service, etc.)
  • Azure CLI (az login)
  • Service Principal

Features

  • Version pinning: ${azure:my-secret#<version-id>}
  • Configurable TTL caching (default 5 min)
  • Supports tenantId/clientId/clientSecret overrides per provider config

Config

{
  "secrets": {
    "providers": {
      "azure": {
        "vaultUrl": "https://my-vault.vault.azure.net",
        "tenantId": "<tenant-id>"
      }
    }
  }
}

Usage: ${azure:my-secret-name}

Notes

Split from #24272 per @vincentkoc's request.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime size: XL labels Mar 12, 2026
@greptile-apps

greptile-apps Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces AzureSecretProvider, wiring it into the new secret-resolution.ts module alongside the existing GcpSecretProvider. The Azure provider is well-structured: lazy SDK loading, proper error mapping, name validation, rotation tag helpers, and a clean injected-client interface for testing. The test suite is thorough. However, a few issues need attention before merge:

  • Cache TTL for Azure is silently ignoredresolveConfigSecrets derives cacheTtlMs exclusively from the GCP provider config. If only Azure is configured with a custom cacheTtlSeconds, the default 5-minute TTL is used regardless.
  • Empty vaultUrl silently registeredbuildSecretProviders falls back to "" when vaultUrl is absent, deferring a configuration error to a cryptic runtime failure.
  • Documentation inconsistency — the newly added table in secrets.md lists Azure Key Vault as "Planned" even though this PR ships it; additionally, frontmatter-style metadata was accidentally left as body text in the same section.
  • GCP SDK added as a hard dependency@google-cloud/secret-manager is added to dependencies despite being lazy-loaded via import(), inconsistent with the stated optional peer-dep design for Azure SDK packages.

Confidence Score: 3/5

  • Safe to merge after fixing the cache TTL bug and the empty vaultUrl silent failure; documentation and dependency inconsistencies should also be addressed.
  • The core provider implementation and test coverage are solid. Two logic-level bugs lower the score: the GCP-only cache TTL read means Azure users with custom TTLs silently get wrong behavior, and the empty vaultUrl means misconfigured Azure providers fail at runtime with unhelpful errors rather than at startup. The documentation issues are cosmetic but visible to users.
  • src/config/secret-resolution.ts needs attention for the cache TTL and vaultUrl validation issues. docs/gateway/secrets.md needs the "Planned" status corrected and the stray frontmatter removed.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/secret-resolution.ts
Line: 490-494

Comment:
**Azure `cacheTtlSeconds` is silently ignored**

The `cacheTtlMs` variable is derived exclusively from the GCP provider's configuration. If a user configures only the Azure provider with a custom `cacheTtlSeconds`, that value is never read and the default 5-minute TTL is used instead.

```ts
// Current — only GCP's TTL is respected
const cacheTtlMs =
  secretsConfig?.providers?.gcp?.cacheTtlSeconds != null
    ? secretsConfig.providers.gcp.cacheTtlSeconds * 1000
    : defaultTtlMs;
```

`AzureSecretProvider` exposes a `cacheTtlMillis` getter precisely so callers can read the per-provider TTL. The simplest fix is to resolve each secret using its own provider's TTL inside `resolveAny` / `fetchWithCache`, or at minimum also check the azure config here:

```ts
const gcpTtl = secretsConfig?.providers?.gcp?.cacheTtlSeconds;
const azureTtl = secretsConfig?.providers?.azure?.cacheTtlSeconds;
const cacheTtlMs =
  (gcpTtl ?? azureTtl ?? null) != null
    ? (gcpTtl ?? azureTtl)! * 1000
    : defaultTtlMs;
```

Ideally, `fetchWithCache` should accept the provider's own TTL so each provider's configured value is used independently.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/secret-resolution.ts
Line: 333

Comment:
**Azure provider registered with empty `vaultUrl`, masking misconfiguration**

When `vaultUrl` is absent from the config, the `AzureSecretProvider` is constructed with an empty string (`""`). The provider is silently registered and will only fail later at runtime — when a secret is actually fetched — with a potentially cryptic Azure SDK networking error rather than a clear configuration error.

Consider validating the presence of `vaultUrl` before registering the provider:

```ts
if (name === "azure") {
  if (!config?.vaultUrl) {
    throw new Error(
      'Azure secret provider config is missing required field "vaultUrl". ' +
      'Set secrets.providers.azure.vaultUrl in openclaw.json.',
    );
  }
  providers.set("azure", new AzureSecretProvider({ ... }));
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: docs/gateway/secrets.md
Line: 492

Comment:
**Documentation marks Azure Key Vault as "Planned" — but this PR ships it**

The newly-added supported providers table shows:

```
| Azure Key Vault     | Planned      |
```

Since this PR is implementing and shipping `AzureSecretProvider`, the status should be updated to `✅ Supported` to match the GCP row and accurately reflect what this release provides.

```suggestion
| Azure Key Vault     | ✅ Supported |
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: docs/gateway/secrets.md
Line: 453-465

Comment:
**Frontmatter-style metadata rendered as body text in the new section**

The block starting immediately after the `## Additional Secrets Providers` heading:

```
summary: "External secrets management: store credentials in GCP Secret Manager..."
read_when:
- You want to stop storing API keys...
  title: "Secrets"
```

looks like it was intended as frontmatter or an internal document header but was accidentally placed in the body. Readers will see this raw key-value syntax rendered as plain text, which is confusing. Either move it to proper frontmatter at the top of the file (if the docs platform supports per-section frontmatter) or remove it entirely.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: package.json
Line: 346

Comment:
**`@google-cloud/secret-manager` added as a hard dependency, inconsistent with Azure's optional approach**

The PR description states that Azure SDK packages are *lazy-loaded as optional peer dependencies*. The `GcpSecretProvider` uses the same lazy `import()` pattern (`await import("@google-cloud/secret-manager")`), yet `@google-cloud/secret-manager` is being added to `dependencies` in `package.json`, making it always installed.

This means every consumer of this package pays the install cost for GCP's SDK even if they only use Azure Key Vault (or no secrets provider at all). For consistency with the stated design, consider moving `@google-cloud/secret-manager` to `peerDependenciesMeta` as optional, mirroring how `@azure/keyvault-secrets` and `@azure/identity` are handled.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: de12897

Comment on lines +490 to +494
const defaultTtlMs = 300_000;
const cacheTtlMs =
secretsConfig?.providers?.gcp?.cacheTtlSeconds != null
? secretsConfig.providers.gcp.cacheTtlSeconds * 1000
: defaultTtlMs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure cacheTtlSeconds is silently ignored

The cacheTtlMs variable is derived exclusively from the GCP provider's configuration. If a user configures only the Azure provider with a custom cacheTtlSeconds, that value is never read and the default 5-minute TTL is used instead.

// Current — only GCP's TTL is respected
const cacheTtlMs =
  secretsConfig?.providers?.gcp?.cacheTtlSeconds != null
    ? secretsConfig.providers.gcp.cacheTtlSeconds * 1000
    : defaultTtlMs;

AzureSecretProvider exposes a cacheTtlMillis getter precisely so callers can read the per-provider TTL. The simplest fix is to resolve each secret using its own provider's TTL inside resolveAny / fetchWithCache, or at minimum also check the azure config here:

const gcpTtl = secretsConfig?.providers?.gcp?.cacheTtlSeconds;
const azureTtl = secretsConfig?.providers?.azure?.cacheTtlSeconds;
const cacheTtlMs =
  (gcpTtl ?? azureTtl ?? null) != null
    ? (gcpTtl ?? azureTtl)! * 1000
    : defaultTtlMs;

Ideally, fetchWithCache should accept the provider's own TTL so each provider's configured value is used independently.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/secret-resolution.ts
Line: 490-494

Comment:
**Azure `cacheTtlSeconds` is silently ignored**

The `cacheTtlMs` variable is derived exclusively from the GCP provider's configuration. If a user configures only the Azure provider with a custom `cacheTtlSeconds`, that value is never read and the default 5-minute TTL is used instead.

```ts
// Current — only GCP's TTL is respected
const cacheTtlMs =
  secretsConfig?.providers?.gcp?.cacheTtlSeconds != null
    ? secretsConfig.providers.gcp.cacheTtlSeconds * 1000
    : defaultTtlMs;
```

`AzureSecretProvider` exposes a `cacheTtlMillis` getter precisely so callers can read the per-provider TTL. The simplest fix is to resolve each secret using its own provider's TTL inside `resolveAny` / `fetchWithCache`, or at minimum also check the azure config here:

```ts
const gcpTtl = secretsConfig?.providers?.gcp?.cacheTtlSeconds;
const azureTtl = secretsConfig?.providers?.azure?.cacheTtlSeconds;
const cacheTtlMs =
  (gcpTtl ?? azureTtl ?? null) != null
    ? (gcpTtl ?? azureTtl)! * 1000
    : defaultTtlMs;
```

Ideally, `fetchWithCache` should accept the provider's own TTL so each provider's configured value is used independently.

How can I resolve this? If you propose a fix, please make it concise.

if (name === "azure") {
providers.set(
"azure",
new AzureSecretProvider({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure provider registered with empty vaultUrl, masking misconfiguration

When vaultUrl is absent from the config, the AzureSecretProvider is constructed with an empty string (""). The provider is silently registered and will only fail later at runtime — when a secret is actually fetched — with a potentially cryptic Azure SDK networking error rather than a clear configuration error.

Consider validating the presence of vaultUrl before registering the provider:

if (name === "azure") {
  if (!config?.vaultUrl) {
    throw new Error(
      'Azure secret provider config is missing required field "vaultUrl". ' +
      'Set secrets.providers.azure.vaultUrl in openclaw.json.',
    );
  }
  providers.set("azure", new AzureSecretProvider({ ... }));
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/secret-resolution.ts
Line: 333

Comment:
**Azure provider registered with empty `vaultUrl`, masking misconfiguration**

When `vaultUrl` is absent from the config, the `AzureSecretProvider` is constructed with an empty string (`""`). The provider is silently registered and will only fail later at runtime — when a secret is actually fetched — with a potentially cryptic Azure SDK networking error rather than a clear configuration error.

Consider validating the presence of `vaultUrl` before registering the provider:

```ts
if (name === "azure") {
  if (!config?.vaultUrl) {
    throw new Error(
      'Azure secret provider config is missing required field "vaultUrl". ' +
      'Set secrets.providers.azure.vaultUrl in openclaw.json.',
    );
  }
  providers.set("azure", new AzureSecretProvider({ ... }));
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread docs/gateway/secrets.md
| HashiCorp Vault | Planned |
| Azure Key Vault | Planned |

## Quick Start

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation marks Azure Key Vault as "Planned" — but this PR ships it

The newly-added supported providers table shows:

| Azure Key Vault     | Planned      |

Since this PR is implementing and shipping AzureSecretProvider, the status should be updated to ✅ Supported to match the GCP row and accurately reflect what this release provides.

Suggested change
## Quick Start
| Azure Key Vault | ✅ Supported |
Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/gateway/secrets.md
Line: 492

Comment:
**Documentation marks Azure Key Vault as "Planned" — but this PR ships it**

The newly-added supported providers table shows:

```
| Azure Key Vault     | Planned      |
```

Since this PR is implementing and shipping `AzureSecretProvider`, the status should be updated to `✅ Supported` to match the GCP row and accurately reflect what this release provides.

```suggestion
| Azure Key Vault     | ✅ Supported |
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread docs/gateway/secrets.md
Comment on lines 453 to +465
- Security posture: [Security](/gateway/security)
- Environment precedence: [Environment Variables](/help/environment)

---

## Additional Secrets Providers

summary: "External secrets management: store credentials in GCP Secret Manager instead of plaintext files"
read_when:

- You want to stop storing API keys in plaintext config files
- You want per-agent secret isolation in multi-agent setups
- You want to set up GCP Secret Manager for OpenClaw

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontmatter-style metadata rendered as body text in the new section

The block starting immediately after the ## Additional Secrets Providers heading:

summary: "External secrets management: store credentials in GCP Secret Manager..."
read_when:
- You want to stop storing API keys...
  title: "Secrets"

looks like it was intended as frontmatter or an internal document header but was accidentally placed in the body. Readers will see this raw key-value syntax rendered as plain text, which is confusing. Either move it to proper frontmatter at the top of the file (if the docs platform supports per-section frontmatter) or remove it entirely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/gateway/secrets.md
Line: 453-465

Comment:
**Frontmatter-style metadata rendered as body text in the new section**

The block starting immediately after the `## Additional Secrets Providers` heading:

```
summary: "External secrets management: store credentials in GCP Secret Manager..."
read_when:
- You want to stop storing API keys...
  title: "Secrets"
```

looks like it was intended as frontmatter or an internal document header but was accidentally placed in the body. Readers will see this raw key-value syntax rendered as plain text, which is confusing. Either move it to proper frontmatter at the top of the file (if the docs platform supports per-section frontmatter) or remove it entirely.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread package.json
@@ -343,6 +343,7 @@
"@buape/carbon": "0.0.0-beta-20260216184201",
"@clack/prompts": "^1.1.0",
"@discordjs/voice": "^0.19.1",
"@google-cloud/secret-manager": "^6.1.1",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@google-cloud/secret-manager added as a hard dependency, inconsistent with Azure's optional approach

The PR description states that Azure SDK packages are lazy-loaded as optional peer dependencies. The GcpSecretProvider uses the same lazy import() pattern (await import("@google-cloud/secret-manager")), yet @google-cloud/secret-manager is being added to dependencies in package.json, making it always installed.

This means every consumer of this package pays the install cost for GCP's SDK even if they only use Azure Key Vault (or no secrets provider at all). For consistency with the stated design, consider moving @google-cloud/secret-manager to peerDependenciesMeta as optional, mirroring how @azure/keyvault-secrets and @azure/identity are handled.

Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 346

Comment:
**`@google-cloud/secret-manager` added as a hard dependency, inconsistent with Azure's optional approach**

The PR description states that Azure SDK packages are *lazy-loaded as optional peer dependencies*. The `GcpSecretProvider` uses the same lazy `import()` pattern (`await import("@google-cloud/secret-manager")`), yet `@google-cloud/secret-manager` is being added to `dependencies` in `package.json`, making it always installed.

This means every consumer of this package pays the install cost for GCP's SDK even if they only use Azure Key Vault (or no secrets provider at all). For consistency with the stated design, consider moving `@google-cloud/secret-manager` to `peerDependenciesMeta` as optional, mirroring how `@azure/keyvault-secrets` and `@azure/identity` are handled.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de12897b0f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

version: string | undefined,
cacheTtlMs: number,
): Promise<string> {
const cacheKey = `${provider.name}:${name}#${version ?? "latest"}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope secret cache keys to provider configuration

fetchWithCache uses provider.name plus secret/version as the entire cache key, so two different provider instances of the same type (for example, different Azure vault URLs or GCP projects across config reloads/multi-agent runs) will share cache entries. In that scenario, the second resolution can return a secret from the previous provider context without calling getSecret, which can serve incorrect credentials or leak values across contexts; include provider identity (project/vault/provider alias) in the key or clear cache per config.

Useful? React with 👍 / 👎.

Comment on lines +492 to +494
secretsConfig?.providers?.gcp?.cacheTtlSeconds != null
? secretsConfig.providers.gcp.cacheTtlSeconds * 1000
: defaultTtlMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor Azure cacheTtlSeconds during secret resolution

resolveConfigSecrets computes the shared cache TTL only from secrets.providers.gcp.cacheTtlSeconds, so Azure's cacheTtlSeconds setting is ignored. In an Azure-only configuration, users cannot lower or raise cache duration as documented, which means secret rotations may not take effect until the hardcoded 5-minute default expires; TTL needs to be selected per referenced provider instead of hardcoding the GCP field.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-azure-provider branch from de12897 to 609f289 Compare March 12, 2026 15:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 609f2893f3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +482 to +485
export async function resolveConfigSecrets(
obj: unknown,
secretsConfig: SecretsConfig | undefined,
providersOverride?: Map<string, SecretProvider>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire resolveConfigSecrets into loadConfig

This resolver is introduced but never executed in the config read path: in this commit, loadConfig in src/config/io.ts goes from include/env resolution to validation/return without calling resolveConfigSecrets, so ${gcp:...} and ${azure:...} placeholders remain literal strings at runtime. In practice, users following the new secrets docs will pass unresolved placeholders as credentials and hit downstream auth failures instead of secret lookup.

Useful? React with 👍 / 👎.


async getSecret(secretName: string, version?: string): Promise<string> {
const ver = version ?? "latest";
const cacheKey = `gcp:${secretName}#${ver}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope GCP provider cache keys by project

GcpSecretProvider.getSecret caches using only gcp:${secretName}#${ver} in the shared secretCache, so project/credential context is not part of the key. If config is reloaded to a different GCP project (or multiple provider instances are used in one process), the method can return a stale secret from the previous project without calling Secret Manager; even fixing fetchWithCache would not address this provider-level collision.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-azure-provider branch from 609f289 to 3cee8ba Compare March 12, 2026 20:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3cee8bac2b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +322 to +326
if (name === "gcp" && config && config.project) {
providers.set(
"gcp",
new GcpSecretProvider(
config as { project: string; cacheTtlSeconds?: number; credentialsFile?: string },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align provider config shape with existing secrets schema

buildSecretProviders assumes entries like secrets.providers.gcp.project/azure.vaultUrl, but config validation still enforces secrets.providers.* as the existing discriminated source union in src/config/zod-schema.core.ts (env|file|exec). In practice, a config that follows this new provider shape is rejected during validation before any resolution can run, so users cannot enable the new provider configuration path at all.

Useful? React with 👍 / 👎.

Comment on lines +131 to +132
const identityMod = await lazyImport<{
DefaultAzureCredential: new () => AzureCredential;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare Azure SDK packages required by dynamic imports

The provider hard-imports @azure/identity (and later @azure/keyvault-secrets) at runtime, but this commit does not add either package to dependencies or peerDependencies in package.json (only @google-cloud/secret-manager is added). On a clean install, ${azure:...} resolution will fail immediately with the install error unless users manually add extra packages, so the Azure provider is not usable out of the box.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-azure-provider branch 2 times, most recently from 22c8525 to 8d03542 Compare March 13, 2026 02:05
Adds AzureSecretProvider for resolving secrets from Azure Key Vault.
Uses `@azure/keyvault-secrets` + `@azure/identity` (lazy-loaded optional
peer dependencies — no startup cost unless the provider is used).

Authentication via DefaultAzureCredential chain:
- Environment variables (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)
- Managed Identity
- Azure CLI (`az login`)
- Service Principal

Features:
- Version pinning: ${azure:my-secret#version-id}
- Configurable TTL caching (default 5 min)
- Custom credentials file support

Config example:
  "secrets": {
    "providers": {
      "azure": {
        "vaultUrl": "https://my-vault.vault.azure.net",
        "tenantId": "<tenant-id>"
      }
    }
  }

Usage: ${azure:my-secret-name}

Split from openclaw#24272 per @vincentkoc's request.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d0354240b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

);

for (const { full, value: sv } of resolved) {
working = working.replace(full, sv);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace refs by position to avoid mutating resolved secret text

The replacement loop uses working = working.replace(full, sv) for each matched placeholder, which always replaces the first matching substring in the current string. If an earlier secret value contains text that looks like another placeholder (for example ${gcp:a} resolves to a string containing ${gcp:b} while ${gcp:b} is also present in the original config), later iterations can rewrite the injected secret value instead of the original placeholder and leave the real placeholder unresolved. This can corrupt resolved credentials/config values in real deployments where secret payloads include template-like text.

Useful? React with 👍 / 👎.

Comment on lines +371 to +372
const value = await provider.getSecret(name, version);
secretCache.set(cacheKey, { value, expiresAt: Date.now() + cacheTtlMs });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate in-flight secret fetches during parallel resolution

resolveAny resolves object/array fields in parallel, but fetchWithCache only caches completed values. When the same reference appears multiple times in one config load, concurrent calls all miss the cache and each executes provider.getSecret(...), causing redundant backend reads (and potential throttling/slow startup) even though a shared cache exists. Add in-flight promise deduplication (or pre-deduplicate references) so identical refs are fetched once per resolution pass.

Useful? React with 👍 / 👎.

@akoscz akoscz force-pushed the feature/secrets-azure-provider branch from 8d03542 to 5d90fdc Compare March 13, 2026 05:53

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d90fdc318

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

// Restore escaped refs
working = working.replaceAll(ESCAPE_PLACEHOLDER, "${");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid mutating resolved secrets during escape restoration

The escape restoration step uses a fixed sentinel (___OPENCLAW_ESC___) and then blindly runs replaceAll on the fully resolved string, so any resolved secret value containing that substring is rewritten (for example, abc___OPENCLAW_ESC___def becomes abc${def). This can silently corrupt credentials/config values in deployments where secret payloads include that token-like text; use a per-call unique marker or positional reconstruction so only original $${...} escapes are restored.

Useful? React with 👍 / 👎.

@akoscz

akoscz commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the contribution! After reviewing this PR alongside the related secrets provider PRs (#43640, #43641, #43643, #43644, #43645, #43646), we need to close these for an architectural rework before they can land.

The issue: parallel implementation instead of extending the existing system

OpenClaw already has a mature secrets infrastructure in src/secrets/ (resolve.ts, runtime.ts, ref-contract.ts, etc.) and src/config/types.secrets.ts. That system uses SecretRef / SecretProviderConfig as the core provider contract and already supports exec and file provider types wired into the gateway and runtime.

These PRs introduce a new parallel system — a new src/config/secret-resolution.ts module, a new SecretProvider / getSecret() interface, and a new ${provider:name} syntax — without connecting to the existing infrastructure at all.

There's also a hidden dependency across the PR set: the aws, azure, keyring, and vault branches all import SecretProvider from ./secret-resolution.js, but that file only exists in #43640 (env-provider). Those four PRs cannot compile against main independently.

What we need instead

New cloud-backend providers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, OS keyring, env vars) should be implemented as resolvers within the existing src/secrets/ provider contract — plugging into SecretProviderConfig so they're available to the full resolution pipeline already wired into the gateway and runtime.

src/secrets/resolve.ts, src/config/types.secrets.ts, and src/gateway/server-methods/secrets.ts are the right starting points to understand the provider contract.

We'd love to see these re-submitted with the corrected architecture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant