Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails (#35094)

This commit is contained in:
Josh Avant
2026-03-05 12:53:56 -06:00
committed by GitHub
parent bc66a8fa81
commit 72cf9253fc
112 changed files with 5750 additions and 465 deletions

View File

@@ -0,0 +1,147 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { makeTempWorkspace } from "../../test-helpers/workspace.js";
import { captureEnv } from "../../test-utils/env.js";
const runtimeLogs: string[] = [];
const runtimeErrors: string[] = [];
const serviceMock = vi.hoisted(() => ({
label: "Gateway",
loadedText: "loaded",
notLoadedText: "not loaded",
install: vi.fn(async (_opts?: { environment?: Record<string, string | undefined> }) => {}),
uninstall: vi.fn(async () => {}),
stop: vi.fn(async () => {}),
restart: vi.fn(async () => {}),
isLoaded: vi.fn(async () => false),
readCommand: vi.fn(async () => null),
readRuntime: vi.fn(async () => ({ status: "stopped" as const })),
}));
vi.mock("../../daemon/service.js", () => ({
resolveGatewayService: () => serviceMock,
}));
vi.mock("../../runtime.js", () => ({
defaultRuntime: {
log: (message: string) => runtimeLogs.push(message),
error: (message: string) => runtimeErrors.push(message),
exit: (code: number) => {
throw new Error(`__exit__:${code}`);
},
},
}));
const { runDaemonInstall } = await import("./install.js");
const { clearConfigCache } = await import("../../config/config.js");
async function readJson(filePath: string): Promise<Record<string, unknown>> {
return JSON.parse(await fs.readFile(filePath, "utf8")) as Record<string, unknown>;
}
describe("runDaemonInstall integration", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
let tempHome: string;
let configPath: string;
beforeAll(async () => {
envSnapshot = captureEnv([
"HOME",
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
"CLAWDBOT_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
"CLAWDBOT_GATEWAY_PASSWORD",
]);
tempHome = await makeTempWorkspace("openclaw-daemon-install-int-");
configPath = path.join(tempHome, "openclaw.json");
process.env.HOME = tempHome;
process.env.OPENCLAW_STATE_DIR = tempHome;
process.env.OPENCLAW_CONFIG_PATH = configPath;
});
afterAll(async () => {
envSnapshot.restore();
await fs.rm(tempHome, { recursive: true, force: true });
});
beforeEach(async () => {
runtimeLogs.length = 0;
runtimeErrors.length = 0;
vi.clearAllMocks();
delete process.env.OPENCLAW_GATEWAY_TOKEN;
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
delete process.env.CLAWDBOT_GATEWAY_PASSWORD;
serviceMock.isLoaded.mockResolvedValue(false);
await fs.writeFile(configPath, JSON.stringify({}, null, 2));
clearConfigCache();
});
it("fails closed when token SecretRef is required but unresolved", async () => {
await fs.writeFile(
configPath,
JSON.stringify(
{
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
auth: {
mode: "token",
token: {
source: "env",
provider: "default",
id: "MISSING_GATEWAY_TOKEN",
},
},
},
},
null,
2,
),
);
clearConfigCache();
await expect(runDaemonInstall({ json: true })).rejects.toThrow("__exit__:1");
expect(serviceMock.install).not.toHaveBeenCalled();
const joined = runtimeLogs.join("\n");
expect(joined).toContain("SecretRef is configured but unresolved");
expect(joined).toContain("MISSING_GATEWAY_TOKEN");
});
it("auto-mints token when no source exists and persists the same token used for install env", async () => {
await fs.writeFile(
configPath,
JSON.stringify(
{
gateway: {
auth: {
mode: "token",
},
},
},
null,
2,
),
);
clearConfigCache();
await runDaemonInstall({ json: true });
expect(serviceMock.install).toHaveBeenCalledTimes(1);
const updated = await readJson(configPath);
const gateway = (updated.gateway ?? {}) as { auth?: { token?: string } };
const persistedToken = gateway.auth?.token;
expect(typeof persistedToken).toBe("string");
expect((persistedToken ?? "").length).toBeGreaterThan(0);
const installEnv = serviceMock.install.mock.calls[0]?.[0]?.environment;
expect(installEnv?.OPENCLAW_GATEWAY_TOKEN).toBe(persistedToken);
});
});

View File

@@ -0,0 +1,249 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DaemonActionResponse } from "./response.js";
const loadConfigMock = vi.hoisted(() => vi.fn());
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
const resolveGatewayPortMock = vi.hoisted(() => vi.fn(() => 18789));
const writeConfigFileMock = vi.hoisted(() => vi.fn());
const resolveIsNixModeMock = vi.hoisted(() => vi.fn(() => false));
const resolveSecretInputRefMock = vi.hoisted(() =>
vi.fn((): { ref: unknown } => ({ ref: undefined })),
);
const resolveGatewayAuthMock = vi.hoisted(() =>
vi.fn(() => ({
mode: "token",
token: undefined,
password: undefined,
allowTailscale: false,
})),
);
const resolveSecretRefValuesMock = vi.hoisted(() => vi.fn());
const randomTokenMock = vi.hoisted(() => vi.fn(() => "generated-token"));
const buildGatewayInstallPlanMock = vi.hoisted(() =>
vi.fn(async () => ({
programArguments: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment: {},
})),
);
const parsePortMock = vi.hoisted(() => vi.fn(() => null));
const isGatewayDaemonRuntimeMock = vi.hoisted(() => vi.fn(() => true));
const installDaemonServiceAndEmitMock = vi.hoisted(() => vi.fn(async () => {}));
const actionState = vi.hoisted(() => ({
warnings: [] as string[],
emitted: [] as DaemonActionResponse[],
failed: [] as Array<{ message: string; hints?: string[] }>,
}));
const service = vi.hoisted(() => ({
label: "Gateway",
loadedText: "loaded",
notLoadedText: "not loaded",
isLoaded: vi.fn(async () => false),
install: vi.fn(async () => {}),
uninstall: vi.fn(async () => {}),
restart: vi.fn(async () => {}),
stop: vi.fn(async () => {}),
readCommand: vi.fn(async () => null),
readRuntime: vi.fn(async () => ({ status: "stopped" as const })),
}));
vi.mock("../../config/config.js", () => ({
loadConfig: loadConfigMock,
readConfigFileSnapshot: readConfigFileSnapshotMock,
resolveGatewayPort: resolveGatewayPortMock,
writeConfigFile: writeConfigFileMock,
}));
vi.mock("../../config/paths.js", () => ({
resolveIsNixMode: resolveIsNixModeMock,
}));
vi.mock("../../config/types.secrets.js", () => ({
resolveSecretInputRef: resolveSecretInputRefMock,
}));
vi.mock("../../gateway/auth.js", () => ({
resolveGatewayAuth: resolveGatewayAuthMock,
}));
vi.mock("../../secrets/resolve.js", () => ({
resolveSecretRefValues: resolveSecretRefValuesMock,
}));
vi.mock("../../commands/onboard-helpers.js", () => ({
randomToken: randomTokenMock,
}));
vi.mock("../../commands/daemon-install-helpers.js", () => ({
buildGatewayInstallPlan: buildGatewayInstallPlanMock,
}));
vi.mock("./shared.js", () => ({
parsePort: parsePortMock,
}));
vi.mock("../../commands/daemon-runtime.js", () => ({
DEFAULT_GATEWAY_DAEMON_RUNTIME: "node",
isGatewayDaemonRuntime: isGatewayDaemonRuntimeMock,
}));
vi.mock("../../daemon/service.js", () => ({
resolveGatewayService: () => service,
}));
vi.mock("./response.js", () => ({
buildDaemonServiceSnapshot: vi.fn(),
createDaemonActionContext: vi.fn(() => ({
stdout: process.stdout,
warnings: actionState.warnings,
emit: (payload: DaemonActionResponse) => {
actionState.emitted.push(payload);
},
fail: (message: string, hints?: string[]) => {
actionState.failed.push({ message, hints });
},
})),
installDaemonServiceAndEmit: installDaemonServiceAndEmitMock,
}));
const runtimeLogs: string[] = [];
vi.mock("../../runtime.js", () => ({
defaultRuntime: {
log: (message: string) => runtimeLogs.push(message),
error: vi.fn(),
exit: vi.fn(),
},
}));
const { runDaemonInstall } = await import("./install.js");
describe("runDaemonInstall", () => {
beforeEach(() => {
loadConfigMock.mockReset();
readConfigFileSnapshotMock.mockReset();
resolveGatewayPortMock.mockClear();
writeConfigFileMock.mockReset();
resolveIsNixModeMock.mockReset();
resolveSecretInputRefMock.mockReset();
resolveGatewayAuthMock.mockReset();
resolveSecretRefValuesMock.mockReset();
randomTokenMock.mockReset();
buildGatewayInstallPlanMock.mockReset();
parsePortMock.mockReset();
isGatewayDaemonRuntimeMock.mockReset();
installDaemonServiceAndEmitMock.mockReset();
service.isLoaded.mockReset();
runtimeLogs.length = 0;
actionState.warnings.length = 0;
actionState.emitted.length = 0;
actionState.failed.length = 0;
loadConfigMock.mockReturnValue({ gateway: { auth: { mode: "token" } } });
readConfigFileSnapshotMock.mockResolvedValue({ exists: false, valid: true, config: {} });
resolveGatewayPortMock.mockReturnValue(18789);
resolveIsNixModeMock.mockReturnValue(false);
resolveSecretInputRefMock.mockReturnValue({ ref: undefined });
resolveGatewayAuthMock.mockReturnValue({
mode: "token",
token: undefined,
password: undefined,
allowTailscale: false,
});
resolveSecretRefValuesMock.mockResolvedValue(new Map());
randomTokenMock.mockReturnValue("generated-token");
buildGatewayInstallPlanMock.mockResolvedValue({
programArguments: ["openclaw", "gateway", "run"],
workingDirectory: "/tmp",
environment: {},
});
parsePortMock.mockReturnValue(null);
isGatewayDaemonRuntimeMock.mockReturnValue(true);
installDaemonServiceAndEmitMock.mockResolvedValue(undefined);
service.isLoaded.mockResolvedValue(false);
});
it("fails install when token auth requires an unresolved token SecretRef", async () => {
resolveSecretInputRefMock.mockReturnValue({
ref: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
});
resolveSecretRefValuesMock.mockRejectedValue(new Error("secret unavailable"));
await runDaemonInstall({ json: true });
expect(actionState.failed[0]?.message).toContain("gateway.auth.token SecretRef is configured");
expect(actionState.failed[0]?.message).toContain("unresolved");
expect(buildGatewayInstallPlanMock).not.toHaveBeenCalled();
expect(installDaemonServiceAndEmitMock).not.toHaveBeenCalled();
});
it("validates token SecretRef but does not serialize resolved token into service env", async () => {
resolveSecretInputRefMock.mockReturnValue({
ref: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
});
resolveSecretRefValuesMock.mockResolvedValue(
new Map([["env:default:OPENCLAW_GATEWAY_TOKEN", "resolved-from-secretref"]]),
);
await runDaemonInstall({ json: true });
expect(actionState.failed).toEqual([]);
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({
token: undefined,
}),
);
expect(writeConfigFileMock).not.toHaveBeenCalled();
expect(
actionState.warnings.some((warning) =>
warning.includes("gateway.auth.token is SecretRef-managed"),
),
).toBe(true);
});
it("does not treat env-template gateway.auth.token as plaintext during install", async () => {
loadConfigMock.mockReturnValue({
gateway: { auth: { mode: "token", token: "${OPENCLAW_GATEWAY_TOKEN}" } },
});
resolveSecretInputRefMock.mockReturnValue({
ref: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
});
resolveSecretRefValuesMock.mockResolvedValue(
new Map([["env:default:OPENCLAW_GATEWAY_TOKEN", "resolved-from-secretref"]]),
);
await runDaemonInstall({ json: true });
expect(actionState.failed).toEqual([]);
expect(resolveSecretRefValuesMock).toHaveBeenCalledTimes(1);
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({
token: undefined,
}),
);
});
it("auto-mints and persists token when no source exists", async () => {
randomTokenMock.mockReturnValue("minted-token");
readConfigFileSnapshotMock.mockResolvedValue({
exists: true,
valid: true,
config: { gateway: { auth: { mode: "token" } } },
});
await runDaemonInstall({ json: true });
expect(actionState.failed).toEqual([]);
expect(writeConfigFileMock).toHaveBeenCalledTimes(1);
const writtenConfig = writeConfigFileMock.mock.calls[0]?.[0] as {
gateway?: { auth?: { token?: string } };
};
expect(writtenConfig.gateway?.auth?.token).toBe("minted-token");
expect(buildGatewayInstallPlanMock).toHaveBeenCalledWith(
expect.objectContaining({ token: "minted-token", port: 18789 }),
);
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
expect(actionState.warnings.some((warning) => warning.includes("Auto-generated"))).toBe(true);
});
});

View File

@@ -3,16 +3,10 @@ import {
DEFAULT_GATEWAY_DAEMON_RUNTIME,
isGatewayDaemonRuntime,
} from "../../commands/daemon-runtime.js";
import { randomToken } from "../../commands/onboard-helpers.js";
import {
loadConfig,
readConfigFileSnapshot,
resolveGatewayPort,
writeConfigFile,
} from "../../config/config.js";
import { resolveGatewayInstallToken } from "../../commands/gateway-install-token.js";
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
import { resolveIsNixMode } from "../../config/paths.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { resolveGatewayAuth } from "../../gateway/auth.js";
import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import {
@@ -75,78 +69,29 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
}
}
// Resolve effective auth mode to determine if token auto-generation is needed.
// Password-mode and Tailscale-only installs do not need a token.
const resolvedAuth = resolveGatewayAuth({
authConfig: cfg.gateway?.auth,
tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
const tokenResolution = await resolveGatewayInstallToken({
config: cfg,
env: process.env,
explicitToken: opts.token,
autoGenerateWhenMissing: true,
persistGeneratedToken: true,
});
const needsToken =
resolvedAuth.mode === "token" && !resolvedAuth.token && !resolvedAuth.allowTailscale;
let token: string | undefined =
opts.token ||
cfg.gateway?.auth?.token ||
process.env.OPENCLAW_GATEWAY_TOKEN ||
process.env.CLAWDBOT_GATEWAY_TOKEN;
if (!token && needsToken) {
token = randomToken();
const warnMsg = "No gateway token found. Auto-generated one and saving to config.";
if (tokenResolution.unavailableReason) {
fail(`Gateway install blocked: ${tokenResolution.unavailableReason}`);
return;
}
for (const warning of tokenResolution.warnings) {
if (json) {
warnings.push(warnMsg);
warnings.push(warning);
} else {
defaultRuntime.log(warnMsg);
}
// Persist to config file so the gateway reads it at runtime
// (launchd does not inherit shell env vars, and CLI tools also
// read gateway.auth.token from config for gateway calls).
try {
const snapshot = await readConfigFileSnapshot();
if (snapshot.exists && !snapshot.valid) {
// Config file exists but is corrupt/unparseable — don't risk overwriting.
// Token is still embedded in the plist EnvironmentVariables.
const msg = "Warning: config file exists but is invalid; skipping token persistence.";
if (json) {
warnings.push(msg);
} else {
defaultRuntime.log(msg);
}
} else {
const baseConfig = snapshot.exists ? snapshot.config : {};
if (!baseConfig.gateway?.auth?.token) {
await writeConfigFile({
...baseConfig,
gateway: {
...baseConfig.gateway,
auth: {
...baseConfig.gateway?.auth,
mode: baseConfig.gateway?.auth?.mode ?? "token",
token,
},
},
});
} else {
// Another process wrote a token between loadConfig() and now.
token = baseConfig.gateway.auth.token;
}
}
} catch (err) {
// Non-fatal: token is still embedded in the plist EnvironmentVariables.
const msg = `Warning: could not persist token to config: ${String(err)}`;
if (json) {
warnings.push(msg);
} else {
defaultRuntime.log(msg);
}
defaultRuntime.log(warning);
}
}
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port,
token,
token: tokenResolution.token,
runtime: runtimeRaw,
warn: (message) => {
if (json) {

View File

@@ -5,7 +5,10 @@ import { checkTokenDrift } from "../../daemon/service-audit.js";
import type { GatewayService } from "../../daemon/service.js";
import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
import { resolveGatewayCredentialsFromConfig } from "../../gateway/credentials.js";
import {
isGatewaySecretRefUnavailableError,
resolveGatewayCredentialsFromConfig,
} from "../../gateway/credentials.js";
import { isWSL } from "../../infra/wsl.js";
import { defaultRuntime } from "../../runtime.js";
import {
@@ -299,8 +302,15 @@ export async function runServiceRestart(params: {
}
}
}
} catch {
// Non-fatal: token drift check is best-effort
} catch (err) {
if (isGatewaySecretRefUnavailableError(err, "gateway.auth.token")) {
const warning =
"Unable to verify gateway token drift: gateway.auth.token SecretRef is configured but unavailable in this command path.";
warnings.push(warning);
if (!json) {
defaultRuntime.log(`\n⚠ ${warning}\n`);
}
}
}
}

View File

@@ -123,12 +123,14 @@ describe("gatherDaemonStatus", () => {
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_GATEWAY_PASSWORD",
"DAEMON_GATEWAY_TOKEN",
"DAEMON_GATEWAY_PASSWORD",
]);
process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-cli";
process.env.OPENCLAW_CONFIG_PATH = "/tmp/openclaw-cli/openclaw.json";
delete process.env.OPENCLAW_GATEWAY_TOKEN;
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
delete process.env.DAEMON_GATEWAY_TOKEN;
delete process.env.DAEMON_GATEWAY_PASSWORD;
callGatewayStatusProbe.mockClear();
loadGatewayTlsRuntime.mockClear();
@@ -218,6 +220,37 @@ describe("gatherDaemonStatus", () => {
);
});
it("resolves daemon gateway auth token SecretRef values before probing", async () => {
daemonLoadedConfig = {
gateway: {
bind: "lan",
tls: { enabled: true },
auth: {
mode: "token",
token: "${DAEMON_GATEWAY_TOKEN}",
},
},
secrets: {
providers: {
default: { source: "env" },
},
},
};
process.env.DAEMON_GATEWAY_TOKEN = "daemon-secretref-token";
await gatherDaemonStatus({
rpc: {},
probe: true,
deep: false,
});
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
expect.objectContaining({
token: "daemon-secretref-token",
}),
);
});
it("does not resolve daemon password SecretRef when token auth is configured", async () => {
daemonLoadedConfig = {
gateway: {

View File

@@ -9,7 +9,11 @@ import type {
GatewayBindMode,
GatewayControlUiConfig,
} from "../../config/types.js";
import { normalizeSecretInputString, resolveSecretInputRef } from "../../config/types.secrets.js";
import {
hasConfiguredSecretInput,
normalizeSecretInputString,
resolveSecretInputRef,
} from "../../config/types.secrets.js";
import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js";
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
import { findExtraGatewayServices } from "../../daemon/inspect.js";
@@ -114,6 +118,61 @@ function readGatewayTokenEnv(env: Record<string, string | undefined>): string |
return trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN) ?? trimToUndefined(env.CLAWDBOT_GATEWAY_TOKEN);
}
function readGatewayPasswordEnv(env: Record<string, string | undefined>): string | undefined {
return (
trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD) ?? trimToUndefined(env.CLAWDBOT_GATEWAY_PASSWORD)
);
}
async function resolveDaemonProbeToken(params: {
daemonCfg: OpenClawConfig;
mergedDaemonEnv: Record<string, string | undefined>;
explicitToken?: string;
explicitPassword?: string;
}): Promise<string | undefined> {
const explicitToken = trimToUndefined(params.explicitToken);
if (explicitToken) {
return explicitToken;
}
const envToken = readGatewayTokenEnv(params.mergedDaemonEnv);
if (envToken) {
return envToken;
}
const defaults = params.daemonCfg.secrets?.defaults;
const configured = params.daemonCfg.gateway?.auth?.token;
const { ref } = resolveSecretInputRef({
value: configured,
defaults,
});
if (!ref) {
return normalizeSecretInputString(configured);
}
const authMode = params.daemonCfg.gateway?.auth?.mode;
if (authMode === "password" || authMode === "none" || authMode === "trusted-proxy") {
return undefined;
}
if (authMode !== "token") {
const passwordCandidate =
trimToUndefined(params.explicitPassword) ||
readGatewayPasswordEnv(params.mergedDaemonEnv) ||
(hasConfiguredSecretInput(params.daemonCfg.gateway?.auth?.password, defaults)
? "__configured__"
: undefined);
if (passwordCandidate) {
return undefined;
}
}
const resolved = await resolveSecretRefValues([ref], {
config: params.daemonCfg,
env: params.mergedDaemonEnv as NodeJS.ProcessEnv,
});
const token = trimToUndefined(resolved.get(secretRefKey(ref)));
if (!token) {
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
}
return token;
}
async function resolveDaemonProbePassword(params: {
daemonCfg: OpenClawConfig;
mergedDaemonEnv: Record<string, string | undefined>;
@@ -124,7 +183,7 @@ async function resolveDaemonProbePassword(params: {
if (explicitPassword) {
return explicitPassword;
}
const envPassword = trimToUndefined(params.mergedDaemonEnv.OPENCLAW_GATEWAY_PASSWORD);
const envPassword = readGatewayPasswordEnv(params.mergedDaemonEnv);
if (envPassword) {
return envPassword;
}
@@ -145,7 +204,9 @@ async function resolveDaemonProbePassword(params: {
const tokenCandidate =
trimToUndefined(params.explicitToken) ||
readGatewayTokenEnv(params.mergedDaemonEnv) ||
trimToUndefined(params.daemonCfg.gateway?.auth?.token);
(hasConfiguredSecretInput(params.daemonCfg.gateway?.auth?.token, defaults)
? "__configured__"
: undefined);
if (tokenCandidate) {
return undefined;
}
@@ -290,14 +351,19 @@ export async function gatherDaemonStatus(
explicitPassword: opts.rpc.password,
})
: undefined;
const daemonProbeToken = opts.probe
? await resolveDaemonProbeToken({
daemonCfg,
mergedDaemonEnv,
explicitToken: opts.rpc.token,
explicitPassword: opts.rpc.password,
})
: undefined;
const rpc = opts.probe
? await probeGatewayStatus({
url: probeUrl,
token:
opts.rpc.token ||
mergedDaemonEnv.OPENCLAW_GATEWAY_TOKEN ||
daemonCfg.gateway?.auth?.token,
token: daemonProbeToken,
password: daemonProbePassword,
tlsFingerprint:
shouldUseLocalTlsRuntime && tlsRuntime?.enabled