Merge 94084a2549b411db9d609d1ac4357c86b38002a2 into 5e417b44e1540f528d2ae63e3e20229a902d1db2
This commit is contained in:
commit
0a013e7b50
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user