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
223 lines
7.3 KiB
JavaScript
223 lines
7.3 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
const workspaceRoot = resolve(dirname(scriptPath), "../..");
|
|
const roots = ["services/tuttid", "packages/agent/daemon", "apps/desktop/src"];
|
|
const sourceExtensions = [".go", ".ts", ".tsx"];
|
|
|
|
// Registry declarations and generated protocol enums own provider identity.
|
|
// Provider-owned adapters and parsers may translate a provider-specific wire
|
|
// contract, but cross-provider policy files must use registry descriptors.
|
|
const exemptPrefixes = [
|
|
"packages/agent/daemon/providerregistry/",
|
|
"services/tuttid/api/generated/"
|
|
];
|
|
const exemptFiles = new Set([
|
|
"packages/agent/daemon/activity/events/activity_types.go",
|
|
"packages/agent/daemon/runtime/types.go",
|
|
"packages/agent/daemon/runtime/codex_appserver_adapter.go",
|
|
"packages/agent/daemon/runtime/codex_appserver_startup_trace.go",
|
|
"packages/agent/daemon/runtime/claude_sdk_adapter.go",
|
|
"packages/agent/daemon/runtime/claude_sdk_execution.go",
|
|
"packages/agent/daemon/runtime/claude_sdk_lifecycle.go",
|
|
"packages/agent/daemon/runtime/claude_sdk_live_state.go",
|
|
"packages/agent/daemon/runtime/acp_provider_cursor.go",
|
|
"packages/agent/daemon/runtime/acp_provider_nexight.go",
|
|
"packages/agent/daemon/runtime/acp_provider_openclaw.go",
|
|
"services/tuttid/biz/agentprovider/provider.go",
|
|
"services/tuttid/service/agent/external_import_parse.go",
|
|
// The Claude archive importer translates Claude's provider-owned export
|
|
// format. Cross-provider archive eligibility stays descriptor-driven.
|
|
"services/tuttid/service/agent/external_import_claude_export.go",
|
|
"services/tuttid/service/agentstatus/registry.go"
|
|
]);
|
|
const providerBehaviorCollectionName =
|
|
/(?:action|dispatch|enabled|fallback|handler|hidden|install|login|policy|priority|probe|runtime|status|strategy|supported|visibility)/iu;
|
|
|
|
if (isMainModule()) {
|
|
const providerIds = readRegistryProviderIds();
|
|
const violations = scanWorkspace(providerIds);
|
|
if (violations.length > 0) {
|
|
console.error(
|
|
"Provider behavior must dispatch through providerregistry strategies. " +
|
|
"Provider identity is allowed only in registry declarations, generated enums, and exact provider-owned adapter/parser exemptions:\n" +
|
|
violations.join("\n")
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
export function findProviderIdentityViolations(path, source, providerIds) {
|
|
if (isTestSource(path) || isExemptPath(path)) {
|
|
return [];
|
|
}
|
|
const patterns = createProviderIdentityPatterns(providerIds);
|
|
const violations = [];
|
|
for (const [index, line] of source.split(/\r?\n/u).entries()) {
|
|
if (Object.values(patterns).some((pattern) => pattern.test(line))) {
|
|
violations.push(`${path}:${index + 1}: ${line.trim()}`);
|
|
}
|
|
}
|
|
violations.push(
|
|
...findProviderIdentityCollectionMembershipViolations(
|
|
path,
|
|
source,
|
|
providerIds
|
|
)
|
|
);
|
|
return violations;
|
|
}
|
|
|
|
function findProviderIdentityCollectionMembershipViolations(
|
|
path,
|
|
source,
|
|
providerIds
|
|
) {
|
|
const providerLiteral = new RegExp(
|
|
`["\`](?:${providerIds.map(escapeRegExp).join("|")})["\`]`,
|
|
"gu"
|
|
);
|
|
const collectionDeclaration =
|
|
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:new\s+Set(?:<[^;=]*?>)?\s*\(\s*)?\[([\s\S]*?)\]\s*\)?(?:\s+as\s+const)?(?:\s+satisfies\s+[^;]+)?\s*;/gu;
|
|
const violations = [];
|
|
for (const declaration of source.matchAll(collectionDeclaration)) {
|
|
const name = declaration[1];
|
|
const items = declaration[2];
|
|
if (!providerBehaviorCollectionName.test(name)) {
|
|
continue;
|
|
}
|
|
const membership = new RegExp(
|
|
`\\b${escapeRegExp(name)}\\s*\\.(?:has|includes)\\s*\\(`,
|
|
"u"
|
|
);
|
|
if (!membership.test(source)) {
|
|
continue;
|
|
}
|
|
for (const literal of items.matchAll(providerLiteral)) {
|
|
const literalOffset =
|
|
(declaration.index ?? 0) +
|
|
declaration[0].indexOf(items) +
|
|
literal.index;
|
|
const lineNumber = source.slice(0, literalOffset).split(/\r?\n/u).length;
|
|
violations.push(`${path}:${lineNumber}: ${literal[0]}`);
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
export function isExemptPath(path) {
|
|
return (
|
|
exemptFiles.has(path) ||
|
|
exemptPrefixes.some((prefix) => path.startsWith(prefix))
|
|
);
|
|
}
|
|
|
|
export function createProviderIdentityPatterns(providerIds) {
|
|
const ids = validateProviderIds(providerIds);
|
|
const idAlternation = ids.map(escapeRegExp).join("|");
|
|
const stemAlternation = ids
|
|
.map(providerIdToGoStem)
|
|
.map(escapeRegExp)
|
|
.join("|");
|
|
const constantIdentity = new RegExp(
|
|
`(?:\\bagentprovider(?:biz)?\\.(?:${stemAlternation})|\\bproviderregistry\\.(?:${stemAlternation})ProviderID|\\bProvider(?:${stemAlternation})|\\b(?:${stemAlternation})ProviderID)\\b`,
|
|
"iu"
|
|
);
|
|
const comparedLiteral = new RegExp(
|
|
`(?:==|!=)\\s*["\`](?:${idAlternation})["\`]|["\`](?:${idAlternation})["\`]\\s*(?:==|!=)`,
|
|
"u"
|
|
);
|
|
const caseLiteral = new RegExp(
|
|
`\\bcase\\s+[^:]*["\`](?:${idAlternation})["\`][^:]*:`,
|
|
"u"
|
|
);
|
|
return { constantIdentity, comparedLiteral, caseLiteral };
|
|
}
|
|
|
|
function scanWorkspace(providerIds) {
|
|
const violations = [];
|
|
for (const root of roots) {
|
|
for (const file of sourceFiles(join(workspaceRoot, root))) {
|
|
const path = relative(workspaceRoot, file).replaceAll("\\", "/");
|
|
violations.push(
|
|
...findProviderIdentityViolations(
|
|
path,
|
|
readFileSync(file, "utf8"),
|
|
providerIds
|
|
)
|
|
);
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function readRegistryProviderIds() {
|
|
const daemonRoot = join(workspaceRoot, "packages/agent/daemon");
|
|
const output = execFileSync(
|
|
"go",
|
|
["run", "./providerregistry/cmd/provideridentitycatalog"],
|
|
{ cwd: daemonRoot, encoding: "utf8" }
|
|
);
|
|
const catalog = JSON.parse(output);
|
|
if (!Array.isArray(catalog)) {
|
|
throw new TypeError("provider identity catalog must be an array");
|
|
}
|
|
return validateProviderIds(catalog.map((entry) => entry?.providerId));
|
|
}
|
|
|
|
function validateProviderIds(providerIds) {
|
|
if (
|
|
!Array.isArray(providerIds) ||
|
|
providerIds.length === 0 ||
|
|
providerIds.some(
|
|
(providerId) =>
|
|
typeof providerId !== "string" ||
|
|
providerId.trim() === "" ||
|
|
providerId !== providerId.trim()
|
|
) ||
|
|
new Set(providerIds).size !== providerIds.length
|
|
) {
|
|
throw new TypeError("provider ids must be unique non-empty strings");
|
|
}
|
|
return providerIds;
|
|
}
|
|
|
|
function providerIdToGoStem(providerId) {
|
|
return providerId
|
|
.split(/[^a-zA-Z0-9]+/u)
|
|
.filter(Boolean)
|
|
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
.join("");
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
}
|
|
|
|
function isMainModule() {
|
|
return Boolean(process.argv[1] && resolve(process.argv[1]) === scriptPath);
|
|
}
|
|
|
|
function isTestSource(path) {
|
|
return /(?:_test\.go|\.(?:test|spec)\.tsx?)$/u.test(path);
|
|
}
|
|
|
|
function sourceFiles(directory) {
|
|
const result = [];
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
const path = join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
result.push(...sourceFiles(path));
|
|
} else if (
|
|
entry.isFile() &&
|
|
sourceExtensions.some((extension) => entry.name.endsWith(extension))
|
|
) {
|
|
result.push(path);
|
|
}
|
|
}
|
|
return result;
|
|
}
|