mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
perf(config): use generated SecretRef policy metadata
This commit is contained in:
@@ -9,10 +9,10 @@
|
||||
"hooks.gmail.pushToken",
|
||||
"hooks.mappings[].sessionKey",
|
||||
"auth-profiles.oauth.*",
|
||||
"channels.discord.threadBindings.webhookToken",
|
||||
"channels.discord.accounts.*.threadBindings.webhookToken",
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json"
|
||||
"channels.discord.threadBindings.webhookToken",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
"channels.whatsapp.creds.json"
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadBundledPluginPublicArtifactModuleSync } from "../src/plugins/public-surface-loader.js";
|
||||
import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts";
|
||||
|
||||
const GENERATED_BY = "scripts/generate-bundled-channel-config-metadata.ts";
|
||||
@@ -63,6 +64,11 @@ type BundledChannelConfigMetadata = {
|
||||
description?: string;
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: Record<string, unknown>;
|
||||
unsupportedSecretRefSurfacePatterns?: readonly string[];
|
||||
};
|
||||
|
||||
type BundledChannelSecuritySurface = {
|
||||
unsupportedSecretRefSurfacePatterns?: readonly string[];
|
||||
};
|
||||
|
||||
function resolveChannelConfigSchemaModulePath(rootDir: string): string | null {
|
||||
@@ -131,6 +137,34 @@ function formatTypeScriptModule(source: string, outputPath: string, repoRoot: st
|
||||
});
|
||||
}
|
||||
|
||||
function resolveChannelUnsupportedSecretRefSurfacePatterns(
|
||||
source: BundledPluginSource,
|
||||
channelId: string,
|
||||
): string[] {
|
||||
try {
|
||||
const surface = loadBundledPluginPublicArtifactModuleSync<BundledChannelSecuritySurface>({
|
||||
dirName: source.dirName,
|
||||
artifactBasename: "security-contract-api.js",
|
||||
});
|
||||
const prefix = `channels.${channelId}.`;
|
||||
return [
|
||||
...new Set(
|
||||
(surface.unsupportedSecretRefSurfacePatterns ?? []).filter(
|
||||
(pattern): pattern is string => typeof pattern === "string" && pattern.startsWith(prefix),
|
||||
),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith("Unable to resolve bundled plugin public surface ")
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: string }) {
|
||||
const repoRoot = path.resolve(params?.repoRoot ?? process.cwd());
|
||||
const sources = collectBundledPluginSources({ repoRoot, requirePackageJson: true });
|
||||
@@ -156,6 +190,10 @@ export async function collectBundledChannelConfigMetadata(params?: { repoRoot?:
|
||||
for (const channelId of channelIds) {
|
||||
const label = resolveRootLabel(source, channelId);
|
||||
const description = resolveRootDescription(source, channelId);
|
||||
const unsupportedSecretRefSurfacePatterns = resolveChannelUnsupportedSecretRefSurfacePatterns(
|
||||
source,
|
||||
channelId,
|
||||
);
|
||||
entries.push({
|
||||
pluginId: source.manifest.id,
|
||||
channelId,
|
||||
@@ -163,6 +201,9 @@ export async function collectBundledChannelConfigMetadata(params?: { repoRoot?:
|
||||
...(description ? { description } : {}),
|
||||
schema: surface.schema,
|
||||
...(Object.keys(surface.uiHints ?? {}).length > 0 ? { uiHints: surface.uiHints } : {}),
|
||||
...(unsupportedSecretRefSurfacePatterns.length > 0
|
||||
? { unsupportedSecretRefSurfacePatterns }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3129,6 +3129,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
sensitive: true,
|
||||
},
|
||||
},
|
||||
unsupportedSecretRefSurfacePatterns: [
|
||||
"channels.discord.accounts.*.threadBindings.webhookToken",
|
||||
"channels.discord.threadBindings.webhookToken",
|
||||
],
|
||||
},
|
||||
{
|
||||
pluginId: "feishu",
|
||||
@@ -15432,6 +15436,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
},
|
||||
unsupportedSecretRefSurfacePatterns: [
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
"channels.whatsapp.creds.json",
|
||||
],
|
||||
},
|
||||
{
|
||||
pluginId: "zalo",
|
||||
|
||||
@@ -1,88 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { loadBundledChannelSecurityContractApiMock, loadPluginManifestRegistryMock } = vi.hoisted(
|
||||
() => ({
|
||||
loadBundledChannelSecurityContractApiMock: vi.fn((channelId: string) => {
|
||||
if (channelId === "discord") {
|
||||
return {
|
||||
unsupportedSecretRefSurfacePatterns: [
|
||||
"channels.discord.threadBindings.webhookToken",
|
||||
"channels.discord.accounts.*.threadBindings.webhookToken",
|
||||
],
|
||||
collectUnsupportedSecretRefConfigCandidates: (raw: Record<string, unknown>) => {
|
||||
const discord = (raw.channels as Record<string, unknown> | undefined)?.discord as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const candidates: Array<{ path: string; value: unknown }> = [];
|
||||
const threadBindings = discord?.threadBindings as Record<string, unknown> | undefined;
|
||||
candidates.push({
|
||||
path: "channels.discord.threadBindings.webhookToken",
|
||||
value: threadBindings?.webhookToken,
|
||||
});
|
||||
const accounts = discord?.accounts as Record<string, unknown> | undefined;
|
||||
for (const [accountId, account] of Object.entries(accounts ?? {})) {
|
||||
const accountThreadBindings = (account as Record<string, unknown>).threadBindings as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
candidates.push({
|
||||
path: `channels.discord.accounts.${accountId}.threadBindings.webhookToken`,
|
||||
value: accountThreadBindings?.webhookToken,
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (channelId === "whatsapp") {
|
||||
return {
|
||||
unsupportedSecretRefSurfacePatterns: [
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
],
|
||||
collectUnsupportedSecretRefConfigCandidates: (raw: Record<string, unknown>) => {
|
||||
const whatsapp = (raw.channels as Record<string, unknown> | undefined)?.whatsapp as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const candidates: Array<{ path: string; value: unknown }> = [];
|
||||
const creds = whatsapp?.creds as Record<string, unknown> | undefined;
|
||||
candidates.push({
|
||||
path: "channels.whatsapp.creds.json",
|
||||
value: creds?.json,
|
||||
});
|
||||
const accounts = whatsapp?.accounts as Record<string, unknown> | undefined;
|
||||
for (const [accountId, account] of Object.entries(accounts ?? {})) {
|
||||
const accountCreds = (account as Record<string, unknown>).creds as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
candidates.push({
|
||||
path: `channels.whatsapp.accounts.${accountId}.creds.json`,
|
||||
value: accountCreds?.json,
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
loadPluginManifestRegistryMock: vi.fn(() => ({
|
||||
plugins: [
|
||||
{ id: "discord", origin: "bundled", channels: ["discord"] },
|
||||
{ id: "whatsapp", origin: "bundled", channels: ["whatsapp"] },
|
||||
],
|
||||
diagnostics: [],
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: loadPluginManifestRegistryMock,
|
||||
}));
|
||||
|
||||
vi.mock("./channel-contract-api.js", () => ({
|
||||
loadBundledChannelSecurityContractApi: loadBundledChannelSecurityContractApiMock,
|
||||
}));
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectUnsupportedSecretRefConfigCandidates,
|
||||
getUnsupportedSecretRefSurfacePatterns,
|
||||
@@ -90,17 +6,19 @@ import {
|
||||
|
||||
describe("unsupported SecretRef surface policy metadata", () => {
|
||||
it("exposes the canonical unsupported surface patterns", () => {
|
||||
expect(getUnsupportedSecretRefSurfacePatterns()).toEqual([
|
||||
"commands.ownerDisplaySecret",
|
||||
"hooks.token",
|
||||
"hooks.gmail.pushToken",
|
||||
"hooks.mappings[].sessionKey",
|
||||
"auth-profiles.oauth.*",
|
||||
"channels.discord.threadBindings.webhookToken",
|
||||
"channels.discord.accounts.*.threadBindings.webhookToken",
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
]);
|
||||
expect(getUnsupportedSecretRefSurfacePatterns().toSorted()).toEqual(
|
||||
[
|
||||
"commands.ownerDisplaySecret",
|
||||
"hooks.token",
|
||||
"hooks.gmail.pushToken",
|
||||
"hooks.mappings[].sessionKey",
|
||||
"auth-profiles.oauth.*",
|
||||
"channels.discord.threadBindings.webhookToken",
|
||||
"channels.discord.accounts.*.threadBindings.webhookToken",
|
||||
"channels.whatsapp.creds.json",
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
].toSorted(),
|
||||
);
|
||||
});
|
||||
|
||||
it("discovers concrete config candidates for unsupported mutable surfaces", () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "../config/bundled-channel-config-metadata.generated.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { loadBundledChannelSecurityContractApi } from "./channel-contract-api.js";
|
||||
|
||||
const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
||||
"commands.ownerDisplaySecret",
|
||||
@@ -10,33 +9,149 @@ const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
|
||||
"auth-profiles.oauth.*",
|
||||
] as const;
|
||||
|
||||
function listBundledChannelIds(): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
loadPluginManifestRegistry({})
|
||||
.plugins.filter((entry) => entry.origin === "bundled")
|
||||
.flatMap((entry) => entry.channels),
|
||||
const CORE_UNSUPPORTED_SECRETREF_CONFIG_CANDIDATE_PATTERNS = [
|
||||
"commands.ownerDisplaySecret",
|
||||
"hooks.token",
|
||||
"hooks.gmail.pushToken",
|
||||
"hooks.mappings[].sessionKey",
|
||||
] as const;
|
||||
|
||||
type PatternToken =
|
||||
| { kind: "key"; key: string }
|
||||
| { kind: "array"; key: string }
|
||||
| { kind: "wildcard" };
|
||||
|
||||
const bundledChannelUnsupportedSecretRefSurfacePatterns = [
|
||||
...new Set(
|
||||
GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.flatMap((entry) =>
|
||||
"unsupportedSecretRefSurfacePatterns" in entry
|
||||
? (entry.unsupportedSecretRefSurfacePatterns ?? [])
|
||||
: [],
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] {
|
||||
const patterns: string[] = [];
|
||||
for (const channelId of listBundledChannelIds()) {
|
||||
const contract = loadBundledChannelSecurityContractApi(channelId);
|
||||
patterns.push(...(contract?.unsupportedSecretRefSurfacePatterns ?? []));
|
||||
const unsupportedSecretRefSurfacePatterns = [
|
||||
...CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS,
|
||||
...bundledChannelUnsupportedSecretRefSurfacePatterns,
|
||||
];
|
||||
|
||||
const unsupportedSecretRefConfigCandidatePatterns = [
|
||||
...CORE_UNSUPPORTED_SECRETREF_CONFIG_CANDIDATE_PATTERNS,
|
||||
...bundledChannelUnsupportedSecretRefSurfacePatterns,
|
||||
];
|
||||
|
||||
const parsedPatternCache = new Map<string, PatternToken[]>();
|
||||
|
||||
function parseUnsupportedSecretRefSurfacePattern(pattern: string): PatternToken[] {
|
||||
const cached = parsedPatternCache.get(pattern);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return patterns;
|
||||
const parsed = pattern
|
||||
.split(".")
|
||||
.filter((segment) => segment.length > 0)
|
||||
.map<PatternToken>((segment) => {
|
||||
if (segment === "*") {
|
||||
return { kind: "wildcard" };
|
||||
}
|
||||
if (segment.endsWith("[]")) {
|
||||
return {
|
||||
kind: "array",
|
||||
key: segment.slice(0, -2),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "key",
|
||||
key: segment,
|
||||
};
|
||||
});
|
||||
parsedPatternCache.set(pattern, parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
let cachedUnsupportedSecretRefSurfacePatterns: string[] | null = null;
|
||||
function collectPatternCandidates(params: {
|
||||
current: unknown;
|
||||
tokens: readonly PatternToken[];
|
||||
tokenIndex: number;
|
||||
pathSegments: string[];
|
||||
candidates: UnsupportedSecretRefConfigCandidate[];
|
||||
}): void {
|
||||
if (params.tokenIndex >= params.tokens.length) {
|
||||
params.candidates.push({
|
||||
path: params.pathSegments.join("."),
|
||||
value: params.current,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = params.tokens[params.tokenIndex];
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.kind === "wildcard") {
|
||||
if (Array.isArray(params.current)) {
|
||||
for (const [index, value] of params.current.entries()) {
|
||||
collectPatternCandidates({
|
||||
...params,
|
||||
current: value,
|
||||
tokenIndex: params.tokenIndex + 1,
|
||||
pathSegments: [...params.pathSegments, String(index)],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isRecord(params.current)) {
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of Object.entries(params.current)) {
|
||||
collectPatternCandidates({
|
||||
...params,
|
||||
current: value,
|
||||
tokenIndex: params.tokenIndex + 1,
|
||||
pathSegments: [...params.pathSegments, key],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(params.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.kind === "array") {
|
||||
if (!Object.hasOwn(params.current, token.key)) {
|
||||
return;
|
||||
}
|
||||
const value = params.current[token.key];
|
||||
if (!Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
for (const [index, entry] of value.entries()) {
|
||||
collectPatternCandidates({
|
||||
...params,
|
||||
current: entry,
|
||||
tokenIndex: params.tokenIndex + 1,
|
||||
pathSegments: [...params.pathSegments, token.key, String(index)],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(params.current, token.key)) {
|
||||
return;
|
||||
}
|
||||
collectPatternCandidates({
|
||||
...params,
|
||||
current: params.current[token.key],
|
||||
tokenIndex: params.tokenIndex + 1,
|
||||
pathSegments: [...params.pathSegments, token.key],
|
||||
});
|
||||
}
|
||||
|
||||
export function getUnsupportedSecretRefSurfacePatterns(): string[] {
|
||||
cachedUnsupportedSecretRefSurfacePatterns ??= [
|
||||
...CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS,
|
||||
...collectChannelUnsupportedSecretRefSurfacePatterns(),
|
||||
];
|
||||
return cachedUnsupportedSecretRefSurfacePatterns;
|
||||
return [...unsupportedSecretRefSurfacePatterns];
|
||||
}
|
||||
|
||||
export type UnsupportedSecretRefConfigCandidate = {
|
||||
@@ -52,51 +167,14 @@ export function collectUnsupportedSecretRefConfigCandidates(
|
||||
}
|
||||
|
||||
const candidates: UnsupportedSecretRefConfigCandidate[] = [];
|
||||
|
||||
const commands = isRecord(raw.commands) ? raw.commands : null;
|
||||
if (commands) {
|
||||
candidates.push({
|
||||
path: "commands.ownerDisplaySecret",
|
||||
value: commands.ownerDisplaySecret,
|
||||
for (const pattern of unsupportedSecretRefConfigCandidatePatterns) {
|
||||
collectPatternCandidates({
|
||||
current: raw,
|
||||
tokens: parseUnsupportedSecretRefSurfacePattern(pattern),
|
||||
tokenIndex: 0,
|
||||
pathSegments: [],
|
||||
candidates,
|
||||
});
|
||||
}
|
||||
|
||||
const hooks = isRecord(raw.hooks) ? raw.hooks : null;
|
||||
if (hooks) {
|
||||
candidates.push({ path: "hooks.token", value: hooks.token });
|
||||
|
||||
const gmail = isRecord(hooks.gmail) ? hooks.gmail : null;
|
||||
if (gmail) {
|
||||
candidates.push({
|
||||
path: "hooks.gmail.pushToken",
|
||||
value: gmail.pushToken,
|
||||
});
|
||||
}
|
||||
|
||||
const mappings = hooks.mappings;
|
||||
if (Array.isArray(mappings)) {
|
||||
for (const [index, mapping] of mappings.entries()) {
|
||||
if (!isRecord(mapping)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({
|
||||
path: `hooks.mappings.${index}.sessionKey`,
|
||||
value: mapping.sessionKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecord(raw.channels)) {
|
||||
for (const channelId of Object.keys(raw.channels)) {
|
||||
const contract = loadBundledChannelSecurityContractApi(channelId);
|
||||
const channelCandidates = contract?.collectUnsupportedSecretRefConfigCandidates?.(raw);
|
||||
if (!channelCandidates?.length) {
|
||||
continue;
|
||||
}
|
||||
candidates.push(...channelCandidates);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user