Compare commits
4 Commits
main
...
codex/pr-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44cb4dd5d6 | ||
|
|
d6d7a2bb4f | ||
|
|
b5192d6e6b | ||
|
|
031dcdb8e6 |
@ -83,6 +83,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Control UI/auth token separation: keep the shared gateway token in browser auth validation while reserving cached device tokens for signed device payloads, preventing false `device token mismatch` disconnects after restart/rotation. Landed from contributor PR #37382 by @FradSer. Thanks @FradSer.
|
- Control UI/auth token separation: keep the shared gateway token in browser auth validation while reserving cached device tokens for signed device payloads, preventing false `device token mismatch` disconnects after restart/rotation. Landed from contributor PR #37382 by @FradSer. Thanks @FradSer.
|
||||||
- Gateway/browser auth reconnect hardening: stop counting missing token/password submissions as auth rate-limit failures, and stop auto-reconnecting Control UI clients on non-recoverable auth errors so misconfigured browser tabs no longer lock out healthy sessions. Landed from contributor PR #38725 by @ademczuk. Thanks @ademczuk.
|
- Gateway/browser auth reconnect hardening: stop counting missing token/password submissions as auth rate-limit failures, and stop auto-reconnecting Control UI clients on non-recoverable auth errors so misconfigured browser tabs no longer lock out healthy sessions. Landed from contributor PR #38725 by @ademczuk. Thanks @ademczuk.
|
||||||
- Gateway/service token drift repair: stop persisting shared auth tokens into installed gateway service units, flag stale embedded service tokens for reinstall, and treat tokenless service env as canonical so token rotation/reboot flows stay aligned with config/env resolution. Landed from contributor PR #28428 by @l0cka. Thanks @l0cka.
|
- Gateway/service token drift repair: stop persisting shared auth tokens into installed gateway service units, flag stale embedded service tokens for reinstall, and treat tokenless service env as canonical so token rotation/reboot flows stay aligned with config/env resolution. Landed from contributor PR #28428 by @l0cka. Thanks @l0cka.
|
||||||
|
- Gateway/auth follow-up hardening: preserve systemd `EnvironmentFile=` precedence/source provenance in daemon audits and doctor repairs, block shared-password override flows from piggybacking cached device tokens, and fail closed when config-first gateway SecretRefs cannot resolve. Follow-up to #39241.
|
||||||
- Agents/context pruning: guard assistant thinking/text char estimation against malformed blocks (missing `thinking`/`text` strings or null entries) so pruning no longer crashes with malformed provider content. (openclaw#35146) thanks @Sid-Qin.
|
- Agents/context pruning: guard assistant thinking/text char estimation against malformed blocks (missing `thinking`/`text` strings or null entries) so pruning no longer crashes with malformed provider content. (openclaw#35146) thanks @Sid-Qin.
|
||||||
- Agents/transcript policy: set `preserveSignatures` to Anthropic-only handling in `resolveTranscriptPolicy` so Anthropic thinking signatures are preserved while non-Anthropic providers remain unchanged. (#32813) thanks @Sid-Qin.
|
- Agents/transcript policy: set `preserveSignatures` to Anthropic-only handling in `resolveTranscriptPolicy` so Anthropic thinking signatures are preserved while non-Anthropic providers remain unchanged. (#32813) thanks @Sid-Qin.
|
||||||
- Agents/schema cleaning: detect Venice + Grok model IDs as xAI-proxied targets so unsupported JSON Schema keywords are stripped before requests, preventing Venice/Grok `Invalid arguments` failures. (openclaw#35355) thanks @Sid-Qin.
|
- Agents/schema cleaning: detect Venice + Grok model IDs as xAI-proxied targets so unsupported JSON Schema keywords are stripped before requests, preventing Venice/Grok `Invalid arguments` failures. (openclaw#35355) thanks @Sid-Qin.
|
||||||
|
|||||||
@ -39,6 +39,15 @@ vi.mock("../daemon/runtime-paths.js", () => ({
|
|||||||
vi.mock("../daemon/service-audit.js", () => ({
|
vi.mock("../daemon/service-audit.js", () => ({
|
||||||
auditGatewayServiceConfig: mocks.auditGatewayServiceConfig,
|
auditGatewayServiceConfig: mocks.auditGatewayServiceConfig,
|
||||||
needsNodeRuntimeMigration: vi.fn(() => false),
|
needsNodeRuntimeMigration: vi.fn(() => false),
|
||||||
|
readEmbeddedGatewayToken: (
|
||||||
|
command: {
|
||||||
|
environment?: Record<string, string>;
|
||||||
|
environmentValueSources?: Record<string, "inline" | "file">;
|
||||||
|
} | null,
|
||||||
|
) =>
|
||||||
|
command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN === "file"
|
||||||
|
? undefined
|
||||||
|
: command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined,
|
||||||
SERVICE_AUDIT_CODES: {
|
SERVICE_AUDIT_CODES: {
|
||||||
gatewayEntrypointMismatch: "gateway-entrypoint-mismatch",
|
gatewayEntrypointMismatch: "gateway-entrypoint-mismatch",
|
||||||
},
|
},
|
||||||
@ -299,6 +308,49 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not persist EnvironmentFile-backed service tokens into config", async () => {
|
||||||
|
await withEnvAsync(
|
||||||
|
{
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: undefined,
|
||||||
|
CLAWDBOT_GATEWAY_TOKEN: undefined,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
mocks.readCommand.mockResolvedValue({
|
||||||
|
programArguments: gatewayProgramArguments,
|
||||||
|
environment: {
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: "env-file-token",
|
||||||
|
},
|
||||||
|
environmentValueSources: {
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mocks.auditGatewayServiceConfig.mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
issues: [],
|
||||||
|
});
|
||||||
|
mocks.buildGatewayInstallPlan.mockResolvedValue({
|
||||||
|
programArguments: gatewayProgramArguments,
|
||||||
|
workingDirectory: "/tmp",
|
||||||
|
environment: {},
|
||||||
|
});
|
||||||
|
mocks.install.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
gateway: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await runRepair(cfg);
|
||||||
|
|
||||||
|
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.buildGatewayInstallPlan).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
config: cfg,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("maybeScanExtraGatewayServices", () => {
|
describe("maybeScanExtraGatewayServices", () => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { renderSystemNodeWarning, resolveSystemNodeInfo } from "../daemon/runtim
|
|||||||
import {
|
import {
|
||||||
auditGatewayServiceConfig,
|
auditGatewayServiceConfig,
|
||||||
needsNodeRuntimeMigration,
|
needsNodeRuntimeMigration,
|
||||||
|
readEmbeddedGatewayToken,
|
||||||
SERVICE_AUDIT_CODES,
|
SERVICE_AUDIT_CODES,
|
||||||
} from "../daemon/service-audit.js";
|
} from "../daemon/service-audit.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
@ -230,7 +231,7 @@ export async function maybeRepairGatewayServiceConfig(
|
|||||||
command,
|
command,
|
||||||
expectedGatewayToken,
|
expectedGatewayToken,
|
||||||
});
|
});
|
||||||
const serviceToken = command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim();
|
const serviceToken = readEmbeddedGatewayToken(command);
|
||||||
if (tokenRefConfigured && serviceToken) {
|
if (tokenRefConfigured && serviceToken) {
|
||||||
audit.issues.push({
|
audit.issues.push({
|
||||||
code: SERVICE_AUDIT_CODES.gatewayTokenMismatch,
|
code: SERVICE_AUDIT_CODES.gatewayTokenMismatch,
|
||||||
@ -316,7 +317,7 @@ export async function maybeRepairGatewayServiceConfig(
|
|||||||
if (!repair) {
|
if (!repair) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const serviceEmbeddedToken = command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
const serviceEmbeddedToken = readEmbeddedGatewayToken(command);
|
||||||
const gatewayTokenForRepair = expectedGatewayToken ?? serviceEmbeddedToken;
|
const gatewayTokenForRepair = expectedGatewayToken ?? serviceEmbeddedToken;
|
||||||
const configuredGatewayToken =
|
const configuredGatewayToken =
|
||||||
typeof cfg.gateway?.auth?.token === "string"
|
typeof cfg.gateway?.auth?.token === "string"
|
||||||
|
|||||||
@ -126,6 +126,30 @@ describe("auditGatewayServiceConfig", () => {
|
|||||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
|
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat EnvironmentFile-backed tokens as embedded", async () => {
|
||||||
|
const audit = await auditGatewayServiceConfig({
|
||||||
|
env: { HOME: "/tmp" },
|
||||||
|
platform: "linux",
|
||||||
|
expectedGatewayToken: "new-token",
|
||||||
|
command: {
|
||||||
|
programArguments: ["/usr/bin/node", "gateway"],
|
||||||
|
environment: {
|
||||||
|
PATH: "/usr/local/bin:/usr/bin:/bin",
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: "old-token",
|
||||||
|
},
|
||||||
|
environmentValueSources: {
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenEmbedded),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayTokenMismatch),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkTokenDrift", () => {
|
describe("checkTokenDrift", () => {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type GatewayServiceCommand = {
|
|||||||
programArguments: string[];
|
programArguments: string[];
|
||||||
workingDirectory?: string;
|
workingDirectory?: string;
|
||||||
environment?: Record<string, string>;
|
environment?: Record<string, string>;
|
||||||
|
environmentValueSources?: Record<string, "inline" | "file">;
|
||||||
sourcePath?: string;
|
sourcePath?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
@ -209,7 +210,7 @@ function auditGatewayToken(
|
|||||||
issues: ServiceConfigIssue[],
|
issues: ServiceConfigIssue[],
|
||||||
expectedGatewayToken?: string,
|
expectedGatewayToken?: string,
|
||||||
) {
|
) {
|
||||||
const serviceToken = command?.environment?.OPENCLAW_GATEWAY_TOKEN?.trim();
|
const serviceToken = readEmbeddedGatewayToken(command);
|
||||||
if (!serviceToken) {
|
if (!serviceToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -232,6 +233,16 @@ function auditGatewayToken(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readEmbeddedGatewayToken(command: GatewayServiceCommand): string | undefined {
|
||||||
|
if (!command) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (command.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN === "file") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return command.environment?.OPENCLAW_GATEWAY_TOKEN?.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function getPathModule(platform: NodeJS.Platform) {
|
function getPathModule(platform: NodeJS.Platform) {
|
||||||
return platform === "win32" ? path.win32 : path.posix;
|
return platform === "win32" ? path.win32 : path.posix;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export type GatewayServiceCommandConfig = {
|
|||||||
programArguments: string[];
|
programArguments: string[];
|
||||||
workingDirectory?: string;
|
workingDirectory?: string;
|
||||||
environment?: Record<string, string>;
|
environment?: Record<string, string>;
|
||||||
|
environmentValueSources?: Record<string, "inline" | "file">;
|
||||||
sourcePath?: string;
|
sourcePath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -460,7 +460,7 @@ describe("readSystemdServiceExecStart", () => {
|
|||||||
expect(readFileSpy).toHaveBeenCalledTimes(2);
|
expect(readFileSpy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lets inline Environment override EnvironmentFile values", async () => {
|
it("lets EnvironmentFile override inline Environment values", async () => {
|
||||||
vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
vi.spyOn(fs, "readFile").mockImplementation(async (pathname) => {
|
||||||
const pathValue = pathLikeToString(pathname);
|
const pathValue = pathLikeToString(pathname);
|
||||||
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
if (pathValue.endsWith("/openclaw-gateway.service")) {
|
||||||
@ -478,7 +478,8 @@ describe("readSystemdServiceExecStart", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
const command = await readSystemdServiceExecStart({ HOME: "/home/test" });
|
||||||
expect(command?.environment?.OPENCLAW_GATEWAY_TOKEN).toBe("inline-token");
|
expect(command?.environment?.OPENCLAW_GATEWAY_TOKEN).toBe("env-file-token");
|
||||||
|
expect(command?.environmentValueSources?.OPENCLAW_GATEWAY_TOKEN).toBe("file");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores missing optional EnvironmentFile entries", async () => {
|
it("ignores missing optional EnvironmentFile entries", async () => {
|
||||||
@ -598,6 +599,10 @@ describe("readSystemdServiceExecStart", () => {
|
|||||||
OPENCLAW_GATEWAY_TOKEN: "quoted token",
|
OPENCLAW_GATEWAY_TOKEN: "quoted token",
|
||||||
OPENCLAW_GATEWAY_PASSWORD: "quoted-password", // pragma: allowlist secret
|
OPENCLAW_GATEWAY_PASSWORD: "quoted-password", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
|
expect(command?.environmentValueSources).toEqual({
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: "file",
|
||||||
|
OPENCLAW_GATEWAY_PASSWORD: "file",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export async function readSystemdServiceExecStart(
|
|||||||
const content = await fs.readFile(unitPath, "utf8");
|
const content = await fs.readFile(unitPath, "utf8");
|
||||||
let execStart = "";
|
let execStart = "";
|
||||||
let workingDirectory = "";
|
let workingDirectory = "";
|
||||||
const environment: Record<string, string> = {};
|
const inlineEnvironment: Record<string, string> = {};
|
||||||
const environmentFileSpecs: string[] = [];
|
const environmentFileSpecs: string[] = [];
|
||||||
for (const rawLine of content.split("\n")) {
|
for (const rawLine of content.split("\n")) {
|
||||||
const line = rawLine.trim();
|
const line = rawLine.trim();
|
||||||
@ -80,7 +80,7 @@ export async function readSystemdServiceExecStart(
|
|||||||
const raw = line.slice("Environment=".length).trim();
|
const raw = line.slice("Environment=".length).trim();
|
||||||
const parsed = parseSystemdEnvAssignment(raw);
|
const parsed = parseSystemdEnvAssignment(raw);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
environment[parsed.key] = parsed.value;
|
inlineEnvironment[parsed.key] = parsed.value;
|
||||||
}
|
}
|
||||||
} else if (line.startsWith("EnvironmentFile=")) {
|
} else if (line.startsWith("EnvironmentFile=")) {
|
||||||
const raw = line.slice("EnvironmentFile=".length).trim();
|
const raw = line.slice("EnvironmentFile=".length).trim();
|
||||||
@ -98,14 +98,21 @@ export async function readSystemdServiceExecStart(
|
|||||||
unitPath,
|
unitPath,
|
||||||
});
|
});
|
||||||
const mergedEnvironment = {
|
const mergedEnvironment = {
|
||||||
...environmentFromFiles,
|
...inlineEnvironment,
|
||||||
...environment,
|
...environmentFromFiles.environment,
|
||||||
|
};
|
||||||
|
const mergedEnvironmentSources = {
|
||||||
|
...buildEnvironmentValueSources(inlineEnvironment, "inline"),
|
||||||
|
...buildEnvironmentValueSources(environmentFromFiles.environment, "file"),
|
||||||
};
|
};
|
||||||
const programArguments = parseSystemdExecStart(execStart);
|
const programArguments = parseSystemdExecStart(execStart);
|
||||||
return {
|
return {
|
||||||
programArguments,
|
programArguments,
|
||||||
...(workingDirectory ? { workingDirectory } : {}),
|
...(workingDirectory ? { workingDirectory } : {}),
|
||||||
...(Object.keys(mergedEnvironment).length > 0 ? { environment: mergedEnvironment } : {}),
|
...(Object.keys(mergedEnvironment).length > 0 ? { environment: mergedEnvironment } : {}),
|
||||||
|
...(Object.keys(mergedEnvironmentSources).length > 0
|
||||||
|
? { environmentValueSources: mergedEnvironmentSources }
|
||||||
|
: {}),
|
||||||
sourcePath: unitPath,
|
sourcePath: unitPath,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@ -113,6 +120,13 @@ export async function readSystemdServiceExecStart(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEnvironmentValueSources(
|
||||||
|
environment: Record<string, string>,
|
||||||
|
source: "inline" | "file",
|
||||||
|
): Record<string, "inline" | "file"> {
|
||||||
|
return Object.fromEntries(Object.keys(environment).map((key) => [key, source]));
|
||||||
|
}
|
||||||
|
|
||||||
function expandSystemdSpecifier(input: string, env: GatewayServiceEnv): string {
|
function expandSystemdSpecifier(input: string, env: GatewayServiceEnv): string {
|
||||||
// Support the common unit-specifier used in user services.
|
// Support the common unit-specifier used in user services.
|
||||||
return input.replaceAll("%h", toPosixPath(resolveHomeDir(env)));
|
return input.replaceAll("%h", toPosixPath(resolveHomeDir(env)));
|
||||||
@ -165,10 +179,10 @@ async function resolveSystemdEnvironmentFiles(params: {
|
|||||||
environmentFileSpecs: string[];
|
environmentFileSpecs: string[];
|
||||||
env: GatewayServiceEnv;
|
env: GatewayServiceEnv;
|
||||||
unitPath: string;
|
unitPath: string;
|
||||||
}): Promise<Record<string, string>> {
|
}): Promise<{ environment: Record<string, string> }> {
|
||||||
const resolved: Record<string, string> = {};
|
const resolved: Record<string, string> = {};
|
||||||
if (params.environmentFileSpecs.length === 0) {
|
if (params.environmentFileSpecs.length === 0) {
|
||||||
return resolved;
|
return { environment: resolved };
|
||||||
}
|
}
|
||||||
const unitDir = path.posix.dirname(params.unitPath);
|
const unitDir = path.posix.dirname(params.unitPath);
|
||||||
for (const specRaw of params.environmentFileSpecs) {
|
for (const specRaw of params.environmentFileSpecs) {
|
||||||
@ -193,7 +207,7 @@ async function resolveSystemdEnvironmentFiles(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resolved;
|
return { environment: resolved };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SystemdServiceInfo = {
|
export type SystemdServiceInfo = {
|
||||||
|
|||||||
@ -402,6 +402,26 @@ describe("GatewayClient connect auth payload", () => {
|
|||||||
client.stop();
|
client.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses explicit shared password and does not inject stored device token", () => {
|
||||||
|
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
||||||
|
const client = new GatewayClient({
|
||||||
|
url: "ws://127.0.0.1:18789",
|
||||||
|
password: "shared-password", // pragma: allowlist secret
|
||||||
|
});
|
||||||
|
|
||||||
|
client.start();
|
||||||
|
const ws = getLatestWs();
|
||||||
|
ws.emitOpen();
|
||||||
|
emitConnectChallenge(ws);
|
||||||
|
|
||||||
|
expect(connectFrameFrom(ws)).toMatchObject({
|
||||||
|
password: "shared-password", // pragma: allowlist secret
|
||||||
|
});
|
||||||
|
expect(connectFrameFrom(ws).token).toBeUndefined();
|
||||||
|
expect(connectFrameFrom(ws).deviceToken).toBeUndefined();
|
||||||
|
client.stop();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses stored device token when shared token is not provided", () => {
|
it("uses stored device token when shared token is not provided", () => {
|
||||||
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
loadDeviceAuthTokenMock.mockReturnValue({ token: "stored-device-token" });
|
||||||
const client = new GatewayClient({
|
const client = new GatewayClient({
|
||||||
|
|||||||
@ -254,9 +254,12 @@ export class GatewayClient {
|
|||||||
? loadDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role })?.token
|
? loadDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role })?.token
|
||||||
: null;
|
: null;
|
||||||
// Keep shared gateway credentials explicit. Persisted per-device tokens only
|
// Keep shared gateway credentials explicit. Persisted per-device tokens only
|
||||||
// participate when no explicit shared token is provided.
|
// participate when no explicit shared token/password is provided.
|
||||||
const resolvedDeviceToken =
|
const resolvedDeviceToken =
|
||||||
explicitDeviceToken ?? (!explicitGatewayToken ? (storedToken ?? undefined) : undefined);
|
explicitDeviceToken ??
|
||||||
|
(!(explicitGatewayToken || this.opts.password?.trim())
|
||||||
|
? (storedToken ?? undefined)
|
||||||
|
: undefined);
|
||||||
// Legacy compatibility: keep `auth.token` populated for device-token auth when
|
// Legacy compatibility: keep `auth.token` populated for device-token auth when
|
||||||
// no explicit shared token is present.
|
// no explicit shared token is present.
|
||||||
const authToken = explicitGatewayToken ?? resolvedDeviceToken;
|
const authToken = explicitGatewayToken ?? resolvedDeviceToken;
|
||||||
|
|||||||
@ -343,4 +343,77 @@ describe("resolveGatewayConnectionAuth", () => {
|
|||||||
password: "config-first-password", // pragma: allowlist secret
|
password: "config-first-password", // pragma: allowlist secret
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws when config-first token SecretRef cannot resolve even if env token exists", async () => {
|
||||||
|
const config = cfg({
|
||||||
|
gateway: {
|
||||||
|
mode: "local",
|
||||||
|
auth: {
|
||||||
|
token: { source: "env", provider: "default", id: "MISSING_CONFIG_FIRST_TOKEN" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
providers: {
|
||||||
|
default: { source: "env" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const env = {
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
||||||
|
} as NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveGatewayConnectionAuth({
|
||||||
|
config,
|
||||||
|
env,
|
||||||
|
includeLegacyEnv: false,
|
||||||
|
localTokenPrecedence: "config-first",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("gateway.auth.token");
|
||||||
|
expect(() =>
|
||||||
|
resolveGatewayConnectionAuthFromConfig({
|
||||||
|
cfg: config,
|
||||||
|
env,
|
||||||
|
includeLegacyEnv: false,
|
||||||
|
localTokenPrecedence: "config-first",
|
||||||
|
}),
|
||||||
|
).toThrow("gateway.auth.token");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when config-first password SecretRef cannot resolve even if env password exists", async () => {
|
||||||
|
const config = cfg({
|
||||||
|
gateway: {
|
||||||
|
mode: "local",
|
||||||
|
auth: {
|
||||||
|
mode: "password",
|
||||||
|
password: { source: "env", provider: "default", id: "MISSING_CONFIG_FIRST_PASSWORD" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
providers: {
|
||||||
|
default: { source: "env" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const env = {
|
||||||
|
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
|
||||||
|
} as NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveGatewayConnectionAuth({
|
||||||
|
config,
|
||||||
|
env,
|
||||||
|
includeLegacyEnv: false,
|
||||||
|
localPasswordPrecedence: "config-first",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("gateway.auth.password");
|
||||||
|
expect(() =>
|
||||||
|
resolveGatewayConnectionAuthFromConfig({
|
||||||
|
cfg: config,
|
||||||
|
env,
|
||||||
|
includeLegacyEnv: false,
|
||||||
|
localPasswordPrecedence: "config-first",
|
||||||
|
}),
|
||||||
|
).toThrow("gateway.auth.password");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -254,6 +254,24 @@ export function resolveGatewayCredentialsFromConfig(params: {
|
|||||||
authMode !== "none" &&
|
authMode !== "none" &&
|
||||||
authMode !== "trusted-proxy" &&
|
authMode !== "trusted-proxy" &&
|
||||||
!localResolved.password);
|
!localResolved.password);
|
||||||
|
if (
|
||||||
|
localTokenRef &&
|
||||||
|
localTokenPrecedence === "config-first" &&
|
||||||
|
!localToken &&
|
||||||
|
Boolean(envToken) &&
|
||||||
|
localTokenCanWin
|
||||||
|
) {
|
||||||
|
throwUnresolvedGatewaySecretInput("gateway.auth.token");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
localPasswordRef &&
|
||||||
|
localPasswordPrecedence === "config-first" &&
|
||||||
|
!localPassword &&
|
||||||
|
Boolean(envPassword) &&
|
||||||
|
localPasswordCanWin
|
||||||
|
) {
|
||||||
|
throwUnresolvedGatewaySecretInput("gateway.auth.password");
|
||||||
|
}
|
||||||
if (localTokenRef && !localResolved.token && !envToken && localTokenCanWin) {
|
if (localTokenRef && !localResolved.token && !envToken && localTokenCanWin) {
|
||||||
throwUnresolvedGatewaySecretInput("gateway.auth.token");
|
throwUnresolvedGatewaySecretInput("gateway.auth.token");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user