Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { format } from "prettier";
|
|
import YAML from "yaml";
|
|
|
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(scriptDirectory, "../..");
|
|
const daemonRoot = resolve(repoRoot, "packages/agent/daemon");
|
|
const outputPath = resolve(
|
|
repoRoot,
|
|
"packages/agent/gui/generated/providerIdentityCatalog.ts"
|
|
);
|
|
const capabilityOutputPath = resolve(
|
|
repoRoot,
|
|
"packages/agent/activity-core/src/generated/agentCapabilityKeys.ts"
|
|
);
|
|
const openapiPath = resolve(
|
|
repoRoot,
|
|
"services/tuttid/api/openapi/tuttid.v1.yaml"
|
|
);
|
|
const prettierConfigPath = resolve(
|
|
repoRoot,
|
|
"packages/configs/prettier/base.mjs"
|
|
);
|
|
const checkOnly = process.argv.includes("--check");
|
|
|
|
const { default: prettierConfig } = await import(
|
|
pathToFileURL(prettierConfigPath).href
|
|
);
|
|
|
|
const openapiProviderSchemaNames = [
|
|
"DesktopAgentComposerDefaultsByProvider",
|
|
"DesktopAgentGuiConversationRailCollapsedByProvider"
|
|
];
|
|
const openProviderIDPattern = "^[a-z][a-z0-9._:-]*$";
|
|
const providerStatusKinds = [
|
|
"codex_cli",
|
|
"claude_cli",
|
|
"opencode_cli",
|
|
"generic_cli"
|
|
];
|
|
|
|
if (isMainModule()) {
|
|
const catalog = readRegistryCatalog();
|
|
const capabilityKeys = readRegistryCapabilityKeys();
|
|
const openapi = YAML.parse(readFileSync(openapiPath, "utf8"));
|
|
validateRegistryCatalogAgainstOpenAPI(catalog, openapi);
|
|
validateCapabilityKeysAgainstOpenAPI(capabilityKeys, openapi);
|
|
const source = await renderProviderIdentityCatalog(catalog);
|
|
const capabilitySource = await renderAgentCapabilityKeys(capabilityKeys);
|
|
|
|
if (checkOnly) {
|
|
const existing = readFileSync(outputPath, "utf8");
|
|
if (existing !== source) {
|
|
throw new Error(
|
|
`Generated AgentGUI provider identity catalog is out of date: ${outputPath}`
|
|
);
|
|
}
|
|
const existingCapabilitySource = readFileSync(capabilityOutputPath, "utf8");
|
|
if (existingCapabilitySource !== capabilitySource) {
|
|
throw new Error(
|
|
`Generated agent capability keys are out of date: ${capabilityOutputPath}`
|
|
);
|
|
}
|
|
} else {
|
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
writeFileSync(outputPath, source, "utf8");
|
|
mkdirSync(dirname(capabilityOutputPath), { recursive: true });
|
|
writeFileSync(capabilityOutputPath, capabilitySource, "utf8");
|
|
}
|
|
}
|
|
|
|
function readRegistryCapabilityKeys() {
|
|
const output = execFileSync(
|
|
"go",
|
|
["run", "./providerregistry/cmd/providercapabilitycatalog"],
|
|
{ cwd: daemonRoot, encoding: "utf8" }
|
|
);
|
|
const keys = JSON.parse(output);
|
|
if (
|
|
!Array.isArray(keys) ||
|
|
keys.length === 0 ||
|
|
keys.some((key) => typeof key !== "string" || key.trim() === "") ||
|
|
new Set(keys).size !== keys.length
|
|
) {
|
|
throw new TypeError("provider capability catalog must be unique strings");
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
export async function renderAgentCapabilityKeys(keys) {
|
|
return await format(
|
|
`// Code generated by tools/scripts/generate-agent-gui-provider-catalog.mjs. DO NOT EDIT.\n\nexport const AGENT_CAPABILITY_KEYS = ${JSON.stringify(keys, null, 2)} as const;\nexport type AgentCapabilityKey = (typeof AGENT_CAPABILITY_KEYS)[number];\n`,
|
|
{ ...prettierConfig, parser: "typescript" }
|
|
);
|
|
}
|
|
|
|
function isMainModule() {
|
|
return Boolean(
|
|
process.argv[1] &&
|
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
);
|
|
}
|
|
|
|
function readRegistryCatalog() {
|
|
const output = execFileSync(
|
|
"go",
|
|
["run", "./providerregistry/cmd/provideridentitycatalog"],
|
|
{
|
|
cwd: daemonRoot,
|
|
encoding: "utf8"
|
|
}
|
|
);
|
|
const catalog = JSON.parse(output);
|
|
validateRegistryCatalog(catalog);
|
|
return catalog;
|
|
}
|
|
|
|
export async function renderProviderIdentityCatalog(catalog) {
|
|
validateRegistryCatalog(catalog);
|
|
return await format(
|
|
`// Code generated by tools/scripts/generate-agent-gui-provider-catalog.mjs. DO NOT EDIT.\n\nexport const generatedProviderIdentityCatalog = ${JSON.stringify(catalog, null, 2)} as const;\n`,
|
|
{
|
|
...prettierConfig,
|
|
parser: "typescript"
|
|
}
|
|
);
|
|
}
|
|
|
|
export function validateRegistryCatalog(catalog) {
|
|
if (!Array.isArray(catalog)) {
|
|
throw new TypeError("provider identity catalog must be an array");
|
|
}
|
|
|
|
const providerIds = new Set();
|
|
const targetIds = new Set();
|
|
for (const entry of catalog) {
|
|
if (!entry || typeof entry !== "object") {
|
|
throw new TypeError("provider identity catalog entry must be an object");
|
|
}
|
|
for (const key of ["providerId", "displayName", "iconKey", "localeKey"]) {
|
|
if (typeof entry[key] !== "string" || entry[key].trim() === "") {
|
|
throw new TypeError(`provider identity catalog ${key} is required`);
|
|
}
|
|
}
|
|
if (!["", "anthropic", "openai"].includes(entry.modelPlanProtocol)) {
|
|
throw new TypeError(
|
|
`invalid model plan protocol for provider: ${entry.providerId}`
|
|
);
|
|
}
|
|
if (!providerStatusKinds.includes(entry.statusKind)) {
|
|
throw new TypeError(
|
|
`invalid status kind for provider: ${entry.providerId}`
|
|
);
|
|
}
|
|
if (!Array.isArray(entry.aliases)) {
|
|
throw new TypeError("provider identity catalog aliases must be an array");
|
|
}
|
|
if (providerIds.has(entry.providerId)) {
|
|
throw new TypeError(`duplicate provider id: ${entry.providerId}`);
|
|
}
|
|
providerIds.add(entry.providerId);
|
|
|
|
const target = entry.target;
|
|
if (
|
|
!target ||
|
|
typeof target !== "object" ||
|
|
typeof target.id !== "string" ||
|
|
target.id.trim() === "" ||
|
|
typeof target.launchRefType !== "string" ||
|
|
target.launchRefType.trim() === "" ||
|
|
typeof target.enabled !== "boolean" ||
|
|
!Number.isInteger(target.sortOrder)
|
|
) {
|
|
throw new TypeError(`invalid target for provider: ${entry.providerId}`);
|
|
}
|
|
if (targetIds.has(target.id)) {
|
|
throw new TypeError(`duplicate target id: ${target.id}`);
|
|
}
|
|
targetIds.add(target.id);
|
|
const desktop = entry.desktop;
|
|
if (
|
|
!desktop ||
|
|
typeof desktop !== "object" ||
|
|
typeof desktop.managed !== "boolean" ||
|
|
!Number.isInteger(desktop.managedOrder) ||
|
|
desktop.managedOrder < 0 ||
|
|
desktop.managed !== desktop.managedOrder > 0 ||
|
|
!Number.isInteger(desktop.statusProbePriority) ||
|
|
desktop.statusProbePriority < 0 ||
|
|
desktop.managed !== desktop.statusProbePriority > 0 ||
|
|
typeof desktop.usageProbeKind !== "string" ||
|
|
typeof desktop.visibilityGate !== "string" ||
|
|
typeof desktop.runtimeProbeFallback !== "string" ||
|
|
typeof desktop.installBootstrap !== "boolean" ||
|
|
typeof desktop.refreshOnAccountChange !== "boolean" ||
|
|
(!desktop.managed && desktop.installBootstrap) ||
|
|
(!desktop.managed && desktop.refreshOnAccountChange) ||
|
|
typeof desktop.developerLogs !== "boolean" ||
|
|
typeof desktop.defaultProviderEligible !== "boolean" ||
|
|
!Number.isInteger(desktop.defaultProviderPriority) ||
|
|
desktop.defaultProviderPriority < 0 ||
|
|
desktop.defaultProviderEligible !== desktop.defaultProviderPriority > 0 ||
|
|
!Number.isInteger(desktop.unavailableDockOrderOffset) ||
|
|
desktop.unavailableDockOrderOffset < 0
|
|
) {
|
|
throw new TypeError(
|
|
`invalid desktop integration for provider: ${entry.providerId}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function validateRegistryCatalogAgainstOpenAPI(catalog, openapi) {
|
|
validateRegistryCatalog(catalog);
|
|
const migratedProviderIds = catalog.map((entry) => entry.providerId);
|
|
validateOpenProviderSchema(
|
|
openapi,
|
|
"AgentTargetProvider",
|
|
migratedProviderIds
|
|
);
|
|
validateOpenProviderSchema(
|
|
openapi,
|
|
"WorkspaceAgentProvider",
|
|
migratedProviderIds
|
|
);
|
|
for (const schemaName of openapiProviderSchemaNames) {
|
|
const schema = openapi?.components?.schemas?.[schemaName];
|
|
if (
|
|
!schema ||
|
|
typeof schema !== "object" ||
|
|
schema.type !== "object" ||
|
|
schema.additionalProperties !== false ||
|
|
!schema.properties ||
|
|
typeof schema.properties !== "object" ||
|
|
Array.isArray(schema.properties)
|
|
) {
|
|
throw new TypeError(
|
|
`openapi provider-keyed schema must remain a closed object: ${schemaName}`
|
|
);
|
|
}
|
|
const actualProviderIds = Object.keys(schema.properties);
|
|
assertSameStringSet(
|
|
actualProviderIds,
|
|
migratedProviderIds,
|
|
`openapi provider schema drift: ${schemaName}`
|
|
);
|
|
}
|
|
|
|
const defaultProviderIds =
|
|
openapi?.components?.schemas?.DesktopDefaultAgentProvider?.enum;
|
|
if (!Array.isArray(defaultProviderIds)) {
|
|
throw new TypeError(
|
|
"openapi provider schema is missing: DesktopDefaultAgentProvider"
|
|
);
|
|
}
|
|
const supportedProviderIds = new Set(migratedProviderIds);
|
|
for (const providerId of defaultProviderIds) {
|
|
if (!supportedProviderIds.has(providerId)) {
|
|
throw new TypeError(
|
|
`openapi default provider is not registered: ${providerId}`
|
|
);
|
|
}
|
|
}
|
|
assertSameStringSet(
|
|
catalog
|
|
.filter((entry) => entry.desktop.defaultProviderEligible)
|
|
.map((entry) => entry.providerId),
|
|
defaultProviderIds,
|
|
"desktop default provider descriptor drift"
|
|
);
|
|
}
|
|
|
|
function validateOpenProviderSchema(openapi, schemaName, migratedProviderIds) {
|
|
const schema = openapi?.components?.schemas?.[schemaName];
|
|
if (
|
|
!schema ||
|
|
typeof schema !== "object" ||
|
|
schema.type !== "string" ||
|
|
schema.enum !== undefined ||
|
|
schema.minLength !== 1 ||
|
|
schema.maxLength !== 128 ||
|
|
schema.pattern !== openProviderIDPattern
|
|
) {
|
|
throw new TypeError(
|
|
`openapi ${schemaName} must remain an open, bounded string schema`
|
|
);
|
|
}
|
|
const pattern = new RegExp(schema.pattern, "u");
|
|
for (const providerId of [...migratedProviderIds, "acp:extension-example"]) {
|
|
if (!pattern.test(providerId)) {
|
|
throw new TypeError(
|
|
`openapi ${schemaName} rejects valid provider id: ${providerId}`
|
|
);
|
|
}
|
|
}
|
|
for (const invalid of ["BadProvider", "bad provider", "$(provider)"]) {
|
|
if (pattern.test(invalid)) {
|
|
throw new TypeError(
|
|
`openapi ${schemaName} accepts unsafe provider id: ${invalid}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function validateCapabilityKeysAgainstOpenAPI(keys, openapi) {
|
|
const schema = openapi?.components?.schemas?.WorkspaceAgentCapabilities;
|
|
if (!schema || typeof schema !== "object") {
|
|
throw new TypeError("openapi WorkspaceAgentCapabilities schema is missing");
|
|
}
|
|
assertSameStringSet(
|
|
Object.keys(schema.properties ?? {}),
|
|
keys,
|
|
"openapi agent capability vocabulary drift"
|
|
);
|
|
}
|
|
|
|
function assertSameStringSet(actual, expected, label) {
|
|
const normalizedActual = [...new Set(actual)].sort();
|
|
const normalizedExpected = [...new Set(expected)].sort();
|
|
if (
|
|
normalizedActual.length !== actual.length ||
|
|
normalizedExpected.length !== expected.length ||
|
|
JSON.stringify(normalizedActual) !== JSON.stringify(normalizedExpected)
|
|
) {
|
|
throw new TypeError(
|
|
`${label}: actual=${JSON.stringify(normalizedActual)} expected=${JSON.stringify(normalizedExpected)}`
|
|
);
|
|
}
|
|
}
|