Add dynamic AI Workspace run profiles
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
const SECRET_TOKEN = "secret-run-token-for-smoke";
|
||||
|
||||
const adapter = {
|
||||
id: "ops",
|
||||
appId: "ops",
|
||||
title: "NODE.DC Ops",
|
||||
};
|
||||
|
||||
const adapterPayload = {
|
||||
ok: true,
|
||||
appGrants: {
|
||||
ops: {
|
||||
appTitle: "NODE.DC Ops",
|
||||
surface: "ops",
|
||||
grantMode: "token-scoped-run-grants",
|
||||
context: {
|
||||
opsWorkspaceSlug: "nodedc",
|
||||
opsProjectId: "86629a11-eaff-4ad2-9f89-e5245a344fcc",
|
||||
},
|
||||
scopes: ["workspace:read", "project:read", "issue:read"],
|
||||
mcpServers: [
|
||||
{
|
||||
serverName: "nodedc ops agent",
|
||||
url: "https://ops-agents.nodedc.ru/mcp",
|
||||
required: true,
|
||||
startupTimeoutSec: 20,
|
||||
toolTimeoutSec: 60,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${SECRET_TOKEN}`,
|
||||
Accept: "application/json",
|
||||
"MCP-Protocol-Version": "2025-06-18",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const appGrants = normalizeEntitlementAdapterAppGrants(adapterPayload, adapter);
|
||||
const mcpServers = runProfileMcpServersFromAppGrants({}, appGrants);
|
||||
const appGrantSummary = summarizeRunAppGrants({ appGrants });
|
||||
const runProfile = {
|
||||
schemaVersion: "ai-workspace.run-profile.v1",
|
||||
runId: randomUUID(),
|
||||
createdAt: new Date("2026-06-13T00:00:00.000Z").toISOString(),
|
||||
owner: {
|
||||
ownerKey: "user:smoke-user",
|
||||
userId: "smoke-user",
|
||||
email: "smoke@example.test",
|
||||
role: "member",
|
||||
groups: ["engineering"],
|
||||
},
|
||||
sourceSurface: "engine",
|
||||
modeId: "ops",
|
||||
targetContexts: {
|
||||
ops: {
|
||||
opsWorkspaceSlug: "nodedc",
|
||||
opsProjectId: "86629a11-eaff-4ad2-9f89-e5245a344fcc",
|
||||
},
|
||||
},
|
||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||
appGrants: appGrantSummary,
|
||||
toolProfile: {
|
||||
schemaVersion: "ai-workspace.tool-profile.v1",
|
||||
enabledToolPacks: ["engine", "ops", "ndc-agent-core"],
|
||||
mcpServers,
|
||||
mcpServerNames: mcpServers.map((server) => server.serverName),
|
||||
requiredMcpServerNames: mcpServers.filter((server) => server.required).map((server) => server.serverName),
|
||||
},
|
||||
diagnostics: {
|
||||
schemaVersion: "ai-workspace.run-profile.diagnostics.v1",
|
||||
dynamicProfile: true,
|
||||
sourceSurface: "engine",
|
||||
modeId: "ops",
|
||||
entitlementAdapters: {
|
||||
source: "adapters+settings",
|
||||
adapters: [{ appId: "ops", status: "ok", required: true }],
|
||||
},
|
||||
mcpServerNames: mcpServers.map((server) => server.serverName),
|
||||
},
|
||||
};
|
||||
runProfile.diagnostics.profileHash = runProfileHash(runProfile);
|
||||
const publicProfile = redactRunProfile(runProfile);
|
||||
|
||||
assert.deepEqual(Object.keys(appGrants), ["ops"]);
|
||||
assert.equal(appGrants.ops.source, "entitlement-adapter");
|
||||
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(mcpServers.length, 1);
|
||||
assert.equal(mcpServers[0].appId, "ops");
|
||||
assert.equal(mcpServers[0].serverName, "nodedc_ops_agent");
|
||||
assert.equal(mcpServers[0].required, true);
|
||||
assert.equal(mcpServers[0].httpHeaders.Authorization, `Bearer ${SECRET_TOKEN}`);
|
||||
assert.equal(mcpServers[0].httpHeaders.Accept, "application/json");
|
||||
|
||||
assert.equal(publicProfile.toolProfile.mcpServers.length, 1);
|
||||
assert.equal(publicProfile.toolProfile.mcpServers[0].serverName, "nodedc_ops_agent");
|
||||
assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Authorization, "<redacted>");
|
||||
assert.equal(publicProfile.toolProfile.mcpServers[0].httpHeaders.Accept, "<redacted>");
|
||||
assert.equal(publicProfile.toolProfile.mcpServers[0].headers, undefined);
|
||||
assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false);
|
||||
assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"adapter_grant_normalized",
|
||||
"token_scoped_ops_mcp_in_run_profile",
|
||||
"public_run_profile_redacts_mcp_headers",
|
||||
"stable_public_profile_hash",
|
||||
],
|
||||
mcpServerNames: runProfile.toolProfile.mcpServerNames,
|
||||
profileHash: runProfile.diagnostics.profileHash,
|
||||
}, null, 2));
|
||||
|
||||
function normalizeEntitlementAdapterAppGrants(payload, currentAdapter) {
|
||||
const source = isPlainObject(payload?.appGrants)
|
||||
? payload.appGrants
|
||||
: Array.isArray(payload?.appGrants)
|
||||
? payload.appGrants
|
||||
: payload?.grant || payload?.appGrant || payload?.entitlement || payload?.entitlements || payload?.grants || payload;
|
||||
const out = {};
|
||||
if (Array.isArray(source)) {
|
||||
for (const item of source) {
|
||||
const grant = normalizeEntitlementAdapterGrant(item, currentAdapter);
|
||||
if (grant?.appId) out[grant.appId] = grant;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (!isPlainObject(source)) return out;
|
||||
if (source.mcpServers || source.scopes || source.appId || source.app_id || source.surface) {
|
||||
const grant = normalizeEntitlementAdapterGrant(source, currentAdapter);
|
||||
if (grant?.appId) out[grant.appId] = grant;
|
||||
return out;
|
||||
}
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
const grant = normalizeEntitlementAdapterGrant(value, { ...currentAdapter, appId: normalizeKey(key) || currentAdapter.appId });
|
||||
if (grant?.appId) out[grant.appId] = grant;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeEntitlementAdapterGrant(value, currentAdapter) {
|
||||
if (!isPlainObject(value)) return null;
|
||||
const appId = normalizeKey(value.appId || value.app_id || currentAdapter.appId);
|
||||
if (!appId) return null;
|
||||
return {
|
||||
...value,
|
||||
appId,
|
||||
appTitle: optionalString(value.appTitle || value.app_title || value.title || currentAdapter.title),
|
||||
surface: optionalString(value.surface) || appId,
|
||||
source: "entitlement-adapter",
|
||||
adapterId: currentAdapter.id,
|
||||
};
|
||||
}
|
||||
|
||||
function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) {
|
||||
const servers = [];
|
||||
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
|
||||
collectInstallerMcpServers(servers, metadata.mcpServers, {});
|
||||
const grants = isPlainObject(appGrantsInput) ? appGrantsInput : {};
|
||||
for (const [appId, grant] of Object.entries(grants)) {
|
||||
if (!isPlainObject(grant)) continue;
|
||||
collectInstallerMcpServers(servers, grant.mcpServers, {
|
||||
appId: optionalString(grant.appId) || normalizeKey(appId),
|
||||
appTitle: optionalString(grant.appTitle || grant.title),
|
||||
});
|
||||
}
|
||||
|
||||
const byServerName = new Map();
|
||||
for (const server of servers.map(sanitizeInstallerMcpServer).filter(Boolean)) {
|
||||
if (server.enabled === false) continue;
|
||||
byServerName.set(server.serverName, server);
|
||||
}
|
||||
return Array.from(byServerName.values());
|
||||
}
|
||||
|
||||
function summarizeRunAppGrants(metadata) {
|
||||
const grants = isPlainObject(metadata?.appGrants) ? metadata.appGrants : {};
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(grants)) {
|
||||
if (!isPlainObject(value)) continue;
|
||||
const appId = optionalString(value.appId) || normalizeKey(key);
|
||||
if (!appId) continue;
|
||||
const mcpServers = Array.isArray(value.mcpServers)
|
||||
? value.mcpServers
|
||||
: isPlainObject(value.mcpServers)
|
||||
? Object.values(value.mcpServers)
|
||||
: [];
|
||||
out[appId] = {
|
||||
appId,
|
||||
appTitle: optionalString(value.appTitle || value.title),
|
||||
surface: optionalString(value.surface) || appId,
|
||||
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,
|
||||
mcpServerNames: mcpServers
|
||||
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
||||
.filter(Boolean),
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectInstallerMcpServers(target, value, defaults = {}) {
|
||||
const items = Array.isArray(value)
|
||||
? value
|
||||
: isPlainObject(value)
|
||||
? Object.values(value)
|
||||
: [];
|
||||
for (const item of items) {
|
||||
if (!isPlainObject(item)) continue;
|
||||
target.push({ ...defaults, ...item });
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeInstallerMcpServer(raw) {
|
||||
if (!isPlainObject(raw)) return null;
|
||||
const serverName = safeMcpServerName(raw.serverName || raw.server_name || raw.name);
|
||||
const url = optionalString(raw.url);
|
||||
if (!serverName || !url) return null;
|
||||
const httpHeaders = sanitizeInstallerMcpHeaders(raw.httpHeaders || raw.http_headers || raw.headers);
|
||||
return {
|
||||
appId: normalizeKey(raw.appId || raw.app_id) || "global",
|
||||
appTitle: optionalString(raw.appTitle || raw.app_title || raw.title),
|
||||
serverName,
|
||||
url,
|
||||
enabled: raw.enabled !== false,
|
||||
required: raw.required === true,
|
||||
startupTimeoutSec: sanitizeInteger(raw.startupTimeoutSec || raw.startup_timeout_sec, 20, 1, 600),
|
||||
toolTimeoutSec: sanitizeInteger(raw.toolTimeoutSec || raw.tool_timeout_sec, 60, 1, 3600),
|
||||
httpHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeInstallerMcpHeaders(value) {
|
||||
if (!isPlainObject(value)) return {};
|
||||
const headers = {};
|
||||
for (const [key, rawValue] of Object.entries(value)) {
|
||||
const headerName = optionalString(key);
|
||||
const headerValue = optionalString(rawValue);
|
||||
if (!headerName || !headerValue || headerName.length > 120 || headerValue.length > 4000) continue;
|
||||
headers[headerName] = headerValue;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function runProfileHash(runProfile) {
|
||||
const publicProfile = redactRunProfile(runProfile);
|
||||
const stableProfile = {
|
||||
...publicProfile,
|
||||
runId: undefined,
|
||||
createdAt: undefined,
|
||||
diagnostics: {
|
||||
...(isPlainObject(publicProfile?.diagnostics) ? publicProfile.diagnostics : {}),
|
||||
profileHash: undefined,
|
||||
},
|
||||
};
|
||||
return createHash("sha256").update(JSON.stringify(stableProfile)).digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
function redactRunProfile(runProfile) {
|
||||
if (!isPlainObject(runProfile)) return null;
|
||||
return {
|
||||
...runProfile,
|
||||
targetContexts: redactForPublicDiagnostics(runProfile.targetContexts),
|
||||
appGrants: redactForPublicDiagnostics(runProfile.appGrants),
|
||||
toolProfile: {
|
||||
...(isPlainObject(runProfile.toolProfile) ? runProfile.toolProfile : {}),
|
||||
mcpServers: Array.isArray(runProfile.toolProfile?.mcpServers)
|
||||
? runProfile.toolProfile.mcpServers.map(redactMcpServer)
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function redactMcpServer(server) {
|
||||
if (!isPlainObject(server)) return {};
|
||||
const headers = isPlainObject(server.httpHeaders) ? server.httpHeaders : {};
|
||||
return {
|
||||
...server,
|
||||
httpHeaders: Object.fromEntries(Object.keys(headers).map((key) => [key, "<redacted>"])),
|
||||
headers: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function redactForPublicDiagnostics(value, depth = 0) {
|
||||
if (depth > 6) return "[max-depth]";
|
||||
if (Array.isArray(value)) return value.map((item) => redactForPublicDiagnostics(item, depth + 1));
|
||||
if (!isPlainObject(value)) return value;
|
||||
const out = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
const normalizedKey = normalizeKey(key);
|
||||
if (
|
||||
normalizedKey.includes("token")
|
||||
|| normalizedKey.includes("secret")
|
||||
|| normalizedKey.includes("password")
|
||||
|| normalizedKey === "authorization"
|
||||
|| normalizedKey === "cookie"
|
||||
|| normalizedKey === "setcookie"
|
||||
) {
|
||||
out[key] = "<redacted>";
|
||||
} else {
|
||||
out[key] = redactForPublicDiagnostics(item, depth + 1);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function safeMcpServerName(value) {
|
||||
const text = optionalString(value);
|
||||
if (!text) return "";
|
||||
return text.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
|
||||
}
|
||||
|
||||
function sanitizeInteger(value, fallback, min, max) {
|
||||
const number = Number(value || fallback);
|
||||
if (!Number.isFinite(number)) return fallback;
|
||||
return Math.min(Math.max(Math.trunc(number), min), max);
|
||||
}
|
||||
|
||||
function uniqueStrings(values) {
|
||||
return Array.from(new Set((Array.isArray(values) ? values : []).map(optionalString).filter(Boolean)));
|
||||
}
|
||||
|
||||
function normalizeKey(value) {
|
||||
return optionalString(value).toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
||||
}
|
||||
|
||||
function optionalString(value) {
|
||||
if (value === null || value === undefined) return "";
|
||||
const text = String(value).trim();
|
||||
return text.length ? text : "";
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user