fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev

Co-authored-by: Val Alexander <bunsthedev@gmail.com>
This commit is contained in:
Val Alexander
2026-04-17 02:48:30 -05:00
committed by GitHub
parent 3ea1bf4232
commit 0b6c39be18
7 changed files with 80 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Onboarding/non-interactive: preserve existing gateway auth tokens during re-onboard so active local gateway clients are not disconnected by an implicit token rotation. (#67821) Thanks @BKF-Gitty.
- Gateway/hello-ok: always report negotiated auth metadata for successful shared-auth handshakes, including control-ui bypass coverage when no device token is issued. (#67810) Thanks @BunsDev.
- OpenAI Codex/Responses: unify native Responses API capability detection so Codex OAuth requests emit the required `store: false` field on the native Responses path. (#67918) Thanks @obviyus.
- WhatsApp/setup: guard personal-phone and allowlist prompt values so setup fails with clear validation errors instead of crashing on undefined prompt text. (#67895) Thanks @lawrence3699.
- Models/config: preserve an existing `models.json` provider `baseUrl` during merge-mode regeneration so custom endpoints do not get reset on restart. (#67893) Thanks @lawrence3699.

View File

@@ -89,7 +89,21 @@ Gateway → Client:
```
`server`, `features`, `snapshot`, and `policy` are all required by the schema
(`src/gateway/protocol/schema/frames.ts`). `auth` and `canvasHostUrl` are optional.
(`src/gateway/protocol/schema/frames.ts`). `canvasHostUrl` is optional. `auth`
reports the negotiated role/scopes when available, and includes `deviceToken`
when the gateway issues one.
When no device token is issued, `hello-ok.auth` can still report the negotiated
permissions:
```json
{
"auth": {
"role": "operator",
"scopes": ["operator.read", "operator.write"]
}
}
```
When a device token is issued, `hello-ok` also includes:

View File

@@ -6,6 +6,8 @@ import {
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../config/config.js";
import { clearPluginDiscoveryCache } from "../plugins/discovery.js";
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
import { withPathResolutionEnv } from "../test-utils/env.js";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
@@ -127,6 +129,8 @@ afterAll(async () => {
afterEach(() => {
clearRuntimeConfigSnapshot();
clearPluginDiscoveryCache();
clearPluginManifestRegistryCache();
});
describe("buildWorkspaceSkillCommandSpecs", () => {

View File

@@ -92,7 +92,7 @@ export const HelloOkSchema = Type.Object(
auth: Type.Optional(
Type.Object(
{
deviceToken: NonEmptyString,
deviceToken: Type.Optional(NonEmptyString),
role: NonEmptyString,
scopes: Type.Array(NonEmptyString),
issuedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),

View File

@@ -421,7 +421,18 @@ export function registerControlUiAndPairingSuite(): void {
},
});
expect(res.ok).toBe(true);
expect((res.payload as { auth?: unknown } | undefined)?.auth).toBeUndefined();
const helloOk = res.payload as
| {
auth?: {
role?: unknown;
scopes?: unknown;
deviceToken?: unknown;
};
}
| undefined;
expect(helloOk?.auth?.role).toBe("operator");
expect(helloOk?.auth?.scopes).toEqual(["operator.read"]);
expect(helloOk?.auth?.deviceToken).toBeUndefined();
const health = await rpcReq(staleDeviceWs, "health");
expect(health.ok).toBe(true);
staleDeviceWs.close();
@@ -435,6 +446,18 @@ export function registerControlUiAndPairingSuite(): void {
},
});
expect(scopedRes.ok, "requested scope bypass").toBe(true);
const scopedHelloOk = scopedRes.payload as
| {
auth?: {
role?: unknown;
scopes?: unknown;
deviceToken?: unknown;
};
}
| undefined;
expect(scopedHelloOk?.auth?.role).toBe("operator");
expect(scopedHelloOk?.auth?.scopes).toEqual(["operator.read"]);
expect(scopedHelloOk?.auth?.deviceToken).toBeUndefined();
const scopedHealth = await rpcReq(scopedWs, "health");
expect(scopedHealth.ok).toBe(true);

View File

@@ -248,6 +248,28 @@ export function registerDefaultAuthTokenSuite(): void {
}
});
test("hello-ok reports granted auth metadata for device-less shared token auth", async () => {
const ws = await openWs(port);
try {
const res = await connectReq(ws, { scopes: ["operator.read"], device: null });
expect(res.ok).toBe(true);
const helloOk = res.payload as
| {
auth?: {
role?: unknown;
scopes?: unknown;
deviceToken?: unknown;
};
}
| undefined;
expect(helloOk?.auth?.role).toBe("operator");
expect(helloOk?.auth?.scopes).toEqual([]);
expect(helloOk?.auth?.deviceToken).toBeUndefined();
} finally {
ws.close();
}
});
test("does not grant admin when scopes are omitted", async () => {
const ws = await openWs(port);
const token = resolveGatewayTokenOrEnv();

View File

@@ -1227,17 +1227,19 @@ export function attachGatewayWsMessageHandler(params: {
features: { methods: gatewayMethods, events },
snapshot,
canvasHostUrl: scopedCanvasHostUrl,
auth: deviceToken
? {
deviceToken: deviceToken.token,
role: deviceToken.role,
scopes: deviceToken.scopes,
issuedAtMs: deviceToken.rotatedAtMs ?? deviceToken.createdAtMs,
...(bootstrapDeviceTokens.length > 1
? { deviceTokens: bootstrapDeviceTokens.slice(1) }
: {}),
}
: undefined,
auth: {
role,
scopes,
...(deviceToken
? {
deviceToken: deviceToken.token,
issuedAtMs: deviceToken.rotatedAtMs ?? deviceToken.createdAtMs,
...(bootstrapDeviceTokens.length > 1
? { deviceTokens: bootstrapDeviceTokens.slice(1) }
: {}),
}
: {}),
},
policy: {
maxPayload: MAX_PAYLOAD_BYTES,
maxBufferedBytes: MAX_BUFFERED_BYTES,