feat: wire Launcher AI Workspace entitlements
This commit is contained in:
@@ -5,8 +5,14 @@ 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", mcpServerNames: [], deniedText: ACCESS_DENIED_TEXT },
|
||||
{ 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 },
|
||||
];
|
||||
@@ -60,6 +66,37 @@ const assistantActions = {
|
||||
};
|
||||
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 runProfile = {
|
||||
schemaVersion: "ai-workspace.run-profile.v1",
|
||||
runId: randomUUID(),
|
||||
@@ -138,6 +175,12 @@ assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.status, "
|
||||
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.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.deepEqual(runProfile.appAccess.grantedAppIds, ["ops"]);
|
||||
assert.deepEqual(runProfile.appAccess.deniedAppIds, ["engine", "launcher"]);
|
||||
assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
|
||||
@@ -150,6 +193,8 @@ console.log(JSON.stringify({
|
||||
"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",
|
||||
"denied_apps_have_standard_text",
|
||||
"public_run_profile_redacts_mcp_headers",
|
||||
"public_run_profile_redacts_assistant_action_gateway_token",
|
||||
@@ -188,7 +233,7 @@ function normalizeEntitlementAdapterAppGrants(payload, currentAdapter) {
|
||||
|
||||
function normalizeEntitlementAdapterGrant(value, currentAdapter) {
|
||||
if (!isPlainObject(value)) return null;
|
||||
const appId = normalizeKey(value.appId || value.app_id || currentAdapter.appId);
|
||||
const appId = canonicalRunAppId(value.appId || value.app_id || currentAdapter.appId);
|
||||
if (!appId) return null;
|
||||
return {
|
||||
...value,
|
||||
@@ -200,13 +245,60 @@ function normalizeEntitlementAdapterGrant(value, currentAdapter) {
|
||||
};
|
||||
}
|
||||
|
||||
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 = isPlainObject(appGrantsInput) ? appGrantsInput : {};
|
||||
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),
|
||||
@@ -222,11 +314,11 @@ function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
|
||||
}
|
||||
|
||||
function summarizeRunAppGrants(metadata) {
|
||||
const grants = isPlainObject(metadata?.appGrants) ? metadata.appGrants : {};
|
||||
const grants = normalizeRunAppGrantsObject(metadata?.appGrants);
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(grants)) {
|
||||
if (!isPlainObject(value)) continue;
|
||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||
const appId = canonicalRunAppId(value.appId || key);
|
||||
if (!appId) continue;
|
||||
const denied = isRunAppGrantDenied(value);
|
||||
const mcpServers = Array.isArray(value.mcpServers)
|
||||
@@ -256,7 +348,7 @@ function summarizeRunAppGrants(metadata) {
|
||||
}
|
||||
|
||||
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
|
||||
const grants = isPlainObject(appGrants) ? appGrants : {};
|
||||
const grants = normalizeRunAppGrantsObject(appGrants);
|
||||
const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : [])
|
||||
.map((server) => server?.serverName)
|
||||
.filter(Boolean));
|
||||
@@ -276,7 +368,7 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
|
||||
granted: status === "granted",
|
||||
denied: status !== "granted",
|
||||
deniedText: status !== "granted" ? entry.deniedText : null,
|
||||
actionIds: actionIds.filter((actionId) => String(actionId || "").startsWith(`${entry.appId}.`)),
|
||||
actionIds: actionIds.filter((actionId) => actionIdPrefixesForEntry(entry).some((prefix) => String(actionId || "").startsWith(prefix))),
|
||||
mcpServerNames: status === "granted"
|
||||
? uniqueStrings([
|
||||
...entry.mcpServerNames,
|
||||
@@ -288,6 +380,14 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -306,7 +406,7 @@ function isRunAppGrantDenied(grant) {
|
||||
grant.allowed === false ||
|
||||
grant.granted === false ||
|
||||
grant.denied === true ||
|
||||
["denied", "disabled", "blocked", "revoked", "not_granted", "forbidden"].includes(status);
|
||||
["denied", "disabled", "blocked", "revoked", "not-granted", "not_granted", "forbidden"].includes(status);
|
||||
}
|
||||
|
||||
function collectInstallerMcpServers(target, value, defaults = {}) {
|
||||
|
||||
Reference in New Issue
Block a user