feat: wire Launcher AI Workspace entitlements
This commit is contained in:
@@ -28,6 +28,12 @@ const ASSISTANT_ACTION_TOOL_PROFILE_BASE = {
|
||||
},
|
||||
};
|
||||
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",
|
||||
@@ -43,7 +49,9 @@ const APP_ROUTING_CATALOG = [
|
||||
"invites",
|
||||
"entitlements",
|
||||
],
|
||||
actionNamespaces: ["launcher.*", "access.*"],
|
||||
appAliases: ["hub"],
|
||||
actionNamespaces: ["hub.*", "launcher.*", "access.*"],
|
||||
actionIdPrefixes: ["hub.", "launcher.", "access."],
|
||||
mcpServerNames: [],
|
||||
requiredScopes: ["launcher:access:read"],
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
@@ -2144,7 +2152,7 @@ function delay(ms) {
|
||||
async function resolveRunAppGrants({ owner, context, ownerSettings }) {
|
||||
const metadata = isPlainObject(ownerSettings?.metadata) ? ownerSettings.metadata : {};
|
||||
const staticAppGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {};
|
||||
const appGrants = { ...staticAppGrants };
|
||||
const appGrants = normalizeRunAppGrantsObject(staticAppGrants);
|
||||
const adapterDiagnostics = [];
|
||||
const adapters = Array.isArray(config.entitlementAdapters) ? config.entitlementAdapters : [];
|
||||
if (!adapters.length) {
|
||||
@@ -2177,8 +2185,10 @@ async function resolveRunAppGrants({ owner, context, ownerSettings }) {
|
||||
continue;
|
||||
}
|
||||
const adapterAppGrants = normalizeEntitlementAdapterAppGrants(result.payload, adapter);
|
||||
for (const [appId, grant] of Object.entries(adapterAppGrants)) {
|
||||
for (const [rawAppId, grant] of Object.entries(adapterAppGrants)) {
|
||||
if (!isPlainObject(grant)) continue;
|
||||
const appId = canonicalRunAppId(grant.appId || rawAppId);
|
||||
if (!appId) continue;
|
||||
const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {};
|
||||
const merged = {
|
||||
...existing,
|
||||
@@ -2217,6 +2227,27 @@ async function resolveRunAppGrants({ owner, context, ownerSettings }) {
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function fetchRunEntitlementAdapter({ adapter, owner, context, ownerSettings }) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), adapter.timeoutMs);
|
||||
@@ -2288,7 +2319,7 @@ function normalizeEntitlementAdapterAppGrants(payload, adapter) {
|
||||
|
||||
function normalizeEntitlementAdapterGrant(value, adapter) {
|
||||
if (!isPlainObject(value)) return null;
|
||||
const appId = normalizeKey(value.appId || value.app_id || adapter.appId);
|
||||
const appId = canonicalRunAppId(value.appId || value.app_id || adapter.appId);
|
||||
if (!appId) return null;
|
||||
return {
|
||||
...value,
|
||||
@@ -2300,9 +2331,15 @@ function normalizeEntitlementAdapterGrant(value, adapter) {
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalRunAppId(value) {
|
||||
const appId = normalizeKey(value);
|
||||
if (!appId) return "";
|
||||
return RUN_APP_ID_ALIASES.get(appId) || appId;
|
||||
}
|
||||
|
||||
function runProfileMcpServersFromSettings(settings) {
|
||||
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
|
||||
const appGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {};
|
||||
const appGrants = normalizeRunAppGrantsObject(metadata.appGrants);
|
||||
return runProfileMcpServersFromAppGrants(settings, appGrants);
|
||||
}
|
||||
|
||||
@@ -2313,7 +2350,7 @@ function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
|
||||
if (!hasEntitlementAdapters) {
|
||||
collectInstallerMcpServers(servers, metadata.mcpServers, {});
|
||||
}
|
||||
const appGrants = isPlainObject(appGrantsInput) ? appGrantsInput : {};
|
||||
const appGrants = normalizeRunAppGrantsObject(appGrantsInput);
|
||||
for (const [appId, grant] of Object.entries(appGrants)) {
|
||||
if (!isPlainObject(grant)) continue;
|
||||
if (isRunAppGrantDenied(grant)) continue;
|
||||
@@ -2332,11 +2369,11 @@ function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
|
||||
}
|
||||
|
||||
function summarizeRunAppGrants(metadata) {
|
||||
const appGrants = isPlainObject(metadata?.appGrants) ? metadata.appGrants : {};
|
||||
const appGrants = normalizeRunAppGrantsObject(metadata?.appGrants);
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(appGrants)) {
|
||||
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)
|
||||
@@ -2366,9 +2403,9 @@ function summarizeRunAppGrants(metadata) {
|
||||
}
|
||||
|
||||
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
|
||||
const grants = isPlainObject(appGrants) ? appGrants : {};
|
||||
const grants = normalizeRunAppGrantsObject(appGrants);
|
||||
const dynamicAppIds = Object.keys(grants)
|
||||
.map(normalizeKey)
|
||||
.map(canonicalRunAppId)
|
||||
.filter(Boolean)
|
||||
.filter((appId) => !APP_ROUTING_CATALOG.some((entry) => entry.appId === appId));
|
||||
const catalog = [
|
||||
@@ -2380,6 +2417,7 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
|
||||
skillId: `${appId}-context`,
|
||||
whenToUse: [],
|
||||
actionNamespaces: [`${appId}.*`],
|
||||
actionIdPrefixes: [`${appId}.`],
|
||||
mcpServerNames: [],
|
||||
requiredScopes: [],
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
@@ -2417,6 +2455,11 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
|
||||
...grantMcpServerNames,
|
||||
...(mcpByAppId.get(entry.appId) || []),
|
||||
]);
|
||||
const actionIdPrefixes = uniqueStrings([
|
||||
...(Array.isArray(entry.actionIdPrefixes) ? entry.actionIdPrefixes : []),
|
||||
`${entry.appId}.`,
|
||||
...(Array.isArray(entry.appAliases) ? entry.appAliases.map((alias) => `${normalizeKey(alias)}.`) : []),
|
||||
].filter(Boolean));
|
||||
return {
|
||||
schemaVersion: "ai-workspace.app-route.v1",
|
||||
appId: entry.appId,
|
||||
@@ -2434,7 +2477,7 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
|
||||
: null,
|
||||
whenToUse: uniqueStrings(entry.whenToUse),
|
||||
actionNamespaces: uniqueStrings(entry.actionNamespaces),
|
||||
actionIds: actionIds.filter((actionId) => String(actionId || "").startsWith(`${entry.appId}.`)),
|
||||
actionIds: actionIds.filter((actionId) => actionIdPrefixes.some((prefix) => String(actionId || "").startsWith(prefix))),
|
||||
mcpServerNames: status === "granted" ? advertisedMcpServerNames : [],
|
||||
requiredScopes: uniqueStrings(grant?.requiredScopes || grant?.required_scopes || entry.requiredScopes),
|
||||
scopes: uniqueStrings(Array.isArray(grant?.scopes) ? grant.scopes : []),
|
||||
@@ -4009,12 +4052,35 @@ function parseEntitlementAdapters() {
|
||||
token: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN,
|
||||
required: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED,
|
||||
});
|
||||
collectEntitlementAdapter(adapters, "launcher", {
|
||||
url:
|
||||
process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL ||
|
||||
process.env.NDC_AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL ||
|
||||
process.env.AI_WORKSPACE_HUB_ENTITLEMENT_URL ||
|
||||
process.env.NDC_AI_WORKSPACE_HUB_ENTITLEMENT_URL,
|
||||
token:
|
||||
process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_TOKEN ||
|
||||
process.env.NDC_AI_WORKSPACE_LAUNCHER_ENTITLEMENT_TOKEN ||
|
||||
process.env.AI_WORKSPACE_HUB_ENTITLEMENT_TOKEN ||
|
||||
process.env.NDC_AI_WORKSPACE_HUB_ENTITLEMENT_TOKEN,
|
||||
required:
|
||||
process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_REQUIRED ||
|
||||
process.env.NDC_AI_WORKSPACE_LAUNCHER_ENTITLEMENT_REQUIRED ||
|
||||
process.env.AI_WORKSPACE_HUB_ENTITLEMENT_REQUIRED ||
|
||||
process.env.NDC_AI_WORKSPACE_HUB_ENTITLEMENT_REQUIRED,
|
||||
});
|
||||
|
||||
const byAppId = new Map();
|
||||
for (const adapter of adapters) {
|
||||
byAppId.set(adapter.appId, adapter);
|
||||
}
|
||||
return Array.from(byAppId.values());
|
||||
const order = new Map([
|
||||
["ops", 10],
|
||||
["engine", 20],
|
||||
["launcher", 90],
|
||||
]);
|
||||
return Array.from(byAppId.values())
|
||||
.sort((left, right) => (order.get(left.appId) || 50) - (order.get(right.appId) || 50));
|
||||
}
|
||||
|
||||
function parseJsonEnv(value) {
|
||||
@@ -4045,7 +4111,7 @@ function collectEntitlementAdapter(target, defaultAppId, value) {
|
||||
|
||||
function sanitizeEntitlementAdapter(value, defaultAppId = "") {
|
||||
if (!isPlainObject(value)) return null;
|
||||
const appId = normalizeKey(value.appId || value.app_id || defaultAppId);
|
||||
const appId = canonicalRunAppId(value.appId || value.app_id || defaultAppId);
|
||||
const url = cleanHttpEndpoint(value.url || value.endpoint);
|
||||
if (!appId || !url) return null;
|
||||
const tokenEnv = optionalString(value.tokenEnv || value.token_env);
|
||||
|
||||
Reference in New Issue
Block a user