feat: add AI Workspace app routing catalog
This commit is contained in:
@@ -4,6 +4,12 @@ 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 APP_ROUTING_CATALOG = [
|
||||
{ appId: "launcher", appTitle: "NODE.DC Launcher", surface: "launcher", skillId: "launcher-context", 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",
|
||||
@@ -52,6 +58,8 @@ const assistantActions = {
|
||||
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 runProfile = {
|
||||
schemaVersion: "ai-workspace.run-profile.v1",
|
||||
runId: randomUUID(),
|
||||
@@ -73,6 +81,8 @@ const runProfile = {
|
||||
},
|
||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||
appGrants: appGrantSummary,
|
||||
appCatalog,
|
||||
appAccess,
|
||||
toolProfile: {
|
||||
schemaVersion: "ai-workspace.tool-profile.v1",
|
||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||
@@ -91,6 +101,9 @@ const runProfile = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
runProfile.diagnostics.profileHash = runProfileHash(runProfile);
|
||||
@@ -102,6 +115,8 @@ 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");
|
||||
@@ -119,6 +134,12 @@ assert.equal(publicProfile.toolProfile.assistantActions.endpoint, "/api/ai-works
|
||||
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.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")?.deniedText, ACCESS_DENIED_TEXT);
|
||||
assert.deepEqual(runProfile.appAccess.grantedAppIds, ["ops"]);
|
||||
assert.deepEqual(runProfile.appAccess.deniedAppIds, ["engine", "launcher"]);
|
||||
assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
|
||||
assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);
|
||||
|
||||
@@ -128,6 +149,8 @@ console.log(JSON.stringify({
|
||||
"adapter_grant_normalized",
|
||||
"token_scoped_ops_mcp_in_run_profile",
|
||||
"assistant_action_relay_in_run_profile",
|
||||
"app_routing_catalog_in_run_profile",
|
||||
"denied_apps_have_standard_text",
|
||||
"public_run_profile_redacts_mcp_headers",
|
||||
"public_run_profile_redacts_assistant_action_gateway_token",
|
||||
"stable_public_profile_hash",
|
||||
@@ -205,6 +228,7 @@ function summarizeRunAppGrants(metadata) {
|
||||
if (!isPlainObject(value)) continue;
|
||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||
if (!appId) continue;
|
||||
const denied = isRunAppGrantDenied(value);
|
||||
const mcpServers = Array.isArray(value.mcpServers)
|
||||
? value.mcpServers
|
||||
: isPlainObject(value.mcpServers)
|
||||
@@ -214,10 +238,15 @@ function summarizeRunAppGrants(metadata) {
|
||||
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: mcpServers.length > 0,
|
||||
hasMcpServers: !denied && mcpServers.length > 0,
|
||||
mcpServerNames: mcpServers
|
||||
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||
.filter(Boolean),
|
||||
@@ -226,6 +255,60 @@ function summarizeRunAppGrants(metadata) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
|
||||
const grants = isPlainObject(appGrants) ? 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 !== "granted",
|
||||
deniedText: status !== "granted" ? entry.deniedText : null,
|
||||
actionIds: actionIds.filter((actionId) => String(actionId || "").startsWith(`${entry.appId}.`)),
|
||||
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 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?.granted !== true).map((app) => app.appId).sort(),
|
||||
availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(),
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
};
|
||||
}
|
||||
|
||||
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", "forbidden"].includes(status);
|
||||
}
|
||||
|
||||
function collectInstallerMcpServers(target, value, defaults = {}) {
|
||||
const items = Array.isArray(value)
|
||||
? value
|
||||
|
||||
Reference in New Issue
Block a user