mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 18:42:25 +00:00
fix(feishu): enforce account tool family gates (#93363)
* fix(feishu): enforce account tool family gates * fix(feishu): cover perm contextual account gate
This commit is contained in:
@@ -5,6 +5,14 @@ import { createToolFactoryHarness } from "./tool-factory-test-harness.js";
|
||||
|
||||
const createFeishuClientMock = vi.fn((creds: { appId?: string } | undefined) => ({
|
||||
__appId: creds?.appId,
|
||||
application: {
|
||||
scope: {
|
||||
list: vi.fn(async () => ({
|
||||
code: 0,
|
||||
data: { scopes: [] },
|
||||
})),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function feishuClientAppId(callIndex: number): string | undefined {
|
||||
@@ -61,6 +69,28 @@ describe("feishu_doc account selection", () => {
|
||||
} as OpenClawPluginApi["config"];
|
||||
}
|
||||
|
||||
function createMixedToolConfig(): OpenClawPluginApi["config"] {
|
||||
return {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
a: {
|
||||
appId: "app-a",
|
||||
appSecret: "sec-a", // pragma: allowlist secret
|
||||
tools: { doc: false, scopes: false },
|
||||
},
|
||||
b: {
|
||||
appId: "app-b",
|
||||
appSecret: "sec-b", // pragma: allowlist secret
|
||||
tools: { doc: true, scopes: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawPluginApi["config"];
|
||||
}
|
||||
|
||||
test("uses agentAccountId context when params omit accountId", async () => {
|
||||
const cfg = createDocEnabledConfig();
|
||||
|
||||
@@ -93,4 +123,44 @@ describe("feishu_doc account selection", () => {
|
||||
|
||||
expect(feishuClientAppId(-1)).toBe("app-a");
|
||||
});
|
||||
|
||||
test("rejects a disabled contextual account when another account enables docs", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(createMixedToolConfig());
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const docTool = resolveTool("feishu_doc", { agentAccountId: "a" });
|
||||
const result = await docTool.execute("call-disabled", {
|
||||
action: "list_blocks",
|
||||
doc_token: "d",
|
||||
});
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu Doc tools are disabled for account "a"');
|
||||
});
|
||||
|
||||
test("rejects an explicit disabled account override for docs", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(createMixedToolConfig());
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const docTool = resolveTool("feishu_doc", { agentAccountId: "b" });
|
||||
const result = await docTool.execute("call-disabled", {
|
||||
action: "list_blocks",
|
||||
doc_token: "d",
|
||||
accountId: "a",
|
||||
});
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu Doc tools are disabled for account "a"');
|
||||
});
|
||||
|
||||
test("rejects a disabled contextual account when another account enables app scopes", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(createMixedToolConfig());
|
||||
registerFeishuDocTools(api);
|
||||
|
||||
const scopesTool = resolveTool("feishu_app_scopes", { agentAccountId: "a" });
|
||||
const result = await scopesTool.execute("call-disabled", {});
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu App Scopes tools are disabled for account "a"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1384,14 +1384,23 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
type FeishuDocExecuteParams = FeishuDocParams & { accountId?: string };
|
||||
|
||||
const getClient = (params: { accountId?: string } | undefined, defaultAccountId?: string) =>
|
||||
createFeishuToolClient({ api, executeParams: params, defaultAccountId });
|
||||
createFeishuToolClient({
|
||||
api,
|
||||
executeParams: params,
|
||||
defaultAccountId,
|
||||
requiredTool: { family: "doc", label: "Doc" },
|
||||
});
|
||||
|
||||
const getMediaMaxBytes = (
|
||||
params: { accountId?: string } | undefined,
|
||||
defaultAccountId?: string,
|
||||
) =>
|
||||
(resolveFeishuToolAccount({ api, executeParams: params, defaultAccountId }).config
|
||||
?.mediaMaxMb ?? 30) *
|
||||
(resolveFeishuToolAccount({
|
||||
api,
|
||||
executeParams: params,
|
||||
defaultAccountId,
|
||||
requiredTool: { family: "doc", label: "Doc" },
|
||||
}).config?.mediaMaxMb ?? 30) *
|
||||
1024 *
|
||||
1024;
|
||||
|
||||
@@ -1584,7 +1593,13 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
parameters: Type.Object({}),
|
||||
async execute() {
|
||||
try {
|
||||
const result = await listAppScopes(getClient(undefined, ctx.agentAccountId));
|
||||
const result = await listAppScopes(
|
||||
createFeishuToolClient({
|
||||
api,
|
||||
defaultAccountId: ctx.agentAccountId,
|
||||
requiredTool: { family: "scopes", label: "App Scopes" },
|
||||
}),
|
||||
);
|
||||
return json(result);
|
||||
} catch (err) {
|
||||
return json({ error: formatErrorMessage(err) });
|
||||
|
||||
@@ -765,6 +765,7 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId,
|
||||
requiredTool: { family: "drive", label: "Drive" },
|
||||
});
|
||||
switch (p.action) {
|
||||
case "list":
|
||||
|
||||
@@ -145,6 +145,7 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId,
|
||||
requiredTool: { family: "perm", label: "Perm" },
|
||||
});
|
||||
switch (p.action) {
|
||||
case "list":
|
||||
|
||||
@@ -119,6 +119,21 @@ describe("feishu tool account routing", () => {
|
||||
expect(lastClientAppId()).toBe("app-b");
|
||||
});
|
||||
|
||||
test("wiki tool implicit fallback selects an account with wiki enabled", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
toolsA: { drive: true, wiki: false },
|
||||
toolsB: { wiki: true },
|
||||
}),
|
||||
);
|
||||
registerFeishuWikiTools(api);
|
||||
|
||||
const tool = resolveTool("feishu_wiki");
|
||||
await tool.execute("call", { action: "search" });
|
||||
|
||||
expect(lastClientAppId()).toBe("app-b");
|
||||
});
|
||||
|
||||
test("wiki tool prefers the active contextual account over configured defaultAccount", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
@@ -190,6 +205,22 @@ describe("feishu tool account routing", () => {
|
||||
expect(lastClientAppId()).toBe("app-b");
|
||||
});
|
||||
|
||||
test("drive tool rejects a disabled contextual account when another account enables it", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
toolsA: { drive: false },
|
||||
toolsB: { drive: true },
|
||||
}),
|
||||
);
|
||||
registerFeishuDriveTools(api);
|
||||
|
||||
const tool = resolveTool("feishu_drive", { agentAccountId: "a" });
|
||||
const result = await tool.execute("call", { action: "unknown_action" });
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu Drive tools are disabled for account "a"');
|
||||
});
|
||||
|
||||
test("perm tool registers when only second account enables it and routes to agentAccountId", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
@@ -205,6 +236,38 @@ describe("feishu tool account routing", () => {
|
||||
expect(lastClientAppId()).toBe("app-b");
|
||||
});
|
||||
|
||||
test("perm tool rejects a disabled contextual account when another account enables it", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
toolsA: { perm: false },
|
||||
toolsB: { perm: true },
|
||||
}),
|
||||
);
|
||||
registerFeishuPermTools(api);
|
||||
|
||||
const tool = resolveTool("feishu_perm", { agentAccountId: "a" });
|
||||
const result = await tool.execute("call", { action: "unknown_action" });
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu Perm tools are disabled for account "a"');
|
||||
});
|
||||
|
||||
test("perm tool rejects an explicit disabled account override", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
toolsA: { perm: false },
|
||||
toolsB: { perm: true },
|
||||
}),
|
||||
);
|
||||
registerFeishuPermTools(api);
|
||||
|
||||
const tool = resolveTool("feishu_perm", { agentAccountId: "b" });
|
||||
const result = await tool.execute("call", { action: "unknown_action", accountId: "a" });
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu Perm tools are disabled for account "a"');
|
||||
});
|
||||
|
||||
test("bitable tool registers when only second account enables it and routes to agentAccountId", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
@@ -386,6 +449,22 @@ describe("feishu tool account routing", () => {
|
||||
expect(lastClientAppId()).toBe("app-a");
|
||||
});
|
||||
|
||||
test("wiki tool rejects an explicit disabled account override", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness(
|
||||
createConfig({
|
||||
toolsA: { wiki: false },
|
||||
toolsB: { wiki: true },
|
||||
}),
|
||||
);
|
||||
registerFeishuWikiTools(api);
|
||||
|
||||
const tool = resolveTool("feishu_wiki", { agentAccountId: "b" });
|
||||
const result = await tool.execute("call", { action: "search", accountId: "a" });
|
||||
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(result.details.error).toBe('Feishu Wiki tools are disabled for account "a"');
|
||||
});
|
||||
|
||||
test("does not silently fall back when the contextual account is real but uses non-env SecretRefs", async () => {
|
||||
const { api, resolveTool } = createToolFactoryHarness({
|
||||
channels: {
|
||||
|
||||
@@ -12,11 +12,17 @@ import { resolveToolsConfig } from "./tools-config.js";
|
||||
import type { FeishuToolsConfig, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
type AccountAwareParams = { accountId?: string };
|
||||
type FeishuToolFamily = keyof FeishuToolsConfig;
|
||||
type FeishuToolRequirement = {
|
||||
family: FeishuToolFamily;
|
||||
label: string;
|
||||
};
|
||||
|
||||
function resolveImplicitToolAccountId(params: {
|
||||
api: Pick<OpenClawPluginApi, "config">;
|
||||
executeParams?: AccountAwareParams;
|
||||
defaultAccountId?: string;
|
||||
requiredTool?: FeishuToolRequirement;
|
||||
}): string | undefined {
|
||||
const explicitAccountId = normalizeOptionalString(params.executeParams?.accountId);
|
||||
if (explicitAccountId) {
|
||||
@@ -45,6 +51,19 @@ function resolveImplicitToolAccountId(params: {
|
||||
return configuredDefaultAccountId;
|
||||
}
|
||||
|
||||
if (params.requiredTool && params.api.config) {
|
||||
for (const accountId of listFeishuAccountIds(params.api.config)) {
|
||||
const account = resolveFeishuAccount({ cfg: params.api.config, accountId });
|
||||
if (
|
||||
account.enabled &&
|
||||
account.configured &&
|
||||
resolveToolsConfig(account.config.tools)[params.requiredTool.family]
|
||||
) {
|
||||
return accountId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -52,20 +71,31 @@ export function resolveFeishuToolAccount(params: {
|
||||
api: Pick<OpenClawPluginApi, "config">;
|
||||
executeParams?: AccountAwareParams;
|
||||
defaultAccountId?: string;
|
||||
requiredTool?: FeishuToolRequirement;
|
||||
}): ResolvedFeishuAccount {
|
||||
if (!params.api.config) {
|
||||
throw new Error("Feishu config unavailable");
|
||||
}
|
||||
return resolveFeishuRuntimeAccount({
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg: params.api.config,
|
||||
accountId: resolveImplicitToolAccountId(params),
|
||||
});
|
||||
if (
|
||||
params.requiredTool &&
|
||||
!resolveToolsConfig(account.config.tools)[params.requiredTool.family]
|
||||
) {
|
||||
throw new Error(
|
||||
`Feishu ${params.requiredTool.label} tools are disabled for account "${account.accountId}"`,
|
||||
);
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
export function createFeishuToolClient(params: {
|
||||
api: Pick<OpenClawPluginApi, "config">;
|
||||
executeParams?: AccountAwareParams;
|
||||
defaultAccountId?: string;
|
||||
requiredTool?: FeishuToolRequirement;
|
||||
}): Lark.Client {
|
||||
return createFeishuClient(resolveFeishuToolAccount(params));
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId,
|
||||
requiredTool: { family: "wiki", label: "Wiki" },
|
||||
});
|
||||
switch (p.action) {
|
||||
case "spaces":
|
||||
|
||||
Reference in New Issue
Block a user