mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 14:00:51 +00:00
Guardrails: pin runtime-api export seams (#49371)
* Guardrails: pin runtime-api export seams * Guardrails: tighten runtime-api keyed lookup * Changelog: note runtime-api guardrails * Tests: harden runtime-api guardrail parsing * Tests: align runtime-api guardrails with current seams
This commit is contained in:
@@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/network: unify API and media fetches under the same sticky IPv4 and pinned-IP fallback chain, and re-validate pinned override addresses against SSRF policy. (#49148) Thanks @obviyus.
|
||||
- Agents/prompt composition: append bootstrap truncation warnings to the current-turn prompt and add regression coverage for stable system-prompt cache invariants. (#49237) Thanks @scoootscooob.
|
||||
- Gateway/auth: add regression coverage that keeps device-less trusted-proxy Control UI sessions off privileged pairing approval RPCs. Thanks @vincentkoc.
|
||||
- Plugins/runtime-api: pin extension runtime-api export seams with explicit guardrail coverage so future surface creep becomes a deliberate diff. Thanks @vincentkoc.
|
||||
|
||||
### Breaking
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ const GUARDED_CHANNEL_EXTENSIONS = new Set([
|
||||
"msteams",
|
||||
"nostr",
|
||||
"nextcloud-talk",
|
||||
"nostr",
|
||||
"signal",
|
||||
"slack",
|
||||
"synology-chat",
|
||||
|
||||
148
src/plugin-sdk/runtime-api-guardrails.test.ts
Normal file
148
src/plugin-sdk/runtime-api-guardrails.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
"extensions/discord/runtime-api.ts": [
|
||||
'export * from "./src/audit.js";',
|
||||
'export * from "./src/actions/runtime.js";',
|
||||
'export * from "./src/actions/runtime.moderation-shared.js";',
|
||||
'export * from "./src/actions/runtime.shared.js";',
|
||||
'export * from "./src/channel-actions.js";',
|
||||
'export * from "./src/directory-live.js";',
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/monitor/gateway-plugin.js";',
|
||||
'export * from "./src/monitor/gateway-registry.js";',
|
||||
'export * from "./src/monitor/presence-cache.js";',
|
||||
'export * from "./src/monitor/thread-bindings.js";',
|
||||
'export * from "./src/monitor/thread-bindings.manager.js";',
|
||||
'export * from "./src/monitor/timeouts.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/resolve-channels.js";',
|
||||
'export * from "./src/resolve-users.js";',
|
||||
'export * from "./src/send.js";',
|
||||
],
|
||||
"extensions/imessage/runtime-api.ts": [
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/send.js";',
|
||||
],
|
||||
"extensions/nextcloud-talk/runtime-api.ts": [
|
||||
'export * from "openclaw/plugin-sdk/nextcloud-talk";',
|
||||
],
|
||||
"extensions/signal/runtime-api.ts": ['export * from "./src/index.js";'],
|
||||
"extensions/slack/runtime-api.ts": [
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/directory-live.js";',
|
||||
'export * from "./src/index.js";',
|
||||
'export * from "./src/resolve-channels.js";',
|
||||
'export * from "./src/resolve-users.js";',
|
||||
],
|
||||
"extensions/telegram/runtime-api.ts": [
|
||||
'export * from "./src/audit.js";',
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/channel-actions.js";',
|
||||
'export * from "./src/monitor.js";',
|
||||
'export * from "./src/probe.js";',
|
||||
'export * from "./src/send.js";',
|
||||
'export * from "./src/thread-bindings.js";',
|
||||
'export * from "./src/token.js";',
|
||||
],
|
||||
"extensions/whatsapp/runtime-api.ts": [
|
||||
'export * from "./src/active-listener.js";',
|
||||
'export * from "./src/action-runtime.js";',
|
||||
'export * from "./src/agent-tools-login.js";',
|
||||
'export * from "./src/auth-store.js";',
|
||||
'export * from "./src/auto-reply.js";',
|
||||
'export * from "./src/inbound.js";',
|
||||
'export * from "./src/login.js";',
|
||||
'export * from "./src/media.js";',
|
||||
'export * from "./src/send.js";',
|
||||
'export * from "./src/session.js";',
|
||||
],
|
||||
} as const;
|
||||
|
||||
function collectRuntimeApiFiles(): string[] {
|
||||
const extensionsDir = resolve(ROOT_DIR, "..", "extensions");
|
||||
const files: string[] = [];
|
||||
const stack = [extensionsDir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||
const fullPath = resolve(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
|
||||
continue;
|
||||
}
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile() || entry.name !== "runtime-api.ts") {
|
||||
continue;
|
||||
}
|
||||
files.push(relative(resolve(ROOT_DIR, ".."), fullPath).replaceAll("\\", "/"));
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function readExportStatements(path: string): string[] {
|
||||
const sourceText = readFileSync(resolve(ROOT_DIR, "..", path), "utf8");
|
||||
const sourceFile = ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true);
|
||||
|
||||
return sourceFile.statements.flatMap((statement) => {
|
||||
if (!ts.isExportDeclaration(statement)) {
|
||||
if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
||||
return [];
|
||||
}
|
||||
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
|
||||
}
|
||||
|
||||
const moduleSpecifier = statement.moduleSpecifier;
|
||||
if (!moduleSpecifier || !ts.isStringLiteral(moduleSpecifier)) {
|
||||
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
|
||||
}
|
||||
|
||||
if (!statement.exportClause) {
|
||||
const prefix = statement.isTypeOnly ? "export type *" : "export *";
|
||||
return [`${prefix} from ${moduleSpecifier.getText(sourceFile)};`];
|
||||
}
|
||||
|
||||
if (!ts.isNamedExports(statement.exportClause)) {
|
||||
return [statement.getText(sourceFile).replaceAll(/\s+/g, " ").trim()];
|
||||
}
|
||||
|
||||
const specifiers = statement.exportClause.elements.map((element) => {
|
||||
const imported = element.propertyName?.text;
|
||||
const exported = element.name.text;
|
||||
const alias = imported ? `${imported} as ${exported}` : exported;
|
||||
return element.isTypeOnly ? `type ${alias}` : alias;
|
||||
});
|
||||
const exportPrefix = statement.isTypeOnly ? "export type" : "export";
|
||||
return [
|
||||
`${exportPrefix} { ${specifiers.join(", ")} } from ${moduleSpecifier.getText(sourceFile)};`,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
describe("runtime api guardrails", () => {
|
||||
it("keeps runtime api seams on an explicit export allowlist", () => {
|
||||
const runtimeApiFiles = collectRuntimeApiFiles();
|
||||
expect(runtimeApiFiles).toEqual(
|
||||
expect.arrayContaining(Object.keys(RUNTIME_API_EXPORT_GUARDS).toSorted()),
|
||||
);
|
||||
|
||||
for (const file of Object.keys(RUNTIME_API_EXPORT_GUARDS).toSorted()) {
|
||||
expect(readExportStatements(file), `${file} runtime api exports changed`).toEqual(
|
||||
RUNTIME_API_EXPORT_GUARDS[file],
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user