NODEDC_PLATFORM/services/ai-workspace-assistant/src/scripts/smoke-run-profile.mjs

569 lines
22 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import { createHash, randomUUID } from "node:crypto";
const SECRET_TOKEN = "secret-run-token-for-smoke";
const ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы.";
const RUN_APP_ID_ALIASES = new Map([
["hub", "launcher"],
["nodedc-hub", "launcher"],
["nodedc_launcher", "launcher"],
["nodedc-launcher", "launcher"],
]);
const APP_ROUTING_CATALOG = [
{ appId: "launcher", appTitle: "NODE.DC Launcher", surface: "launcher", skillId: "launcher-context", appAliases: ["hub"], actionIdPrefixes: ["hub.", "launcher.", "access."], mcpServerNames: [], deniedText: ACCESS_DENIED_TEXT },
{ appId: "engine", appTitle: "NODE.DC Engine / InJoin", surface: "engine", skillId: "engine-context", mcpServerNames: ["nodedc-engine", "nodedc-agent-core"], deniedText: ACCESS_DENIED_TEXT },
{ appId: "ops", appTitle: "NODE.DC Ops / Tasker", surface: "ops", skillId: "ops-context", mcpServerNames: ["nodedc_ops_agent"], deniedText: ACCESS_DENIED_TEXT },
];
const adapter = {
id: "ops",
appId: "ops",
title: "NODE.DC Ops",
};
const adapterPayload = {
ok: true,
appGrants: {
ops: {
appTitle: "NODE.DC Ops",
surface: "ops",
grantMode: "token-scoped-run-grants",
context: {
opsWorkspaceSlug: "nodedc",
opsProjectId: "86629a11-eaff-4ad2-9f89-e5245a344fcc",
},
scopes: ["workspace:read", "project:read", "issue:read"],
mcpServers: [
{
serverName: "nodedc ops agent",
url: "https://ops-agents.nodedc.ru/mcp",
required: true,
startupTimeoutSec: 20,
toolTimeoutSec: 60,
httpHeaders: {
Authorization: `Bearer ${SECRET_TOKEN}`,
Accept: "application/json",
"MCP-Protocol-Version": "2025-06-18",
},
},
],
},
},
};
const appGrants = normalizeEntitlementAdapterAppGrants(adapterPayload, adapter);
const mcpServers = runProfileMcpServersFromAppGrants({}, appGrants);
const appGrantSummary = summarizeRunAppGrants({ appGrants });
const assistantActions = {
schemaVersion: "ai-workspace.assistant-actions.v1",
endpoint: "/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions",
gatewayUrl: "https://ai-hub.nodedc.ru/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions",
gatewayToken: SECRET_TOKEN,
actionIds: ["hub.access_request.list_pending", "hub.user.read_admin_summary"],
phases: ["preview", "execute"],
};
const appCatalog = buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions });
const appAccess = summarizeRunAppAccess(appCatalog);
const launcherAdapter = { id: "launcher", appId: "launcher", title: "NODE.DC Launcher" };
const launcherGrants = normalizeEntitlementAdapterAppGrants({
ok: true,
appGrants: {
hub: {
appTitle: "NODE.DC Launcher",
surface: "launcher",
status: "granted",
scopes: ["launcher:access:read", "launcher_admin_scope.any"],
},
},
}, launcherAdapter);
const catalogWithLauncherGrant = buildRunProfileAppCatalog({
appGrants: { ...appGrants, ...launcherGrants },
mcpServers,
assistantActions,
});
const deniedByLauncher = mergeRunAppGrants(appGrants, normalizeEntitlementAdapterAppGrants({
ok: true,
appGrants: {
ops: {
appTitle: "NODE.DC Ops",
surface: "ops",
status: "denied",
allowed: false,
reason: "launcher_service_access_denied",
mcpServers: [],
},
},
}, launcherAdapter));
const deniedMcpServers = runProfileMcpServersFromAppGrants({}, deniedByLauncher);
const explicitDeniedCatalog = buildRunProfileAppCatalog({
appGrants: deniedByLauncher,
mcpServers: deniedMcpServers,
assistantActions,
});
const runProfile = {
schemaVersion: "ai-workspace.run-profile.v1",
runId: randomUUID(),
createdAt: new Date("2026-06-13T00:00:00.000Z").toISOString(),
owner: {
ownerKey: "user:smoke-user",
userId: "smoke-user",
email: "smoke@example.test",
role: "member",
groups: ["engineering"],
},
sourceSurface: "engine",
modeId: "ops",
targetContexts: {
ops: {
opsWorkspaceSlug: "nodedc",
opsProjectId: "86629a11-eaff-4ad2-9f89-e5245a344fcc",
},
},
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
appGrants: appGrantSummary,
appCatalog,
appAccess,
toolProfile: {
schemaVersion: "ai-workspace.tool-profile.v1",
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
mcpServers,
mcpServerNames: mcpServers.map((server) => server.serverName),
requiredMcpServerNames: mcpServers.filter((server) => server.required).map((server) => server.serverName),
assistantActions,
},
diagnostics: {
schemaVersion: "ai-workspace.run-profile.diagnostics.v1",
dynamicProfile: true,
sourceSurface: "engine",
modeId: "ops",
entitlementAdapters: {
source: "adapters+settings",
adapters: [{ appId: "ops", status: "ok", required: true }],
},
mcpServerNames: mcpServers.map((server) => server.serverName),
appCatalogIds: appCatalog.map((app) => app.appId).sort(),
grantedAppIds: appAccess.grantedAppIds,
deniedAppIds: appAccess.deniedAppIds,
notGrantedAppIds: appAccess.notGrantedAppIds,
},
};
runProfile.diagnostics.profileHash = runProfileHash(runProfile);
const publicProfile = redactRunProfile(runProfile);
assert.deepEqual(Object.keys(appGrants), ["ops"]);
assert.equal(appGrants.ops.source, "entitlement-adapter");
assert.equal(appGrants.ops.appId, "ops");
assert.deepEqual(appGrantSummary.ops.scopes, ["workspace:read", "project:read", "issue:read"]);
assert.equal(appGrantSummary.ops.hasMcpServers, true);
assert.deepEqual(appGrantSummary.ops.mcpServerNames, ["nodedc_ops_agent"]);
assert.equal(appGrantSummary.ops.status, "granted");
assert.equal(appGrantSummary.ops.denied, false);
assert.equal(mcpServers.length, 1);
assert.equal(mcpServers[0].appId, "ops");
assert.equal(mcpServers[0].serverName, "nodedc_ops_agent");
assert.equal(mcpServers[0].required, true);
assert.equal(mcpServers[0].httpHeaders.Authorization, `Bearer ${SECRET_TOKEN}`);
assert.equal(mcpServers[0].httpHeaders.Accept, "application/json");
assert.equal(publicProfile.toolProfile.mcpServers.length, 1);
assert.equal(publicProfile.toolProfile.mcpServers[0].serverName, "nodedc_ops_agent");
assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Authorization, "<redacted>");
assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Accept, "<redacted>");
assert.equal(publicProfile.toolProfile.mcpServers[0].headers, undefined);
assert.equal(publicProfile.toolProfile.assistantActions.endpoint, "/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions");
assert.equal(publicProfile.toolProfile.assistantActions.gatewayUrl, "https://ai-hub.nodedc.ru/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions");
assert.equal(publicProfile.toolProfile.assistantActions.gatewayToken, "<redacted>");
assert.deepEqual(publicProfile.toolProfile.assistantActions.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.status, "granted");
assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.denied, false);
assert.deepEqual(runProfile.appCatalog.find((app) => app.appId === "ops")?.mcpServerNames, ["nodedc_ops_agent"]);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.status, "not-granted");
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.denied, false);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.notGranted, true);
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.deniedText, null);
assert.equal(launcherGrants.launcher.appId, "launcher");
assert.equal(launcherGrants.hub, undefined);
assert.deepEqual(catalogWithLauncherGrant.find((app) => app.appId === "launcher")?.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]);
assert.equal(deniedByLauncher.ops.status, "denied");
assert.deepEqual(deniedByLauncher.ops.mcpServers, []);
assert.deepEqual(deniedMcpServers, []);
assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.denied, true);
assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.deniedText, ACCESS_DENIED_TEXT);
assert.deepEqual(runProfile.appAccess.grantedAppIds, ["ops"]);
assert.deepEqual(runProfile.appAccess.deniedAppIds, []);
assert.deepEqual(runProfile.appAccess.notGrantedAppIds, ["engine", "launcher"]);
assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);
console.log(JSON.stringify({
ok: true,
checks: [
"adapter_grant_normalized",
"token_scoped_ops_mcp_in_run_profile",
"assistant_action_relay_in_run_profile",
"app_routing_catalog_in_run_profile",
"hub_actions_route_to_launcher",
"launcher_denial_removes_ops_mcp",
"only_explicit_denied_apps_have_standard_text",
"not_granted_apps_do_not_claim_denied_access",
"public_run_profile_redacts_mcp_headers",
"public_run_profile_redacts_assistant_action_gateway_token",
"stable_public_profile_hash",
],
mcpServerNames: runProfile.toolProfile.mcpServerNames,
profileHash: runProfile.diagnostics.profileHash,
}, null, 2));
function normalizeEntitlementAdapterAppGrants(payload, currentAdapter) {
const source = isPlainObject(payload?.appGrants)
? payload.appGrants
: Array.isArray(payload?.appGrants)
? payload.appGrants
: payload?.grant || payload?.appGrant || payload?.entitlement || payload?.entitlements || payload?.grants || payload;
const out = {};
if (Array.isArray(source)) {
for (const item of source) {
const grant = normalizeEntitlementAdapterGrant(item, currentAdapter);
if (grant?.appId) out[grant.appId] = grant;
}
return out;
}
if (!isPlainObject(source)) return out;
if (source.mcpServers || source.scopes || source.appId || source.app_id || source.surface) {
const grant = normalizeEntitlementAdapterGrant(source, currentAdapter);
if (grant?.appId) out[grant.appId] = grant;
return out;
}
for (const [key, value] of Object.entries(source)) {
const grant = normalizeEntitlementAdapterGrant(value, { ...currentAdapter, appId: normalizeKey(key) || currentAdapter.appId });
if (grant?.appId) out[grant.appId] = grant;
}
return out;
}
function normalizeEntitlementAdapterGrant(value, currentAdapter) {
if (!isPlainObject(value)) return null;
const appId = canonicalRunAppId(value.appId || value.app_id || currentAdapter.appId);
if (!appId) return null;
return {
...value,
appId,
appTitle: optionalString(value.appTitle || value.app_title || value.title || currentAdapter.title),
surface: optionalString(value.surface) || appId,
source: "entitlement-adapter",
adapterId: currentAdapter.id,
};
}
function canonicalRunAppId(value) {
const appId = normalizeKey(value);
if (!appId) return "";
return RUN_APP_ID_ALIASES.get(appId) || appId;
}
function normalizeRunAppGrantsObject(value) {
const out = {};
if (!isPlainObject(value)) return out;
for (const [key, rawGrant] of Object.entries(value)) {
if (!isPlainObject(rawGrant)) continue;
const appId = canonicalRunAppId(rawGrant.appId || rawGrant.app_id || key);
if (!appId) continue;
const existing = isPlainObject(out[appId]) ? out[appId] : {};
const grant = {
...existing,
...rawGrant,
appId,
};
grant.mcpServers = isRunAppGrantDenied(grant)
? []
: Object.hasOwn(rawGrant, "mcpServers") ? rawGrant.mcpServers : existing.mcpServers;
out[appId] = grant;
}
return out;
}
function mergeRunAppGrants(base, next) {
const out = normalizeRunAppGrantsObject(base);
for (const [rawAppId, grant] of Object.entries(normalizeRunAppGrantsObject(next))) {
const appId = canonicalRunAppId(grant.appId || rawAppId);
if (!appId) continue;
const existing = isPlainObject(out[appId]) ? out[appId] : {};
const merged = {
...existing,
...grant,
appId,
};
merged.mcpServers = isRunAppGrantDenied(merged)
? []
: Object.hasOwn(grant, "mcpServers") ? grant.mcpServers : existing.mcpServers;
out[appId] = merged;
}
return out;
}
function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
const servers = [];
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
collectInstallerMcpServers(servers, metadata.mcpServers, {});
const grants = normalizeRunAppGrantsObject(appGrantsInput);
for (const [appId, grant] of Object.entries(grants)) {
if (!isPlainObject(grant)) continue;
if (isRunAppGrantDenied(grant)) continue;
collectInstallerMcpServers(servers, grant.mcpServers, {
appId: optionalString(grant.appId) || normalizeKey(appId),
appTitle: optionalString(grant.appTitle || grant.title),
});
}
const byServerName = new Map();
for (const server of servers.map(sanitizeInstallerMcpServer).filter(Boolean)) {
if (server.enabled === false) continue;
byServerName.set(server.serverName, server);
}
return Array.from(byServerName.values());
}
function summarizeRunAppGrants(metadata) {
const grants = normalizeRunAppGrantsObject(metadata?.appGrants);
const out = {};
for (const [key, value] of Object.entries(grants)) {
if (!isPlainObject(value)) continue;
const appId = canonicalRunAppId(value.appId || key);
if (!appId) continue;
const denied = isRunAppGrantDenied(value);
const mcpServers = Array.isArray(value.mcpServers)
? value.mcpServers
: isPlainObject(value.mcpServers)
? Object.values(value.mcpServers)
: [];
out[appId] = {
appId,
appTitle: optionalString(value.appTitle || value.title),
surface: optionalString(value.surface) || appId,
status: denied ? "denied" : "granted",
granted: !denied,
denied,
deniedReason: denied ? optionalString(value.reason || value.deniedReason || value.denied_reason || value.status) : null,
deniedText: denied ? optionalString(value.deniedText || value.denied_text) || ACCESS_DENIED_TEXT : null,
updatedAt: optionalString(value.updatedAt || value.updated_at),
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
hasMcpServers: !denied && mcpServers.length > 0,
mcpServerNames: mcpServers
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
.filter(Boolean),
};
}
return out;
}
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
const grants = normalizeRunAppGrantsObject(appGrants);
const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : [])
.map((server) => server?.serverName)
.filter(Boolean));
const actionIds = Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds : [];
return APP_ROUTING_CATALOG.map((entry) => {
const grant = isPlainObject(grants[entry.appId]) ? grants[entry.appId] : null;
const grantedByLegacyMcp = !grant && entry.mcpServerNames.some((name) => allMcpServerNames.includes(name));
const denied = grant ? isRunAppGrantDenied(grant) : !grantedByLegacyMcp;
const status = grant ? (denied ? "denied" : "granted") : grantedByLegacyMcp ? "granted" : "not-granted";
return {
schemaVersion: "ai-workspace.app-route.v1",
appId: entry.appId,
appTitle: entry.appTitle,
surface: entry.surface,
skillId: entry.skillId,
status,
granted: status === "granted",
denied: status === "denied",
notGranted: status === "not-granted",
deniedText: status === "denied" ? entry.deniedText : null,
actionIds: actionIds.filter((actionId) => actionIdPrefixesForEntry(entry).some((prefix) => String(actionId || "").startsWith(prefix))),
mcpServerNames: status === "granted"
? uniqueStrings([
...entry.mcpServerNames,
...((Array.isArray(grant?.mcpServers) ? grant.mcpServers : []).map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))),
])
: [],
scopes: uniqueStrings(Array.isArray(grant?.scopes) ? grant.scopes : []),
};
});
}
function actionIdPrefixesForEntry(entry) {
return uniqueStrings([
...(Array.isArray(entry.actionIdPrefixes) ? entry.actionIdPrefixes : []),
`${entry.appId}.`,
...(Array.isArray(entry.appAliases) ? entry.appAliases.map((alias) => `${normalizeKey(alias)}.`) : []),
].filter(Boolean));
}
function summarizeRunAppAccess(appCatalog) {
const apps = Array.isArray(appCatalog) ? appCatalog : [];
return {
schemaVersion: "ai-workspace.app-access.v1",
grantedAppIds: apps.filter((app) => app?.granted === true).map((app) => app.appId).sort(),
deniedAppIds: apps.filter((app) => app?.status === "denied" || app?.denied === true).map((app) => app.appId).sort(),
notGrantedAppIds: apps.filter((app) => app?.status === "not-granted").map((app) => app.appId).sort(),
availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(),
};
}
function isRunAppGrantDenied(grant) {
if (!isPlainObject(grant)) return false;
const status = normalizeKey(grant.status || grant.state || grant.accessStatus || grant.access_status);
return grant.enabled === false ||
grant.allowed === false ||
grant.granted === false ||
grant.denied === true ||
["denied", "disabled", "blocked", "revoked", "not-granted", "not_granted", "forbidden"].includes(status);
}
function collectInstallerMcpServers(target, value, defaults = {}) {
const items = Array.isArray(value)
? value
: isPlainObject(value)
? Object.values(value)
: [];
for (const item of items) {
if (!isPlainObject(item)) continue;
target.push({ ...defaults, ...item });
}
}
function sanitizeInstallerMcpServer(raw) {
if (!isPlainObject(raw)) return null;
const serverName = safeMcpServerName(raw.serverName || raw.server_name || raw.name);
const url = optionalString(raw.url);
if (!serverName || !url) return null;
const httpHeaders = sanitizeInstallerMcpHeaders(raw.httpHeaders || raw.http_headers || raw.headers);
return {
appId: normalizeKey(raw.appId || raw.app_id) || "global",
appTitle: optionalString(raw.appTitle || raw.app_title || raw.title),
serverName,
url,
enabled: raw.enabled !== false,
required: raw.required === true,
startupTimeoutSec: sanitizeInteger(raw.startupTimeoutSec || raw.startup_timeout_sec, 20, 1, 600),
toolTimeoutSec: sanitizeInteger(raw.toolTimeoutSec || raw.tool_timeout_sec, 60, 1, 3600),
httpHeaders,
};
}
function sanitizeInstallerMcpHeaders(value) {
if (!isPlainObject(value)) return {};
const headers = {};
for (const [key, rawValue] of Object.entries(value)) {
const headerName = optionalString(key);
const headerValue = optionalString(rawValue);
if (!headerName || !headerValue || headerName.length > 120 || headerValue.length > 4000) continue;
headers[headerName] = headerValue;
}
return headers;
}
function runProfileHash(runProfile) {
const publicProfile = redactRunProfile(runProfile);
const stableProfile = {
...publicProfile,
runId: undefined,
createdAt: undefined,
diagnostics: {
...(isPlainObject(publicProfile?.diagnostics) ? publicProfile.diagnostics : {}),
profileHash: undefined,
},
};
return createHash("sha256").update(JSON.stringify(stableProfile)).digest("hex").slice(0, 16);
}
function redactRunProfile(runProfile) {
if (!isPlainObject(runProfile)) return null;
return {
...runProfile,
targetContexts: redactForPublicDiagnostics(runProfile.targetContexts),
appGrants: redactForPublicDiagnostics(runProfile.appGrants),
toolProfile: {
...(isPlainObject(runProfile.toolProfile) ? runProfile.toolProfile : {}),
assistantActions: redactAssistantActions(runProfile.toolProfile?.assistantActions),
mcpServers: Array.isArray(runProfile.toolProfile?.mcpServers)
? runProfile.toolProfile.mcpServers.map(redactMcpServer)
: [],
},
};
}
function redactAssistantActions(value) {
if (!isPlainObject(value)) return value;
return {
...value,
...(value.gatewayToken ? { gatewayToken: "<redacted>" } : {}),
};
}
function redactMcpServer(server) {
if (!isPlainObject(server)) return {};
const headers = isPlainObject(server.httpHeaders) ? server.httpHeaders : {};
return {
...server,
httpHeaders: Object.fromEntries(Object.keys(headers).map((key) => [key, "<redacted>"])),
headers: undefined,
};
}
function redactForPublicDiagnostics(value, depth = 0) {
if (depth > 6) return "[max-depth]";
if (Array.isArray(value)) return value.map((item) => redactForPublicDiagnostics(item, depth + 1));
if (!isPlainObject(value)) return value;
const out = {};
for (const [key, item] of Object.entries(value)) {
const normalizedKey = normalizeKey(key);
if (
normalizedKey.includes("token")
|| normalizedKey.includes("secret")
|| normalizedKey.includes("password")
|| normalizedKey === "authorization"
|| normalizedKey === "cookie"
|| normalizedKey === "setcookie"
) {
out[key] = "<redacted>";
} else {
out[key] = redactForPublicDiagnostics(item, depth + 1);
}
}
return out;
}
function safeMcpServerName(value) {
const text = optionalString(value);
if (!text) return "";
return text.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
}
function sanitizeInteger(value, fallback, min, max) {
const number = Number(value || fallback);
if (!Number.isFinite(number)) return fallback;
return Math.min(Math.max(Math.trunc(number), min), max);
}
function uniqueStrings(values) {
return Array.from(new Set((Array.isArray(values) ? values : []).map(optionalString).filter(Boolean)));
}
function normalizeKey(value) {
return optionalString(value).toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
}
function optionalString(value) {
if (value === null || value === undefined) return "";
const text = String(value).trim();
return text.length ? text : "";
}
function isPlainObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}