feat: add AI Workspace app routing catalog
This commit is contained in:
parent
79928d822e
commit
7003efa75a
|
|
@ -4,6 +4,12 @@ import assert from "node:assert/strict";
|
||||||
import { createHash, randomUUID } from "node:crypto";
|
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 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 = {
|
const adapter = {
|
||||||
id: "ops",
|
id: "ops",
|
||||||
|
|
@ -52,6 +58,8 @@ const assistantActions = {
|
||||||
actionIds: ["hub.access_request.list_pending", "hub.user.read_admin_summary"],
|
actionIds: ["hub.access_request.list_pending", "hub.user.read_admin_summary"],
|
||||||
phases: ["preview", "execute"],
|
phases: ["preview", "execute"],
|
||||||
};
|
};
|
||||||
|
const appCatalog = buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions });
|
||||||
|
const appAccess = summarizeRunAppAccess(appCatalog);
|
||||||
const runProfile = {
|
const runProfile = {
|
||||||
schemaVersion: "ai-workspace.run-profile.v1",
|
schemaVersion: "ai-workspace.run-profile.v1",
|
||||||
runId: randomUUID(),
|
runId: randomUUID(),
|
||||||
|
|
@ -73,6 +81,8 @@ const runProfile = {
|
||||||
},
|
},
|
||||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||||
appGrants: appGrantSummary,
|
appGrants: appGrantSummary,
|
||||||
|
appCatalog,
|
||||||
|
appAccess,
|
||||||
toolProfile: {
|
toolProfile: {
|
||||||
schemaVersion: "ai-workspace.tool-profile.v1",
|
schemaVersion: "ai-workspace.tool-profile.v1",
|
||||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||||
|
|
@ -91,6 +101,9 @@ const runProfile = {
|
||||||
adapters: [{ appId: "ops", status: "ok", required: true }],
|
adapters: [{ appId: "ops", status: "ok", required: true }],
|
||||||
},
|
},
|
||||||
mcpServerNames: mcpServers.map((server) => server.serverName),
|
mcpServerNames: mcpServers.map((server) => server.serverName),
|
||||||
|
appCatalogIds: appCatalog.map((app) => app.appId).sort(),
|
||||||
|
grantedAppIds: appAccess.grantedAppIds,
|
||||||
|
deniedAppIds: appAccess.deniedAppIds,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
runProfile.diagnostics.profileHash = runProfileHash(runProfile);
|
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.deepEqual(appGrantSummary.ops.scopes, ["workspace:read", "project:read", "issue:read"]);
|
||||||
assert.equal(appGrantSummary.ops.hasMcpServers, true);
|
assert.equal(appGrantSummary.ops.hasMcpServers, true);
|
||||||
assert.deepEqual(appGrantSummary.ops.mcpServerNames, ["nodedc_ops_agent"]);
|
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.length, 1);
|
||||||
assert.equal(mcpServers[0].appId, "ops");
|
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.gatewayUrl, "https://ai-hub.nodedc.ru/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions");
|
||||||
assert.equal(publicProfile.toolProfile.assistantActions.gatewayToken, "<redacted>");
|
assert.equal(publicProfile.toolProfile.assistantActions.gatewayToken, "<redacted>");
|
||||||
assert.deepEqual(publicProfile.toolProfile.assistantActions.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]);
|
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.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
|
||||||
assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);
|
assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);
|
||||||
|
|
||||||
|
|
@ -128,6 +149,8 @@ console.log(JSON.stringify({
|
||||||
"adapter_grant_normalized",
|
"adapter_grant_normalized",
|
||||||
"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",
|
||||||
|
"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",
|
||||||
"stable_public_profile_hash",
|
"stable_public_profile_hash",
|
||||||
|
|
@ -205,6 +228,7 @@ function summarizeRunAppGrants(metadata) {
|
||||||
if (!isPlainObject(value)) continue;
|
if (!isPlainObject(value)) continue;
|
||||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||||
if (!appId) continue;
|
if (!appId) continue;
|
||||||
|
const denied = isRunAppGrantDenied(value);
|
||||||
const mcpServers = Array.isArray(value.mcpServers)
|
const mcpServers = Array.isArray(value.mcpServers)
|
||||||
? value.mcpServers
|
? value.mcpServers
|
||||||
: isPlainObject(value.mcpServers)
|
: isPlainObject(value.mcpServers)
|
||||||
|
|
@ -214,10 +238,15 @@ function summarizeRunAppGrants(metadata) {
|
||||||
appId,
|
appId,
|
||||||
appTitle: optionalString(value.appTitle || value.title),
|
appTitle: optionalString(value.appTitle || value.title),
|
||||||
surface: optionalString(value.surface) || appId,
|
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),
|
updatedAt: optionalString(value.updatedAt || value.updated_at),
|
||||||
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
|
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
|
||||||
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
|
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
|
||||||
hasMcpServers: mcpServers.length > 0,
|
hasMcpServers: !denied && mcpServers.length > 0,
|
||||||
mcpServerNames: mcpServers
|
mcpServerNames: mcpServers
|
||||||
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
|
|
@ -226,6 +255,60 @@ function summarizeRunAppGrants(metadata) {
|
||||||
return out;
|
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 = {}) {
|
function collectInstallerMcpServers(target, value, defaults = {}) {
|
||||||
const items = Array.isArray(value)
|
const items = Array.isArray(value)
|
||||||
? value
|
? value
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,65 @@ const ASSISTANT_ACTION_TOOL_PROFILE_BASE = {
|
||||||
destructive: "forbidden",
|
destructive: "forbidden",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы.";
|
||||||
|
const APP_ROUTING_CATALOG = [
|
||||||
|
{
|
||||||
|
appId: "launcher",
|
||||||
|
appTitle: "NODE.DC Launcher",
|
||||||
|
surface: "launcher",
|
||||||
|
skillId: "launcher-context",
|
||||||
|
whenToUse: [
|
||||||
|
"users",
|
||||||
|
"roles",
|
||||||
|
"admin scopes",
|
||||||
|
"application access",
|
||||||
|
"company/workspace contours",
|
||||||
|
"invites",
|
||||||
|
"entitlements",
|
||||||
|
],
|
||||||
|
actionNamespaces: ["launcher.*", "access.*"],
|
||||||
|
mcpServerNames: [],
|
||||||
|
requiredScopes: ["launcher:access:read"],
|
||||||
|
deniedText: ACCESS_DENIED_TEXT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: "engine",
|
||||||
|
appTitle: "NODE.DC Engine / InJoin",
|
||||||
|
surface: "engine",
|
||||||
|
skillId: "engine-context",
|
||||||
|
whenToUse: [
|
||||||
|
"workflow graph",
|
||||||
|
"code",
|
||||||
|
"runtime",
|
||||||
|
"agent node",
|
||||||
|
"NDC Agent Core",
|
||||||
|
"engineering automation",
|
||||||
|
],
|
||||||
|
actionNamespaces: ["engine.*", "ndc-agent-core.*"],
|
||||||
|
mcpServerNames: ["nodedc-engine", "nodedc-agent-core"],
|
||||||
|
requiredScopes: ["engine:workspace:read"],
|
||||||
|
deniedText: ACCESS_DENIED_TEXT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: "ops",
|
||||||
|
appTitle: "NODE.DC Ops / Tasker",
|
||||||
|
surface: "ops",
|
||||||
|
skillId: "ops-context",
|
||||||
|
whenToUse: [
|
||||||
|
"projects",
|
||||||
|
"cards",
|
||||||
|
"comments",
|
||||||
|
"structured blocks",
|
||||||
|
"checkers",
|
||||||
|
"labels",
|
||||||
|
"operational reports",
|
||||||
|
],
|
||||||
|
actionNamespaces: ["ops.*", "tasker_*"],
|
||||||
|
mcpServerNames: ["nodedc-ops-agent", "nodedc_ops_agent"],
|
||||||
|
requiredScopes: ["ops:project:read"],
|
||||||
|
deniedText: ACCESS_DENIED_TEXT,
|
||||||
|
},
|
||||||
|
];
|
||||||
const ASSISTANT_ACTION_CACHE_TTL_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_ACTION_CACHE_TTL_MS || 30_000);
|
const ASSISTANT_ACTION_CACHE_TTL_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_ACTION_CACHE_TTL_MS || 30_000);
|
||||||
const ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_TIMEOUT_MS || 25_000);
|
const ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_TIMEOUT_MS || 25_000);
|
||||||
const ASSISTANT_ACTION_RELAY_POLL_IDLE_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_IDLE_MS || 500);
|
const ASSISTANT_ACTION_RELAY_POLL_IDLE_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_IDLE_MS || 500);
|
||||||
|
|
@ -1868,6 +1927,12 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
|
||||||
const mcpServers = runProfileMcpServersFromAppGrants(ownerSettings, grantResolution.appGrants);
|
const mcpServers = runProfileMcpServersFromAppGrants(ownerSettings, grantResolution.appGrants);
|
||||||
const mcpServerNames = mcpServers.map((server) => server.serverName).filter(Boolean);
|
const mcpServerNames = mcpServers.map((server) => server.serverName).filter(Boolean);
|
||||||
const assistantActions = await assistantActionToolProfileForRun();
|
const assistantActions = await assistantActionToolProfileForRun();
|
||||||
|
const appCatalog = buildRunProfileAppCatalog({
|
||||||
|
appGrants: grantResolution.appGrants,
|
||||||
|
mcpServers,
|
||||||
|
assistantActions,
|
||||||
|
});
|
||||||
|
const appAccess = summarizeRunAppAccess(appCatalog);
|
||||||
const requiredMcpServerNames = mcpServers
|
const requiredMcpServerNames = mcpServers
|
||||||
.filter((server) => server.required === true)
|
.filter((server) => server.required === true)
|
||||||
.map((server) => server.serverName)
|
.map((server) => server.serverName)
|
||||||
|
|
@ -1882,6 +1947,9 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
|
||||||
targetSurfaces: Object.keys(targetContexts).map(normalizeKey).filter(Boolean).sort(),
|
targetSurfaces: Object.keys(targetContexts).map(normalizeKey).filter(Boolean).sort(),
|
||||||
enabledToolPacks,
|
enabledToolPacks,
|
||||||
appGrantIds: Object.keys(appGrants).sort(),
|
appGrantIds: Object.keys(appGrants).sort(),
|
||||||
|
appCatalogIds: appCatalog.map((app) => app.appId).sort(),
|
||||||
|
grantedAppIds: appAccess.grantedAppIds,
|
||||||
|
deniedAppIds: appAccess.deniedAppIds,
|
||||||
entitlementAdapters: grantResolution.diagnostics,
|
entitlementAdapters: grantResolution.diagnostics,
|
||||||
mcpServerNames,
|
mcpServerNames,
|
||||||
requiredMcpServerNames,
|
requiredMcpServerNames,
|
||||||
|
|
@ -1907,6 +1975,8 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
|
||||||
targetContexts: redactForPublicDiagnostics(targetContexts),
|
targetContexts: redactForPublicDiagnostics(targetContexts),
|
||||||
enabledToolPacks,
|
enabledToolPacks,
|
||||||
appGrants,
|
appGrants,
|
||||||
|
appCatalog,
|
||||||
|
appAccess,
|
||||||
toolProfile: {
|
toolProfile: {
|
||||||
schemaVersion: "ai-workspace.tool-profile.v1",
|
schemaVersion: "ai-workspace.tool-profile.v1",
|
||||||
enabledToolPacks,
|
enabledToolPacks,
|
||||||
|
|
@ -2110,12 +2180,15 @@ async function resolveRunAppGrants({ owner, context, ownerSettings }) {
|
||||||
for (const [appId, grant] of Object.entries(adapterAppGrants)) {
|
for (const [appId, grant] of Object.entries(adapterAppGrants)) {
|
||||||
if (!isPlainObject(grant)) continue;
|
if (!isPlainObject(grant)) continue;
|
||||||
const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {};
|
const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {};
|
||||||
appGrants[appId] = {
|
const merged = {
|
||||||
...existing,
|
...existing,
|
||||||
...grant,
|
...grant,
|
||||||
appId,
|
appId,
|
||||||
mcpServers: Object.hasOwn(grant, "mcpServers") ? grant.mcpServers : existing.mcpServers,
|
|
||||||
};
|
};
|
||||||
|
merged.mcpServers = isRunAppGrantDenied(merged)
|
||||||
|
? []
|
||||||
|
: Object.hasOwn(grant, "mcpServers") ? grant.mcpServers : existing.mcpServers;
|
||||||
|
appGrants[appId] = merged;
|
||||||
}
|
}
|
||||||
adapterDiagnostics.push({
|
adapterDiagnostics.push({
|
||||||
appId: adapter.appId,
|
appId: adapter.appId,
|
||||||
|
|
@ -2236,10 +2309,14 @@ function runProfileMcpServersFromSettings(settings) {
|
||||||
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, {});
|
const hasEntitlementAdapters = Array.isArray(config.entitlementAdapters) && config.entitlementAdapters.length > 0;
|
||||||
|
if (!hasEntitlementAdapters) {
|
||||||
|
collectInstallerMcpServers(servers, metadata.mcpServers, {});
|
||||||
|
}
|
||||||
const appGrants = isPlainObject(appGrantsInput) ? appGrantsInput : {};
|
const appGrants = isPlainObject(appGrantsInput) ? 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;
|
||||||
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),
|
||||||
|
|
@ -2261,6 +2338,7 @@ function summarizeRunAppGrants(metadata) {
|
||||||
if (!isPlainObject(value)) continue;
|
if (!isPlainObject(value)) continue;
|
||||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||||
if (!appId) continue;
|
if (!appId) continue;
|
||||||
|
const denied = isRunAppGrantDenied(value);
|
||||||
const mcpServers = Array.isArray(value.mcpServers)
|
const mcpServers = Array.isArray(value.mcpServers)
|
||||||
? value.mcpServers
|
? value.mcpServers
|
||||||
: isPlainObject(value.mcpServers)
|
: isPlainObject(value.mcpServers)
|
||||||
|
|
@ -2270,10 +2348,15 @@ function summarizeRunAppGrants(metadata) {
|
||||||
appId,
|
appId,
|
||||||
appTitle: optionalString(value.appTitle || value.title),
|
appTitle: optionalString(value.appTitle || value.title),
|
||||||
surface: optionalString(value.surface) || appId,
|
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),
|
updatedAt: optionalString(value.updatedAt || value.updated_at),
|
||||||
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
|
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
|
||||||
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
|
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
|
||||||
hasMcpServers: mcpServers.length > 0,
|
hasMcpServers: !denied && mcpServers.length > 0,
|
||||||
mcpServerNames: mcpServers
|
mcpServerNames: mcpServers
|
||||||
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
|
|
@ -2282,6 +2365,104 @@ function summarizeRunAppGrants(metadata) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
|
||||||
|
const grants = isPlainObject(appGrants) ? appGrants : {};
|
||||||
|
const dynamicAppIds = Object.keys(grants)
|
||||||
|
.map(normalizeKey)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((appId) => !APP_ROUTING_CATALOG.some((entry) => entry.appId === appId));
|
||||||
|
const catalog = [
|
||||||
|
...APP_ROUTING_CATALOG,
|
||||||
|
...dynamicAppIds.map((appId) => ({
|
||||||
|
appId,
|
||||||
|
appTitle: appId,
|
||||||
|
surface: appId,
|
||||||
|
skillId: `${appId}-context`,
|
||||||
|
whenToUse: [],
|
||||||
|
actionNamespaces: [`${appId}.*`],
|
||||||
|
mcpServerNames: [],
|
||||||
|
requiredScopes: [],
|
||||||
|
deniedText: ACCESS_DENIED_TEXT,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
const mcpByAppId = new Map();
|
||||||
|
for (const server of Array.isArray(mcpServers) ? mcpServers : []) {
|
||||||
|
const appId = normalizeKey(server?.appId);
|
||||||
|
if (!appId) continue;
|
||||||
|
const list = mcpByAppId.get(appId) || [];
|
||||||
|
if (server?.serverName && !list.includes(server.serverName)) list.push(server.serverName);
|
||||||
|
mcpByAppId.set(appId, list);
|
||||||
|
}
|
||||||
|
const actionIds = Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds : [];
|
||||||
|
const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : [])
|
||||||
|
.map((server) => server?.serverName)
|
||||||
|
.filter(Boolean));
|
||||||
|
return 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";
|
||||||
|
const grantMcpServers = grant
|
||||||
|
? Array.isArray(grant.mcpServers)
|
||||||
|
? grant.mcpServers
|
||||||
|
: isPlainObject(grant.mcpServers)
|
||||||
|
? Object.values(grant.mcpServers)
|
||||||
|
: []
|
||||||
|
: [];
|
||||||
|
const grantMcpServerNames = grantMcpServers
|
||||||
|
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||||
|
.filter(Boolean);
|
||||||
|
const advertisedMcpServerNames = uniqueStrings([
|
||||||
|
...entry.mcpServerNames,
|
||||||
|
...grantMcpServerNames,
|
||||||
|
...(mcpByAppId.get(entry.appId) || []),
|
||||||
|
]);
|
||||||
|
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",
|
||||||
|
deniedReason: status !== "granted"
|
||||||
|
? optionalString(grant?.reason || grant?.deniedReason || grant?.denied_reason || grant?.status) || status
|
||||||
|
: null,
|
||||||
|
deniedText: status !== "granted"
|
||||||
|
? optionalString(grant?.deniedText || grant?.denied_text) || entry.deniedText || ACCESS_DENIED_TEXT
|
||||||
|
: null,
|
||||||
|
whenToUse: uniqueStrings(entry.whenToUse),
|
||||||
|
actionNamespaces: uniqueStrings(entry.actionNamespaces),
|
||||||
|
actionIds: actionIds.filter((actionId) => String(actionId || "").startsWith(`${entry.appId}.`)),
|
||||||
|
mcpServerNames: status === "granted" ? advertisedMcpServerNames : [],
|
||||||
|
requiredScopes: uniqueStrings(grant?.requiredScopes || grant?.required_scopes || entry.requiredScopes),
|
||||||
|
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", "not_granted", "forbidden"].includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions }) {
|
function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions }) {
|
||||||
const lines = [
|
const lines = [
|
||||||
"AI Workspace dynamic run profile:",
|
"AI Workspace dynamic run profile:",
|
||||||
|
|
@ -2291,12 +2472,15 @@ function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions })
|
||||||
`- context ready: ${diagnostics.contextReady ? "yes" : "no"}`,
|
`- context ready: ${diagnostics.contextReady ? "yes" : "no"}`,
|
||||||
`- entitlement source: ${diagnostics.entitlementAdapters?.source || "settings"}`,
|
`- entitlement source: ${diagnostics.entitlementAdapters?.source || "settings"}`,
|
||||||
`- enabled tool packs: ${diagnostics.enabledToolPacks.length ? diagnostics.enabledToolPacks.join(", ") : "none"}`,
|
`- enabled tool packs: ${diagnostics.enabledToolPacks.length ? diagnostics.enabledToolPacks.join(", ") : "none"}`,
|
||||||
|
`- app routes granted: ${diagnostics.grantedAppIds.length ? diagnostics.grantedAppIds.join(", ") : "none"}`,
|
||||||
|
`- app routes denied/not granted: ${diagnostics.deniedAppIds.length ? diagnostics.deniedAppIds.join(", ") : "none"}`,
|
||||||
`- MCP servers available in this run: ${diagnostics.mcpServerNames.length ? diagnostics.mcpServerNames.join(", ") : "none"}`,
|
`- MCP servers available in this run: ${diagnostics.mcpServerNames.length ? diagnostics.mcpServerNames.join(", ") : "none"}`,
|
||||||
`- assistant action ids available: ${Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds.join(", ") : "none"}`,
|
`- assistant action ids available: ${Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds.join(", ") : "none"}`,
|
||||||
"- Interpret the user's natural-language request first; call assistant actions only after selecting a structured action id.",
|
"- Interpret the user's natural-language request first; call assistant actions only after selecting a structured action id.",
|
||||||
"- Read assistant actions may execute after structured action selection. Privileged/write assistant actions require preview, explicit user confirmation, then execute.",
|
"- Read assistant actions may execute after structured action selection. Privileged/write assistant actions require preview, explicit user confirmation, then execute.",
|
||||||
"- Ops card actions advertised in this run are valid assistant actions: use ops.card.list_recent for reading cards, ops.card.create for creating cards, and ops.card.add_comment for comments instead of refusing because direct Ops MCP tools are absent.",
|
"- Ops card actions advertised in this run are valid assistant actions: use ops.card.list_recent for reading cards, ops.card.create for creating cards, and ops.card.add_comment for comments instead of refusing because direct Ops MCP tools are absent.",
|
||||||
"- Destructive assistant actions are forbidden; offer safe alternatives such as block/disable instead of delete.",
|
"- Destructive assistant actions are forbidden; offer safe alternatives such as block/disable instead of delete.",
|
||||||
|
`- If the user requests a denied or not-granted app capability, do not bypass it. Say exactly: ${ACCESS_DENIED_TEXT}`,
|
||||||
"- MCP tokens and headers are runtime secrets and must never be printed in public answers.",
|
"- MCP tokens and headers are runtime secrets and must never be printed in public answers.",
|
||||||
];
|
];
|
||||||
const opsContext = isPlainObject(context?.contexts?.ops) ? context.contexts.ops : {};
|
const opsContext = isPlainObject(context?.contexts?.ops) ? context.contexts.ops : {};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue