mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user