fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b
BunsDev
·
2026-04-17
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
|
7 | 7 | ### Fixes |
8 | 8 | |
9 | 9 | - 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. |
| 10 | +- 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. |
10 | 11 | - 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. |
11 | 12 | - 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. |
12 | 13 | - 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. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -89,7 +89,21 @@ Gateway → Client:
|
89 | 89 | ``` |
90 | 90 | |
91 | 91 | `server`, `features`, `snapshot`, and `policy` are all required by the schema |
92 | | -(`src/gateway/protocol/schema/frames.ts`). `auth` and `canvasHostUrl` are optional. |
| 92 | +(`src/gateway/protocol/schema/frames.ts`). `canvasHostUrl` is optional. `auth` |
| 93 | +reports the negotiated role/scopes when available, and includes `deviceToken` |
| 94 | +when the gateway issues one. |
| 95 | + |
| 96 | +When no device token is issued, `hello-ok.auth` can still report the negotiated |
| 97 | +permissions: |
| 98 | + |
| 99 | +```json |
| 100 | +{ |
| 101 | +"auth": { |
| 102 | +"role": "operator", |
| 103 | +"scopes": ["operator.read", "operator.write"] |
| 104 | + } |
| 105 | +} |
| 106 | +``` |
93 | 107 | |
94 | 108 | When a device token is issued, `hello-ok` also includes: |
95 | 109 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -6,6 +6,8 @@ import {
|
6 | 6 | setRuntimeConfigSnapshot, |
7 | 7 | type OpenClawConfig, |
8 | 8 | } from "../config/config.js"; |
| 9 | +import { clearPluginDiscoveryCache } from "../plugins/discovery.js"; |
| 10 | +import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js"; |
9 | 11 | import { withPathResolutionEnv } from "../test-utils/env.js"; |
10 | 12 | import { createFixtureSuite } from "../test-utils/fixture-suite.js"; |
11 | 13 | import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js"; |
@@ -127,6 +129,8 @@ afterAll(async () => {
|
127 | 129 | |
128 | 130 | afterEach(() => { |
129 | 131 | clearRuntimeConfigSnapshot(); |
| 132 | +clearPluginDiscoveryCache(); |
| 133 | +clearPluginManifestRegistryCache(); |
130 | 134 | }); |
131 | 135 | |
132 | 136 | describe("buildWorkspaceSkillCommandSpecs", () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -92,7 +92,7 @@ export const HelloOkSchema = Type.Object(
|
92 | 92 | auth: Type.Optional( |
93 | 93 | Type.Object( |
94 | 94 | { |
95 | | -deviceToken: NonEmptyString, |
| 95 | +deviceToken: Type.Optional(NonEmptyString), |
96 | 96 | role: NonEmptyString, |
97 | 97 | scopes: Type.Array(NonEmptyString), |
98 | 98 | issuedAtMs: Type.Optional(Type.Integer({ minimum: 0 })), |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -421,7 +421,18 @@ export function registerControlUiAndPairingSuite(): void {
|
421 | 421 | }, |
422 | 422 | }); |
423 | 423 | expect(res.ok).toBe(true); |
424 | | -expect((res.payload as { auth?: unknown } | undefined)?.auth).toBeUndefined(); |
| 424 | +const helloOk = res.payload as |
| 425 | +| { |
| 426 | +auth?: { |
| 427 | +role?: unknown; |
| 428 | +scopes?: unknown; |
| 429 | +deviceToken?: unknown; |
| 430 | +}; |
| 431 | +} |
| 432 | +| undefined; |
| 433 | +expect(helloOk?.auth?.role).toBe("operator"); |
| 434 | +expect(helloOk?.auth?.scopes).toEqual(["operator.read"]); |
| 435 | +expect(helloOk?.auth?.deviceToken).toBeUndefined(); |
425 | 436 | const health = await rpcReq(staleDeviceWs, "health"); |
426 | 437 | expect(health.ok).toBe(true); |
427 | 438 | staleDeviceWs.close(); |
@@ -435,6 +446,18 @@ export function registerControlUiAndPairingSuite(): void {
|
435 | 446 | }, |
436 | 447 | }); |
437 | 448 | expect(scopedRes.ok, "requested scope bypass").toBe(true); |
| 449 | +const scopedHelloOk = scopedRes.payload as |
| 450 | +| { |
| 451 | +auth?: { |
| 452 | +role?: unknown; |
| 453 | +scopes?: unknown; |
| 454 | +deviceToken?: unknown; |
| 455 | +}; |
| 456 | +} |
| 457 | +| undefined; |
| 458 | +expect(scopedHelloOk?.auth?.role).toBe("operator"); |
| 459 | +expect(scopedHelloOk?.auth?.scopes).toEqual(["operator.read"]); |
| 460 | +expect(scopedHelloOk?.auth?.deviceToken).toBeUndefined(); |
438 | 461 | |
439 | 462 | const scopedHealth = await rpcReq(scopedWs, "health"); |
440 | 463 | expect(scopedHealth.ok).toBe(true); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -248,6 +248,28 @@ export function registerDefaultAuthTokenSuite(): void {
|
248 | 248 | } |
249 | 249 | }); |
250 | 250 | |
| 251 | +test("hello-ok reports granted auth metadata for device-less shared token auth", async () => { |
| 252 | +const ws = await openWs(port); |
| 253 | +try { |
| 254 | +const res = await connectReq(ws, { scopes: ["operator.read"], device: null }); |
| 255 | +expect(res.ok).toBe(true); |
| 256 | +const helloOk = res.payload as |
| 257 | +| { |
| 258 | +auth?: { |
| 259 | +role?: unknown; |
| 260 | +scopes?: unknown; |
| 261 | +deviceToken?: unknown; |
| 262 | +}; |
| 263 | +} |
| 264 | +| undefined; |
| 265 | +expect(helloOk?.auth?.role).toBe("operator"); |
| 266 | +expect(helloOk?.auth?.scopes).toEqual([]); |
| 267 | +expect(helloOk?.auth?.deviceToken).toBeUndefined(); |
| 268 | +} finally { |
| 269 | +ws.close(); |
| 270 | +} |
| 271 | +}); |
| 272 | + |
251 | 273 | test("does not grant admin when scopes are omitted", async () => { |
252 | 274 | const ws = await openWs(port); |
253 | 275 | const token = resolveGatewayTokenOrEnv(); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1227,17 +1227,19 @@ export function attachGatewayWsMessageHandler(params: {
|
1227 | 1227 | features: { methods: gatewayMethods, events }, |
1228 | 1228 | snapshot, |
1229 | 1229 | canvasHostUrl: scopedCanvasHostUrl, |
1230 | | -auth: deviceToken |
1231 | | - ? { |
1232 | | -deviceToken: deviceToken.token, |
1233 | | -role: deviceToken.role, |
1234 | | -scopes: deviceToken.scopes, |
1235 | | -issuedAtMs: deviceToken.rotatedAtMs ?? deviceToken.createdAtMs, |
1236 | | - ...(bootstrapDeviceTokens.length > 1 |
1237 | | - ? { deviceTokens: bootstrapDeviceTokens.slice(1) } |
1238 | | - : {}), |
1239 | | -} |
1240 | | - : undefined, |
| 1230 | +auth: { |
| 1231 | + role, |
| 1232 | + scopes, |
| 1233 | + ...(deviceToken |
| 1234 | + ? { |
| 1235 | +deviceToken: deviceToken.token, |
| 1236 | +issuedAtMs: deviceToken.rotatedAtMs ?? deviceToken.createdAtMs, |
| 1237 | + ...(bootstrapDeviceTokens.length > 1 |
| 1238 | + ? { deviceTokens: bootstrapDeviceTokens.slice(1) } |
| 1239 | + : {}), |
| 1240 | +} |
| 1241 | + : {}), |
| 1242 | +}, |
1241 | 1243 | policy: { |
1242 | 1244 | maxPayload: MAX_PAYLOAD_BYTES, |
1243 | 1245 | maxBufferedBytes: MAX_BUFFERED_BYTES, |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。