Skip to content

Commit 5cdcfe7

Browse files
committed
addressing ci
1 parent cccde5b commit 5cdcfe7

6 files changed

Lines changed: 100 additions & 5 deletions

File tree

src/plugins/command-registration.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,12 @@ export function pluginCommandSupportsChannel(
313313
export function registerPluginCommand(
314314
pluginId: string,
315315
command: OpenClawPluginCommandDefinition,
316-
opts?: { pluginName?: string; pluginRoot?: string; allowReservedCommandNames?: boolean },
316+
opts?: {
317+
pluginName?: string;
318+
pluginRoot?: string;
319+
allowReservedCommandNames?: boolean;
320+
allowOwnerStatusExposure?: boolean;
321+
},
317322
): CommandRegistrationResult {
318323
// Prevent registration while commands are being processed
319324
if (isPluginCommandRegistryLocked()) {
@@ -368,6 +373,9 @@ export function registerPluginCommand(
368373
pluginId,
369374
pluginName: opts?.pluginName,
370375
pluginRoot: opts?.pluginRoot,
376+
...(opts?.allowOwnerStatusExposure === true && normalizedCommand.exposeSenderIsOwner === true
377+
? { trustedOwnerStatusExposure: true as const }
378+
: {}),
371379
});
372380
logVerbose(`Registered plugin command: ${key} (plugin: ${pluginId})`);
373381
return { ok: true };

src/plugins/command-registry-state.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type RegisteredPluginCommand = OpenClawPluginCommandDefinition & {
1010
pluginId: string;
1111
pluginName?: string;
1212
pluginRoot?: string;
13+
trustedOwnerStatusExposure?: true;
1314
};
1415

1516
type PluginCommandState = {
@@ -58,6 +59,13 @@ export function isTrustedReservedCommandOwner(command: RegisteredPluginCommand):
5859
return command.ownership === "reserved";
5960
}
6061

62+
export function canExposeSenderIsOwner(command: RegisteredPluginCommand): boolean {
63+
return (
64+
(Array.isArray(command.requiredScopes) && command.requiredScopes.length > 0) ||
65+
command.trustedOwnerStatusExposure === true
66+
);
67+
}
68+
6169
export function listRegisteredPluginCommands(): RegisteredPluginCommand[] {
6270
return Array.from(pluginCommands.values());
6371
}

src/plugins/commands.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ describe("registerPluginCommand", () => {
733733
expect(observedOwnerStatus).toBeUndefined();
734734
});
735735

736-
it("exposes owner status to plugin commands that opt in", async () => {
736+
it("ignores owner status opt-in from direct plugin command registration", async () => {
737737
let observedOwnerStatus: boolean | undefined;
738738
registerPluginCommand("demo-plugin", {
739739
name: "voice",
@@ -755,6 +755,84 @@ describe("registerPluginCommand", () => {
755755
config: {},
756756
});
757757

758+
expect(observedOwnerStatus).toBeUndefined();
759+
});
760+
761+
it("ignores owner status opt-in from external plugin registry commands", async () => {
762+
const pluginRegistry = createPluginRegistry({
763+
logger: {
764+
info() {},
765+
warn() {},
766+
error() {},
767+
debug() {},
768+
},
769+
runtime: {} as PluginRuntime,
770+
activateGlobalSideEffects: true,
771+
});
772+
let observedOwnerStatus: boolean | undefined;
773+
pluginRegistry.registerCommand(
774+
{
775+
...createBundledPluginRecord("external-plugin"),
776+
origin: "workspace",
777+
source: "/workspace/external-plugin/index.ts",
778+
rootDir: "/workspace/external-plugin",
779+
},
780+
{
781+
name: "external",
782+
description: "External command",
783+
exposeSenderIsOwner: true,
784+
handler: async (ctx) => {
785+
observedOwnerStatus = ctx.senderIsOwner;
786+
return { text: "ok" };
787+
},
788+
},
789+
);
790+
const match = requirePluginCommandMatch("/external");
791+
792+
await executePluginCommand({
793+
command: match.command,
794+
channel: "telegram",
795+
isAuthorizedSender: true,
796+
senderIsOwner: true,
797+
commandBody: "/external",
798+
config: {},
799+
});
800+
801+
expect(observedOwnerStatus).toBeUndefined();
802+
});
803+
804+
it("exposes owner status to trusted bundled plugin commands that opt in", async () => {
805+
const pluginRegistry = createPluginRegistry({
806+
logger: {
807+
info() {},
808+
warn() {},
809+
error() {},
810+
debug() {},
811+
},
812+
runtime: {} as PluginRuntime,
813+
activateGlobalSideEffects: true,
814+
});
815+
let observedOwnerStatus: boolean | undefined;
816+
pluginRegistry.registerCommand(createBundledPluginRecord("phone-control"), {
817+
name: "phone",
818+
description: "Phone command",
819+
exposeSenderIsOwner: true,
820+
handler: async (ctx) => {
821+
observedOwnerStatus = ctx.senderIsOwner;
822+
return { text: "ok" };
823+
},
824+
});
825+
const match = requirePluginCommandMatch("/phone");
826+
827+
await executePluginCommand({
828+
command: match.command,
829+
channel: "telegram",
830+
isAuthorizedSender: true,
831+
senderIsOwner: true,
832+
commandBody: "/phone",
833+
config: {},
834+
});
835+
758836
expect(observedOwnerStatus).toBe(true);
759837
});
760838

src/plugins/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
validatePluginCommandDefinition,
2323
} from "./command-registration.js";
2424
import {
25+
canExposeSenderIsOwner,
2526
isTrustedReservedCommandOwner,
2627
listRegisteredPluginAgentPromptGuidance,
2728
pluginCommands,
@@ -303,8 +304,7 @@ export async function executePluginCommand(params: {
303304
});
304305
const effectiveAccountId = bindingConversation?.accountId ?? params.accountId;
305306
const senderIsOwnerForCommand =
306-
requiredScopes.length > 0 ||
307-
command.exposeSenderIsOwner === true ||
307+
canExposeSenderIsOwner(command) ||
308308
(isTrustedReservedCommandOwner(command) &&
309309
command.ownership === "reserved" &&
310310
isReservedCommandName(command.name) &&

src/plugins/registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,6 +1761,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
17611761
pluginName: record.name,
17621762
pluginRoot: record.rootDir,
17631763
allowReservedCommandNames,
1764+
allowOwnerStatusExposure: canClaimReservedCommandOwnership(record),
17641765
},
17651766
);
17661767
if (!result.ok) {

src/plugins/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2064,7 +2064,7 @@ export type OpenClawPluginCommandDefinition = {
20642064
requireAuth?: boolean;
20652065
/** Operator scopes required by gateway clients; command owners may satisfy this on chat surfaces. */
20662066
requiredScopes?: OperatorScope[];
2067-
/** Whether the handler needs owner status for subcommand-level authorization. */
2067+
/** Whether a trusted bundled handler needs owner status for subcommand-level authorization. */
20682068
exposeSenderIsOwner?: boolean;
20692069
/**
20702070
* Allows a bundled plugin to claim a command name that is otherwise reserved

0 commit comments

Comments
 (0)