feat: add AI Workspace bridge skills and version gate

This commit is contained in:
Codex
2026-06-24 21:55:20 +03:00
parent 999fe4d906
commit 79928d822e
9 changed files with 371 additions and 2 deletions
@@ -1204,6 +1204,22 @@ async function checkHubExecutor(executor, checkedAt) {
9000
);
const health = isPlainObject(healthPayload.payload) ? healthPayload.payload : {};
const compatibility = bridgeCompatibilityStatus(health, agent);
if (!compatibility.ok) {
return executorCheckResult(
"error",
"agent_update_required",
optionalString(health.runtimeCheckedAt) || agent.lastSeenAt || checkedAt,
{
checkedAt,
mode: "hub",
pairingCode,
agent,
health: summarizeBridgeHealth(health),
bridgeCompatibility: compatibility,
}
);
}
const runtimeOk = health.ok !== false && health.runtimeStatus !== "error";
return executorCheckResult(
runtimeOk ? "online" : "error",
@@ -1254,6 +1270,16 @@ async function checkDirectExecutor(executor, checkedAt) {
try {
const health = await fetchJson(url, { method: "GET", headers: { Accept: "application/json" } }, 7000);
const compatibility = bridgeCompatibilityStatus(health, {});
if (!compatibility.ok) {
return executorCheckResult("error", "agent_update_required", optionalString(health.runtimeCheckedAt) || checkedAt, {
checkedAt,
mode: "direct",
endpoint: executor.endpoint,
health: summarizeBridgeHealth(health),
bridgeCompatibility: compatibility,
});
}
const runtimeOk = health.ok !== false && health.runtimeStatus !== "error";
return executorCheckResult(
runtimeOk ? "online" : "error",
@@ -2579,6 +2605,9 @@ function summarizeBridgeHealth(health) {
return {
ok: health.ok !== false,
service: optionalString(health.service),
agentVersion: optionalString(health.agentVersion),
protocolVersion: optionalString(health.protocolVersion),
capabilities: Array.isArray(health.capabilities) ? uniqueStrings(health.capabilities).slice(0, 80) : [],
host: optionalString(health.host),
machineName: optionalString(health.machineName),
runtimeStatus: optionalString(health.runtimeStatus),
@@ -2589,6 +2618,63 @@ function summarizeBridgeHealth(health) {
};
}
function bridgeCompatibilityStatus(health, agent) {
const agentVersion = optionalString(health?.agentVersion || agent?.agentVersion);
const protocolVersion = optionalString(health?.protocolVersion || agent?.protocolVersion);
const capabilities = uniqueStrings([
...(Array.isArray(agent?.capabilities) ? agent.capabilities : []),
...(Array.isArray(health?.capabilities) ? health.capabilities : []),
]);
const requiredCapabilities = Array.isArray(config.requiredBridgeCapabilities)
? config.requiredBridgeCapabilities
: [];
const missingCapabilities = requiredCapabilities.filter((item) => !capabilities.includes(item));
const minAgentVersion = optionalString(config.minimumBridgeAgentVersion);
const requiredProtocolVersion = optionalString(config.requiredBridgeProtocolVersion);
if (requiredProtocolVersion && protocolVersion !== requiredProtocolVersion) {
return {
ok: false,
reason: "protocol_version_required",
agentVersion,
protocolVersion,
requiredProtocolVersion,
minimumBridgeAgentVersion: minAgentVersion,
missingCapabilities,
};
}
if (minAgentVersion && compareSemanticVersion(agentVersion, minAgentVersion) < 0) {
return {
ok: false,
reason: "agent_version_too_old",
agentVersion,
protocolVersion,
requiredProtocolVersion,
minimumBridgeAgentVersion: minAgentVersion,
missingCapabilities,
};
}
if (missingCapabilities.length) {
return {
ok: false,
reason: "capability_required",
agentVersion,
protocolVersion,
requiredProtocolVersion,
minimumBridgeAgentVersion: minAgentVersion,
missingCapabilities,
};
}
return {
ok: true,
agentVersion,
protocolVersion,
requiredProtocolVersion,
minimumBridgeAgentVersion: minAgentVersion,
missingCapabilities: [],
};
}
function cleanBridgeEventForUi(event) {
if (!isPlainObject(event)) return event;
const kind = optionalString(event.kind) || "";
@@ -3634,6 +3720,28 @@ function normalizeKey(value) {
return String(value || "").trim().toLowerCase().replace(/_/g, "-");
}
function compareSemanticVersion(actual, minimum) {
const actualText = optionalString(actual);
const minimumText = optionalString(minimum);
if (!minimumText) return 0;
if (!actualText) return -1;
const parse = (value) => String(value || "")
.split(/[.-]/)
.slice(0, 4)
.map((part) => {
const match = part.match(/^\d+/);
return match ? Number(match[0]) : 0;
});
const left = parse(actualText);
const right = parse(minimumText);
const length = Math.max(left.length, right.length, 3);
for (let i = 0; i < length; i += 1) {
const delta = Number(left[i] || 0) - Number(right[i] || 0);
if (delta !== 0) return delta > 0 ? 1 : -1;
}
return 0;
}
function isPlainObject(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
@@ -3878,6 +3986,19 @@ function readConfig() {
optionalString(process.env.AI_WORKSPACE_BRIDGE_PACKAGE_NAME) ||
optionalString(process.env.NDC_AI_WORKSPACE_BRIDGE_PACKAGE_NAME) ||
AI_WORKSPACE_BRIDGE_PACKAGE_NAME,
minimumBridgeAgentVersion:
optionalString(process.env.AI_WORKSPACE_MIN_BRIDGE_AGENT_VERSION) ||
optionalString(process.env.NDC_AI_WORKSPACE_MIN_BRIDGE_AGENT_VERSION) ||
"",
requiredBridgeProtocolVersion:
optionalString(process.env.AI_WORKSPACE_REQUIRED_BRIDGE_PROTOCOL_VERSION) ||
optionalString(process.env.NDC_AI_WORKSPACE_REQUIRED_BRIDGE_PROTOCOL_VERSION) ||
"",
requiredBridgeCapabilities: uniqueStrings(String(
process.env.AI_WORKSPACE_REQUIRED_BRIDGE_CAPABILITIES ||
process.env.NDC_AI_WORKSPACE_REQUIRED_BRIDGE_CAPABILITIES ||
""
).split(/[\n,;]+/)),
setupCodeTtlSeconds: sanitizeInteger(
process.env.AI_WORKSPACE_SETUP_CODE_TTL_SECONDS ||
process.env.NDC_AI_WORKSPACE_SETUP_CODE_TTL_SECONDS,