Files
tutti/tools/scripts/check-agent-activity-runtime-boundaries.mjs
T
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

369 lines
11 KiB
JavaScript

import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const defaultWorkspaceRoot = resolve(scriptDirectory, "../..");
const workspaceRoot = process.env.TUTTI_WORKSPACE_ROOT ?? defaultWorkspaceRoot;
const ts = await loadTypeScriptModule();
const agentGuiRoot = resolve(workspaceRoot, "packages/agent/gui");
const desktopRendererRoot = resolve(
workspaceRoot,
"apps/desktop/src/renderer/src/features"
);
const allowedFiles = new Set([
"packages/agent/gui/host/agentHostApi.ts",
"packages/agent/gui/shared/roomShare.ts"
]);
const ignoredPathFragments = [
"/app/preload/types/",
"/dist/",
"/host/",
"/node_modules/",
"/shared/contracts/dto/"
];
const ignoredFilenamePatterns = [
/\.spec\.[cm]?[tj]sx?$/,
/\.test\.[cm]?[tj]sx?$/,
/vitest\./
];
const forbiddenPatterns = [
{
label: "workspaceAgents.*",
pattern: /\bworkspaceAgents\s*(?:\?\.)?\s*\.\s*[A-Za-z_$][\w$]*/g
},
{
label: "legacy compat host helper",
pattern:
/\b(?:listWorkspaceAgentsViaCompatHost|listWorkspaceAgentSessionMessagesViaCompatHost|retainAgentSessionEventsViaCompatHost|releaseAgentSessionEventsViaCompatHost|subscribeAgentSessionEventsViaCompatHost)\b/g
},
{
label: "agentSessions legacy write API",
pattern:
/\bagentSessions\s*(?:\?\.)?\s*\.\s*(?:exec|cancel|submitInteractive|pinSession)\b/g
},
{
label: "agentSessions.retainEventStream",
pattern: /agentSessions\s*(?:\?\.)?\s*\.\s*retainEventStream\b/g
},
{
label: "agentSessions.subscribeEvents",
pattern: /agentSessions\s*(?:\?\.)?\s*\.\s*subscribeEvents\b/g
},
{
label: "AgentHostWorkspaceAgent*",
pattern: /\bAgentHostWorkspaceAgent[A-Za-z0-9_]*/g
},
{
label: "legacy workspaceAgentActivityTypes aggregate",
pattern: /\bworkspaceAgentActivityTypes\b/g
},
{
label: "legacy WorkspaceAgentActivity session mirror",
pattern: /\bWorkspaceAgentActivity(?:Session|Snapshot|Presence)\b/g
},
{
label: "legacy module-global activity runtime resolver",
pattern:
/\b(?:getAgentActivityRuntime|getAgentActivityRuntimeByOrigin|getOptionalAgentActivityRuntime)\b/g
},
{
allowedFiles: new Set([
"apps/desktop/src/renderer/src/features/workspace-agent/services/internal/workspaceAgentActivityDiagnostics.ts",
// Side is an explicitly non-canonical transient projection. It consumes
// the runtime Side envelope directly and must not enter
// AgentSessionEngine merely to satisfy the durable lifecycle vocabulary
// guard enforced for ordinary AgentGUI consumers.
"packages/agent/gui/agentSideConversationController.ts",
// The Side adapter normalizes the same transient envelope into the pure
// ephemeral projector before shared conversation rendering.
"packages/agent/gui/agentSideConversationProjection.ts"
]),
label: "deprecated session lifecycle decision read",
pattern:
/\b[A-Za-z_$][\w$]*\s*(?:\?\.)?\s*\.\s*(?:currentPhase|effectiveStatus|lifecycleStatus|pendingInteractive|submitAvailability|turnLifecycle|turnPhase)\b/g
},
{
label: "direct AgentSessionEngine entity storage access",
pattern:
/\b(?:sessionLifecycle\s*\.\s*(?:sessionsById|turnsById|interactionsById)|pendingIntents\s*\.\s*[A-Za-z_$][\w$]*By[A-Za-z_$][\w$]*|promptQueue\s*\.\s*recordsBySessionId)\b/g
},
{
label: "legacy roomId",
pattern: /\broomId\b/g,
scope: "agent-gui-production"
},
{
label: "legacy room-agent naming",
pattern:
/\b(?:roomAgents|roomAgent|RoomAgent|AgentHostRoomAgent[A-Za-z0-9_]*)\b/g,
scope: "agent-gui-production"
}
];
const violations = [];
const scannedFiles = [agentGuiRoot, desktopRendererRoot]
.filter(existsSync)
.flatMap((scanRoot) => [...walk(scanRoot)])
.filter((filePath) => {
const relativePath = relative(workspaceRoot, filePath);
return isScannedSourceFile(relativePath) && !allowedFiles.has(relativePath);
});
const program = ts.createProgram({
rootNames: scannedFiles,
options: {
allowJs: true,
checkJs: false,
jsx: ts.JsxEmit.Preserve,
noResolve: true,
skipLibCheck: true,
target: ts.ScriptTarget.Latest
}
});
const typeChecker = program.getTypeChecker();
for (const filePath of scannedFiles) {
const relativePath = relative(workspaceRoot, filePath);
const source = readFileSync(filePath, "utf8");
if (relativePath.startsWith("apps/desktop/src/renderer/src/features/")) {
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
throw new Error(`Unable to parse ${relativePath}`);
}
for (const index of findDirectSessionEngineSubscriptions(
sourceFile,
typeChecker
)) {
violations.push({
column: columnNumber(source, index),
file: relativePath,
label: "direct AgentSessionEngine useSyncExternalStore subscription",
line: lineNumber(source, index)
});
}
}
for (const rule of forbiddenPatterns) {
if (!isRuleInScope(rule, relativePath)) {
continue;
}
for (const match of source.matchAll(rule.pattern)) {
violations.push({
column: columnNumber(source, match.index ?? 0),
file: relativePath,
label: rule.label,
line: lineNumber(source, match.index ?? 0)
});
}
}
}
if (violations.length > 0) {
process.stderr.write(
"Agent consumers must use AgentGUIRuntime and AgentSessionEngine selectors.\n"
);
process.stderr.write(
"Move legacy AgentHostWorkspaceAgent access into host compatibility or projection boundary files.\n\n"
);
for (const violation of violations) {
process.stderr.write(
`- ${violation.file}:${violation.line}:${violation.column} uses ${violation.label}\n`
);
}
process.exitCode = 1;
} else {
console.log("Agent GUI runtime boundary check passed");
}
function* walk(directory) {
for (const entry of readdirSync(directory)) {
const filePath = resolve(directory, entry);
const stat = statSync(filePath);
if (stat.isDirectory()) {
yield* walk(filePath);
continue;
}
yield filePath;
}
}
function isScannedSourceFile(relativePath) {
if (!/\.[cm]?[tj]sx?$/.test(relativePath)) {
return false;
}
const normalized = `/${relativePath}`;
if (ignoredPathFragments.some((fragment) => normalized.includes(fragment))) {
return false;
}
return !ignoredFilenamePatterns.some((pattern) => pattern.test(relativePath));
}
function isRuleInScope(rule, relativePath) {
if (rule.allowedFiles?.has(relativePath)) {
return false;
}
if (rule.scope === "agent-gui-production") {
return relativePath.startsWith("packages/agent/gui/agent-gui/");
}
return true;
}
function findDirectSessionEngineSubscriptions(sourceFile, typeChecker) {
const positions = [];
const visit = (node) => {
if (
ts.isCallExpression(node) &&
isUseSyncExternalStoreCall(node.expression) &&
node.arguments.some((argument) =>
expressionDependsOnSessionEngine(
argument,
typeChecker,
new Set(),
new Set()
)
)
) {
positions.push(node.expression.getStart(sourceFile));
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return positions;
}
function isUseSyncExternalStoreCall(expression) {
return (
(ts.isIdentifier(expression) &&
expression.text === "useSyncExternalStore") ||
(ts.isPropertyAccessExpression(expression) &&
expression.name.text === "useSyncExternalStore")
);
}
function expressionDependsOnSessionEngine(
node,
typeChecker,
visitedNodes,
visitedSymbols
) {
if (visitedNodes.has(node)) {
return false;
}
visitedNodes.add(node);
if (ts.isCallExpression(node) && isGetSessionEngineCall(node.expression)) {
return true;
}
if (ts.isIdentifier(node) && !isPropertyNameIdentifier(node)) {
const symbol = typeChecker.getSymbolAtLocation(node);
if (symbol && !visitedSymbols.has(symbol)) {
visitedSymbols.add(symbol);
for (const declaration of symbol.declarations ?? []) {
const valueNode = declarationValueNode(declaration);
if (
valueNode &&
expressionDependsOnSessionEngine(
valueNode,
typeChecker,
visitedNodes,
visitedSymbols
)
) {
return true;
}
}
}
}
let dependsOnSessionEngine = false;
ts.forEachChild(node, (child) => {
if (
!dependsOnSessionEngine &&
expressionDependsOnSessionEngine(
child,
typeChecker,
visitedNodes,
visitedSymbols
)
) {
dependsOnSessionEngine = true;
}
});
return dependsOnSessionEngine;
}
function isGetSessionEngineCall(expression) {
return (
(ts.isIdentifier(expression) && expression.text === "getSessionEngine") ||
(ts.isPropertyAccessExpression(expression) &&
expression.name.text === "getSessionEngine")
);
}
function isPropertyNameIdentifier(node) {
return (
(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) ||
(ts.isPropertyAssignment(node.parent) && node.parent.name === node) ||
(ts.isMethodDeclaration(node.parent) && node.parent.name === node)
);
}
function declarationValueNode(declaration) {
if (
ts.isVariableDeclaration(declaration) ||
ts.isBindingElement(declaration) ||
ts.isParameter(declaration) ||
ts.isPropertyDeclaration(declaration) ||
ts.isPropertyAssignment(declaration)
) {
return declaration.initializer ?? null;
}
if (
ts.isFunctionDeclaration(declaration) ||
ts.isMethodDeclaration(declaration) ||
ts.isGetAccessorDeclaration(declaration)
) {
return declaration.body ?? null;
}
return null;
}
function lineNumber(source, index) {
return source.slice(0, index).split("\n").length;
}
function columnNumber(source, index) {
const previousNewline = source.lastIndexOf("\n", index - 1);
return index - previousNewline;
}
async function loadTypeScriptModule() {
const candidateRoots = [workspaceRoot, defaultWorkspaceRoot];
const candidatePaths = candidateRoots.flatMap((root) => [
join(root, "node_modules/typescript/lib/typescript.js"),
join(root, "apps/desktop/node_modules/typescript/lib/typescript.js"),
join(
root,
"packages/clients/tuttid-ts/node_modules/typescript/lib/typescript.js"
)
]);
for (const candidatePath of candidatePaths) {
if (!existsSync(candidatePath)) {
continue;
}
const module = await import(pathToFileURL(candidatePath).href);
return module.default ?? module;
}
throw new Error(
"Unable to locate a TypeScript runtime for check-agent-activity-runtime-boundaries.mjs"
);
}