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";
|
||||
|
||||
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 = {
|
||||
id: "ops",
|
||||
|
|
@ -52,6 +58,8 @@ const assistantActions = {
|
|||
actionIds: ["hub.access_request.list_pending", "hub.user.read_admin_summary"],
|
||||
phases: ["preview", "execute"],
|
||||
};
|
||||
const appCatalog = buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions });
|
||||
const appAccess = summarizeRunAppAccess(appCatalog);
|
||||
const runProfile = {
|
||||
schemaVersion: "ai-workspace.run-profile.v1",
|
||||
runId: randomUUID(),
|
||||
|
|
@ -73,6 +81,8 @@ const runProfile = {
|
|||
},
|
||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||
appGrants: appGrantSummary,
|
||||
appCatalog,
|
||||
appAccess,
|
||||
toolProfile: {
|
||||
schemaVersion: "ai-workspace.tool-profile.v1",
|
||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||
|
|
@ -91,6 +101,9 @@ const runProfile = {
|
|||
adapters: [{ appId: "ops", status: "ok", required: true }],
|
||||
},
|
||||
mcpServerNames: mcpServers.map((server) => server.serverName),
|
||||
appCatalogIds: appCatalog.map((app) => app.appId).sort(),
|
||||
grantedAppIds: appAccess.grantedAppIds,
|
||||
deniedAppIds: appAccess.deniedAppIds,
|
||||
},
|
||||
};
|
||||
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.equal(appGrantSummary.ops.hasMcpServers, true);
|
||||
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[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.gatewayToken, "<redacted>");
|
||||
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.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);
|
||||
|
||||
|
|
@ -128,6 +149,8 @@ console.log(JSON.stringify({
|
|||
"adapter_grant_normalized",
|
||||
"token_scoped_ops_mcp_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_assistant_action_gateway_token",
|
||||
"stable_public_profile_hash",
|
||||
|
|
@ -205,6 +228,7 @@ function summarizeRunAppGrants(metadata) {
|
|||
if (!isPlainObject(value)) continue;
|
||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||
if (!appId) continue;
|
||||
const denied = isRunAppGrantDenied(value);
|
||||
const mcpServers = Array.isArray(value.mcpServers)
|
||||
? value.mcpServers
|
||||
: isPlainObject(value.mcpServers)
|
||||
|
|
@ -214,10 +238,15 @@ function summarizeRunAppGrants(metadata) {
|
|||
appId,
|
||||
appTitle: optionalString(value.appTitle || value.title),
|
||||
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),
|
||||
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
|
||||
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
|
||||
hasMcpServers: mcpServers.length > 0,
|
||||
hasMcpServers: !denied && mcpServers.length > 0,
|
||||
mcpServerNames: mcpServers
|
||||
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||
.filter(Boolean),
|
||||
|
|
@ -226,6 +255,60 @@ function summarizeRunAppGrants(metadata) {
|
|||
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 = {}) {
|
||||
const items = Array.isArray(value)
|
||||
? value
|
||||
|
|
|
|||
|
|
@ -27,6 +27,65 @@ const ASSISTANT_ACTION_TOOL_PROFILE_BASE = {
|
|||
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_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);
|
||||
|
|
@ -1868,6 +1927,12 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
|
|||
const mcpServers = runProfileMcpServersFromAppGrants(ownerSettings, grantResolution.appGrants);
|
||||
const mcpServerNames = mcpServers.map((server) => server.serverName).filter(Boolean);
|
||||
const assistantActions = await assistantActionToolProfileForRun();
|
||||
const appCatalog = buildRunProfileAppCatalog({
|
||||
appGrants: grantResolution.appGrants,
|
||||
mcpServers,
|
||||
assistantActions,
|
||||
});
|
||||
const appAccess = summarizeRunAppAccess(appCatalog);
|
||||
const requiredMcpServerNames = mcpServers
|
||||
.filter((server) => server.required === true)
|
||||
.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(),
|
||||
enabledToolPacks,
|
||||
appGrantIds: Object.keys(appGrants).sort(),
|
||||
appCatalogIds: appCatalog.map((app) => app.appId).sort(),
|
||||
grantedAppIds: appAccess.grantedAppIds,
|
||||
deniedAppIds: appAccess.deniedAppIds,
|
||||
entitlementAdapters: grantResolution.diagnostics,
|
||||
mcpServerNames,
|
||||
requiredMcpServerNames,
|
||||
|
|
@ -1907,6 +1975,8 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
|
|||
targetContexts: redactForPublicDiagnostics(targetContexts),
|
||||
enabledToolPacks,
|
||||
appGrants,
|
||||
appCatalog,
|
||||
appAccess,
|
||||
toolProfile: {
|
||||
schemaVersion: "ai-workspace.tool-profile.v1",
|
||||
enabledToolPacks,
|
||||
|
|
@ -2110,12 +2180,15 @@ async function resolveRunAppGrants({ owner, context, ownerSettings }) {
|
|||
for (const [appId, grant] of Object.entries(adapterAppGrants)) {
|
||||
if (!isPlainObject(grant)) continue;
|
||||
const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {};
|
||||
appGrants[appId] = {
|
||||
const merged = {
|
||||
...existing,
|
||||
...grant,
|
||||
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({
|
||||
appId: adapter.appId,
|
||||
|
|
@ -2236,10 +2309,14 @@ function runProfileMcpServersFromSettings(settings) {
|
|||
function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
|
||||
const servers = [];
|
||||
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
|
||||
const hasEntitlementAdapters = Array.isArray(config.entitlementAdapters) && config.entitlementAdapters.length > 0;
|
||||
if (!hasEntitlementAdapters) {
|
||||
collectInstallerMcpServers(servers, metadata.mcpServers, {});
|
||||
}
|
||||
const appGrants = isPlainObject(appGrantsInput) ? appGrantsInput : {};
|
||||
for (const [appId, grant] of Object.entries(appGrants)) {
|
||||
if (!isPlainObject(grant)) continue;
|
||||
if (isRunAppGrantDenied(grant)) continue;
|
||||
collectInstallerMcpServers(servers, grant.mcpServers, {
|
||||
appId: optionalString(grant.appId) || normalizeKey(appId),
|
||||
appTitle: optionalString(grant.appTitle || grant.title),
|
||||
|
|
@ -2261,6 +2338,7 @@ function summarizeRunAppGrants(metadata) {
|
|||
if (!isPlainObject(value)) continue;
|
||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||
if (!appId) continue;
|
||||
const denied = isRunAppGrantDenied(value);
|
||||
const mcpServers = Array.isArray(value.mcpServers)
|
||||
? value.mcpServers
|
||||
: isPlainObject(value.mcpServers)
|
||||
|
|
@ -2270,10 +2348,15 @@ function summarizeRunAppGrants(metadata) {
|
|||
appId,
|
||||
appTitle: optionalString(value.appTitle || value.title),
|
||||
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),
|
||||
context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}),
|
||||
scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []),
|
||||
hasMcpServers: mcpServers.length > 0,
|
||||
hasMcpServers: !denied && mcpServers.length > 0,
|
||||
mcpServerNames: mcpServers
|
||||
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||
.filter(Boolean),
|
||||
|
|
@ -2282,6 +2365,104 @@ function summarizeRunAppGrants(metadata) {
|
|||
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 }) {
|
||||
const lines = [
|
||||
"AI Workspace dynamic run profile:",
|
||||
|
|
@ -2291,12 +2472,15 @@ function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions })
|
|||
`- context ready: ${diagnostics.contextReady ? "yes" : "no"}`,
|
||||
`- entitlement source: ${diagnostics.entitlementAdapters?.source || "settings"}`,
|
||||
`- 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"}`,
|
||||
`- 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.",
|
||||
"- 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.",
|
||||
"- 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.",
|
||||
];
|
||||
const opsContext = isPlainObject(context?.contexts?.ops) ? context.contexts.ops : {};
|
||||
|
|
|
|||
Loading…
Reference in New Issue