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

View File

@ -86,6 +86,16 @@ NDC_LAUNCHER_INTERNAL_ACCESS_TOKEN=...
If `NDC_LAUNCHER_INTERNAL_ACCESS_TOKEN` is not set, the service falls back to `NODEDC_INTERNAL_ACCESS_TOKEN`. If `NDC_LAUNCHER_INTERNAL_ACCESS_TOKEN` is not set, the service falls back to `NODEDC_INTERNAL_ACCESS_TOKEN`.
Launcher entitlement adapter configuration:
```text
AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL=http://launcher:5173/api/ai-workspace/internal/v1/entitlements
AI_WORKSPACE_LAUNCHER_ENTITLEMENT_TOKEN=...
AI_WORKSPACE_LAUNCHER_ENTITLEMENT_REQUIRED=true
```
The Launcher adapter is evaluated after app-owned adapters and may return `launcher`, `ops`, and `engine` app grants. A Launcher denial is the top-level service-access decision and clears MCP servers for that app in the run profile; app-owned adapters still own app-local scopes and runtime tokens.
The first HUB action ids exposed to assistants are: The first HUB action ids exposed to assistants are:
- `hub.user.read_admin_summary` - `hub.user.read_admin_summary`

View File

@ -5,8 +5,14 @@ import { createHash, randomUUID } from "node:crypto";
const SECRET_TOKEN = "secret-run-token-for-smoke"; const SECRET_TOKEN = "secret-run-token-for-smoke";
const ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы."; 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 = [ 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: "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 }, { 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 appCatalog = buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions });
const appAccess = summarizeRunAppAccess(appCatalog); 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 = { const runProfile = {
schemaVersion: "ai-workspace.run-profile.v1", schemaVersion: "ai-workspace.run-profile.v1",
runId: randomUUID(), 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.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")?.status, "not-granted");
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.deniedText, ACCESS_DENIED_TEXT); 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.grantedAppIds, ["ops"]);
assert.deepEqual(runProfile.appAccess.deniedAppIds, ["engine", "launcher"]); assert.deepEqual(runProfile.appAccess.deniedAppIds, ["engine", "launcher"]);
assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false); assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
@ -150,6 +193,8 @@ console.log(JSON.stringify({
"token_scoped_ops_mcp_in_run_profile", "token_scoped_ops_mcp_in_run_profile",
"assistant_action_relay_in_run_profile", "assistant_action_relay_in_run_profile",
"app_routing_catalog_in_run_profile", "app_routing_catalog_in_run_profile",
"hub_actions_route_to_launcher",
"launcher_denial_removes_ops_mcp",
"denied_apps_have_standard_text", "denied_apps_have_standard_text",
"public_run_profile_redacts_mcp_headers", "public_run_profile_redacts_mcp_headers",
"public_run_profile_redacts_assistant_action_gateway_token", "public_run_profile_redacts_assistant_action_gateway_token",
@ -188,7 +233,7 @@ function normalizeEntitlementAdapterAppGrants(payload, currentAdapter) {
function normalizeEntitlementAdapterGrant(value, currentAdapter) { function normalizeEntitlementAdapterGrant(value, currentAdapter) {
if (!isPlainObject(value)) return null; 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; if (!appId) return null;
return { return {
...value, ...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 = {}) { function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
const servers = []; const servers = [];
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {}; const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
collectInstallerMcpServers(servers, metadata.mcpServers, {}); collectInstallerMcpServers(servers, metadata.mcpServers, {});
const grants = isPlainObject(appGrantsInput) ? appGrantsInput : {}; const grants = normalizeRunAppGrantsObject(appGrantsInput);
for (const [appId, grant] of Object.entries(grants)) { for (const [appId, grant] of Object.entries(grants)) {
if (!isPlainObject(grant)) continue; if (!isPlainObject(grant)) continue;
if (isRunAppGrantDenied(grant)) continue;
collectInstallerMcpServers(servers, grant.mcpServers, { collectInstallerMcpServers(servers, grant.mcpServers, {
appId: optionalString(grant.appId) || normalizeKey(appId), appId: optionalString(grant.appId) || normalizeKey(appId),
appTitle: optionalString(grant.appTitle || grant.title), appTitle: optionalString(grant.appTitle || grant.title),
@ -222,11 +314,11 @@ function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
} }
function summarizeRunAppGrants(metadata) { function summarizeRunAppGrants(metadata) {
const grants = isPlainObject(metadata?.appGrants) ? metadata.appGrants : {}; const grants = normalizeRunAppGrantsObject(metadata?.appGrants);
const out = {}; const out = {};
for (const [key, value] of Object.entries(grants)) { for (const [key, value] of Object.entries(grants)) {
if (!isPlainObject(value)) continue; if (!isPlainObject(value)) continue;
const appId = optionalString(value.appId) || normalizeKey(key); const appId = canonicalRunAppId(value.appId || key);
if (!appId) continue; if (!appId) continue;
const denied = isRunAppGrantDenied(value); const denied = isRunAppGrantDenied(value);
const mcpServers = Array.isArray(value.mcpServers) const mcpServers = Array.isArray(value.mcpServers)
@ -256,7 +348,7 @@ function summarizeRunAppGrants(metadata) {
} }
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) { function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
const grants = isPlainObject(appGrants) ? appGrants : {}; const grants = normalizeRunAppGrantsObject(appGrants);
const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : []) const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : [])
.map((server) => server?.serverName) .map((server) => server?.serverName)
.filter(Boolean)); .filter(Boolean));
@ -276,7 +368,7 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
granted: status === "granted", granted: status === "granted",
denied: status !== "granted", denied: status !== "granted",
deniedText: status !== "granted" ? entry.deniedText : null, 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" mcpServerNames: status === "granted"
? uniqueStrings([ ? uniqueStrings([
...entry.mcpServerNames, ...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) { function summarizeRunAppAccess(appCatalog) {
const apps = Array.isArray(appCatalog) ? appCatalog : []; const apps = Array.isArray(appCatalog) ? appCatalog : [];
return { return {
@ -306,7 +406,7 @@ function isRunAppGrantDenied(grant) {
grant.allowed === false || grant.allowed === false ||
grant.granted === false || grant.granted === false ||
grant.denied === true || 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 = {}) { function collectInstallerMcpServers(target, value, defaults = {}) {

View File

@ -28,6 +28,12 @@ const ASSISTANT_ACTION_TOOL_PROFILE_BASE = {
}, },
}; };
const ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы."; 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 = [ const APP_ROUTING_CATALOG = [
{ {
appId: "launcher", appId: "launcher",
@ -43,7 +49,9 @@ const APP_ROUTING_CATALOG = [
"invites", "invites",
"entitlements", "entitlements",
], ],
actionNamespaces: ["launcher.*", "access.*"], appAliases: ["hub"],
actionNamespaces: ["hub.*", "launcher.*", "access.*"],
actionIdPrefixes: ["hub.", "launcher.", "access."],
mcpServerNames: [], mcpServerNames: [],
requiredScopes: ["launcher:access:read"], requiredScopes: ["launcher:access:read"],
deniedText: ACCESS_DENIED_TEXT, deniedText: ACCESS_DENIED_TEXT,
@ -2144,7 +2152,7 @@ function delay(ms) {
async function resolveRunAppGrants({ owner, context, ownerSettings }) { async function resolveRunAppGrants({ owner, context, ownerSettings }) {
const metadata = isPlainObject(ownerSettings?.metadata) ? ownerSettings.metadata : {}; const metadata = isPlainObject(ownerSettings?.metadata) ? ownerSettings.metadata : {};
const staticAppGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {}; const staticAppGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {};
const appGrants = { ...staticAppGrants }; const appGrants = normalizeRunAppGrantsObject(staticAppGrants);
const adapterDiagnostics = []; const adapterDiagnostics = [];
const adapters = Array.isArray(config.entitlementAdapters) ? config.entitlementAdapters : []; const adapters = Array.isArray(config.entitlementAdapters) ? config.entitlementAdapters : [];
if (!adapters.length) { if (!adapters.length) {
@ -2177,8 +2185,10 @@ async function resolveRunAppGrants({ owner, context, ownerSettings }) {
continue; continue;
} }
const adapterAppGrants = normalizeEntitlementAdapterAppGrants(result.payload, adapter); 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; if (!isPlainObject(grant)) continue;
const appId = canonicalRunAppId(grant.appId || rawAppId);
if (!appId) continue;
const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {}; const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {};
const merged = { const merged = {
...existing, ...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 }) { async function fetchRunEntitlementAdapter({ adapter, owner, context, ownerSettings }) {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), adapter.timeoutMs); const timeout = setTimeout(() => controller.abort(), adapter.timeoutMs);
@ -2288,7 +2319,7 @@ function normalizeEntitlementAdapterAppGrants(payload, adapter) {
function normalizeEntitlementAdapterGrant(value, adapter) { function normalizeEntitlementAdapterGrant(value, adapter) {
if (!isPlainObject(value)) return null; 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; if (!appId) return null;
return { return {
...value, ...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) { function runProfileMcpServersFromSettings(settings) {
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {}; const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
const appGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {}; const appGrants = normalizeRunAppGrantsObject(metadata.appGrants);
return runProfileMcpServersFromAppGrants(settings, appGrants); return runProfileMcpServersFromAppGrants(settings, appGrants);
} }
@ -2313,7 +2350,7 @@ function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
if (!hasEntitlementAdapters) { if (!hasEntitlementAdapters) {
collectInstallerMcpServers(servers, metadata.mcpServers, {}); collectInstallerMcpServers(servers, metadata.mcpServers, {});
} }
const appGrants = isPlainObject(appGrantsInput) ? appGrantsInput : {}; const appGrants = normalizeRunAppGrantsObject(appGrantsInput);
for (const [appId, grant] of Object.entries(appGrants)) { for (const [appId, grant] of Object.entries(appGrants)) {
if (!isPlainObject(grant)) continue; if (!isPlainObject(grant)) continue;
if (isRunAppGrantDenied(grant)) continue; if (isRunAppGrantDenied(grant)) continue;
@ -2332,11 +2369,11 @@ function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
} }
function summarizeRunAppGrants(metadata) { function summarizeRunAppGrants(metadata) {
const appGrants = isPlainObject(metadata?.appGrants) ? metadata.appGrants : {}; const appGrants = normalizeRunAppGrantsObject(metadata?.appGrants);
const out = {}; const out = {};
for (const [key, value] of Object.entries(appGrants)) { for (const [key, value] of Object.entries(appGrants)) {
if (!isPlainObject(value)) continue; if (!isPlainObject(value)) continue;
const appId = optionalString(value.appId) || normalizeKey(key); const appId = canonicalRunAppId(value.appId || key);
if (!appId) continue; if (!appId) continue;
const denied = isRunAppGrantDenied(value); const denied = isRunAppGrantDenied(value);
const mcpServers = Array.isArray(value.mcpServers) const mcpServers = Array.isArray(value.mcpServers)
@ -2366,9 +2403,9 @@ function summarizeRunAppGrants(metadata) {
} }
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) { function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
const grants = isPlainObject(appGrants) ? appGrants : {}; const grants = normalizeRunAppGrantsObject(appGrants);
const dynamicAppIds = Object.keys(grants) const dynamicAppIds = Object.keys(grants)
.map(normalizeKey) .map(canonicalRunAppId)
.filter(Boolean) .filter(Boolean)
.filter((appId) => !APP_ROUTING_CATALOG.some((entry) => entry.appId === appId)); .filter((appId) => !APP_ROUTING_CATALOG.some((entry) => entry.appId === appId));
const catalog = [ const catalog = [
@ -2380,6 +2417,7 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
skillId: `${appId}-context`, skillId: `${appId}-context`,
whenToUse: [], whenToUse: [],
actionNamespaces: [`${appId}.*`], actionNamespaces: [`${appId}.*`],
actionIdPrefixes: [`${appId}.`],
mcpServerNames: [], mcpServerNames: [],
requiredScopes: [], requiredScopes: [],
deniedText: ACCESS_DENIED_TEXT, deniedText: ACCESS_DENIED_TEXT,
@ -2417,6 +2455,11 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
...grantMcpServerNames, ...grantMcpServerNames,
...(mcpByAppId.get(entry.appId) || []), ...(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 { return {
schemaVersion: "ai-workspace.app-route.v1", schemaVersion: "ai-workspace.app-route.v1",
appId: entry.appId, appId: entry.appId,
@ -2434,7 +2477,7 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions })
: null, : null,
whenToUse: uniqueStrings(entry.whenToUse), whenToUse: uniqueStrings(entry.whenToUse),
actionNamespaces: uniqueStrings(entry.actionNamespaces), 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 : [], mcpServerNames: status === "granted" ? advertisedMcpServerNames : [],
requiredScopes: uniqueStrings(grant?.requiredScopes || grant?.required_scopes || entry.requiredScopes), requiredScopes: uniqueStrings(grant?.requiredScopes || grant?.required_scopes || entry.requiredScopes),
scopes: uniqueStrings(Array.isArray(grant?.scopes) ? grant.scopes : []), 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, 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, 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(); const byAppId = new Map();
for (const adapter of adapters) { for (const adapter of adapters) {
byAppId.set(adapter.appId, adapter); 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) { function parseJsonEnv(value) {
@ -4045,7 +4111,7 @@ function collectEntitlementAdapter(target, defaultAppId, value) {
function sanitizeEntitlementAdapter(value, defaultAppId = "") { function sanitizeEntitlementAdapter(value, defaultAppId = "") {
if (!isPlainObject(value)) return null; 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); const url = cleanHttpEndpoint(value.url || value.endpoint);
if (!appId || !url) return null; if (!appId || !url) return null;
const tokenEnv = optionalString(value.tokenEnv || value.token_env); const tokenEnv = optionalString(value.tokenEnv || value.token_env);