fix(gateway): allow no-auth backend self-pairing

Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
sallyom
2026-05-08 01:17:45 -04:00
committed by Sally O'Malley
parent f29efde73a
commit eebbe41da2
6 changed files with 83 additions and 1 deletions

View File

@@ -210,6 +210,7 @@ Docs: https://docs.openclaw.ai
- Compute plugin callback authorization dynamically [AI]. (#78866) Thanks @pgondhi987.
- fix(active-memory): require admin scope for global toggles [AI]. (#78863) Thanks @pgondhi987.
- Honor owner enforcement for native commands [AI]. (#78864) Thanks @pgondhi987.
- Gateway/auth: allow `gateway.auth.mode: "none"` loopback backend RPC clients to skip device identity only for local non-browser backend connections, restoring subagent spawns and gateway tools without opening remote or browser-origin bypasses. Fixes #75780. Thanks @yozakura-ava.
- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
- fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987.
@@ -1204,7 +1205,6 @@ Docs: https://docs.openclaw.ai
- Agents/replies: defer implicit image model discovery and keep OAuth auth-store adoption on persisted profiles during reply startup, cutting OCM MarCodex warm prep to sub-second in live checks. Thanks @shakkernerd.
- Plugins/tools: enforce `contracts.tools` as the manifest ownership contract for plugin tool registration, rejecting undeclared runtime tool names and adding bundled plugin drift coverage. Thanks @shakkernerd.
- Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash.
- Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme.
- Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer.
- Gateway/config: allow `gateway config.patch` to update documented subagent thinking defaults. Fixes #75764. (#75802) Thanks @kAIborg24.

View File

@@ -333,6 +333,52 @@ describe("gateway auth compatibility baseline", () => {
}
});
test("allows auth-none local backend connects without device identity", async () => {
const ws = await openWs(port);
try {
const res = await connectReq(ws, {
skipDefaultAuth: true,
client: { ...BACKEND_GATEWAY_CLIENT },
scopes: ["operator.admin"],
device: null,
});
expect(res.ok, JSON.stringify(res)).toBe(true);
const helloOk = res.payload as
| {
auth?: {
scopes?: unknown;
};
}
| undefined;
expect(helloOk?.auth?.scopes).toEqual(["operator.admin"]);
const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false });
expect(adminRes.ok).toBe(true);
} finally {
ws.close();
}
});
test("rejects auth-none browser-origin backend connects without device identity", async () => {
const ws = await openWs(port, { origin: originForPort(port) });
try {
const res = await connectReq(ws, {
skipDefaultAuth: true,
client: { ...BACKEND_GATEWAY_CLIENT },
scopes: ["operator.admin"],
device: null,
});
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("device identity required");
expect((res.error?.details as { code?: string } | undefined)?.code).toBe(
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED,
);
} finally {
ws.close();
}
});
test("keeps auth-none control ui first-connect token absence unchanged", async () => {
const ws = await openWs(port, { origin: originForPort(port) });
try {

View File

@@ -82,6 +82,7 @@ describe("resolveConnectAuthDecision", () => {
const state = await resolveConnectAuthState({
resolvedAuth: {
mode: "none",
allowTailscale: false,
} satisfies ResolvedGatewayAuth,
connectAuth: {},
hasDeviceIdentity: false,

View File

@@ -128,6 +128,36 @@ describe("ws connect policy", () => {
}).kind,
).toBe("allow");
expect(
evaluateMissingDeviceIdentity({
hasDeviceIdentity: false,
role: "operator",
isControlUi: false,
controlUiAuthPolicy: policy,
trustedProxyAuthOk: false,
localBackendSelfPairingOk: true,
sharedAuthOk: false,
authOk: true,
hasSharedAuth: false,
isLocalClient: true,
}).kind,
).toBe("allow");
expect(
evaluateMissingDeviceIdentity({
hasDeviceIdentity: false,
role: "node",
isControlUi: false,
controlUiAuthPolicy: policy,
trustedProxyAuthOk: false,
localBackendSelfPairingOk: true,
sharedAuthOk: false,
authOk: true,
hasSharedAuth: false,
isLocalClient: true,
}).kind,
).toBe("reject-device-required");
expect(
evaluateMissingDeviceIdentity({
hasDeviceIdentity: false,

View File

@@ -111,6 +111,7 @@ export function evaluateMissingDeviceIdentity(params: {
isControlUi: boolean;
controlUiAuthPolicy: ControlUiAuthPolicy;
trustedProxyAuthOk?: boolean;
localBackendSelfPairingOk?: boolean;
sharedAuthOk: boolean;
authOk: boolean;
hasSharedAuth: boolean;
@@ -130,6 +131,9 @@ export function evaluateMissingDeviceIdentity(params: {
// registrations (see #45405 review).
return { kind: "allow" };
}
if (params.localBackendSelfPairingOk && params.role === "operator") {
return { kind: "allow" };
}
if (params.isControlUi && !params.controlUiAuthPolicy.allowBypass) {
// Allow localhost Control UI connections when allowInsecureAuth is configured.
// Localhost has no network interception risk, and browser SubtleCrypto

View File

@@ -673,6 +673,7 @@ export function attachGatewayWsMessageHandler(params: GatewayWsMessageHandlerPar
isControlUi,
controlUiAuthPolicy,
trustedProxyAuthOk,
localBackendSelfPairingOk: skipLocalBackendSelfPairing,
sharedAuthOk,
authOk,
hasSharedAuth,