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

推荐订阅源

月光博客
月光博客
Martin Fowler
Martin Fowler
Last Week in AI
Last Week in AI
罗磊的独立博客
阮一峰的网络日志
阮一峰的网络日志
博客园 - 【当耐特】
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
雷峰网
雷峰网
博客园_首页
人人都是产品经理
人人都是产品经理
量子位
美团技术团队
The Cloudflare Blog
小众软件
小众软件
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
博客园 - Franky

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
fix(device-pair): require pairing scope for pair command ...
pgondhi987 · 2026-05-05 · via Recent Commits to openclaw:main

@@ -258,6 +258,7 @@ describe("device-pair /pair qr", () => {

258258259259

it("returns an inline QR image for webchat surfaces", async () => {

260260

const command = registerPairCommand();

261+

expect(command.requiredScopes).toEqual(["operator.pairing"]);

261262

const result = await command.handler(

262263

createCommandContext({

263264

channel: "webchat",

@@ -296,7 +297,24 @@ describe("device-pair /pair qr", () => {

296297297298

expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();

298299

expect(result).toEqual({

299-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

300+

text: "⚠️ This command requires operator.pairing.",

301+

});

302+

});

303+304+

it("rejects qr setup for non-gateway command surfaces without pairing scopes", async () => {

305+

const command = registerPairCommand();

306+

const result = await command.handler(

307+

createCommandContext({

308+

channel: "telegram",

309+

args: "qr",

310+

commandBody: "/pair qr",

311+

gatewayClientScopes: undefined,

312+

}),

313+

);

314+315+

expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();

316+

expect(result).toEqual({

317+

text: "⚠️ This command requires operator.pairing.",

300318

});

301319

});

302320

@@ -433,7 +451,12 @@ describe("device-pair /pair qr", () => {

433451

runtime: createChannelRuntime(testCase.runtimeKey, testCase.sendKey, sendMessage),

434452

});

435453436-

const result = await command.handler(createCommandContext(testCase.ctx));

454+

const result = await command.handler(

455+

createCommandContext({

456+

...testCase.ctx,

457+

gatewayClientScopes: INTERNAL_PAIRING_SCOPES,

458+

}),

459+

);

437460

const text = requireText(result);

438461439462

expect(sendMessage).toHaveBeenCalledTimes(1);

@@ -479,6 +502,7 @@ describe("device-pair /pair qr", () => {

479502

createCommandContext({

480503

channel: "discord",

481504

senderId: "123",

505+

gatewayClientScopes: INTERNAL_PAIRING_SCOPES,

482506

}),

483507

);

484508

const text = requireText(result);

@@ -497,6 +521,7 @@ describe("device-pair /pair qr", () => {

497521

createCommandContext({

498522

channel: "msteams",

499523

senderId: "8:orgid:123",

524+

gatewayClientScopes: INTERNAL_PAIRING_SCOPES,

500525

}),

501526

);

502527

const text = requireText(result);

@@ -514,6 +539,7 @@ describe("device-pair /pair qr", () => {

514539

channel: "telegram",

515540

args: "cleanup",

516541

commandBody: "/pair cleanup",

542+

gatewayClientScopes: INTERNAL_PAIRING_SCOPES,

517543

}),

518544

);

519545

@@ -534,7 +560,7 @@ describe("device-pair /pair qr", () => {

534560535561

expect(pluginApiMocks.clearDeviceBootstrapTokens).not.toHaveBeenCalled();

536562

expect(result).toEqual({

537-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

563+

text: "⚠️ This command requires operator.pairing.",

538564

});

539565

});

540566

@@ -551,7 +577,24 @@ describe("device-pair /pair qr", () => {

551577552578

expect(pluginApiMocks.clearDeviceBootstrapTokens).not.toHaveBeenCalled();

553579

expect(result).toEqual({

554-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

580+

text: "⚠️ This command requires operator.pairing.",

581+

});

582+

});

583+584+

it("rejects status for non-gateway command surfaces without pairing scopes", async () => {

585+

const command = registerPairCommand();

586+

const result = await command.handler(

587+

createCommandContext({

588+

channel: "telegram",

589+

args: "status",

590+

commandBody: "/pair status",

591+

gatewayClientScopes: undefined,

592+

}),

593+

);

594+595+

expect(vi.mocked(listDevicePairing)).not.toHaveBeenCalled();

596+

expect(result).toEqual({

597+

text: "⚠️ This command requires operator.pairing.",

555598

});

556599

});

557600

});

@@ -578,7 +621,7 @@ describe("device-pair /pair default setup code", () => {

578621579622

expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();

580623

expect(result).toEqual({

581-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

624+

text: "⚠️ This command requires operator.pairing.",

582625

});

583626

});

584627

@@ -595,7 +638,7 @@ describe("device-pair /pair default setup code", () => {

595638596639

expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();

597640

expect(result).toEqual({

598-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

641+

text: "⚠️ This command requires operator.pairing.",

599642

});

600643

});

601644

@@ -612,10 +655,44 @@ describe("device-pair /pair default setup code", () => {

612655613656

expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();

614657

expect(result).toEqual({

615-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

658+

text: "⚠️ This command requires operator.pairing.",

616659

});

617660

});

618661662+

it("fails closed for non-gateway setup code issuance when scopes are absent", async () => {

663+

const command = registerPairCommand();

664+

const result = await command.handler(

665+

createCommandContext({

666+

channel: "telegram",

667+

args: "",

668+

commandBody: "/pair",

669+

gatewayClientScopes: undefined,

670+

}),

671+

);

672+673+

expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();

674+

expect(result).toEqual({

675+

text: "⚠️ This command requires operator.pairing.",

676+

});

677+

});

678+679+

it("allows command owners to issue setup codes from non-gateway command surfaces", async () => {

680+

const command = registerPairCommand();

681+

const result = await command.handler(

682+

createCommandContext({

683+

channel: "telegram",

684+

args: "",

685+

commandBody: "/pair",

686+

gatewayClientScopes: undefined,

687+

senderIsOwner: true,

688+

}),

689+

);

690+

const text = requireText(result);

691+692+

expect(pluginApiMocks.issueDeviceBootstrapToken).toHaveBeenCalledTimes(1);

693+

expect(text).toContain("Pairing setup code generated.");

694+

});

695+619696

it("normalizes secure bare publicUrl host ports before issuing setup codes", async () => {

620697

const command = registerPairCommand({

621698

config: {

@@ -909,7 +986,7 @@ describe("device-pair /pair approve", () => {

909986910987

expect(vi.mocked(approveDevicePairing)).not.toHaveBeenCalled();

911988

expect(result).toEqual({

912-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

989+

text: "⚠️ This command requires operator.pairing.",

913990

});

914991

});

915992

@@ -924,7 +1001,24 @@ describe("device-pair /pair approve", () => {

9241001

expect(result).toEqual({ text: "✅ Paired Victim Phone (ios)." });

9251002

});

9261003927-

it("does not force an empty caller scope context for external approvals", async () => {

1004+

it("rejects non-gateway approvals without pairing scopes", async () => {

1005+

const command = registerPairCommand();

1006+

const result = await command.handler(

1007+

createCommandContext({

1008+

channel: "telegram",

1009+

args: "approve latest",

1010+

commandBody: "/pair approve latest",

1011+

gatewayClientScopes: undefined,

1012+

}),

1013+

);

1014+1015+

expect(vi.mocked(approveDevicePairing)).not.toHaveBeenCalled();

1016+

expect(result).toEqual({

1017+

text: "⚠️ This command requires operator.pairing.",

1018+

});

1019+

});

1020+1021+

it("allows command owners to approve from non-gateway command surfaces", async () => {

9281022

mockPendingPairingList();

9291023

vi.mocked(approveDevicePairing).mockResolvedValueOnce(makeApprovedPairingResult());

9301024

@@ -935,10 +1029,32 @@ describe("device-pair /pair approve", () => {

9351029

args: "approve latest",

9361030

commandBody: "/pair approve latest",

9371031

gatewayClientScopes: undefined,

1032+

senderIsOwner: true,

9381033

}),

9391034

);

9401035941-

expect(vi.mocked(approveDevicePairing)).toHaveBeenCalledWith("req-1");

1036+

expect(vi.mocked(approveDevicePairing)).toHaveBeenCalledWith("req-1", {

1037+

callerScopes: ["operator.pairing"],

1038+

});

1039+

expect(result).toEqual({ text: "✅ Paired Victim Phone (ios)." });

1040+

});

1041+1042+

it("preserves gateway caller scopes for command-owner approvals", async () => {

1043+

mockPendingPairingList();

1044+

vi.mocked(approveDevicePairing).mockResolvedValueOnce(makeApprovedPairingResult());

1045+1046+

const command = registerPairCommand();

1047+

const result = await command.handler(

1048+

createCommandContext({

1049+

channel: "telegram",

1050+

args: "approve latest",

1051+

commandBody: "/pair approve latest",

1052+

gatewayClientScopes: INTERNAL_PAIRING_SCOPES,

1053+

senderIsOwner: true,

1054+

}),

1055+

);

1056+1057+

expectApproveCalledWithInternalPairingScopes();

9421058

expect(result).toEqual({ text: "✅ Paired Victim Phone (ios)." });

9431059

});

9441060

@@ -957,7 +1073,7 @@ describe("device-pair /pair approve", () => {

95710739581074

expect(vi.mocked(approveDevicePairing)).not.toHaveBeenCalled();

9591075

expect(result).toEqual({

960-

text: "⚠️ This command requires operator.pairing for internal gateway callers.",

1076+

text: "⚠️ This command requires operator.pairing.",

9611077

});

9621078

});

9631079

@@ -978,36 +1094,21 @@ describe("device-pair /pair approve", () => {

9781094

});

9791095

});

9801096981-

it("preserves approvals for non-gateway command surfaces", async () => {

1097+

it("approves from command surfaces that carry pairing scopes", async () => {

9821098

mockPendingPairingList();

983-

vi.mocked(approveDevicePairing).mockResolvedValueOnce(

984-

makeApprovedPairingResult({

985-

device: {

986-

scopes: ["operator.admin"],

987-

approvedScopes: ["operator.admin"],

988-

tokens: {

989-

operator: {

990-

token: "token-1",

991-

role: "operator",

992-

scopes: ["operator.admin"],

993-

createdAtMs: Date.now(),

994-

},

995-

},

996-

},

997-

}),

998-

);

1099+

vi.mocked(approveDevicePairing).mockResolvedValueOnce(makeApprovedPairingResult());

999110010001101

const command = registerPairCommand();

10011102

const result = await command.handler(

10021103

createCommandContext({

10031104

channel: "telegram",

10041105

args: "approve latest",

10051106

commandBody: "/pair approve latest",

1006-

gatewayClientScopes: undefined,

1107+

gatewayClientScopes: INTERNAL_PAIRING_SCOPES,

10071108

}),

10081109

);

100911101010-

expect(vi.mocked(approveDevicePairing)).toHaveBeenCalledWith("req-1");

1111+

expectApproveCalledWithInternalPairingScopes();

10111112

expect(result).toEqual({ text: "✅ Paired Victim Phone (ios)." });

10121113

});

10131114

});