Gateway: fail closed unresolved local auth SecretRefs (#42672)

* Gateway: fail closed unresolved local auth SecretRefs

* Docs: align node-host gateway auth precedence

* CI: resolve rebase breakages in checks lanes

* Tests: isolate LOCAL_REMOTE_FALLBACK_TOKEN env state

* Gateway: remove stale remote.enabled auth-surface semantics

* Changelog: note gateway SecretRef fail-closed fix
This commit is contained in:
Josh Avant 2026-03-10 21:41:56 -05:00 committed by GitHub
parent a52104c235
commit 0125ce1f44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 197 additions and 48 deletions

View File

@ -84,7 +84,7 @@ Docs: https://docs.openclaw.ai
- Browser/Browserbase 429 handling: surface stable no-retry rate-limit guidance without buffering discarded HTTP 429 response bodies from remote browser services. (#40491) thanks @mvanhorn.
- Gateway/auth: allow one trusted device-token retry on shared-token mismatch with recovery hints to prevent reconnect churn during token drift. (#42507) Thanks @joshavant.
- Channels/allowlists: remove stale matcher caching so same-array allowlist edits and wildcard replacements take effect immediately, with regression coverage for in-place mutation cases.
- Gateway/auth: fail closed when local `gateway.auth.*` SecretRefs are configured but unavailable, instead of silently falling back to `gateway.remote.*` credentials in local mode. Thanks @tdjackey.
- Gateway/auth: fail closed when local `gateway.auth.*` SecretRefs are configured but unavailable, instead of silently falling back to `gateway.remote.*` credentials in local mode. (#42672) Thanks @joshavant.
- Sandbox/fs bridge: pin staged writes to verified parent directories so temporary write files cannot materialize outside the allowed mount before atomic replace. Thanks @tdjackey.
- Commands/config writes: enforce `configWrites` against both the originating account and the targeted account scope for `/config` and config-backed `/allowlist` edits, blocking sibling-account mutations while preserving gateway `operator.admin` flows. Thanks @tdjackey for reporting.

View File

@ -1337,6 +1337,8 @@ public struct SessionsPatchParams: Codable, Sendable {
public let model: AnyCodable?
public let spawnedby: AnyCodable?
public let spawndepth: AnyCodable?
public let subagentrole: AnyCodable?
public let subagentcontrolscope: AnyCodable?
public let sendpolicy: AnyCodable?
public let groupactivation: AnyCodable?
@ -1355,6 +1357,8 @@ public struct SessionsPatchParams: Codable, Sendable {
model: AnyCodable?,
spawnedby: AnyCodable?,
spawndepth: AnyCodable?,
subagentrole: AnyCodable?,
subagentcontrolscope: AnyCodable?,
sendpolicy: AnyCodable?,
groupactivation: AnyCodable?)
{
@ -1372,6 +1376,8 @@ public struct SessionsPatchParams: Codable, Sendable {
self.model = model
self.spawnedby = spawnedby
self.spawndepth = spawndepth
self.subagentrole = subagentrole
self.subagentcontrolscope = subagentcontrolscope
self.sendpolicy = sendpolicy
self.groupactivation = groupactivation
}
@ -1391,6 +1397,8 @@ public struct SessionsPatchParams: Codable, Sendable {
case model
case spawnedby = "spawnedBy"
case spawndepth = "spawnDepth"
case subagentrole = "subagentRole"
case subagentcontrolscope = "subagentControlScope"
case sendpolicy = "sendPolicy"
case groupactivation = "groupActivation"
}
@ -3046,7 +3054,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String
public let command: String?
public let commandargv: [String]?
public let systemrunplan: [String: AnyCodable]?
public let env: [String: AnyCodable]?
@ -3067,7 +3075,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
public init(
id: String?,
command: String,
command: String?,
commandargv: [String]?,
systemrunplan: [String: AnyCodable]?,
env: [String: AnyCodable]?,

View File

@ -1337,6 +1337,8 @@ public struct SessionsPatchParams: Codable, Sendable {
public let model: AnyCodable?
public let spawnedby: AnyCodable?
public let spawndepth: AnyCodable?
public let subagentrole: AnyCodable?
public let subagentcontrolscope: AnyCodable?
public let sendpolicy: AnyCodable?
public let groupactivation: AnyCodable?
@ -1355,6 +1357,8 @@ public struct SessionsPatchParams: Codable, Sendable {
model: AnyCodable?,
spawnedby: AnyCodable?,
spawndepth: AnyCodable?,
subagentrole: AnyCodable?,
subagentcontrolscope: AnyCodable?,
sendpolicy: AnyCodable?,
groupactivation: AnyCodable?)
{
@ -1372,6 +1376,8 @@ public struct SessionsPatchParams: Codable, Sendable {
self.model = model
self.spawnedby = spawnedby
self.spawndepth = spawndepth
self.subagentrole = subagentrole
self.subagentcontrolscope = subagentcontrolscope
self.sendpolicy = sendpolicy
self.groupactivation = groupactivation
}
@ -1391,6 +1397,8 @@ public struct SessionsPatchParams: Codable, Sendable {
case model
case spawnedby = "spawnedBy"
case spawndepth = "spawnDepth"
case subagentrole = "subagentRole"
case subagentcontrolscope = "subagentControlScope"
case sendpolicy = "sendPolicy"
case groupactivation = "groupActivation"
}
@ -3046,7 +3054,7 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String
public let command: String?
public let commandargv: [String]?
public let systemrunplan: [String: AnyCodable]?
public let env: [String: AnyCodable]?
@ -3067,7 +3075,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
public init(
id: String?,
command: String,
command: String?,
commandargv: [String]?,
systemrunplan: [String: AnyCodable]?,
env: [String: AnyCodable]?,

View File

@ -946,7 +946,7 @@ Default slash command settings:
Gateway auth for this handler uses the same shared credential resolution contract as other Gateway clients:
- env-first local auth (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD` then `gateway.auth.*`)
- in local mode, `gateway.remote.*` can be used as fallback when `gateway.auth.*` is unset
- in local mode, `gateway.remote.*` can be used as fallback only when `gateway.auth.*` is unset; configured-but-unresolved local SecretRefs fail closed
- remote-mode support via `gateway.remote.*` when applicable
- URL overrides are override-safe: CLI overrides do not reuse implicit credentials, and env overrides use env credentials only

View File

@ -273,7 +273,7 @@ Security note:
- `--token` and `--password` can be visible in local process listings on some systems.
- Prefer `--token-file`/`--password-file` or environment variables (`OPENCLAW_GATEWAY_TOKEN`, `OPENCLAW_GATEWAY_PASSWORD`).
- Gateway auth resolution follows the shared contract used by other Gateway clients:
- local mode: env (`OPENCLAW_GATEWAY_*`) -> `gateway.auth.*` -> `gateway.remote.*` fallback when `gateway.auth.*` is unset
- local mode: env (`OPENCLAW_GATEWAY_*`) -> `gateway.auth.*` -> `gateway.remote.*` fallback only when `gateway.auth.*` is unset (configured-but-unresolved local SecretRefs fail closed)
- remote mode: `gateway.remote.*` with env/config fallback per remote precedence rules
- `--url` is override-safe and does not reuse implicit config/env credentials; pass explicit `--token`/`--password` (or file variants)
- ACP runtime backend child processes receive `OPENCLAW_SHELL=acp`, which can be used for context-specific shell/profile rules.

View File

@ -1018,7 +1018,7 @@ Subcommands:
Auth notes:
- `node` resolves gateway auth from env/config (no `--token`/`--password` flags): `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`, then `gateway.auth.*`, with remote-mode support via `gateway.remote.*`.
- `node` resolves gateway auth from env/config (no `--token`/`--password` flags): `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`, then `gateway.auth.*`. In local mode, node host intentionally ignores `gateway.remote.*`; in `gateway.mode=remote`, `gateway.remote.*` participates per remote precedence rules.
- Legacy `CLAWDBOT_GATEWAY_*` env vars are intentionally ignored for node-host auth resolution.
## Nodes

View File

@ -64,7 +64,8 @@ Options:
- `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD` are checked first.
- Then local config fallback: `gateway.auth.token` / `gateway.auth.password`.
- In local mode, `gateway.remote.token` / `gateway.remote.password` are also eligible as fallback when `gateway.auth.*` is unset.
- In local mode, node host intentionally does not inherit `gateway.remote.token` / `gateway.remote.password`.
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, node auth resolution fails closed (no remote fallback masking).
- In `gateway.mode=remote`, remote client fields (`gateway.remote.token` / `gateway.remote.password`) are also eligible per remote precedence rules.
- Legacy `CLAWDBOT_GATEWAY_*` env vars are ignored for node host auth resolution.

View File

@ -2470,7 +2470,8 @@ See [Plugins](/tools/plugin).
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
- `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`: client-side break-glass override that allows plaintext `ws://` to trusted private-network IPs; default remains loopback-only for plaintext.
- `gateway.remote.token` / `.password` are remote-client credential fields. They do not configure gateway auth by themselves.
- Local gateway call paths can use `gateway.remote.*` as fallback when `gateway.auth.*` is unset.
- Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
- `allowRealIpFallback`: when `true`, the gateway accepts `X-Real-IP` if `X-Forwarded-For` is missing. Default `false` for fail-closed behavior.
- `gateway.tools.deny`: extra tool names blocked for HTTP `POST /tools/invoke` (extends default deny list).

View File

@ -103,18 +103,19 @@ When the gateway is loopback-only, keep the URL at `ws://127.0.0.1:18789` and op
## Credential precedence
Gateway credential resolution follows one shared contract across call/probe/status paths, Discord exec-approval monitoring, and node-host connections:
Gateway credential resolution follows one shared contract across call/probe/status paths and Discord exec-approval monitoring. Node-host uses the same base contract with one local-mode exception (it intentionally ignores `gateway.remote.*`):
- Explicit credentials (`--token`, `--password`, or tool `gatewayToken`) always win on call paths that accept explicit auth.
- URL override safety:
- CLI URL overrides (`--url`) never reuse implicit config/env credentials.
- Env URL overrides (`OPENCLAW_GATEWAY_URL`) may use env credentials only (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`).
- Local mode defaults:
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token`
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password`
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token` (remote fallback applies only when local auth token input is unset)
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password` (remote fallback applies only when local auth password input is unset)
- Remote mode defaults:
- token: `gateway.remote.token` -> `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token`
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.remote.password` -> `gateway.auth.password`
- Node-host local-mode exception: `gateway.remote.token` / `gateway.remote.password` are ignored.
- Remote probe/status token checks are strict by default: they use `gateway.remote.token` only (no local token fallback) when targeting remote mode.
- Legacy `CLAWDBOT_GATEWAY_*` env vars are only used by compatibility call paths; probe/status/auth resolution uses `OPENCLAW_GATEWAY_*` only.
@ -140,7 +141,8 @@ Short version: **keep the Gateway loopback-only** unless youre sure you need
set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` on the client process as break-glass.
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords.
- `gateway.remote.token` / `.password` are client credential sources. They do **not** configure server auth by themselves.
- Local call paths can use `gateway.remote.*` as fallback when `gateway.auth.*` is unset.
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
- **Tailscale Serve** can authenticate Control UI/WebSocket traffic via identity
headers when `gateway.auth.allowTailscale: true`; HTTP API endpoints still

View File

@ -41,13 +41,13 @@ Examples of inactive surfaces:
- Web search provider-specific keys that are not selected by `tools.web.search.provider`.
In auto mode (provider unset), keys are consulted by precedence for provider auto-detection until one resolves.
After selection, non-selected provider keys are treated as inactive until selected.
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active (when `gateway.remote.enabled` is not `false`) if one of these is true:
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active if one of these is true:
- `gateway.mode=remote`
- `gateway.remote.url` is configured
- `gateway.tailscale.mode` is `serve` or `funnel`
In local mode without those remote surfaces:
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
- In local mode without those remote surfaces:
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
- `gateway.auth.token` SecretRef is inactive for startup auth resolution when `OPENCLAW_GATEWAY_TOKEN` (or `CLAWDBOT_GATEWAY_TOKEN`) is set, because env token input wins for that runtime.
## Gateway auth surface diagnostics

View File

@ -754,8 +754,10 @@ Doctor can generate one for you: `openclaw doctor --generate-gateway-token`.
Note: `gateway.remote.token` / `.password` are client credential sources. They
do **not** protect local WS access by themselves.
Local call paths can use `gateway.remote.*` as fallback when `gateway.auth.*`
Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*`
is unset.
If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via
SecretRef and unresolved, resolution fails closed (no remote fallback masking).
Optional: pin remote TLS with `gateway.remote.tlsFingerprint` when using `wss://`.
Plaintext `ws://` is loopback-only by default. For trusted private-network
paths, set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` on the client process as break-glass.

View File

@ -1452,7 +1452,8 @@ Non-loopback binds **require auth**. Configure `gateway.auth.mode` + `gateway.au
Notes:
- `gateway.remote.token` / `.password` do **not** enable local gateway auth by themselves.
- Local call paths can use `gateway.remote.*` as fallback when `gateway.auth.*` is unset.
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
- The Control UI authenticates via `connect.params.auth.token` (stored in app/UI settings). Avoid putting tokens in URLs.
### Why do I need a token on localhost now

View File

@ -92,7 +92,10 @@ Notes:
- `openclaw node run` supports token or password auth.
- Env vars are preferred: `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`.
- Config fallback is `gateway.auth.token` / `gateway.auth.password`; in remote mode, `gateway.remote.token` / `gateway.remote.password` are also eligible.
- Config fallback is `gateway.auth.token` / `gateway.auth.password`.
- In local mode, node host intentionally ignores `gateway.remote.token` / `gateway.remote.password`.
- In remote mode, `gateway.remote.token` / `gateway.remote.password` are eligible per remote precedence rules.
- If active local `gateway.auth.*` SecretRefs are configured but unresolved, node-host auth fails closed.
- Legacy `CLAWDBOT_GATEWAY_*` env vars are intentionally ignored by node-host auth resolution.
### Start a node host (service)

View File

@ -198,16 +198,6 @@ function appendCronDeliveryInstruction(params: {
return `${params.commandBody}\n\nReturn your summary as plain text; it will be delivered automatically. If the task explicitly calls for messaging a specific external recipient, note who/where it should go instead of sending it yourself.`.trim();
}
function resolveCronEmbeddedAgentLane(lane?: string) {
const trimmed = lane?.trim();
// Cron jobs already execute inside the cron command lane. Reusing that same
// lane for the nested embedded-agent run deadlocks: the outer cron task holds
// the lane while the inner run waits to reacquire it.
if (!trimmed || trimmed === "cron") {
return CommandLane.Nested;
}
return trimmed;
}
export async function runCronIsolatedAgentTurn(params: {
cfg: OpenClawConfig;
deps: CliDeps;

View File

@ -655,6 +655,7 @@ describe("callGateway password resolution", () => {
envSnapshot = captureEnv([
"OPENCLAW_GATEWAY_PASSWORD",
"OPENCLAW_GATEWAY_TOKEN",
"LOCAL_REMOTE_FALLBACK_TOKEN",
"LOCAL_REF_PASSWORD",
"REMOTE_REF_TOKEN",
"REMOTE_REF_PASSWORD",
@ -662,6 +663,7 @@ describe("callGateway password resolution", () => {
resetGatewayCallMocks();
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
delete process.env.OPENCLAW_GATEWAY_TOKEN;
delete process.env.LOCAL_REMOTE_FALLBACK_TOKEN;
delete process.env.LOCAL_REF_PASSWORD;
delete process.env.REMOTE_REF_TOKEN;
delete process.env.REMOTE_REF_PASSWORD;
@ -813,6 +815,30 @@ describe("callGateway password resolution", () => {
expect(lastClientOptions?.password).toBe("resolved-local-fallback-password"); // pragma: allowlist secret
});
it("fails closed when unresolved local token SecretRef would otherwise fall back to remote token", async () => {
process.env.LOCAL_REMOTE_FALLBACK_TOKEN = "resolved-local-remote-fallback-token";
loadConfig.mockReturnValue({
gateway: {
mode: "local",
bind: "loopback",
auth: {
mode: "token",
token: { source: "env", provider: "default", id: "MISSING_LOCAL_REF_TOKEN" },
},
remote: {
token: { source: "env", provider: "default", id: "LOCAL_REMOTE_FALLBACK_TOKEN" },
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
} as unknown as OpenClawConfig);
await expect(callGateway({ method: "health" })).rejects.toThrow("gateway.auth.token");
});
it.each(["none", "trusted-proxy"] as const)(
"ignores unresolved local password ref when auth mode is %s",
async (mode) => {

View File

@ -416,4 +416,74 @@ describe("resolveGatewayConnectionAuth", () => {
}),
).toThrow("gateway.auth.password");
});
it("fails closed when local token SecretRef is unresolved and remote token fallback exists", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
mode: "token",
token: { source: "env", provider: "default", id: "MISSING_LOCAL_TOKEN" },
},
remote: {
token: "remote-token",
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
await expect(
resolveGatewayConnectionAuth({
config,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).rejects.toThrow("gateway.auth.token");
expect(() =>
resolveGatewayConnectionAuthFromConfig({
cfg: config,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).toThrow("gateway.auth.token");
});
it("fails closed when local password SecretRef is unresolved and remote password fallback exists", async () => {
const config = cfg({
gateway: {
mode: "local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "MISSING_LOCAL_PASSWORD" },
},
remote: {
password: "remote-password", // pragma: allowlist secret
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
});
await expect(
resolveGatewayConnectionAuth({
config,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).rejects.toThrow("gateway.auth.password");
expect(() =>
resolveGatewayConnectionAuthFromConfig({
cfg: config,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).toThrow("gateway.auth.password");
});
});

View File

@ -33,7 +33,6 @@ export type GatewayCredentialPlan = {
remoteMode: boolean;
remoteUrlConfigured: boolean;
tailscaleRemoteExposure: boolean;
remoteEnabled: boolean;
remoteConfiguredSurface: boolean;
remoteTokenFallbackActive: boolean;
remoteTokenActive: boolean;
@ -187,7 +186,6 @@ export function createGatewayCredentialPlan(params: {
const remoteUrlConfigured = Boolean(trimToUndefined(remote?.url));
const tailscaleRemoteExposure =
gateway?.tailscale?.mode === "serve" || gateway?.tailscale?.mode === "funnel";
const remoteEnabled = remote?.enabled !== false;
const remoteConfiguredSurface = remoteMode || remoteUrlConfigured || tailscaleRemoteExposure;
const remoteTokenFallbackActive = localTokenCanWin && !envToken && !localToken.configured;
const remotePasswordFallbackActive = !envPassword && !localPassword.configured && passwordCanWin;
@ -209,12 +207,10 @@ export function createGatewayCredentialPlan(params: {
remoteMode,
remoteUrlConfigured,
tailscaleRemoteExposure,
remoteEnabled,
remoteConfiguredSurface,
remoteTokenFallbackActive,
remoteTokenActive: remoteEnabled && (remoteConfiguredSurface || remoteTokenFallbackActive),
remoteTokenActive: remoteConfiguredSurface || remoteTokenFallbackActive,
remotePasswordFallbackActive,
remotePasswordActive:
remoteEnabled && (remoteConfiguredSurface || remotePasswordFallbackActive),
remotePasswordActive: remoteConfiguredSurface || remotePasswordFallbackActive,
};
}

View File

@ -158,6 +158,58 @@ describe("resolveGatewayCredentialsFromConfig", () => {
});
});
it("fails closed when local token SecretRef is unresolved and remote token fallback exists", () => {
expect(() =>
resolveGatewayCredentialsFromConfig({
cfg: {
gateway: {
mode: "local",
auth: {
mode: "token",
token: { source: "env", provider: "default", id: "MISSING_LOCAL_TOKEN" },
},
remote: {
token: "remote-token",
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
} as unknown as OpenClawConfig,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).toThrow("gateway.auth.token");
});
it("fails closed when local password SecretRef is unresolved and remote password fallback exists", () => {
expect(() =>
resolveGatewayCredentialsFromConfig({
cfg: {
gateway: {
mode: "local",
auth: {
mode: "password",
password: { source: "env", provider: "default", id: "MISSING_LOCAL_PASSWORD" },
},
remote: {
password: "remote-password", // pragma: allowlist secret
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
} as unknown as OpenClawConfig,
env: {} as NodeJS.ProcessEnv,
includeLegacyEnv: false,
}),
).toThrow("gateway.auth.password");
});
it("throws when local password auth relies on an unresolved SecretRef", () => {
expect(() =>
resolveGatewayCredentialsFromConfig({

View File

@ -111,7 +111,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
gateway: {
mode: "local",
remote: {
enabled: true,
token: envRef("GW_REMOTE_TOKEN"),
},
},
@ -131,7 +130,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
mode: "password",
},
remote: {
enabled: true,
token: envRef("GW_REMOTE_TOKEN"),
},
},
@ -153,7 +151,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
token: envRef("GW_AUTH_TOKEN"),
},
remote: {
enabled: true,
token: envRef("GW_REMOTE_TOKEN"),
},
},
@ -170,7 +167,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
const states = evaluate({
gateway: {
remote: {
enabled: true,
url: "wss://gateway.example.com",
password: envRef("GW_REMOTE_PASSWORD"),
},
@ -190,7 +186,6 @@ describe("evaluateGatewayAuthSurfaceStates", () => {
mode: "token",
},
remote: {
enabled: true,
password: envRef("GW_REMOTE_PASSWORD"),
},
},

View File

@ -166,9 +166,6 @@ export function evaluateGatewayAuthSurfaceStates(params: {
if (!remote) {
return "gateway.remote is not configured.";
}
if (!plan.remoteEnabled) {
return "gateway.remote.enabled is false.";
}
if (plan.remoteConfiguredSurface) {
return `remote surface is active: ${remoteSurfaceReason}.`;
}
@ -191,9 +188,6 @@ export function evaluateGatewayAuthSurfaceStates(params: {
if (!remote) {
return "gateway.remote is not configured.";
}
if (!plan.remoteEnabled) {
return "gateway.remote.enabled is false.";
}
if (plan.remoteConfiguredSurface) {
return `remote surface is active: ${remoteSurfaceReason}.`;
}