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
397 lines
12 KiB
TypeScript
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,
|
|
);
|
|
}
|