569 lines
22 KiB
JavaScript
569 lines
22 KiB
JavaScript
#!/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 ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы.";
|
|
const RUN_APP_ID_ALIASES = new Map([
|
|
["hub", "launcher"],
|
|
["nodedc-hub", "launcher"],
|
|
["nodedc_launcher", "launcher"],
|
|
["nodedc-launcher", "launcher"],
|
|
]);
|
|
const APP_ROUTING_CATALOG = [
|
|
{ appId: "launcher", 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: "ops", appTitle: "NODE.DC Ops / Tasker", surface: "ops", skillId: "ops-context", mcpServerNames: ["nodedc_ops_agent"], deniedText: ACCESS_DENIED_TEXT },
|
|
];
|
|
|
|
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 assistantActions = {
|
|
schemaVersion: "ai-workspace.assistant-actions.v1",
|
|
endpoint: "/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions",
|
|
gatewayUrl: "https://ai-hub.nodedc.ru/api/ai-workspace/hub/v1/assistant-relays/local-dev/actions",
|
|
gatewayToken: SECRET_TOKEN,
|
|
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 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 explicitDeniedCatalog = buildRunProfileAppCatalog({
|
|
appGrants: deniedByLauncher,
|
|
mcpServers: deniedMcpServers,
|
|
assistantActions,
|
|
});
|
|
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,
|
|
appCatalog,
|
|
appAccess,
|
|
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),
|
|
assistantActions,
|
|
},
|
|
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),
|
|
appCatalogIds: appCatalog.map((app) => app.appId).sort(),
|
|
grantedAppIds: appAccess.grantedAppIds,
|
|
deniedAppIds: appAccess.deniedAppIds,
|
|
notGrantedAppIds: appAccess.notGrantedAppIds,
|
|
},
|
|
};
|
|
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(appGrantSummary.ops.status, "granted");
|
|
assert.equal(appGrantSummary.ops.denied, false);
|
|
|
|
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(publicProfile.toolProfile.assistantActions.endpoint, "/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.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.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.denied, false);
|
|
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")?.denied, false);
|
|
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.notGranted, true);
|
|
assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.deniedText, null);
|
|
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.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.denied, true);
|
|
assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.deniedText, ACCESS_DENIED_TEXT);
|
|
assert.deepEqual(runProfile.appAccess.grantedAppIds, ["ops"]);
|
|
assert.deepEqual(runProfile.appAccess.deniedAppIds, []);
|
|
assert.deepEqual(runProfile.appAccess.notGrantedAppIds, ["engine", "launcher"]);
|
|
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",
|
|
"assistant_action_relay_in_run_profile",
|
|
"app_routing_catalog_in_run_profile",
|
|
"hub_actions_route_to_launcher",
|
|
"launcher_denial_removes_ops_mcp",
|
|
"only_explicit_denied_apps_have_standard_text",
|
|
"not_granted_apps_do_not_claim_denied_access",
|
|
"public_run_profile_redacts_mcp_headers",
|
|
"public_run_profile_redacts_assistant_action_gateway_token",
|
|
"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 = canonicalRunAppId(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 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 = {}) {
|
|
const servers = [];
|
|
const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {};
|
|
collectInstallerMcpServers(servers, metadata.mcpServers, {});
|
|
const grants = normalizeRunAppGrantsObject(appGrantsInput);
|
|
for (const [appId, grant] of Object.entries(grants)) {
|
|
if (!isPlainObject(grant)) continue;
|
|
if (isRunAppGrantDenied(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 = normalizeRunAppGrantsObject(metadata?.appGrants);
|
|
const out = {};
|
|
for (const [key, value] of Object.entries(grants)) {
|
|
if (!isPlainObject(value)) continue;
|
|
const appId = canonicalRunAppId(value.appId || key);
|
|
if (!appId) continue;
|
|
const denied = isRunAppGrantDenied(value);
|
|
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,
|
|
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: !denied && mcpServers.length > 0,
|
|
mcpServerNames: mcpServers
|
|
.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name))
|
|
.filter(Boolean),
|
|
};
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) {
|
|
const grants = normalizeRunAppGrantsObject(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 === "denied",
|
|
notGranted: status === "not-granted",
|
|
deniedText: status === "denied" ? entry.deniedText : null,
|
|
actionIds: actionIds.filter((actionId) => actionIdPrefixesForEntry(entry).some((prefix) => String(actionId || "").startsWith(prefix))),
|
|
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 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) {
|
|
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?.status === "denied" || app?.denied === true).map((app) => app.appId).sort(),
|
|
notGrantedAppIds: apps.filter((app) => app?.status === "not-granted").map((app) => app.appId).sort(),
|
|
availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(),
|
|
};
|
|
}
|
|
|
|
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 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 : {}),
|
|
assistantActions: redactAssistantActions(runProfile.toolProfile?.assistantActions),
|
|
mcpServers: Array.isArray(runProfile.toolProfile?.mcpServers)
|
|
? runProfile.toolProfile.mcpServers.map(redactMcpServer)
|
|
: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function redactAssistantActions(value) {
|
|
if (!isPlainObject(value)) return value;
|
|
return {
|
|
...value,
|
|
...(value.gatewayToken ? { gatewayToken: "<redacted>" } : {}),
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|