feat: wire Launcher AI Workspace entitlements

This commit is contained in:
Codex
2026-06-24 22:19:40 +03:00
parent e11f9bd7e7
commit f35cd34167
3 changed files with 197 additions and 21 deletions
+79 -13
View File
@@ -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);