From 426d97797df57ca0bc8a79aa1bb868d1959f5134 Mon Sep 17 00:00:00 2001 From: vignesh07 Date: Sat, 21 Feb 2026 17:55:22 -0800 Subject: [PATCH] fix(pairing): treat operator.admin as satisfying operator.write --- src/infra/device-pairing.test.ts | 6 +++--- src/shared/operator-scope-compat.test.ts | 11 +++++++++-- src/shared/operator-scope-compat.ts | 3 +++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/infra/device-pairing.test.ts b/src/infra/device-pairing.test.ts index a3cd0b0e8..7d0f2c895 100644 --- a/src/infra/device-pairing.test.ts +++ b/src/infra/device-pairing.test.ts @@ -168,7 +168,7 @@ describe("device pairing tokens", () => { expect(mismatch.reason).toBe("token-mismatch"); }); - test("accepts operator.read requests with an operator.admin token scope", async () => { + test("accepts operator.read/operator.write requests with an operator.admin token scope", async () => { const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-")); await setupPairedOperatorDevice(baseDir, ["operator.admin"]); const paired = await getPairedDevice("device-1", baseDir); @@ -183,14 +183,14 @@ describe("device pairing tokens", () => { }); expect(readOk.ok).toBe(true); - const writeMismatch = await verifyDeviceToken({ + const writeOk = await verifyDeviceToken({ deviceId: "device-1", token, role: "operator", scopes: ["operator.write"], baseDir, }); - expect(writeMismatch).toEqual({ ok: false, reason: "scope-mismatch" }); + expect(writeOk.ok).toBe(true); }); test("treats multibyte same-length token input as mismatch without throwing", async () => { diff --git a/src/shared/operator-scope-compat.test.ts b/src/shared/operator-scope-compat.test.ts index ae8645d6b..166d7b18c 100644 --- a/src/shared/operator-scope-compat.test.ts +++ b/src/shared/operator-scope-compat.test.ts @@ -26,14 +26,21 @@ describe("roleScopesAllow", () => { ).toBe(true); }); - it("keeps non-read operator scopes explicit", () => { + it("treats operator.write as satisfied by write/admin scopes", () => { + expect( + roleScopesAllow({ + role: "operator", + requestedScopes: ["operator.write"], + allowedScopes: ["operator.write"], + }), + ).toBe(true); expect( roleScopesAllow({ role: "operator", requestedScopes: ["operator.write"], allowedScopes: ["operator.admin"], }), - ).toBe(false); + ).toBe(true); }); it("uses strict matching for non-operator roles", () => { diff --git a/src/shared/operator-scope-compat.ts b/src/shared/operator-scope-compat.ts index be82117f0..ac53d7414 100644 --- a/src/shared/operator-scope-compat.ts +++ b/src/shared/operator-scope-compat.ts @@ -22,6 +22,9 @@ function operatorScopeSatisfied(requestedScope: string, granted: Set): b granted.has(OPERATOR_ADMIN_SCOPE) ); } + if (requestedScope === OPERATOR_WRITE_SCOPE) { + return granted.has(OPERATOR_WRITE_SCOPE) || granted.has(OPERATOR_ADMIN_SCOPE); + } return granted.has(requestedScope); }