#!/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, ""); assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Accept, ""); 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, ""); 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: "" } : {}), }; } 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, ""])), 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] = ""; } 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); }