mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/WebChat: confirm toolbar New Session button resets before dispatching `/new` while leaving typed `/new` and `/reset` commands immediate. Fixes #45800; refs #27065, #56611, #54499, and #27110. Thanks @aethnova, @kosta228-huli, @adambezemek, and @xss925175263 (xianshishan).
|
||||
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
|
||||
- Gateway/models: add `models.pricing.enabled` so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.
|
||||
- Gateway/startup: warn when legacy `CLAWDBOT_*` or `MOLTBOT_*` environment variables are still present, pointing users to `OPENCLAW_*` names instead of failing silently. Fixes #53482; carries forward #53667. Thanks @lndyzwdxhs.
|
||||
- Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale `OPENCLAW_GATEWAY_TOKEN` or `OPENCLAW_GATEWAY_PASSWORD` values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.
|
||||
- Doctor/state: require an interactive confirmation before archiving orphan transcript files, so `openclaw doctor --fix` no longer silently renames recoverable session history after upgrades regenerate `sessions.json`. Fixes #73106. Thanks @scottgl9.
|
||||
- Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.
|
||||
|
||||
@@ -29,6 +29,9 @@ export {
|
||||
type ResolvedGatewayAuthModeSource,
|
||||
} from "./auth-resolve.js";
|
||||
|
||||
const LEGACY_OPENCLAW_ENV_NOTE =
|
||||
" Legacy CLAWDBOT_* and MOLTBOT_* environment variables are ignored; use OPENCLAW_* names.";
|
||||
|
||||
export type GatewayAuthResult = {
|
||||
ok: boolean;
|
||||
method?:
|
||||
@@ -223,7 +226,7 @@ export function assertGatewayAuthConfigured(
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
"gateway auth mode is token, but no token was configured (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)",
|
||||
`gateway auth mode is token, but no token was configured (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN).${LEGACY_OPENCLAW_ENV_NOTE}`,
|
||||
);
|
||||
}
|
||||
if (auth.mode === "password" && !auth.password) {
|
||||
@@ -235,7 +238,9 @@ export function assertGatewayAuthConfigured(
|
||||
"gateway auth mode is password, but gateway.auth.password contains a provider reference object instead of a resolved string — bootstrap secrets (gateway.auth.password) must be plaintext strings or set via the OPENCLAW_GATEWAY_PASSWORD environment variable because the secrets provider system has not initialised yet at gateway startup", // pragma: allowlist secret
|
||||
);
|
||||
}
|
||||
throw new Error("gateway auth mode is password, but no password was configured");
|
||||
throw new Error(
|
||||
`gateway auth mode is password, but no password was configured.${LEGACY_OPENCLAW_ENV_NOTE}`,
|
||||
);
|
||||
}
|
||||
if (auth.mode === "trusted-proxy") {
|
||||
if (!auth.trustedProxy) {
|
||||
|
||||
102
src/gateway/env-deprecation.test.ts
Normal file
102
src/gateway/env-deprecation.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
resetLegacyOpenClawEnvWarningForTest,
|
||||
warnLegacyOpenClawEnvVars,
|
||||
} from "./env-deprecation.js";
|
||||
|
||||
describe("warnLegacyOpenClawEnvVars", () => {
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
const originalVitest = process.env.VITEST;
|
||||
let emitWarning: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
resetLegacyOpenClawEnvWarningForTest();
|
||||
emitWarning = vi.spyOn(process, "emitWarning").mockImplementation(() => {});
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.VITEST;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
emitWarning.mockRestore();
|
||||
resetLegacyOpenClawEnvWarningForTest();
|
||||
restoreEnv("NODE_ENV", originalNodeEnv);
|
||||
restoreEnv("VITEST", originalVitest);
|
||||
});
|
||||
|
||||
it("warns with counts and prefixes instead of secret-shaped env names", () => {
|
||||
warnLegacyOpenClawEnvVars({
|
||||
CLAWDBOT_GATEWAY_TOKEN: "old-token",
|
||||
MOLTBOT_GATEWAY_PASSWORD: "old-password", // pragma: allowlist secret
|
||||
"CLAWDBOT_MALICIOUS\nforged": "old-value",
|
||||
});
|
||||
|
||||
expect(emitWarning).toHaveBeenCalledOnce();
|
||||
const [message, options] = emitWarning.mock.calls[0] as [
|
||||
string,
|
||||
{ code: string; type: string },
|
||||
];
|
||||
expect(message).toContain("Legacy CLAWDBOT_*, MOLTBOT_* environment variables");
|
||||
expect(message).toContain("3 total");
|
||||
expect(message).toContain("replacing the legacy prefix with OPENCLAW_");
|
||||
expect(message).not.toContain("GATEWAY_TOKEN");
|
||||
expect(message).not.toContain("GATEWAY_PASSWORD");
|
||||
expect(message).not.toContain("forged");
|
||||
expect(options).toEqual({
|
||||
code: "OPENCLAW_LEGACY_ENV_VARS",
|
||||
type: "DeprecationWarning",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not warn for current OPENCLAW names", () => {
|
||||
warnLegacyOpenClawEnvVars({ OPENCLAW_GATEWAY_TOKEN: "token" });
|
||||
|
||||
expect(emitWarning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns only once after a successful emit", () => {
|
||||
warnLegacyOpenClawEnvVars({ CLAWDBOT_GATEWAY_TOKEN: "old-token" });
|
||||
warnLegacyOpenClawEnvVars({ MOLTBOT_GATEWAY_TOKEN: "old-token" });
|
||||
|
||||
expect(emitWarning).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("retries if emitWarning throws before the warning is emitted", () => {
|
||||
emitWarning
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("warning sink failed");
|
||||
})
|
||||
.mockImplementationOnce(() => {});
|
||||
|
||||
expect(() => warnLegacyOpenClawEnvVars({ CLAWDBOT_GATEWAY_TOKEN: "old-token" })).toThrow(
|
||||
"warning sink failed",
|
||||
);
|
||||
warnLegacyOpenClawEnvVars({ CLAWDBOT_GATEWAY_TOKEN: "old-token" });
|
||||
|
||||
expect(emitWarning).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("suppresses warning noise based on the passed env", () => {
|
||||
warnLegacyOpenClawEnvVars({
|
||||
CLAWDBOT_GATEWAY_TOKEN: "old-token",
|
||||
VITEST: "true",
|
||||
});
|
||||
|
||||
expect(emitWarning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not let process.env test flags suppress a synthetic env", () => {
|
||||
process.env.VITEST = "true";
|
||||
|
||||
warnLegacyOpenClawEnvVars({ CLAWDBOT_GATEWAY_TOKEN: "old-token" });
|
||||
|
||||
expect(emitWarning).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
function restoreEnv(name: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[name];
|
||||
return;
|
||||
}
|
||||
process.env[name] = value;
|
||||
}
|
||||
42
src/gateway/env-deprecation.ts
Normal file
42
src/gateway/env-deprecation.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { isVitestRuntimeEnv } from "../infra/env.js";
|
||||
|
||||
const LEGACY_ENV_PREFIXES = ["CLAWDBOT_", "MOLTBOT_"] as const;
|
||||
type LegacyEnvPrefix = (typeof LEGACY_ENV_PREFIXES)[number];
|
||||
|
||||
let warned = false;
|
||||
|
||||
export function warnLegacyOpenClawEnvVars(env: NodeJS.ProcessEnv = process.env): void {
|
||||
if (warned || isVitestRuntimeEnv(env)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefixCounts = new Map<LegacyEnvPrefix, number>();
|
||||
for (const key of Object.keys(env)) {
|
||||
const prefix = LEGACY_ENV_PREFIXES.find((candidate) => key.startsWith(candidate));
|
||||
if (prefix) {
|
||||
prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const legacyVarCount = [...prefixCounts.values()].reduce((total, count) => total + count, 0);
|
||||
if (legacyVarCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detectedPrefixes = LEGACY_ENV_PREFIXES.filter((prefix) => prefixCounts.has(prefix))
|
||||
.map((prefix) => `${prefix}*`)
|
||||
.join(", ");
|
||||
|
||||
process.emitWarning(
|
||||
[
|
||||
`Legacy ${detectedPrefixes} environment variables were detected (${legacyVarCount} total), but OpenClaw only reads OPENCLAW_* names now.`,
|
||||
"Rename them by replacing the legacy prefix with OPENCLAW_; the old names are ignored.",
|
||||
].join("\n"),
|
||||
{ code: "OPENCLAW_LEGACY_ENV_VARS", type: "DeprecationWarning" },
|
||||
);
|
||||
warned = true;
|
||||
}
|
||||
|
||||
export function resetLegacyOpenClawEnvWarningForTest(): void {
|
||||
warned = false;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveGatewayAuth,
|
||||
} from "./auth.js";
|
||||
import { normalizeControlUiBasePath } from "./control-ui-shared.js";
|
||||
import { warnLegacyOpenClawEnvVars } from "./env-deprecation.js";
|
||||
import { resolveHooksConfig } from "./hooks.js";
|
||||
import {
|
||||
defaultGatewayBindMode,
|
||||
@@ -48,6 +49,8 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
auth?: GatewayAuthConfig;
|
||||
tailscale?: GatewayTailscaleConfig;
|
||||
}): Promise<GatewayRuntimeConfig> {
|
||||
warnLegacyOpenClawEnvVars();
|
||||
|
||||
// Tailscale serve/funnel hard-requires loopback. When bind is not
|
||||
// explicitly set, we must resolve Tailscale mode *before* choosing the
|
||||
// bind default so that container auto-detection does not override the
|
||||
@@ -140,7 +143,7 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
}
|
||||
if (!isLoopbackHost(bindHost) && !hasSharedSecret && authMode !== "trusted-proxy") {
|
||||
throw new Error(
|
||||
`refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD)`,
|
||||
`refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD; legacy CLAWDBOT_* and MOLTBOT_* environment variables are ignored)`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
PluginHookGatewayContext,
|
||||
PluginHookGatewayStartEvent,
|
||||
} from "../plugins/hook-types.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const startPluginServices = vi.fn(async () => null);
|
||||
@@ -319,48 +320,53 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
});
|
||||
|
||||
it("starts channels without waiting for primary model prewarm completion", async () => {
|
||||
let resolvePrewarm!: () => void;
|
||||
const prewarmPrimaryModel = vi.fn(
|
||||
async () =>
|
||||
await new Promise<undefined>((resolve) => {
|
||||
resolvePrewarm = () => resolve(undefined);
|
||||
}),
|
||||
await withEnvAsync(
|
||||
{ OPENCLAW_SKIP_CHANNELS: undefined, OPENCLAW_SKIP_PROVIDERS: undefined },
|
||||
async () => {
|
||||
let resolvePrewarm!: () => void;
|
||||
const prewarmPrimaryModel = vi.fn(
|
||||
async () =>
|
||||
await new Promise<undefined>((resolve) => {
|
||||
resolvePrewarm = () => resolve(undefined);
|
||||
}),
|
||||
);
|
||||
const startChannels = vi.fn(async () => undefined);
|
||||
|
||||
const sidecarsPromise = startGatewaySidecars({
|
||||
cfg: {
|
||||
hooks: { internal: { enabled: false } },
|
||||
agents: { defaults: { model: "openai/gpt-5.4" } },
|
||||
} as never,
|
||||
pluginRegistry: createPostAttachParams().pluginRegistry,
|
||||
defaultWorkspaceDir: "/tmp/openclaw-workspace",
|
||||
deps: {} as never,
|
||||
startChannels,
|
||||
prewarmPrimaryModel: prewarmPrimaryModel as never,
|
||||
log: { warn: vi.fn() },
|
||||
logHooks: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
logChannels: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(prewarmPrimaryModel).toHaveBeenCalledTimes(1);
|
||||
expect(startChannels).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
{ timeout: 2_000 },
|
||||
);
|
||||
await sidecarsPromise;
|
||||
|
||||
resolvePrewarm();
|
||||
await Promise.resolve();
|
||||
},
|
||||
);
|
||||
const startChannels = vi.fn(async () => undefined);
|
||||
|
||||
const sidecarsPromise = startGatewaySidecars({
|
||||
cfg: {
|
||||
hooks: { internal: { enabled: false } },
|
||||
agents: { defaults: { model: "openai/gpt-5.4" } },
|
||||
} as never,
|
||||
pluginRegistry: createPostAttachParams().pluginRegistry,
|
||||
defaultWorkspaceDir: "/tmp/openclaw-workspace",
|
||||
deps: {} as never,
|
||||
startChannels,
|
||||
prewarmPrimaryModel: prewarmPrimaryModel as never,
|
||||
log: { warn: vi.fn() },
|
||||
logHooks: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
logChannels: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(prewarmPrimaryModel).toHaveBeenCalledTimes(1);
|
||||
expect(startChannels).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
{ timeout: 2_000 },
|
||||
);
|
||||
await sidecarsPromise;
|
||||
|
||||
resolvePrewarm();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
it("keeps startup-gated methods unavailable while sidecars are still resuming", async () => {
|
||||
|
||||
Reference in New Issue
Block a user