惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
WordPress大学
WordPress大学
U
Unit 42
I
InfoQ
A
About on SuperTechFans
宝玉的分享
宝玉的分享
J
Java Code Geeks
博客园 - 司徒正美
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
G
Google Developers Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
aimingoo的专栏
aimingoo的专栏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
腾讯CDC
Recent Announcements
Recent Announcements

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
Require admin scope for node device token management [AI]...
pgondhi987 · 2026-05-13 · via Recent Commits to openclaw:main

@@ -181,6 +181,40 @@ describe("deviceHandlers", () => {

181181

);

182182

});

183183184+

it("rejects removing mixed-role devices without admin scope", async () => {

185+

getPairedDeviceMock.mockResolvedValue({

186+

deviceId: "device-1",

187+

role: "operator",

188+

roles: ["operator", "node"],

189+

tokens: {

190+

operator: {

191+

token: "operator-token",

192+

role: "operator",

193+

scopes: ["operator.pairing"],

194+

createdAtMs: 100,

195+

},

196+

node: {

197+

token: "node-token",

198+

role: "node",

199+

scopes: [],

200+

createdAtMs: 100,

201+

revokedAtMs: 200,

202+

},

203+

},

204+

});

205+

const opts = createOptions(

206+

"device.pair.remove",

207+

{ deviceId: "device-1" },

208+

{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },

209+

);

210+211+

await deviceHandlers["device.pair.remove"](opts);

212+213+

expect(removePairedDeviceMock).not.toHaveBeenCalled();

214+

expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();

215+

expectRespondedErrorMessage(opts, "device pairing removal denied");

216+

});

217+184218

it("disconnects active clients after revoking a device token", async () => {

185219

revokeDeviceTokenMock.mockResolvedValue({

186220

ok: true,

@@ -234,6 +268,20 @@ describe("deviceHandlers", () => {

234268

);

235269

});

236270271+

it("rejects revoking node tokens without admin scope", async () => {

272+

const opts = createOptions(

273+

"device.token.revoke",

274+

{ deviceId: "device-1", role: "node" },

275+

{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },

276+

);

277+278+

await deviceHandlers["device.token.revoke"](opts);

279+280+

expect(revokeDeviceTokenMock).not.toHaveBeenCalled();

281+

expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();

282+

expectRespondedErrorMessage(opts, "device token revocation denied");

283+

});

284+237285

it("treats normalized device ids as self-owned for token revocation", async () => {

238286

revokeDeviceTokenMock.mockResolvedValue({

239287

ok: true,

@@ -336,6 +384,65 @@ describe("deviceHandlers", () => {

336384

);

337385

});

338386387+

it("allows pairing-scoped device sessions to manage their own operator token", async () => {

388+

rotateDeviceTokenMock.mockResolvedValue({

389+

ok: true,

390+

entry: {

391+

token: "rotated-token",

392+

role: "operator",

393+

scopes: ["operator.pairing"],

394+

createdAtMs: 456,

395+

rotatedAtMs: 789,

396+

},

397+

});

398+

revokeDeviceTokenMock.mockResolvedValue({

399+

ok: true,

400+

entry: { role: "operator", revokedAtMs: 987 },

401+

});

402+403+

const rotateOpts = createOptions(

404+

"device.token.rotate",

405+

{ deviceId: "device-1", role: "operator", scopes: ["operator.pairing"] },

406+

{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },

407+

);

408+

const revokeOpts = createOptions(

409+

"device.token.revoke",

410+

{ deviceId: "device-1", role: "operator" },

411+

{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },

412+

);

413+414+

await deviceHandlers["device.token.rotate"](rotateOpts);

415+

await deviceHandlers["device.token.revoke"](revokeOpts);

416+417+

expect(rotateDeviceTokenMock).toHaveBeenCalledWith({

418+

deviceId: "device-1",

419+

role: "operator",

420+

scopes: ["operator.pairing"],

421+

callerScopes: ["operator.pairing"],

422+

});

423+

expect(revokeDeviceTokenMock).toHaveBeenCalledWith({

424+

deviceId: "device-1",

425+

role: "operator",

426+

callerScopes: ["operator.pairing"],

427+

});

428+

expect(rotateOpts.respond).toHaveBeenCalledWith(

429+

true,

430+

{

431+

deviceId: "device-1",

432+

role: "operator",

433+

token: "rotated-token",

434+

scopes: ["operator.pairing"],

435+

rotatedAtMs: 789,

436+

},

437+

undefined,

438+

);

439+

expect(revokeOpts.respond).toHaveBeenCalledWith(

440+

true,

441+

{ deviceId: "device-1", role: "operator", revokedAtMs: 987 },

442+

undefined,

443+

);

444+

});

445+339446

it("omits rotated tokens when an admin rotates another device token", async () => {

340447

mockPairedOperatorDevice();

341448

mockRotateOperatorTokenSuccess();

@@ -368,8 +475,27 @@ describe("deviceHandlers", () => {

368475

});

369476370477

it("rejects rotating a token for a role that was never approved", async () => {

371-

mockPairedOperatorDevice();

372478

rotateDeviceTokenMock.mockResolvedValue({ ok: false, reason: "unknown-device-or-role" });

479+

const opts = createOptions(

480+

"device.token.rotate",

481+

{ deviceId: "device-1", role: "node" },

482+

{ client: createClient(["operator.admin"], "admin-device", { isDeviceTokenAuth: true }) },

483+

);

484+485+

await deviceHandlers["device.token.rotate"](opts);

486+487+

expect(rotateDeviceTokenMock).toHaveBeenCalledWith({

488+

deviceId: "device-1",

489+

role: "node",

490+

scopes: undefined,

491+

callerScopes: ["operator.admin"],

492+

});

493+

expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();

494+

expectRespondedErrorMessage(opts, "device token rotation denied");

495+

});

496+497+

it("rejects rotating node tokens without admin scope", async () => {

498+

mockPairedOperatorDevice();

373499

const opts = createOptions(

374500

"device.token.rotate",

375501

{

@@ -387,12 +513,7 @@ describe("deviceHandlers", () => {

387513388514

await deviceHandlers["device.token.rotate"](opts);

389515390-

expect(rotateDeviceTokenMock).toHaveBeenCalledWith({

391-

deviceId: "device-1",

392-

role: "node",

393-

scopes: undefined,

394-

callerScopes: ["operator.pairing"],

395-

});

516+

expect(rotateDeviceTokenMock).not.toHaveBeenCalled();

396517

expect(opts.context.disconnectClientsForDevice).not.toHaveBeenCalled();

397518

expectRespondedErrorMessage(opts, "device token rotation denied");

398519

});

@@ -688,6 +809,27 @@ describe("deviceHandlers", () => {

688809

);

689810

});

690811812+

it("rejects approving node roles for the caller device without admin scope", async () => {

813+

getPendingDevicePairingMock.mockResolvedValue({

814+

requestId: "req-1",

815+

deviceId: " device-1 ",

816+

publicKey: "pk-1",

817+

role: "node",

818+

roles: ["node"],

819+

ts: 100,

820+

});

821+

const opts = createOptions(

822+

"device.pair.approve",

823+

{ requestId: "req-1" },

824+

{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },

825+

);

826+827+

await deviceHandlers["device.pair.approve"](opts);

828+829+

expect(approveDevicePairingMock).not.toHaveBeenCalled();

830+

expectRespondedErrorMessage(opts, "device pairing approval denied");

831+

});

832+691833

it("rejects rejecting another device from a non-admin device session", async () => {

692834

getPendingDevicePairingMock.mockResolvedValue({

693835

requestId: "req-2",