Files
elizaos--eliza/packages/scripts/audit-capability-router-plugin-surface.ts
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

397 lines
12 KiB
TypeScript

// Drives repo automation audit capability router plugin surface with explicit CLI and CI behavior.
import { readFileSync } from "node:fs";
import ts from "typescript";
const pluginFile = "packages/core/src/types/plugin.ts";
const capabilityFile = "packages/core/src/capabilities/index.ts";
const conformanceFile =
"packages/agent/src/services/remote-capability-endpoint-conformance.ts";
const fixtureServerFile =
"packages/scripts/capability-router-fixture-server.ts";
const liveReportValidatorFile =
"packages/scripts/validate-capability-router-live-reports.ts";
const remoteSupported = new Map<string, string>([
["name", "name"],
["description", "description"],
["init", "lifecycle:init"],
["dispose", "lifecycle:dispose"],
["applyConfig", "lifecycle:applyConfig"],
["config", "config"],
["services", "services"],
["componentTypes", "componentTypes"],
["actions", "actions"],
["providers", "providers"],
["evaluators", "evaluators"],
["responseHandlerEvaluators", "responseHandlerEvaluators"],
["responseHandlerFieldEvaluators", "responseHandlerFieldEvaluators"],
["models", "models"],
["events", "events"],
["routes", "routes"],
["priority", "priority"],
["schema", "schema"],
["app", "app"],
["appBridge", "appBridge"],
["views", "views"],
["widgets", "widgets"],
["contexts", "contexts"],
]);
const localOnly = new Set([
"mode",
"remote",
"adapter",
"tests",
"dependencies",
"testDependencies",
"autoEnable",
// Pre-LLM shortcut gate (#8791): registered into the runtime ShortcutRegistry,
// not exposed over the capability-router remote boundary (RemotePluginModuleManifest
// has no shortcuts key and no remote-manifest builder reads it).
"shortcuts",
// Pre-action dispatch hooks drained at the top of the chat loop; registered
// into the runtime ChatPreHandlerRegistry in-process, never mirrored over the
// remote wire (no manifest key, no builder reads it).
"chatPreHandlers",
// Package-dir resolution hint for the in-process view/hero/frame registry;
// the host resolves a remote plugin's bundles from its worker manifest, so
// this never crosses the remote boundary.
"packageName",
// Load-time preparation hook the plugin resolver runs before init on the host
// that owns the module; a remote worker prepares its own dependencies, so the
// hook is not part of the mirrored surface.
"preflight",
// Handler-free display/routing facts attached to `models`; consumed in-process
// by the model registry and not carried as its own manifest key.
"modelMetadata",
// Connector source names/aliases registered into the in-process runtime source
// map; connector plugins run direct, so this is not a remote-mirrored surface.
"connectorSources",
"mode",
"remote",
]);
const remoteManifestKeys = new Set(
readTypeMembers(capabilityFile, "RemotePluginModuleManifest"),
);
const pluginKeys = readInterfaceMembers(pluginFile, "Plugin");
const failures: string[] = [];
for (const key of pluginKeys) {
const remoteKey = remoteSupported.get(key);
if (remoteKey) {
if (
!remoteKey.startsWith("lifecycle:") &&
!remoteManifestKeys.has(remoteKey)
) {
failures.push(
`Plugin.${key} is marked remote-supported but RemotePluginModuleManifest lacks ${remoteKey}.`,
);
}
continue;
}
if (localOnly.has(key)) continue;
failures.push(
`Plugin.${key} is not classified for capability-router remote plugins.`,
);
}
for (const key of [...remoteSupported.keys(), ...localOnly]) {
if (!pluginKeys.includes(key)) {
failures.push(`Surface audit references missing Plugin.${key}.`);
}
}
if (!remoteManifestKeys.has("lifecycle")) {
failures.push(
"RemotePluginModuleManifest must keep lifecycle for init/dispose/applyConfig.",
);
}
const pluginRpcMethods = readStringUnionMembers(
capabilityFile,
"RuntimeBrokerCapabilityMethod",
).filter((method) => method.startsWith("plugin."));
const conformanceRequiredMethods = pluginRpcMethods.filter(
(method) => method !== "plugin.modules.list",
);
const conformanceSource = readFileSync(conformanceFile, "utf8");
const fixtureServerSource = readFileSync(fixtureServerFile, "utf8");
const conformanceSurfaces = readStringUnionMembers(
conformanceFile,
"RemoteCapabilityEndpointConformanceSurface",
);
const liveReportValidatorSurfaces = readStringArrayLiteral(
liveReportValidatorFile,
"REQUIRED_SURFACES",
);
const liveReportValidatorRpcMethodRecord = readStringArrayRecord(
liveReportValidatorFile,
"REQUIRED_SURFACE_RPC_METHODS",
);
const liveReportValidatorRpcMethods = new Set(
Object.values(liveReportValidatorRpcMethodRecord).flat(),
);
compareSets(
failures,
"Remote capability endpoint conformance surfaces",
conformanceSurfaces,
"live report required surfaces",
liveReportValidatorSurfaces,
);
compareSets(
failures,
"Live report required surfaces",
liveReportValidatorSurfaces,
"live report RPC method matrix keys",
Object.keys(liveReportValidatorRpcMethodRecord),
);
for (const method of pluginRpcMethods) {
if (!fixtureServerSource.includes(`case "${method}"`)) {
failures.push(
`RuntimeBrokerCapabilityMethod.${method} is missing a capability-router-fixture-server case.`,
);
}
}
for (const method of conformanceRequiredMethods) {
if (!conformanceSource.includes(`"${method}"`)) {
failures.push(
`RuntimeBrokerCapabilityMethod.${method} is not exercised by remote capability endpoint conformance.`,
);
}
if (!liveReportValidatorRpcMethods.has(method)) {
failures.push(
`RuntimeBrokerCapabilityMethod.${method} is not required by live report validation.`,
);
}
}
for (const method of liveReportValidatorRpcMethods) {
if (!conformanceRequiredMethods.includes(method)) {
failures.push(
`Live report validation requires ${method}, but it is not a canonical non-list plugin RPC method.`,
);
}
}
if (failures.length > 0) {
console.error("[capability-router-surface-audit] failed");
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log(
JSON.stringify(
{
ok: true,
pluginFields: pluginKeys.length,
remoteSupported: remoteSupported.size,
localOnly: localOnly.size,
pluginRpcMethods: pluginRpcMethods.length,
conformanceRequiredMethods: conformanceRequiredMethods.length,
},
null,
2,
),
);
function readInterfaceMembers(
fileName: string,
interfaceName: string,
): string[] {
const source = readSourceFile(fileName);
for (const node of source.statements) {
if (ts.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
return memberNames(node.members);
}
}
throw new Error(`Could not find interface ${interfaceName} in ${fileName}.`);
}
function readTypeMembers(fileName: string, typeName: string): string[] {
const source = readSourceFile(fileName);
for (const node of source.statements) {
if (
ts.isTypeAliasDeclaration(node) &&
node.name.text === typeName &&
ts.isTypeLiteralNode(node.type)
) {
return memberNames(node.type.members);
}
}
throw new Error(`Could not find type literal ${typeName} in ${fileName}.`);
}
function readStringUnionMembers(fileName: string, typeName: string): string[] {
const source = readSourceFile(fileName);
for (const node of source.statements) {
if (
ts.isTypeAliasDeclaration(node) &&
node.name.text === typeName &&
ts.isUnionTypeNode(node.type)
) {
return node.type.types.flatMap((member) => {
if (
ts.isLiteralTypeNode(member) &&
ts.isStringLiteral(member.literal)
) {
return [member.literal.text];
}
return [];
});
}
}
throw new Error(`Could not find string union ${typeName} in ${fileName}.`);
}
function readStringArrayLiteral(
fileName: string,
variableName: string,
): string[] {
const source = readSourceFile(fileName);
for (const node of source.statements) {
if (!ts.isVariableStatement(node)) continue;
for (const declaration of node.declarationList.declarations) {
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === variableName &&
declaration.initializer
) {
const initializer = unwrapExpression(declaration.initializer);
if (!ts.isArrayLiteralExpression(initializer)) {
throw new Error(`${variableName} must be a string array.`);
}
return initializer.elements.map((element) => {
if (!ts.isStringLiteral(element)) {
throw new Error(
`${variableName} must contain only string literals.`,
);
}
return element.text;
});
}
}
}
throw new Error(
`Could not find string array ${variableName} in ${fileName}.`,
);
}
function readStringArrayRecord(
fileName: string,
variableName: string,
): Record<string, string[]> {
const source = readSourceFile(fileName);
for (const node of source.statements) {
if (!ts.isVariableStatement(node)) continue;
for (const declaration of node.declarationList.declarations) {
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === variableName &&
declaration.initializer &&
ts.isObjectLiteralExpression(declaration.initializer)
) {
return Object.fromEntries(
declaration.initializer.properties.map((property) => {
if (
!ts.isPropertyAssignment(property) ||
!property.name ||
!ts.isArrayLiteralExpression(property.initializer)
) {
throw new Error(
`${variableName} must be an object of string array properties.`,
);
}
const propertyName = propertyNameText(property.name);
const values = property.initializer.elements.map((element) => {
if (!ts.isStringLiteral(element)) {
throw new Error(
`${variableName}.${propertyName} must contain only string literals.`,
);
}
return element.text;
});
return [propertyName, values] as const;
}),
);
}
}
}
throw new Error(
`Could not find string array record ${variableName} in ${fileName}.`,
);
}
function compareSets(
failures: string[],
leftLabel: string,
left: readonly string[],
rightLabel: string,
right: readonly string[],
): void {
const rightSet = new Set(right);
for (const item of left) {
if (!rightSet.has(item)) {
failures.push(
`${leftLabel} include ${item}, missing from ${rightLabel}.`,
);
}
}
const leftSet = new Set(left);
for (const item of right) {
if (!leftSet.has(item)) {
failures.push(
`${rightLabel} include ${item}, missing from ${leftLabel}.`,
);
}
}
}
function propertyNameText(name: ts.PropertyName): string {
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
return name.text;
}
if (ts.isNumericLiteral(name)) {
return name.text;
}
throw new Error("Unsupported computed property name.");
}
function unwrapExpression(expression: ts.Expression): ts.Expression {
if (
ts.isAsExpression(expression) ||
ts.isTypeAssertionExpression(expression)
) {
return unwrapExpression(expression.expression);
}
return expression;
}
function memberNames(members: ts.NodeArray<ts.TypeElement>): string[] {
return members.flatMap((member) => {
if (
(ts.isPropertySignature(member) || ts.isMethodSignature(member)) &&
member.name
) {
if (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) {
return [member.name.text];
}
}
return [];
});
}
function readSourceFile(fileName: string): ts.SourceFile {
return ts.createSourceFile(
fileName,
readFileSync(fileName, "utf8"),
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
}