Merge origin/main into fix/google-realtime-lazy-queue-bounds-20260801

* origin/main:
  fix(gateway): trim audit.list exact-match filter ids (#110847)
  fix: harden plugin import-boundary guards and remove dead scanner (#117386)
This commit is contained in:
Vincent Koc
2026-08-01 22:55:18 +08:00
17 changed files with 903 additions and 762 deletions

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env node
/**
* Collects architecture smell findings from the configured source roots.
*/
export function collectArchitectureSmells(): Promise<
Array<{
category: string;
file: string;
line: number;
kind: string;
specifier: string;
reason: string;
}>
>;
/**
* Runs the architecture smell check and writes human/JSON output.
*/
export function main(argv: unknown, io: unknown): Promise<number>;

View File

@@ -1,249 +0,0 @@
#!/usr/bin/env node
// Finds core/plugin architecture boundary smells in TypeScript sources.
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import pMap from "p-map";
import { BUNDLED_PLUGIN_PATH_PREFIX } from "./lib/bundled-plugin-paths.mjs";
import {
collectModuleReferencesFromSource,
normalizeRepoPath,
resolveRepoSpecifier,
writeLine,
} from "./lib/guard-inventory-utils.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveSourceRoots,
runAsScript,
} from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const scanRoots = resolveSourceRoots(repoRoot, ["src/plugin-sdk", "src/plugins/runtime"]);
let architectureSmellsPromise;
function compareEntries(left, right) {
return (
left.category.localeCompare(right.category) ||
left.file.localeCompare(right.file) ||
left.line - right.line ||
left.kind.localeCompare(right.kind) ||
left.specifier.localeCompare(right.specifier) ||
left.reason.localeCompare(right.reason)
);
}
function pushEntry(entries, entry) {
entries.push(entry);
}
function scanPluginSdkExtensionFacadeSmells(source, filePath) {
const relativeFile = normalizeRepoPath(repoRoot, filePath);
if (!relativeFile.startsWith("src/plugin-sdk/")) {
return [];
}
const entries = [];
for (const { kind, line, specifier } of collectModuleReferencesFromSource(source)) {
if (kind !== "export") {
continue;
}
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
if (!resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
continue;
}
pushEntry(entries, {
category: "plugin-sdk-extension-facade",
file: relativeFile,
line,
kind,
specifier,
resolvedPath,
reason: "plugin-sdk public surface re-exports extension-owned implementation",
});
}
return entries;
}
function scanRuntimeTypeImplementationSmells(source, filePath) {
const relativeFile = normalizeRepoPath(repoRoot, filePath);
if (!/^src\/plugins\/runtime\/types(?:-[^/]+)?\.ts$/.test(relativeFile)) {
return [];
}
const entries = [];
for (const { kind, line, specifier } of collectModuleReferencesFromSource(source)) {
if (kind !== "dynamic-import") {
continue;
}
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
if (
resolvedPath &&
(/^src\/plugins\/runtime\/runtime-[^/]+\.ts$/.test(resolvedPath) ||
/^extensions\/[^/]+\/runtime-api\.[^/]+$/.test(resolvedPath))
) {
pushEntry(entries, {
category: "runtime-type-implementation-edge",
file: relativeFile,
line,
kind: "import-type",
specifier,
resolvedPath,
reason: "runtime type file references implementation shim directly",
});
}
}
return entries;
}
function scanRuntimeServiceLocatorSmells(source, filePath) {
const relativeFile = normalizeRepoPath(repoRoot, filePath);
if (
!relativeFile.startsWith("src/plugin-sdk/") &&
!relativeFile.startsWith("src/plugins/runtime/")
) {
return [];
}
const entries = [];
const exportedNames = new Set();
const runtimeStoreCalls = [];
const mutableStateNodes = [];
const lines = source.split(/\r?\n/);
for (const [index, line] of lines.entries()) {
const lineNumber = index + 1;
const exportedFunction = line.match(/^\s*export\s+function\s+([A-Za-z_$][\w$]*)/);
if (exportedFunction) {
exportedNames.add(exportedFunction[1]);
}
const exportedVariable = line.match(/^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/);
if (exportedVariable) {
exportedNames.add(exportedVariable[1]);
}
for (const mutableMatch of line.matchAll(/^\s*let\s+([A-Za-z_$][\w$]*)/g)) {
mutableStateNodes.push({ line: lineNumber, text: mutableMatch[1] });
}
if (line.includes("createPluginRuntimeStore")) {
runtimeStoreCalls.push({ line: lineNumber });
}
}
const getterNames = [...exportedNames].filter((name) => /^get[A-Z]/.test(name));
const setterNames = [...exportedNames].filter((name) => /^set[A-Z]/.test(name));
if (runtimeStoreCalls.length > 0 && getterNames.length > 0 && setterNames.length > 0) {
for (const callNode of runtimeStoreCalls) {
pushEntry(entries, {
category: "runtime-service-locator",
file: relativeFile,
line: callNode.line,
kind: "runtime-store",
specifier: "createPluginRuntimeStore",
resolvedPath: relativeFile,
reason: `exports paired runtime accessors (${getterNames.join(", ")} / ${setterNames.join(", ")}) over module-global store state`,
});
}
}
if (mutableStateNodes.length > 0 && getterNames.length > 0 && setterNames.length > 0) {
for (const identifier of mutableStateNodes) {
pushEntry(entries, {
category: "runtime-service-locator",
file: relativeFile,
line: identifier.line,
kind: "mutable-state",
specifier: identifier.text,
resolvedPath: relativeFile,
reason: `module-global mutable state backs exported runtime accessors (${getterNames.join(", ")} / ${setterNames.join(", ")})`,
});
}
}
return entries;
}
/**
* Collects architecture smell findings from the configured source roots.
*/
export async function collectArchitectureSmells() {
if (!architectureSmellsPromise) {
architectureSmellsPromise = (async () => {
const files = (await collectTypeScriptFilesFromRoots(scanRoots)).toSorted((left, right) =>
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
);
const entriesByFile = await pMap(
files,
async (filePath) => {
const source = await fs.readFile(filePath, "utf8");
const entries = scanPluginSdkExtensionFacadeSmells(source, filePath);
entries.push(...scanRuntimeTypeImplementationSmells(source, filePath));
entries.push(...scanRuntimeServiceLocatorSmells(source, filePath));
return entries;
},
{ concurrency: 32, stopOnError: true },
);
return entriesByFile.flat().toSorted(compareEntries);
})();
try {
return await architectureSmellsPromise;
} catch (error) {
architectureSmellsPromise = undefined;
throw error;
}
}
return await architectureSmellsPromise;
}
function formatInventoryHuman(inventory) {
if (inventory.length === 0) {
return "No architecture smells found for the configured checks.";
}
const lines = ["Architecture smell inventory:"];
let activeCategory = "";
let activeFile = "";
for (const entry of inventory) {
if (entry.category !== activeCategory) {
activeCategory = entry.category;
activeFile = "";
lines.push(entry.category);
}
if (entry.file !== activeFile) {
activeFile = entry.file;
lines.push(` ${activeFile}`);
}
lines.push(` - line ${entry.line} [${entry.kind}] ${entry.reason}`);
lines.push(` specifier: ${entry.specifier}`);
lines.push(` resolved: ${entry.resolvedPath}`);
}
return lines.join("\n");
}
async function runArchitectureSmellsCheck(argv, io) {
const args = argv ?? process.argv.slice(2);
const streams = io ?? { stdout: process.stdout, stderr: process.stderr };
const json = args.includes("--json");
const inventory = await collectArchitectureSmells();
if (json) {
writeLine(streams.stdout, JSON.stringify(inventory, null, 2));
return 0;
}
writeLine(streams.stdout, formatInventoryHuman(inventory));
writeLine(streams.stdout, `${inventory.length} smell${inventory.length === 1 ? "" : "s"} found.`);
return 0;
}
/**
* Runs the architecture smell check and writes human/JSON output.
*/
export async function main(argv, io) {
return await runArchitectureSmellsCheck(argv, io);
}
runAsScript(import.meta.url, main);

View File

@@ -4,6 +4,7 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
import { visitModuleSpecifiers } from "./lib/guard-inventory-utils.mjs";
import {
collectTypeScriptFiles,
getPropertyNameText,
@@ -134,51 +135,33 @@ export function findChannelAgnosticBoundaryViolations(
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
const moduleViolations = new Map();
if (checkModuleSpecifiers) {
visitModuleSpecifiers(
ts,
sourceFile,
({ kind, node, specifier, specifierNode }) => {
if (moduleSpecifierMatcher(specifier)) {
const verb =
kind === "export"
? "re-exports"
: kind === "dynamic-import"
? "dynamically imports"
: "imports";
moduleViolations.set(node, {
line: toLine(sourceFile, specifierNode),
reason: verb + ' channel module "' + specifier + '"',
});
}
},
{ includeCommonJs: true, includeImportMetaUrl: true, includeImportTypes: true },
);
}
const visit = (node) => {
if (
checkModuleSpecifiers &&
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier)
) {
const specifier = node.moduleSpecifier.text;
if (moduleSpecifierMatcher(specifier)) {
violations.push({
line: toLine(sourceFile, node.moduleSpecifier),
reason: `imports channel module "${specifier}"`,
});
}
}
if (
checkModuleSpecifiers &&
ts.isExportDeclaration(node) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
const specifier = node.moduleSpecifier.text;
if (moduleSpecifierMatcher(specifier)) {
violations.push({
line: toLine(sourceFile, node.moduleSpecifier),
reason: `re-exports channel module "${specifier}"`,
});
}
}
if (
checkModuleSpecifiers &&
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length > 0 &&
ts.isStringLiteral(node.arguments[0])
) {
const specifier = node.arguments[0].text;
if (moduleSpecifierMatcher(specifier)) {
violations.push({
line: toLine(sourceFile, node.arguments[0]),
reason: `dynamically imports channel module "${specifier}"`,
});
}
const moduleViolation = moduleViolations.get(node);
if (moduleViolation) {
violations.push(moduleViolation);
}
if (

View File

@@ -3,22 +3,26 @@
// Inventories extension imports to enforce plugin SDK boundary rules.
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
BUNDLED_PLUGIN_PATH_PREFIX,
BUNDLED_PLUGIN_ROOT_DIR,
} from "./lib/bundled-plugin-paths.mjs";
import { createExtensionImportBoundaryChecker } from "./lib/extension-import-boundary-checker.mjs";
import { classifyBundledExtensionSourcePath } from "./lib/extension-source-classifier.mjs";
import {
collectModuleReferencesFromSource,
diffInventoryEntries,
normalizeRepoPath,
formatGroupedInventoryHuman,
resolveRepoSpecifier,
writeLine,
} from "./lib/guard-inventory-utils.mjs";
import { listGeneratedExtensionAssetSources } from "./lib/static-extension-assets.mjs";
import { resolveRepoRoot, runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const extensionsRoot = path.join(repoRoot, BUNDLED_PLUGIN_ROOT_DIR);
const repoRoot = resolveRepoRoot(import.meta.url);
// Generated bundles are validated at their build owner; they are not bounded authored source.
const generatedExtensionAssetSources = new Set(
listGeneratedExtensionAssetSources({ rootDir: repoRoot }),
);
const MODES = new Set([
"src-outside-plugin-sdk",
@@ -42,8 +46,6 @@ const baselinePathByMode = {
};
let allInventoryByModePromise;
let extensionModuleReferencesPromise;
const ruleTextByMode = {
"src-outside-plugin-sdk":
"Rule: production bundled plugins must not import src/** outside src/plugin-sdk/**",
@@ -53,82 +55,6 @@ const ruleTextByMode = {
"Rule: production bundled plugins must not use relative imports that escape their own package root",
};
function isCodeFile(fileName) {
return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(fileName);
}
function isBoundaryCanaryFile(fileName) {
return fileName.includes("__rootdir_boundary_canary__");
}
async function collectExtensionSourceFiles(rootDir) {
const out = [];
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === "dist" || entry.name === "node_modules") {
continue;
}
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
continue;
}
if (!entry.isFile() || !isCodeFile(entry.name) || isBoundaryCanaryFile(entry.name)) {
continue;
}
const relativePath = normalizeRepoPath(repoRoot, fullPath);
if (classifyBundledExtensionSourcePath(relativePath).isTestLike) {
continue;
}
out.push(fullPath);
}
}
await walk(rootDir);
return out.toSorted((left, right) =>
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
);
}
async function collectExtensionModuleReferences() {
if (!extensionModuleReferencesPromise) {
extensionModuleReferencesPromise = (async () => {
const files = await collectExtensionSourceFiles(extensionsRoot);
const referenced = await Promise.all(
files.map(async (filePath) => {
const source = await fs.readFile(filePath, "utf8");
if (!mayContainModuleSpecifier(source)) {
return null;
}
return {
filePath,
references: collectModuleReferencesFromSource(source),
};
}),
);
return referenced.filter((entry) => entry && entry.references.length > 0);
})();
}
return await extensionModuleReferencesPromise;
}
function mayContainModuleSpecifier(source) {
return (
/\bfrom\s*["']/.test(source) ||
/\bimport\s*(?:\(|["']|type\b|[\w*{])/.test(source) ||
/\bexport\s*(?:type\s+)?(?:\*|{)[^;\n]*\bfrom\s*["']/.test(source)
);
}
function resolveExtensionRoot(filePath) {
const relativePath = normalizeRepoPath(repoRoot, filePath);
const segments = relativePath.split("/");
if (segments[0] !== BUNDLED_PLUGIN_ROOT_DIR || !segments[1]) {
return null;
}
return `${segments[0]}/${segments[1]}`;
}
function classifyReason(mode, kind, resolvedPath, specifier) {
const verb =
kind === "export"
@@ -168,93 +94,90 @@ function compareEntries(left, right) {
);
}
function shouldReport(mode, resolvedPath) {
if (mode === "relative-outside-package") {
return false;
}
if (!resolvedPath?.startsWith("src/")) {
return false;
}
if (mode === "plugin-sdk-internal") {
return resolvedPath.startsWith("src/plugin-sdk-internal/");
}
return !resolvedPath.startsWith("src/plugin-sdk/");
}
function collectEntriesByModeFromModuleReferences(filePath, references) {
const entriesByMode = {
"src-outside-plugin-sdk": [],
"plugin-sdk-internal": [],
"relative-outside-package": [],
};
const extensionRoot = resolveExtensionRoot(filePath);
const relativeFile = normalizeRepoPath(repoRoot, filePath);
function push(kind, line, specifier) {
function collectBoundaryEntries({ filePath, relativeFile, references }) {
const extensionRoot = relativeFile.split("/").slice(0, 2).join("/");
const entries = [];
for (const { kind, line, specifier } of references) {
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
const baseEntry = {
file: relativeFile,
line,
kind,
specifier,
resolvedPath,
};
if (specifier.startsWith(".") && resolvedPath && extensionRoot) {
if (!(resolvedPath === extensionRoot || resolvedPath.startsWith(`${extensionRoot}/`))) {
entriesByMode["relative-outside-package"].push({
...baseEntry,
reason: classifyReason("relative-outside-package", kind, resolvedPath, specifier),
});
}
const modes = [];
if (
specifier.startsWith(".") &&
resolvedPath !== extensionRoot &&
!resolvedPath.startsWith(extensionRoot + "/")
) {
modes.push("relative-outside-package");
}
for (const mode of ["src-outside-plugin-sdk", "plugin-sdk-internal"]) {
if (!shouldReport(mode, resolvedPath)) {
continue;
}
entriesByMode[mode].push({
...baseEntry,
reason: classifyReason(mode, kind, resolvedPath, specifier),
if (resolvedPath.startsWith("src/") && !resolvedPath.startsWith("src/plugin-sdk/")) {
modes.push("src-outside-plugin-sdk");
}
if (resolvedPath.startsWith("src/plugin-sdk-internal/")) {
modes.push("plugin-sdk-internal");
}
for (const mode of modes) {
entries.push({
mode,
entry: {
file: relativeFile,
line,
kind,
specifier,
resolvedPath,
reason: classifyReason(mode, kind, resolvedPath, specifier),
},
});
}
}
for (const { kind, line, specifier } of references) {
push(kind, line, specifier);
}
return entriesByMode;
return entries;
}
/**
* Collects the current extension plugin SDK boundary inventory.
*/
const extensionBoundaryChecker = createExtensionImportBoundaryChecker({
roots: [BUNDLED_PLUGIN_ROOT_DIR],
sourceOptions: {
fileExtensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
includeTests: true,
skipDirectories: ["dist"],
},
shouldSkipFile(relativeFile) {
return (
generatedExtensionAssetSources.has(relativeFile) ||
path.basename(relativeFile).includes("__rootdir_boundary_canary__") ||
classifyBundledExtensionSourcePath(relativeFile).isTestLike
);
},
acceptSpecifier(specifier, { relativeFile, resolvedPath }) {
if (!resolvedPath) {
return false;
}
const extensionRoot = relativeFile.split("/").slice(0, 2).join("/");
return (
resolvedPath.startsWith("src/") ||
(specifier.startsWith(".") &&
resolvedPath !== extensionRoot &&
!resolvedPath.startsWith(extensionRoot + "/"))
);
},
collectEntries: collectBoundaryEntries,
compareEntries: (left, right) => compareEntries(left.entry, right.entry),
});
/** Collect the current extension plugin SDK boundary inventory. */
async function collectExtensionPluginSdkBoundaryInventory(mode) {
if (!MODES.has(mode)) {
throw new Error(`Unknown mode: ${mode}`);
throw new Error("Unknown mode: " + mode);
}
if (!allInventoryByModePromise) {
allInventoryByModePromise = (async () => {
const files = await collectExtensionModuleReferences();
const inventoryByMode = {
"src-outside-plugin-sdk": [],
"plugin-sdk-internal": [],
"relative-outside-package": [],
};
for (const { filePath, references } of files) {
const entriesByMode = collectEntriesByModeFromModuleReferences(filePath, references);
for (const inventoryMode of MODES) {
inventoryByMode[inventoryMode].push(...entriesByMode[inventoryMode]);
}
}
for (const inventoryMode of MODES) {
inventoryByMode[inventoryMode] = inventoryByMode[inventoryMode].toSorted(compareEntries);
}
return inventoryByMode;
})();
}
const inventoryByMode = await allInventoryByModePromise;
return inventoryByMode[mode];
allInventoryByModePromise ??= extensionBoundaryChecker
.collectInventory()
.then((entries) =>
Object.fromEntries(
[...MODES].map((inventoryMode) => [
inventoryMode,
entries
.filter(({ mode: entryMode }) => entryMode === inventoryMode)
.map(({ entry }) => entry),
]),
),
);
return (await allInventoryByModePromise)[mode];
}
/**
@@ -284,25 +207,15 @@ export function diffInventory(expected, actual) {
return diffInventoryEntries(expected, actual, compareEntries);
}
function formatInventoryHuman(mode, inventory) {
const lines = [ruleTextByMode[mode]];
if (inventory.length === 0) {
lines.push("No extension plugin-sdk boundary violations found.");
return lines.join("\n");
}
lines.push("Extension boundary inventory:");
let activeFile = "";
for (const entry of inventory) {
if (entry.file !== activeFile) {
activeFile = entry.file;
lines.push(activeFile);
}
lines.push(` - line ${entry.line} [${entry.kind}] ${entry.reason}`);
lines.push(` specifier: ${entry.specifier}`);
lines.push(` resolved: ${entry.resolvedPath}`);
}
return lines.join("\n");
}
const formatInventoryHuman = (mode, inventory) =>
formatGroupedInventoryHuman(
{
rule: ruleTextByMode[mode],
cleanMessage: "No extension plugin-sdk boundary violations found.",
inventoryTitle: "Extension boundary inventory:",
},
inventory,
);
/**
* Runs the boundary inventory check with CLI-style inputs and outputs.
@@ -367,6 +280,4 @@ export async function main(argv, io) {
return exitCode;
}
if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
await main();
}
runAsScript(import.meta.url, main);

View File

@@ -3,28 +3,17 @@
// Inventories core plugin imports that cross into bundled extension files.
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { BUNDLED_PLUGIN_PATH_PREFIX } from "./lib/bundled-plugin-paths.mjs";
import { createExtensionImportBoundaryChecker } from "./lib/extension-import-boundary-checker.mjs";
import {
collectTypeScriptInventory,
createCachedAsync,
diffInventoryEntries,
formatGroupedInventoryHuman,
normalizeRepoPath,
runBaselineInventoryCheck,
resolveRepoSpecifier,
visitModuleSpecifiers,
} from "./lib/guard-inventory-utils.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveSourceRoots,
runAsScript,
toLine,
} from "./lib/ts-guard-utils.mjs";
import { resolveRepoRoot, runAsScript } from "./lib/ts-guard-utils.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const scanRoots = resolveSourceRoots(repoRoot, ["src/plugins"]);
const repoRoot = resolveRepoRoot(import.meta.url);
const baselinePath = path.join(
repoRoot,
"test",
@@ -75,44 +64,18 @@ function classifyResolvedExtensionReason(kind, resolvedPath) {
return `${verb} extension-owned file from src/plugins`;
}
function pushEntry(entries, entry) {
entries.push(entry);
}
function scanImportBoundaryViolations(sourceFile, filePath) {
const entries = [];
const relativeFile = normalizeRepoPath(repoRoot, filePath);
visitModuleSpecifiers(ts, sourceFile, ({ kind, specifier, specifierNode }) => {
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
if (!resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
return;
}
pushEntry(entries, {
file: relativeFile,
line: toLine(sourceFile, specifierNode),
kind,
specifier,
resolvedPath,
reason: classifyResolvedExtensionReason(kind, resolvedPath),
});
});
return entries;
}
function scanWebSearchRegistrySmells(sourceFile, filePath) {
const relativeFile = normalizeRepoPath(repoRoot, filePath);
function scanWebSearchRegistrySmells(source, relativeFile) {
if (relativeFile !== "src/plugins/web-search-providers.ts") {
return [];
}
const entries = [];
const lines = sourceFile.text.split(/\r?\n/);
const lines = source.split(/\r?\n/);
for (const [index, line] of lines.entries()) {
const lineNumber = index + 1;
if (line.includes("web-search-plugin-factory.js")) {
pushEntry(entries, {
entries.push({
file: relativeFile,
line: lineNumber,
kind: "registry-smell",
@@ -124,7 +87,7 @@ function scanWebSearchRegistrySmells(sourceFile, filePath) {
const pluginMatch = line.match(/pluginId:\s*"([^"]+)"/);
if (pluginMatch && bundledWebSearchPluginIds.has(pluginMatch[1])) {
pushEntry(entries, {
entries.push({
file: relativeFile,
line: lineNumber,
kind: "registry-smell",
@@ -136,7 +99,7 @@ function scanWebSearchRegistrySmells(sourceFile, filePath) {
const providerMatch = line.match(/id:\s*"(brave|firecrawl|gemini|grok|kimi|perplexity)"/);
if (providerMatch && bundledWebSearchProviders.has(providerMatch[1])) {
pushEntry(entries, {
entries.push({
file: relativeFile,
line: lineNumber,
kind: "registry-smell",
@@ -150,37 +113,37 @@ function scanWebSearchRegistrySmells(sourceFile, filePath) {
return entries;
}
function shouldSkipFile(filePath) {
const relativeFile = normalizeRepoPath(repoRoot, filePath);
return (
relativeFile === "src/plugins/bundled-web-search-registry.ts" ||
relativeFile.startsWith("src/plugins/contracts/") ||
/^src\/plugins\/runtime\/runtime-[^/]+-contract\.[cm]?[jt]s$/u.test(relativeFile)
);
}
/**
* Cached inventory of src/plugins imports that cross into bundled extensions.
*/
export const collectPluginExtensionImportBoundaryInventory = createCachedAsync(async () => {
const files = (await collectTypeScriptFilesFromRoots(scanRoots))
.filter((filePath) => !shouldSkipFile(filePath))
.toSorted((left, right) =>
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
const boundaryChecker = createExtensionImportBoundaryChecker({
roots: ["src/plugins"],
shouldSkipFile(relativeFile) {
return (
relativeFile === "src/plugins/bundled-web-search-registry.ts" ||
relativeFile.startsWith("src/plugins/contracts/") ||
/^src\/plugins\/runtime\/runtime-[^/]+-contract\.[cm]?[jt]s$/u.test(relativeFile)
);
return await collectTypeScriptInventory({
ts,
files,
compareEntries,
collectEntries(sourceFile, filePath) {
return [
...scanImportBoundaryViolations(sourceFile, filePath),
...scanWebSearchRegistrySmells(sourceFile, filePath),
];
},
});
},
collectEntries({ source, filePath, relativeFile, references }) {
return [
...references.map(({ kind, line, specifier }) => {
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
return {
file: relativeFile,
line,
kind,
specifier,
resolvedPath,
reason: classifyResolvedExtensionReason(kind, resolvedPath),
};
}),
...scanWebSearchRegistrySmells(source, relativeFile),
];
},
compareEntries,
});
/** Cached inventory of src/plugins imports that cross into bundled extensions. */
export const collectPluginExtensionImportBoundaryInventory = boundaryChecker.collectInventory;
/**
* Cached expected plugin-extension import inventory baseline.
*/

View File

@@ -48,11 +48,7 @@ function readPrivateLocalOnlySubpaths() {
}
function parsePluginSdkSubpath(specifier) {
if (!specifier.startsWith("openclaw/plugin-sdk/")) {
return null;
}
const subpath = specifier.slice("openclaw/plugin-sdk/".length);
return subpath || null;
return specifier.match(/^@?openclaw\/plugin-sdk\/(.+)$/u)?.[1] ?? null;
}
function isGeneratedBuildArtifact(filePath) {
@@ -153,9 +149,14 @@ async function collectViolations() {
});
}
visitModuleSpecifiers(ts, sourceFile, ({ kind, node, specifier, specifierNode }) => {
push(kind, node, specifierNode, specifier);
});
visitModuleSpecifiers(
ts,
sourceFile,
({ kind, node, specifier, specifierNode }) => {
push(kind, node, specifierNode, specifier);
},
{ includeCommonJs: true, includeImportTypes: true },
);
}
return violations.toSorted(compareEntries);

View File

@@ -18,6 +18,26 @@ import {
const repoRoot = resolveRepoRoot(import.meta.url);
const DEFAULT_BOUNDARY_SOURCE_MAX_BYTES = 2 * 1024 * 1024;
// Escaped plugin paths must reach the scanner without lexing every unrelated escaped source.
const ESCAPED_BUNDLED_PLUGIN_PATH_PREFIX_RE = new RegExp(
Array.from(BUNDLED_PLUGIN_PATH_PREFIX, (character) => {
const hex = character.charCodeAt(0).toString(16).padStart(2, "0");
return (
"(?:" +
character +
"|\\\\(?:x" +
hex +
"|u00" +
hex +
"|u\\{0*" +
hex +
"\\}|" +
character +
"))(?:\\\\(?:\\r\\n|[\\r\\n\\u2028\\u2029]))*"
);
}).join(""),
"iu",
);
function compareEntries(left, right) {
return (
@@ -39,13 +59,11 @@ function classifyResolvedExtensionReason(kind, boundaryLabel) {
return `${verb} bundled plugin file from ${boundaryLabel} boundary`;
}
function scanImportBoundaryViolations(source, filePath, boundaryLabel, allowResolvedPath) {
function scanImportBoundaryViolations(references, filePath, boundaryLabel, allowResolvedPath) {
const entries = [];
const relativeFile = normalizeRepoPath(repoRoot, filePath);
for (const reference of collectModuleReferencesFromSource(source)) {
const kind = reference.kind;
const specifier = reference.specifier;
for (const { kind, line, specifier } of references) {
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
if (!resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) {
continue;
@@ -55,7 +73,7 @@ function scanImportBoundaryViolations(source, filePath, boundaryLabel, allowReso
}
entries.push({
file: relativeFile,
line: reference.line,
line,
kind,
specifier,
resolvedPath,
@@ -96,7 +114,7 @@ export function createExtensionImportBoundaryChecker(params) {
const maxSourceBytes = normalizeMaxSourceBytes(params.maxSourceBytes);
const collectInventory = createCachedAsync(async () => {
const files = (await collectTypeScriptFilesFromRoots(scanRoots))
const files = (await collectTypeScriptFilesFromRoots(scanRoots, params.sourceOptions))
.filter((filePath) => !params.shouldSkipFile?.(normalizeRepoPath(repoRoot, filePath)))
.toSorted((left, right) =>
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
@@ -105,23 +123,36 @@ export function createExtensionImportBoundaryChecker(params) {
files,
async (filePath) => {
const source = await readBoundedSourceFile(filePath, maxSourceBytes);
const relativeFile = normalizeRepoPath(repoRoot, filePath);
if (
params.skipSourcesWithoutBundledPluginPrefix &&
!source.includes(BUNDLED_PLUGIN_PATH_PREFIX)
!source.includes(BUNDLED_PLUGIN_PATH_PREFIX) &&
(!source.includes("\\") || !ESCAPED_BUNDLED_PLUGIN_PATH_PREFIX_RE.test(source))
) {
return [];
}
return scanImportBoundaryViolations(
source,
filePath,
params.boundaryLabel,
params.allowResolvedPath,
);
const references = collectModuleReferencesFromSource(source, {
fileName: filePath,
acceptSpecifier(specifier) {
const resolvedPath = resolveRepoSpecifier(repoRoot, specifier, filePath);
return params.acceptSpecifier
? params.acceptSpecifier(specifier, { filePath, relativeFile, resolvedPath })
: Boolean(resolvedPath?.startsWith(BUNDLED_PLUGIN_PATH_PREFIX));
},
});
return params.collectEntries
? params.collectEntries({ source, filePath, relativeFile, references })
: scanImportBoundaryViolations(
references,
filePath,
params.boundaryLabel,
params.allowResolvedPath,
);
},
{ concurrency: 32, stopOnError: true },
);
const inventory = entriesByFile.flat();
return inventory.toSorted(compareEntries);
return inventory.toSorted(params.compareEntries ?? compareEntries);
});
async function main(argv, io) {

View File

@@ -14,6 +14,7 @@ export function visitModuleSpecifiers(
options?: {
includeCommonJs?: boolean;
includeImportMetaUrl?: boolean;
includeImportTypes?: boolean;
},
): void;
/** Diff expected and actual inventory entries using JSON identity. */
@@ -27,8 +28,15 @@ export function diffInventoryEntries(
};
/** Write one line to a stream without each caller repeating newline handling. */
export function writeLine(stream: unknown, text: unknown): void;
/** Collect import/export/dynamic-import references from source text without full parsing. */
export function collectModuleReferencesFromSource(source: unknown): unknown[];
/** Lexically reject clean files before parsing candidate module-boundary violations. */
export function collectModuleReferencesFromSource(
source: string,
options?: {
acceptSpecifier?: (specifier: string) => boolean;
fileName?: string;
ts?: unknown;
},
): Array<{ kind: string; line: number; specifier: string }>;
/** Memoize an async factory while resetting the cache after failures. */
export function createCachedAsync(factory: unknown): () => Promise<unknown>;
/** Format grouped inventory entries for human-readable guard output. */

View File

@@ -1,9 +1,12 @@
// Shared parsing, diffing, and reporting helpers for inventory guard scripts.
import { promises as fs } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
const parsedTypeScriptSourceCache = new Map();
const sourceTextCache = new Map();
const require = createRequire(import.meta.url);
let cachedTypeScript;
/** Convert an absolute file path to a repo-relative POSIX path. */
export function normalizeRepoPath(repoRoot, filePath) {
@@ -24,50 +27,53 @@ export function resolveRepoSpecifier(repoRoot, specifier, importerFile) {
/** Visit static and dynamic module specifiers in a parsed TypeScript source file. */
export function visitModuleSpecifiers(ts, sourceFile, visit, options = {}) {
function walk(node) {
let kind;
let specifierNode;
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
visit({
kind: "import",
node,
specifier: node.moduleSpecifier.text,
specifierNode: node.moduleSpecifier,
});
kind = "import";
specifierNode = node.moduleSpecifier;
} else if (
ts.isExportDeclaration(node) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
visit({
kind: "export",
node,
specifier: node.moduleSpecifier.text,
specifierNode: node.moduleSpecifier,
});
kind = "export";
specifierNode = node.moduleSpecifier;
} else if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
node.arguments.length === 1 &&
ts.isStringLiteral(node.arguments[0])
node.arguments.length >= 1 &&
ts.isStringLiteralLike(node.arguments[0])
) {
visit({
kind: "dynamic-import",
node,
specifier: node.arguments[0].text,
specifierNode: node.arguments[0],
});
kind = "dynamic-import";
specifierNode = node.arguments[0];
} else if (
options.includeImportTypes &&
ts.isImportTypeNode(node) &&
ts.isLiteralTypeNode(node.argument) &&
ts.isStringLiteralLike(node.argument.literal)
) {
kind = "dynamic-import";
specifierNode = node.argument.literal;
} else if (
options.includeCommonJs &&
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === "require" &&
node.arguments.length === 1 &&
node.arguments.length >= 1 &&
ts.isStringLiteralLike(node.arguments[0])
) {
visit({
kind: "commonjs-require",
node,
specifier: node.arguments[0].text,
specifierNode: node.arguments[0],
});
kind = "commonjs-require";
specifierNode = node.arguments[0];
} else if (
options.includeCommonJs &&
ts.isImportEqualsDeclaration(node) &&
ts.isExternalModuleReference(node.moduleReference) &&
node.moduleReference.expression &&
ts.isStringLiteralLike(node.moduleReference.expression)
) {
kind = "commonjs-require";
specifierNode = node.moduleReference.expression;
} else if (
options.includeImportMetaUrl &&
ts.isNewExpression(node) &&
@@ -81,14 +87,13 @@ export function visitModuleSpecifiers(ts, sourceFile, visit, options = {}) {
node.arguments[1].expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
node.arguments[1].expression.name.text === "meta"
) {
visit({
kind: "import-meta-url",
node,
specifier: node.arguments[0].text,
specifierNode: node.arguments[0],
});
kind = "import-meta-url";
specifierNode = node.arguments[0];
}
if (specifierNode) {
visit({ kind, node, specifier: specifierNode.text, specifierNode });
}
ts.forEachChild(node, walk);
}
@@ -114,44 +119,135 @@ export function writeLine(stream, text) {
stream.write(`${text}\n`);
}
/** Collect import/export/dynamic-import references from source text without full parsing. */
export function collectModuleReferencesFromSource(source) {
const lineStarts = computeLineStarts(source);
const isCodePosition = createCodePositionChecker(source);
const references = [];
const push = (kind, specifier, position, syntaxPosition) => {
if (!isCodePosition(syntaxPosition)) {
return;
}
references.push({
kind,
line: lineFromPosition(lineStarts, position),
specifier,
});
function hasSupplementalModuleReference(ts, source, acceptSpecifier) {
const checkUrl = source.includes("new") && source.includes("import") && source.includes("meta");
const interpolationStart = source.indexOf("${");
const checkRequire =
interpolationStart >= 0 &&
(source.includes("require") || source.indexOf("\\u", interpolationStart + 2) >= 0);
const interpolatedSlash =
interpolationStart < 0 ? -1 : source.indexOf("/", interpolationStart + 2);
const checkTemplateImport =
interpolatedSlash >= 0 && source.indexOf("import", interpolatedSlash + 1) >= 0;
const checkNamespaceExport =
source.includes("export") && source.includes("*") && source.includes("as");
if (!checkUrl && !checkRequire && !checkTemplateImport && !checkNamespaceExport) {
return false;
}
const scanner = ts.createScanner(
ts.ScriptTarget.Latest,
true,
ts.LanguageVariant.Standard,
source,
);
const kinds = ts.SyntaxKind;
const templateBraceDepths = [];
const scanAcceptedSpecifier = () => {
const token = scanner.scan();
return (
(token === kinds.StringLiteral || token === kinds.NoSubstitutionTemplateLiteral) &&
acceptSpecifier(scanner.getTokenValue())
);
};
for (const match of source.matchAll(/\bimport\s*\(\s*(["'])([^"']+)\1/g)) {
push("dynamic-import", match[2], match.index + match[0].lastIndexOf(match[1]), match.index);
for (let token = scanner.scan(); token !== kinds.EndOfFileToken; token = scanner.scan()) {
if (
checkUrl &&
token === kinds.NewKeyword &&
scanner.lookAhead(
() =>
scanner.scan() === kinds.Identifier &&
scanner.getTokenValue() === "URL" &&
scanner.scan() === kinds.OpenParenToken &&
scanAcceptedSpecifier(),
)
) {
return true;
}
if (
checkRequire &&
(token === kinds.RequireKeyword ||
(token === kinds.Identifier && scanner.getTokenValue() === "require")) &&
scanner.lookAhead(() => scanner.scan() === kinds.OpenParenToken && scanAcceptedSpecifier())
) {
return true;
}
if (
checkNamespaceExport &&
token === kinds.ExportKeyword &&
scanner.lookAhead(() => {
let next = scanner.scan();
if (next === kinds.TypeKeyword) {
next = scanner.scan();
}
return next === kinds.AsteriskToken && scanner.scan() === kinds.AsKeyword;
})
) {
return true;
}
// A context-free slash may start a regexp whose `}` would corrupt interpolation tracking.
if (
templateBraceDepths.length > 0 &&
(token === kinds.SlashToken || token === kinds.SlashEqualsToken)
) {
return true;
}
// Resume after each nested interpolation; otherwise template tails swallow later imports.
if (token === kinds.TemplateHead) {
templateBraceDepths.push(0);
} else if (token === kinds.OpenBraceToken && templateBraceDepths.length > 0) {
templateBraceDepths[templateBraceDepths.length - 1] += 1;
} else if (token === kinds.CloseBraceToken && templateBraceDepths.length > 0) {
const index = templateBraceDepths.length - 1;
if (templateBraceDepths[index] > 0) {
templateBraceDepths[index] -= 1;
} else if (scanner.reScanTemplateToken(false) === kinds.TemplateTail) {
templateBraceDepths.pop();
}
}
}
for (const match of source.matchAll(/^\s*import\s*(["'])([^"']+)\1/gm)) {
push(
"import",
match[2],
match.index + match[0].lastIndexOf(match[1]),
match.index + match[0].indexOf("import"),
);
}
for (const match of source.matchAll(
/^\s*(import|export)\s+(?:type\s+)?[^;"']*?\bfrom\s*(["'])([^"']+)\2/gm,
)) {
push(
match[1],
match[3],
match.index + match[0].lastIndexOf(match[2]),
match.index + match[0].indexOf(match[1]),
);
return false;
}
/** Lexically reject clean files before parsing candidate module-boundary violations. */
export function collectModuleReferencesFromSource(source, options = {}) {
const ts = options.ts ?? (cachedTypeScript ??= require("typescript"));
const acceptSpecifier = options.acceptSpecifier ?? (() => true);
const candidates = ts.preProcessFile(source, true, true).importedFiles;
if (
!candidates.some(({ fileName }) => acceptSpecifier(fileName)) &&
!hasSupplementalModuleReference(ts, source, acceptSpecifier)
) {
return [];
}
const sourceFile = ts.createSourceFile(
options.fileName ?? "source.ts",
source,
ts.ScriptTarget.Latest,
true,
);
const references = [];
visitModuleSpecifiers(
ts,
sourceFile,
({ kind, specifier, specifierNode }) => {
if (acceptSpecifier(specifier)) {
references.push({
kind,
line:
sourceFile.getLineAndCharacterOfPosition(specifierNode.getStart(sourceFile)).line + 1,
specifier,
});
}
},
{ includeCommonJs: true, includeImportMetaUrl: true, includeImportTypes: true },
);
return references.toSorted(
(left, right) =>
left.line - right.line ||
@@ -160,77 +256,6 @@ export function collectModuleReferencesFromSource(source) {
);
}
function createCodePositionChecker(source) {
const codePositions = new Uint8Array(source.length);
for (let index = 0; index < source.length; index += 1) {
const char = source[index];
const next = source[index + 1];
if (char === "/" && next === "/") {
index += 2;
while (index < source.length && source.charCodeAt(index) !== 10) {
index += 1;
}
index -= 1;
continue;
}
if (char === "/" && next === "*") {
index += 2;
while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) {
index += 1;
}
index += 1;
continue;
}
if (char === "'" || char === '"' || char === "`") {
const quote = char;
index += 1;
while (index < source.length) {
if (source[index] === "\\") {
index += 2;
continue;
}
if (source[index] === quote) {
break;
}
index += 1;
}
continue;
}
codePositions[index] = 1;
}
return (position) => codePositions[position] === 1;
}
function computeLineStarts(source) {
const lineStarts = [0];
for (let index = 0; index < source.length; index += 1) {
if (source.charCodeAt(index) === 10) {
lineStarts.push(index + 1);
}
}
return lineStarts;
}
function lineFromPosition(lineStarts, position) {
let low = 0;
let high = lineStarts.length - 1;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
if (lineStarts[middle] <= position) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return high + 1;
}
/** Memoize an async factory while resetting the cache after failures. */
export function createCachedAsync(factory) {
let cachedPromise = null;

View File

@@ -13,8 +13,10 @@ export function collectTypeScriptFiles(
targetPath: string,
options?: {
extraTestSuffixes?: string[];
fileExtensions?: string[];
ignoreMissing?: boolean;
includeTests?: boolean;
skipDirectories?: string[];
skipNodeModules?: boolean;
},
): Promise<string[]>;
@@ -25,7 +27,9 @@ export function collectTypeScriptFilesFromRoots(
sourceRoots: string[],
options?: {
extraTestSuffixes?: string[];
fileExtensions?: string[];
includeTests?: boolean;
skipDirectories?: string[];
skipNodeModules?: boolean;
},
): Promise<string[]>;

View File

@@ -49,10 +49,14 @@ function isTestLikeTypeScriptFile(filePath, options = {}) {
* Recursively collects TypeScript files under a file or directory target.
*/
export async function collectTypeScriptFiles(targetPath, options = {}) {
const fileExtensions = options.fileExtensions ?? [".ts"];
const includeTests = options.includeTests ?? false;
const extraTestSuffixes = options.extraTestSuffixes ?? [];
const skipNodeModules = options.skipNodeModules ?? true;
const skipDirectories = options.skipDirectories ?? [];
const ignoreMissing = options.ignoreMissing ?? false;
const isSourceFile = (filePath) =>
fileExtensions.some((extension) => filePath.endsWith(extension));
let stat;
try {
@@ -71,7 +75,7 @@ export async function collectTypeScriptFiles(targetPath, options = {}) {
}
if (stat.isFile()) {
if (!targetPath.endsWith(".ts")) {
if (!isSourceFile(targetPath)) {
return [];
}
if (!includeTests && isTestLikeTypeScriptFile(targetPath, { extraTestSuffixes })) {
@@ -85,13 +89,16 @@ export async function collectTypeScriptFiles(targetPath, options = {}) {
for (const entry of entries) {
const entryPath = path.join(targetPath, entry.name);
if (entry.isDirectory()) {
if (skipNodeModules && entry.name === "node_modules") {
if (
(skipNodeModules && entry.name === "node_modules") ||
skipDirectories.includes(entry.name)
) {
continue;
}
out.push(...(await collectTypeScriptFiles(entryPath, options)));
continue;
}
if (!entry.isFile() || !entryPath.endsWith(".ts")) {
if (!entry.isFile() || !isSourceFile(entryPath)) {
continue;
}
if (!includeTests && isTestLikeTypeScriptFile(entryPath, { extraTestSuffixes })) {

View File

@@ -208,4 +208,27 @@ describe("audit gateway methods", () => {
expect(listAuditEvents).toHaveBeenCalledWith(expect.objectContaining({ cursor: 11 }));
},
);
it.each(["audit.list", "audit.activity.list"] as const)(
"trims exact-match filter ids for %s before store lookup",
async (method) => {
const respond = await runAuditHandler(method, {
agentId: " main ",
sessionKey: " agent:main:main ",
runId: " run-1 ",
});
expect(respond).toHaveBeenCalledWith(true, expect.anything());
expect(listAuditEvents).toHaveBeenCalledWith(
expect.objectContaining({
filters: expect.objectContaining({
agentId: "main",
sessionKey: "agent:main:main",
runId: "run-1",
...(method === "audit.activity.list" ? { includeMessages: true } : {}),
}),
}),
);
},
);
});

View File

@@ -0,0 +1,84 @@
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../../../test/helpers/temp-dir.js";
import { listAuditEvents, recordAuditEvent } from "../../audit/audit-event-store.js";
import {
closeOpenClawStateDatabaseForTest,
type OpenClawStateDatabaseOptions,
} from "../../state/openclaw-state-db.js";
import { auditHandlers } from "./audit.js";
const tempDirs: string[] = [];
function createDatabaseOptions(): OpenClawStateDatabaseOptions {
return { env: { OPENCLAW_STATE_DIR: makeTempDir(tempDirs, "openclaw-audit-trim-") } };
}
afterEach(() => {
closeOpenClawStateDatabaseForTest();
delete process.env.OPENCLAW_STATE_DIR;
});
afterAll(() => {
cleanupTempDirs(tempDirs);
});
describe("audit.list padded filters against a real audit store", () => {
it("returns the planted run for padded agentId/runId filters", async () => {
const database = createDatabaseOptions();
const stateDir = expectDefined(database.env?.OPENCLAW_STATE_DIR, "temp state dir");
process.env.OPENCLAW_STATE_DIR = stateDir;
recordAuditEvent(
{
sourceId: "audit-trim-run-source",
sourceSequence: 1,
occurredAt: Date.now(),
kind: "agent_run",
action: "agent.run.finished",
status: "succeeded",
actorType: "agent",
actorId: "main",
agentId: "main",
sessionKey: "agent:main:main",
runId: "run-trim-1",
},
database,
);
// Negative control: untrimmed filter values miss the planted row at the store.
expect(
listAuditEvents({
limit: 20,
filters: { agentId: " main ", runId: " run-trim-1 " },
database,
}).events,
).toEqual([]);
const respond = vi.fn();
await expectDefined(
auditHandlers["audit.list"],
"audit.list handler",
)({
params: {
agentId: " main ",
sessionKey: " agent:main:main ",
runId: " run-trim-1 ",
},
respond,
} as never);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
events: [
expect.objectContaining({
agentId: "main",
runId: "run-trim-1",
action: "agent.run.finished",
}),
],
}),
);
});
});

View File

@@ -1,4 +1,5 @@
// Metadata-only operator audit queries over the canonical shared SQLite ledger.
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
ErrorCodes,
errorShape,
@@ -90,13 +91,16 @@ export const auditHandlers: GatewayRequestHandlers = {
);
return;
}
const agentId = normalizeOptionalString(params.agentId);
const sessionKey = normalizeOptionalString(params.sessionKey);
const runId = normalizeOptionalString(params.runId);
const page = listAuditEvents({
limit: Math.min(params.limit ?? DEFAULT_AUDIT_LIST_LIMIT, MAX_AUDIT_LIST_LIMIT),
...(parsed.cursor !== undefined ? { cursor: parsed.cursor } : {}),
filters: {
...(params.agentId ? { agentId: params.agentId } : {}),
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.runId ? { runId: params.runId } : {}),
...(agentId ? { agentId } : {}),
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
...(params.kind ? { kind: params.kind } : {}),
...(params.status ? { status: params.status } : {}),
...(params.after !== undefined ? { after: params.after } : {}),
@@ -128,14 +132,17 @@ export const auditHandlers: GatewayRequestHandlers = {
);
return;
}
const agentId = normalizeOptionalString(params.agentId);
const sessionKey = normalizeOptionalString(params.sessionKey);
const runId = normalizeOptionalString(params.runId);
const page = listAuditEvents({
limit: Math.min(params.limit ?? DEFAULT_AUDIT_LIST_LIMIT, MAX_AUDIT_LIST_LIMIT),
...(parsed.cursor !== undefined ? { cursor: parsed.cursor } : {}),
filters: {
includeMessages: true,
...(params.agentId ? { agentId: params.agentId } : {}),
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.runId ? { runId: params.runId } : {}),
...(agentId ? { agentId } : {}),
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
...(params.kind ? { kind: params.kind } : {}),
...(params.status ? { status: params.status } : {}),
...(params.direction ? { direction: params.direction } : {}),

View File

@@ -1,33 +1,229 @@
// Architecture smell tests flag unwanted dependency and layout patterns.
// Architecture boundary tests cover hostile module-specifier syntax in the canonical scanner.
import { describe, expect, it } from "vitest";
import { collectArchitectureSmells, main } from "../scripts/check-architecture-smells.mjs";
import { createCapturedIo } from "./helpers/captured-io.js";
import { collectModuleReferencesFromSource } from "../scripts/lib/guard-inventory-utils.mjs";
const smellsPromise = collectArchitectureSmells();
const forbiddenPath = "../../src/private.js";
const templateQuote = String.fromCharCode(96);
describe("architecture smell inventory", () => {
it("produces stable sorted output", async () => {
const smells = await smellsPromise;
describe("architecture boundary module reference scanner", () => {
it.each([
{
name: "commented side-effect import",
source: 'import /* gap */ "../../src/private.js"',
kind: "import",
},
{
name: "commented dynamic import with attributes",
source: 'await import /* gap */ ("../../src/private.js", { with: { type: "json" } })',
kind: "dynamic-import",
},
{
name: "dynamic import inside template interpolation",
source:
"const message = " +
templateQuote +
"$" +
'{await import("../../src/private.js")}' +
templateQuote,
kind: "dynamic-import",
},
{
name: "dynamic import after a regexp brace inside template interpolation",
source: 'const message = `${/}/.test("safe") ? import("../../src/private.js") : null}`',
kind: "dynamic-import",
},
{
name: "commented dynamic import after a regexp character-class brace",
source:
'const message = `${/[}]/.test("safe") ? import /* gap */ ("../../src/private.js", { with: { type: "json" } }) : null}`',
kind: "dynamic-import",
},
{
name: "CommonJS require",
source: 'require("../../src/private.js")',
kind: "commonjs-require",
},
{
name: "CommonJS require inside template interpolation",
source:
"const message = " +
templateQuote +
"$" +
'{require("../../src/private.js")}' +
templateQuote,
kind: "commonjs-require",
},
{
name: "CommonJS require inside nested template interpolation",
source: 'const message = `outer ${`inner ${require("../../src/private.js")}`} tail`',
kind: "commonjs-require",
},
{
name: "CommonJS require with a Unicode-escaped first character inside interpolation",
source: 'const message = `${\\u0072equire("../../src/private.js")}`',
kind: "commonjs-require",
},
{
name: "CommonJS require with a Unicode-escaped middle character inside interpolation",
source: 'const message = `${r\\u0065quire("../../src/private.js")}`',
kind: "commonjs-require",
},
{
name: "CommonJS require with a Unicode-escaped braced character inside interpolation",
source: 'const message = `${\\u{72}equire("../../src/private.js")}`',
kind: "commonjs-require",
},
{
name: "CommonJS require with a Unicode-escaped last character inside interpolation",
source: 'const message = `${requir\\u0065("../../src/private.js")}`',
kind: "commonjs-require",
},
{
name: "CommonJS require after a regexp brace inside template interpolation",
source: 'const message = `${/}/.test("safe") ? require("../../src/private.js") : null}`',
kind: "commonjs-require",
},
{
name: "CommonJS require after a regexp character-class brace inside interpolation",
source: 'const message = `${/[}]/.test("safe") ? require("../../src/private.js") : null}`',
kind: "commonjs-require",
},
{
name: "CommonJS require after division inside template interpolation",
source: 'const message = `${(24 / 2) ? require("../../src/private.js") : null}`',
kind: "commonjs-require",
},
{
name: "TypeScript import-equals require",
source: 'import privateModule = require("../../src/private.js")',
kind: "commonjs-require",
},
{
name: "import.meta URL with comments",
source: 'new URL("../../src/private.js", import /* gap */ . meta . url)',
kind: "import-meta-url",
},
{
name: "import.meta URL with escaped constructor",
source: 'new \\u0055RL("../../src/private.js", import.meta.url)',
kind: "import-meta-url",
},
{
name: "import.meta URL with a template specifier",
source: "new URL(`../../src/private.js`, import.meta.url)",
kind: "import-meta-url",
},
{
name: "import.meta URL following a completed template interpolation",
source:
'const message = `prefix ${"allowed"} tail`; new URL("../../src/private.js", import.meta.url)',
kind: "import-meta-url",
},
{
name: "import.meta URL after a regexp brace inside template interpolation",
source:
'const message = `${/}/.test("safe") ? new URL("../../src/private.js", import.meta.url) : null}`',
kind: "import-meta-url",
},
{
name: "runtime namespace re-export",
source: 'export /* gap */ * /* gap */ as privateModule from "../../src/private.js"',
kind: "export",
},
{
name: "type namespace re-export",
source: 'export type * as privateModule from "../../src/private.js"',
kind: "export",
},
{
name: "namespace re-export following nested template interpolations",
source:
'const message = `outer ${`inner ${1}`} tail`; export * as privateModule from "../../src/private.js"',
kind: "export",
},
{
name: "TypeScript import type",
source: 'type PrivateModule = typeof import("../../src/private.js")',
kind: "dynamic-import",
},
{
name: "escaped module specifier",
source: 'import "../../src/priv\\u0061te.js"',
kind: "import",
},
])("detects $name", ({ source, kind }) => {
expect(
collectModuleReferencesFromSource(source, {
acceptSpecifier: (specifier) => specifier === forbiddenPath,
}),
).toEqual([{ kind, line: 1, specifier: forbiddenPath }]);
});
it("ignores comments, string contents, and allowed module specifiers", () => {
expect(
collectModuleReferencesFromSource(
[
'// import "../../src/private.js"',
'const text = "require(\\\"../../src/private.js\\\")";',
'import "../../src/allowed.js";',
].join("\n"),
{ acceptSpecifier: (specifier) => specifier === forbiddenPath },
),
).toEqual([]);
});
it.each([
'const message = `${(24 / 2) ? require("../../src/allowed.js") : null}`',
'const message = `${/}/.test("safe") ? require("../../src/allowed.js") : null}`',
'const message = `${\\u0072equire("../../src/allowed.js")}`',
'const message = `${/}/.test("safe") ? import("../../src/allowed.js") : null}`',
])("keeps ordinary division and regexp fallback free of false positives", (source) => {
expect(
collectModuleReferencesFromSource(source, {
acceptSpecifier: (specifier) => specifier === forbiddenPath,
}),
).toEqual([]);
});
it("preserves the actual source line for multiline guarded imports", () => {
expect(
collectModuleReferencesFromSource('\n\nimport /* comment */ "../../src/private.js";', {
acceptSpecifier: (specifier) => specifier === forbiddenPath,
}),
).toEqual([{ kind: "import", line: 3, specifier: forbiddenPath }]);
});
it.each([
{
name: "import.meta URL",
createSource: (comments: string) =>
"new /*" + comments + '! URL("../../src/allowed.js", import.meta.url)',
},
{
name: "embedded CommonJS require",
createSource: (comments: string) =>
"const message = " +
templateQuote +
"$" +
"{require /*" +
comments +
'! ("../../src/allowed.js")}' +
templateQuote,
},
{
name: "namespace re-export",
createSource: (comments: string) =>
"export /*" + comments + '! * as privateModule from "../../src/allowed.js"',
},
])("scans hostile repeated comments in linear time for $name", ({ createSource }) => {
const source = createSource("*//*".repeat(8_192));
const startedAt = performance.now();
expect(
[...smells].toSorted(
(left, right) =>
left.category.localeCompare(right.category) ||
left.file.localeCompare(right.file) ||
left.line - right.line ||
left.kind.localeCompare(right.kind) ||
left.specifier.localeCompare(right.specifier) ||
left.reason.localeCompare(right.reason),
),
).toEqual(smells);
});
it("script json output matches the collector", async () => {
const captured = createCapturedIo();
const exitCode = await main(["--json"], captured.io);
expect(exitCode).toBe(0);
expect(captured.readStderr()).toBe("");
expect(JSON.parse(captured.readStdout())).toEqual(await smellsPromise);
collectModuleReferencesFromSource(source, {
acceptSpecifier: (specifier) => specifier === forbiddenPath,
}),
).toEqual([]);
expect(performance.now() - startedAt).toBeLessThan(1_000);
});
});

View File

@@ -125,4 +125,40 @@ describe("check-channel-agnostic-boundaries", () => {
`;
expect(findSystemMarkLiteralViolations(source)).toStrictEqual([]);
});
it.each([
{
name: "commented dynamic import with attributes",
source: 'await import /* gap */ ("../discord/private.js", { with: { type: "json" } });',
reason: 'dynamically imports channel module "../discord/private.js"',
},
{
name: "CommonJS require",
source: 'require("../telegram/private.js");',
reason: 'imports channel module "../telegram/private.js"',
},
{
name: "import.meta URL",
source: 'new URL("../slack/private.js", import.meta.url);',
reason: 'imports channel module "../slack/private.js"',
},
{
name: "TypeScript import type",
source: 'type PrivateModule = typeof import("../signal/private.js");',
reason: 'dynamically imports channel module "../signal/private.js"',
},
])("flags $name in protected channel-independent sources", ({ source, reason }) => {
expect(findChannelAgnosticBoundaryViolations(source)).toEqual([{ line: 1, reason }]);
});
it("preserves source order when imports and config paths share a line", () => {
expect(
findChannelAgnosticBoundaryViolations(
'import "../telegram/private.js"; const enabled = cfg.channels.discord;',
),
).toEqual([
{ line: 1, reason: 'imports channel module "../telegram/private.js"' },
{ line: 1, reason: 'references config path "channels.discord"' },
]);
});
});

View File

@@ -1,9 +1,10 @@
// Extension import boundary checker tests cover bounded source reads.
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createExtensionImportBoundaryChecker } from "../../scripts/lib/extension-import-boundary-checker.mjs";
import { listGeneratedExtensionAssetSources } from "../../scripts/lib/static-extension-assets.mjs";
const tempDirs: string[] = [];
@@ -36,4 +37,132 @@ describe("extension import boundary checker", () => {
"extension import boundary source file exceeds 32 byte limit",
);
});
it("skips declared generated bundles without admitting oversized handwritten JavaScript", async () => {
const root = makeTempRoot();
const pluginRoot = path.join(root, "extensions", "generated-plugin");
const assetRoot = path.join(pluginRoot, "assets");
const generatedPath = path.join(assetRoot, "generated.js");
const handwrittenPath = path.join(assetRoot, "handwritten.js");
const oversizedBytes = 2 * 1024 * 1024 + 1;
mkdirSync(assetRoot, { recursive: true });
writeFileSync(
path.join(pluginRoot, "package.json"),
JSON.stringify({
openclaw: {
assetScripts: { build: "node generate.mjs" },
build: {
staticAssets: [{ source: "./assets/generated.js", output: "assets/generated.js" }],
},
},
}),
"utf8",
);
// Match CI ordering: the build materializes its declared bundle before source guards run.
writeFileSync(generatedPath, "/".repeat(oversizedBytes), "utf8");
const generatedSources = new Set(
listGeneratedExtensionAssetSources({ rootDir: root }).map((source) =>
path.relative(process.cwd(), path.join(root, source)).replaceAll(path.sep, "/"),
),
);
const createChecker = () =>
createExtensionImportBoundaryChecker({
boundaryLabel: "test",
cleanMessage: "clean",
inventoryTitle: "inventory",
roots: [path.relative(process.cwd(), path.join(root, "extensions"))],
shouldSkipFile: (relativeFile: string) => generatedSources.has(relativeFile),
sourceOptions: { fileExtensions: [".js"] },
});
await expect(createChecker().collectInventory()).resolves.toEqual([]);
const targetPath = path.join(process.cwd(), "extensions", "security-proof", "private.js");
const specifier = path.relative(assetRoot, targetPath).replaceAll(path.sep, "/");
writeFileSync(
handwrittenPath,
"import " + JSON.stringify(specifier) + ";\n" + "/".repeat(oversizedBytes),
"utf8",
);
await expect(createChecker().collectInventory()).rejects.toThrow(
"extension import boundary source file exceeds 2097152 byte limit",
);
});
it.each([
{
name: "comment-obscured side-effect import",
createSource: (specifier: string) => "import /* gap */ " + JSON.stringify(specifier),
kind: "import",
},
{
name: "dynamic import with attributes",
createSource: (specifier: string) =>
"import /* gap */ (" + JSON.stringify(specifier) + ', { with: { type: "json" } })',
kind: "dynamic-import",
},
{
name: "CommonJS require",
createSource: (specifier: string) => "require(" + JSON.stringify(specifier) + ")",
kind: "commonjs-require",
},
{
name: "import.meta URL",
createSource: (specifier: string) =>
"new URL(" + JSON.stringify(specifier) + ", import.meta.url)",
kind: "import-meta-url",
},
{
name: "comment-obscured namespace export",
createSource: (specifier: string) =>
"export /* gap */ * /* gap */ as privateModule from " + JSON.stringify(specifier),
kind: "export",
},
{
name: "escaped plugin-path separator",
createSource: (specifier: string) =>
"import " + JSON.stringify(specifier).replace("extensions/", "extensions\\/"),
kind: "import",
},
{
name: "Unicode-escaped plugin-path character",
createSource: (specifier: string) =>
"import " + JSON.stringify(specifier).replace("extensions/", "exten\\u0073ions/"),
kind: "import",
},
{
name: "hex-escaped plugin-path character",
createSource: (specifier: string) =>
"import " + JSON.stringify(specifier).replace("extensions/", "\\x65xtensions/"),
kind: "import",
},
{
name: "plugin-path line continuation",
createSource: (specifier: string) =>
"import " + JSON.stringify(specifier).replace("extensions/", "exten\\\nsions/"),
kind: "import",
},
])("rejects $name crossing the real plugin boundary", async ({ createSource, kind }) => {
const root = makeTempRoot();
const sourcePath = path.join(root, "guarded.ts");
const targetPath = path.join(process.cwd(), "extensions", "security-proof", "private.js");
const specifier = path.relative(root, targetPath).split(path.sep).join("/");
writeFileSync(sourcePath, createSource(specifier), "utf8");
const checker = createExtensionImportBoundaryChecker({
boundaryLabel: "test",
cleanMessage: "clean",
inventoryTitle: "inventory",
roots: [path.relative(process.cwd(), root)],
skipSourcesWithoutBundledPluginPrefix: true,
});
await expect(checker.collectInventory()).resolves.toEqual([
expect.objectContaining({
kind,
resolvedPath: "extensions/security-proof/private.js",
specifier,
}),
]);
});
});