Merge 94084a2549b411db9d609d1ac4357c86b38002a2 into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
0x4C33 2026-03-21 02:14:49 +00:00 committed by GitHub
commit 0a013e7b50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 14 deletions

View File

@ -61,6 +61,8 @@ Docs: https://docs.openclaw.ai
- CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD.
- Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev.
- Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev.
- Gateway/auth: grant operator.read for device-less token/password auth so CLI and Dashboard can run read RPCs (devices list, status, probe) instead of getting "missing scope: operator.read". Fixes #48167, #46117, #46716, #17095.
- Gateway/handshake: restore handshake timeout to 10s (was reduced to 3s in #44089) so CLI commands like `openclaw devices list` no longer fail with "gateway closed (1000 normal closure)" on slower systems. (#47103)
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman.
- Agents/openai-responses: strip `prompt_cache_key` and `prompt_cache_retention` for non-OpenAI-compatible Responses endpoints while keeping them on direct OpenAI and Azure OpenAI paths, so third-party OpenAI-compatible providers no longer reject those requests with HTTP 400. (#49877) Thanks @ShaunTsai.
@ -143,6 +145,7 @@ Docs: https://docs.openclaw.ai
- Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55.
- macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage.
- Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env.
- Gateway/probe: include device identity in authenticated loopback probes so `openclaw status` and probe RPCs get full paired scopes instead of being scope-limited. Strip identity only for effectively anonymous probes (opts.auth undefined or empty). (#48805)
- Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr.
- Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman.
- Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468.

View File

@ -45,9 +45,20 @@ export async function probeGateway(opts: {
const disableDeviceIdentity = (() => {
try {
const hostname = new URL(opts.url).hostname;
// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password);
// Probes should stay device-bound whenever possible so read/detail RPCs
// are not scope-limited by shared-auth/anonymous scope stripping hardening.
// We used to disable identity for all local probes without token/password,
// but that breaks authenticated status checks when hardening is enabled.
//
// Disable device identity for loopback probes that are effectively
// unauthenticated: opts.auth undefined OR an auth object with no
// credentials. Callers like status/probe pass { token, password } from
// resolveGatewayProbeAuth even when both are missing; treat that as
// anonymous to preserve legacy "no-setup" local status behavior.
const hasCredentials =
(typeof opts.auth?.token === "string" && opts.auth.token.trim().length > 0) ||
(typeof opts.auth?.password === "string" && opts.auth.password.trim().length > 0);
return isLoopbackHost(hostname) && !hasCredentials;
} catch {
return false;
}

View File

@ -21,6 +21,9 @@ export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => {
maxChatHistoryMessagesBytes = value;
}
};
// Allow sufficient time for CLI to load device identity, sign the connect payload,
// and complete the handshake on slow systems (cold start, disk I/O). Too short causes
// premature close with "gateway closed (1000 normal closure)" before connect completes.
export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
export const getHandshakeTimeoutMs = () => {
// User-facing env var (works in all environments); test-only var gated behind VITEST

View File

@ -178,13 +178,14 @@ export function registerDefaultAuthTokenSuite(): void {
expectConnectError?: string;
expectStatusOk?: boolean;
expectStatusError?: string;
expectAdminRestricted?: boolean;
}> = [
{
name: "operator + valid shared token => connected with cleared scopes",
name: "operator + valid shared token => connected with operator.read (fixes #48167)",
opts: { role: "operator", token, device: null },
expectConnectOk: true,
expectStatusOk: false,
expectStatusError: "missing scope",
expectStatusOk: true,
expectAdminRestricted: true,
},
{
name: "node + valid shared token => rejected without device",
@ -220,6 +221,13 @@ export function registerDefaultAuthTokenSuite(): void {
);
}
}
if (scenario.expectAdminRestricted) {
const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false });
expect(adminRes.ok, scenario.name).toBe(false);
expect(adminRes.error?.message ?? "", scenario.name).toContain(
"missing scope: operator.admin",
);
}
} finally {
ws.close();
}

View File

@ -33,6 +33,7 @@ import {
mintCanvasCapabilityToken,
} from "../../canvas-capability.js";
import { normalizeDeviceMetadataForAuth } from "../../device-auth.js";
import { READ_SCOPE } from "../../method-scopes.js";
import {
isLocalishHost,
isLoopbackAddress,
@ -506,9 +507,12 @@ export function attachGatewayWsMessageHandler(params: {
});
close(1008, truncateCloseReason(authMessage));
};
const clearUnboundScopes = () => {
if (scopes.length > 0) {
scopes = [];
const clearUnboundScopes = (grantDefaultForTokenAuth = false) => {
if (scopes.length > 0 || grantDefaultForTokenAuth) {
// When shared token/password auth succeeds for device-less operator, grant
// operator.read instead of zero scopes so CLI/Dashboard can run read RPCs
// (devices list, status, probe, etc.). Fixes #48167, #46117, #46716, #17095.
scopes = grantDefaultForTokenAuth ? [READ_SCOPE] : [];
connectParams.scopes = scopes;
}
};
@ -531,11 +535,22 @@ export function attachGatewayWsMessageHandler(params: {
hasSharedAuth,
isLocalClient,
});
// Shared token/password auth can bypass pairing for trusted operators.
// Device-less clients only keep self-declared scopes on the explicit
// allow path, including trusted token-authenticated backend operators.
if (!device && decision.kind !== "allow") {
clearUnboundScopes();
// Shared token/password auth can bypass pairing for trusted operators, but
// device-less backend clients must not self-declare scopes. Control UI
// keeps its explicitly allowed device-less scopes on the allow path.
// When allowing device-less token/password auth, normalize to operator.read
// so read RPCs work. Do not auto-grant read for trusted-proxy (proxy proves
// identity but must not upgrade scope); preserve explicit scopes there.
const grantReadForTokenAuth =
decision.kind === "allow" &&
sharedAuthOk &&
(authMethod === "token" || authMethod === "password");
if (!device) {
if (decision.kind !== "allow") {
clearUnboundScopes(false);
} else if (!isControlUi && grantReadForTokenAuth) {
clearUnboundScopes(true);
}
}
if (decision.kind === "allow") {
return true;