feat: add AI Workspace bridge skills and version gate
This commit is contained in:
parent
999fe4d906
commit
79928d822e
|
|
@ -14,9 +14,19 @@ Commands:
|
|||
ai-workspace-bridge setup <setup-code> [--gateway <url>] [--install-root <path>] [--workspace <path>] [--port <port>]
|
||||
ai-workspace-bridge status [--install-root <path>]
|
||||
ai-workspace-bridge doctor [--install-root <path>]
|
||||
ai-workspace-bridge update [--install-root <path>] [--no-restart]
|
||||
ai-workspace-bridge start [--install-root <path>]
|
||||
ai-workspace-bridge stop [--install-root <path>]
|
||||
ai-workspace-bridge logs [--install-root <path>]
|
||||
```
|
||||
|
||||
The setup code is one-time and short-lived. The CLI does not print the redeemed pairing code or MCP tokens.
|
||||
|
||||
`setup` and `update` install bundled NODE.DC Codex skills into the bridge `CODEX_HOME/skills` directory:
|
||||
|
||||
- `ai-workspace-context`
|
||||
- `engine-context`
|
||||
- `launcher-context`
|
||||
- `ops-context`
|
||||
|
||||
The worker reports `agentVersion`, `protocolVersion`, and `capabilities` in local health and Hub hello metadata. Server-side version gates are controlled by Assistant environment configuration so legacy PowerShell agents can remain available as a fallback until a deploy explicitly requires a newer npm bridge.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
name: ai-workspace-context
|
||||
description: Use when running inside NODE.DC AI Workspace Bridge to understand platform routing, Hub transport, run profiles, app grants, dynamic MCP, and access-denied behavior.
|
||||
---
|
||||
|
||||
# NODE.DC AI Workspace Context
|
||||
|
||||
You are running through NODE.DC AI Workspace. Treat the current run profile, app catalog, and tool profile as the source of truth for what this run can do.
|
||||
|
||||
Core boundaries:
|
||||
- AI Workspace Hub is transport only: pairing, relay, websocket/http delivery, and setup-code redeem. Do not treat Hub as the owner of product roles or business permissions.
|
||||
- Launcher/access plane owns user roles, app availability, company/workspace contours, and entitlement decisions.
|
||||
- Engine owns workflow, graph, code, run/runtime, and engineering actions.
|
||||
- Ops owns projects, cards, comments, reports, checkers, labels, and operational task state.
|
||||
- Worker/bridge is an executor. It must not invent domain permissions, long-lived secrets, or app-specific business policy.
|
||||
|
||||
Before acting, infer the target app from the user's intent and the current UI context. Prefer the run profile app catalog and available MCP/action namespaces over guessing from names.
|
||||
|
||||
If a requested app or capability is not present in the run profile, do not bypass it through another app. Say: `Доступ к модулю ограничен, обратитесь к администратору системы.` If another part of the request is available, complete only that available part and clearly separate the blocked part.
|
||||
|
||||
Never print MCP tokens, headers, setup codes, pairing codes, gateway tokens, or run-scoped secrets.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: engine-context
|
||||
description: Use for NODE.DC Engine/InJoin workflow, graph, code, runtime, NDC Agent Core, agent-node, and engineering automation tasks inside AI Workspace.
|
||||
---
|
||||
|
||||
# NODE.DC Engine Context
|
||||
|
||||
Use Engine when the user asks to code, inspect or change workflows, work with graph nodes, NDC Agent Core, InJoin, runtime errors, generated code, integrations, or workflow-level automation.
|
||||
|
||||
Routing rules:
|
||||
- Use Engine capabilities only when the run profile exposes Engine or NDC Agent Core tools/actions.
|
||||
- Treat Ops as a separate product. Creating cards, comments, and operational reports belongs to Ops, even if the request starts from Engine UI.
|
||||
- Treat Launcher/access as a separate product. Changing user access or roles belongs to Launcher.
|
||||
|
||||
For write actions, prefer the structured Engine capability exposed in the current run profile. If the action is risky, present a preview and wait for confirmation when the run profile or action policy requires it.
|
||||
|
||||
If Engine is not available in the current run profile, do not simulate Engine writes with filesystem edits or unrelated tools. Say: `Доступ к модулю ограничен, обратитесь к администратору системы.`
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: launcher-context
|
||||
description: Use for NODE.DC Launcher/access-plane tasks: users, app access, admin roles, company/workspace contours, invitations, and entitlement decisions.
|
||||
---
|
||||
|
||||
# NODE.DC Launcher Context
|
||||
|
||||
Launcher/access plane is the source of truth for who can access which NODE.DC applications and administrative surfaces.
|
||||
|
||||
Use Launcher when the user asks about:
|
||||
- enabling or disabling access to Ops, Engine, Launcher, or future apps;
|
||||
- assigning admin scope, user role, company/workspace contour, or application access;
|
||||
- reading pending access requests, invites, memberships, or access matrix state;
|
||||
- explaining why an assistant cannot reach an application.
|
||||
|
||||
Do not treat AI Workspace Hub as Launcher. Hub only transports bridge traffic and setup-code redeem. It does not decide product access.
|
||||
|
||||
Do not delete users, workspaces, projects, or role records through the assistant. Prefer safe actions such as block, disable, revoke access, or create an audit note when those actions are explicitly available.
|
||||
|
||||
If Launcher capability is unavailable in the current run profile, say: `Доступ к модулю ограничен, обратитесь к администратору системы.`
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
name: ops-context
|
||||
description: Use for NODE.DC Ops/Tasker projects, cards, comments, structured blocks, checkers, labels, assignees, reports, and operational task updates inside AI Workspace.
|
||||
---
|
||||
|
||||
# NODE.DC Ops Context
|
||||
|
||||
Use Ops when the user asks to create, read, update, summarize, or report on projects, cards, comments, structured blocks, checkers, labels, assignees, or operational task state.
|
||||
|
||||
Inside AI Workspace, use the Ops MCP/actions exposed by the current run profile. Do not assume direct local Ops tools exist unless the run profile advertises them.
|
||||
|
||||
Card-writing rules:
|
||||
- Keep titles concise and operational.
|
||||
- Put current architecture, planned architecture, implementation notes, and validation into structured text blocks when the available Ops capability supports them.
|
||||
- Put short verifiable work items into checker blocks.
|
||||
- Do not delete or archive cards unless a safe, explicitly exposed capability and confirmation policy allow the requested operation. Prefer comments or status updates.
|
||||
|
||||
If Ops is not available in the current run profile, say: `Доступ к модулю ограничен, обратитесь к администратору системы.`
|
||||
|
|
@ -43,6 +43,18 @@ const PAIRING_CODE = String(process.env.AI_BRIDGE_PAIRING_CODE || '').trim()
|
|||
const MACHINE_NAME = String(process.env.AI_BRIDGE_MACHINE_NAME || os.hostname()).trim()
|
||||
const HUB_RECONNECT_MS = Number(process.env.AI_BRIDGE_HUB_RECONNECT_MS || 5000)
|
||||
const HUB_CONNECT_TIMEOUT_MS = Number(process.env.AI_BRIDGE_HUB_CONNECT_TIMEOUT_MS || 10000)
|
||||
const BRIDGE_AGENT_VERSION = String(process.env.AI_BRIDGE_AGENT_VERSION || '0.0.0').trim()
|
||||
const BRIDGE_PROTOCOL_VERSION = String(process.env.AI_BRIDGE_PROTOCOL_VERSION || 'ai-workspace-bridge.v1').trim()
|
||||
const BRIDGE_CAPABILITIES = parseBridgeCapabilities(process.env.AI_BRIDGE_CAPABILITIES || [
|
||||
'hub-outbound',
|
||||
'local-health',
|
||||
'codex-exec',
|
||||
'codex-resume',
|
||||
'codex-stop',
|
||||
'dynamic-run-profile',
|
||||
'dynamic-mcp',
|
||||
'bundled-skills',
|
||||
].join(','))
|
||||
const hubState = {
|
||||
mode: Boolean(HUB_URLS.length && PAIRING_CODE),
|
||||
connected: false,
|
||||
|
|
@ -129,6 +141,19 @@ function parseHubUrls(raw, primary = '') {
|
|||
return values
|
||||
}
|
||||
|
||||
function parseBridgeCapabilities(raw) {
|
||||
const values = Array.isArray(raw)
|
||||
? raw
|
||||
: String(raw || '').split(/[\n,;]+/)
|
||||
const out = []
|
||||
for (const value of values) {
|
||||
const text = String(value || '').trim().replace(/[^a-zA-Z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '')
|
||||
if (!text || out.includes(text)) continue
|
||||
out.push(text.slice(0, 80))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function nowMs() {
|
||||
return Date.now()
|
||||
}
|
||||
|
|
@ -1916,6 +1941,9 @@ async function handleBridgeCommand(command, payload = {}, onEvent = () => {}, me
|
|||
ok: runtime.ok,
|
||||
error: runtime.error,
|
||||
service: 'ai-workspace-bridge',
|
||||
agentVersion: BRIDGE_AGENT_VERSION,
|
||||
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
||||
capabilities: BRIDGE_CAPABILITIES,
|
||||
host: os.hostname(),
|
||||
machineName: MACHINE_NAME,
|
||||
runtimeStatus: runtime.ok ? 'ready' : 'error',
|
||||
|
|
@ -2046,6 +2074,9 @@ function attachHubSocket(socket, hubUrl) {
|
|||
socket.send(JSON.stringify({
|
||||
type: 'hello',
|
||||
machineName: MACHINE_NAME,
|
||||
agentVersion: BRIDGE_AGENT_VERSION,
|
||||
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
||||
capabilities: BRIDGE_CAPABILITIES,
|
||||
host: os.hostname(),
|
||||
cwd: CODEX_CWD,
|
||||
runtimeStatus: runtime.ok ? 'ready' : 'error',
|
||||
|
|
@ -2059,6 +2090,9 @@ function attachHubSocket(socket, hubUrl) {
|
|||
socket.send(JSON.stringify({
|
||||
type: 'hello',
|
||||
machineName: MACHINE_NAME,
|
||||
agentVersion: BRIDGE_AGENT_VERSION,
|
||||
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
||||
capabilities: BRIDGE_CAPABILITIES,
|
||||
host: os.hostname(),
|
||||
cwd: CODEX_CWD,
|
||||
runtimeStatus: 'error',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
||||
import fssync from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
|
@ -11,6 +11,18 @@ const PACKAGE_NAME = "@nodedc/ai-workspace-bridge";
|
|||
const SERVICE_NAME = "NDC AI Workspace Bridge";
|
||||
const WINDOWS_TASK_NAME = "NDC AI Workspace Bridge";
|
||||
const HEALTH_PATH = "/api/ai-workspace/bridge/v1/health";
|
||||
const BRIDGE_PROTOCOL_VERSION = "ai-workspace-bridge.v1";
|
||||
const BRIDGE_CAPABILITIES = [
|
||||
"hub-outbound",
|
||||
"local-health",
|
||||
"codex-exec",
|
||||
"codex-resume",
|
||||
"codex-stop",
|
||||
"dynamic-run-profile",
|
||||
"dynamic-mcp",
|
||||
"bundled-skills",
|
||||
"self-update",
|
||||
];
|
||||
const DEFAULT_CODEX_ARGS = [
|
||||
"exec",
|
||||
"--json",
|
||||
|
|
@ -31,6 +43,7 @@ const DEFAULT_CODEX_ARGS = [
|
|||
const CLI_FILE = fileURLToPath(import.meta.url);
|
||||
const PACKAGE_ROOT = path.resolve(path.dirname(CLI_FILE), "..");
|
||||
const ASSETS_DIR = path.join(PACKAGE_ROOT, "assets");
|
||||
const PACKAGE_VERSION = readPackageVersion();
|
||||
|
||||
class UsageError extends Error {}
|
||||
|
||||
|
|
@ -44,6 +57,7 @@ async function main() {
|
|||
if (args.command === "setup") return runSetup(args);
|
||||
if (args.command === "status") return runStatus(args, { doctor: false });
|
||||
if (args.command === "doctor") return runStatus(args, { doctor: true });
|
||||
if (args.command === "update") return runUpdate(args);
|
||||
if (args.command === "start") return runStart(args);
|
||||
if (args.command === "stop") return runStop(args);
|
||||
if (args.command === "logs") return runLogs(args);
|
||||
|
|
@ -81,6 +95,10 @@ async function runSetup(args) {
|
|||
await writeConfig(paths.configPath, {
|
||||
service: SERVICE_NAME,
|
||||
packageName: PACKAGE_NAME,
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
agentVersion: PACKAGE_VERSION,
|
||||
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
||||
capabilities: BRIDGE_CAPABILITIES,
|
||||
installedAt: new Date().toISOString(),
|
||||
gateway,
|
||||
installRoot,
|
||||
|
|
@ -142,6 +160,11 @@ async function runStatus(args, { doctor }) {
|
|||
console.log(`Install root: ${config.installRoot || installRoot}`);
|
||||
console.log(`Workspace: ${config.workspace || ""}`);
|
||||
console.log(`Port: ${config.port || ""}`);
|
||||
console.log(`Agent: ${config.agentVersion || config.packageVersion || "unknown"}`);
|
||||
console.log(`Protocol: ${config.protocolVersion || "unknown"}`);
|
||||
if (Array.isArray(config.capabilities) && config.capabilities.length) {
|
||||
console.log(`Capabilities: ${config.capabilities.join(", ")}`);
|
||||
}
|
||||
console.log(`Hub: ${health.ok ? "online" : "offline"}`);
|
||||
if (doctor) {
|
||||
console.log(`Node: ${process.version}`);
|
||||
|
|
@ -172,10 +195,56 @@ async function runLogs(args) {
|
|||
console.log(tailLines(text, Number(args.lines || 120)));
|
||||
}
|
||||
|
||||
async function runUpdate(args) {
|
||||
ensureNodeRuntime();
|
||||
const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
|
||||
const config = await readLocalConfig(installRoot);
|
||||
if (!config) {
|
||||
console.log(`${SERVICE_NAME} is not installed at ${installRoot}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = buildInstallPaths(installRoot);
|
||||
const pathDirs = resolvePathDirs();
|
||||
await mkdir(paths.logsDir, { recursive: true });
|
||||
await copyFile(path.join(ASSETS_DIR, "worker.mjs"), paths.workerPath);
|
||||
await copyFile(path.join(ASSETS_DIR, "ndcAgentMcpServer.mjs"), paths.ndcAgentMcpServerPath);
|
||||
await writeStartScript(paths.startScriptPath);
|
||||
await setupCodexHome(config.codexHome || path.join(installRoot, "codex-home"), config.appMcpServers);
|
||||
|
||||
await writeConfig(paths.configPath, {
|
||||
...config,
|
||||
service: SERVICE_NAME,
|
||||
packageName: PACKAGE_NAME,
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
agentVersion: PACKAGE_VERSION,
|
||||
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
||||
capabilities: BRIDGE_CAPABILITIES,
|
||||
updatedAt: new Date().toISOString(),
|
||||
worker: paths.workerPath,
|
||||
ndcAgentMcpServer: paths.ndcAgentMcpServerPath,
|
||||
startScript: paths.startScriptPath,
|
||||
nodePath: process.execPath,
|
||||
pathDirs,
|
||||
});
|
||||
|
||||
if (!args.noRestart) {
|
||||
await stopBridge(installRoot).catch(() => {});
|
||||
await startBridge(installRoot);
|
||||
const health = await waitForHealth(config.port || 8787, 15000);
|
||||
console.log(`${SERVICE_NAME} updated to ${PACKAGE_VERSION}.`);
|
||||
console.log(`Health: ${health.ok ? "online" : "not ready"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${SERVICE_NAME} updated to ${PACKAGE_VERSION}. Restart skipped.`);
|
||||
}
|
||||
|
||||
function parseArgs(rawArgs) {
|
||||
let args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
|
||||
let command = "setup";
|
||||
if (["setup", "status", "doctor", "start", "stop", "logs"].includes(args[0])) {
|
||||
if (["setup", "status", "doctor", "update", "start", "stop", "logs"].includes(args[0])) {
|
||||
command = args[0];
|
||||
args = args.slice(1);
|
||||
} else if (args[0] === "help") {
|
||||
|
|
@ -194,6 +263,7 @@ function parseArgs(rawArgs) {
|
|||
noAutostart: false,
|
||||
noCodexInstall: false,
|
||||
noCodexLogin: false,
|
||||
noRestart: false,
|
||||
};
|
||||
const positional = [];
|
||||
|
||||
|
|
@ -255,6 +325,10 @@ function parseArgs(rawArgs) {
|
|||
parsed.noCodexLogin = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--no-restart") {
|
||||
parsed.noRestart = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--setup-code" || arg === "--code") {
|
||||
parsed.setupCode = requireValue(args, ++i, arg);
|
||||
continue;
|
||||
|
|
@ -291,6 +365,7 @@ Usage:
|
|||
ai-workspace-bridge setup <setup-code> [--gateway <url>] [--install-root <path>] [--workspace <path>] [--port <port>]
|
||||
ai-workspace-bridge status [--install-root <path>]
|
||||
ai-workspace-bridge doctor [--install-root <path>]
|
||||
ai-workspace-bridge update [--install-root <path>] [--no-restart]
|
||||
ai-workspace-bridge start [--install-root <path>]
|
||||
ai-workspace-bridge stop [--install-root <path>]
|
||||
ai-workspace-bridge logs [--install-root <path>]
|
||||
|
|
@ -396,6 +471,9 @@ function buildEnv() {
|
|||
AI_BRIDGE_HUB_URLS: Array.isArray(config.hubUrls) ? config.hubUrls.join(",") : "",
|
||||
AI_BRIDGE_PAIRING_CODE: config.pairingCode || "",
|
||||
AI_BRIDGE_MACHINE_NAME: config.machineName || "",
|
||||
AI_BRIDGE_AGENT_VERSION: config.agentVersion || config.packageVersion || "",
|
||||
AI_BRIDGE_PROTOCOL_VERSION: config.protocolVersion || "",
|
||||
AI_BRIDGE_CAPABILITIES: Array.isArray(config.capabilities) ? config.capabilities.join(",") : "",
|
||||
AI_BRIDGE_ACTIVE_MIRROR_DIR: path.join(config.workspace || root, ".nodedc", "ai-workspace"),
|
||||
AI_BRIDGE_ACTIVE_MIRROR_OPEN_IDE: "1",
|
||||
PATH: [...pathDirs, process.env.PATH || ""].filter(Boolean).join(delimiter),
|
||||
|
|
@ -441,6 +519,31 @@ async function setupCodexHome(codexHome, appMcpServers) {
|
|||
await mkdir(codexHome, { recursive: true });
|
||||
await copyIfExists(path.join(os.homedir(), ".codex", "auth.json"), path.join(codexHome, "auth.json"));
|
||||
await mergeCodexMcpConfig(path.join(codexHome, "config.toml"), appMcpServers);
|
||||
await installBundledSkills(codexHome);
|
||||
}
|
||||
|
||||
async function installBundledSkills(codexHome) {
|
||||
const sourceRoot = path.join(ASSETS_DIR, "skills");
|
||||
const entries = await readdir(sourceRoot, { withFileTypes: true }).catch(() => []);
|
||||
if (!entries.length) return;
|
||||
const targetRoot = path.join(codexHome, "skills");
|
||||
await mkdir(targetRoot, { recursive: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
await cp(path.join(sourceRoot, entry.name), path.join(targetRoot, entry.name), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function readPackageVersion() {
|
||||
try {
|
||||
const parsed = JSON.parse(fssync.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
||||
return cleanString(parsed.version, 80) || "0.0.0";
|
||||
} catch {
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeCodexMcpConfig(configPath, appMcpServers) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ function attachAgent(socket, req) {
|
|||
pairingCode,
|
||||
machineName: cleanString(url.searchParams.get("machineName"), 120),
|
||||
host: "",
|
||||
agentVersion: "",
|
||||
protocolVersion: "",
|
||||
capabilities: [],
|
||||
runtimeStatus: "",
|
||||
runtimeError: "",
|
||||
runtimeCheckedAt: "",
|
||||
|
|
@ -182,6 +185,9 @@ function handleAgentMessage(agent, raw) {
|
|||
if (message?.type === "hello") {
|
||||
agent.machineName = cleanString(message.machineName || agent.machineName, 120);
|
||||
agent.host = cleanString(message.host, 120);
|
||||
agent.agentVersion = cleanString(message.agentVersion, 80);
|
||||
agent.protocolVersion = cleanString(message.protocolVersion, 120);
|
||||
agent.capabilities = cleanStringList(message.capabilities, 80, 80);
|
||||
agent.runtimeStatus = cleanString(message.runtimeStatus, 40);
|
||||
agent.runtimeError = cleanString(message.runtimeError, 1000);
|
||||
agent.runtimeCheckedAt = cleanString(message.runtimeCheckedAt, 80);
|
||||
|
|
@ -189,6 +195,8 @@ function handleAgentMessage(agent, raw) {
|
|||
kind: "hello",
|
||||
message: "Bridge agent hello received.",
|
||||
cwd: cleanString(message.cwd, 500),
|
||||
agentVersion: agent.agentVersion,
|
||||
protocolVersion: agent.protocolVersion,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -564,6 +572,9 @@ function publicAgent(agent) {
|
|||
pairingCode: agent.pairingCode,
|
||||
machineName: agent.machineName,
|
||||
host: agent.host,
|
||||
agentVersion: agent.agentVersion,
|
||||
protocolVersion: agent.protocolVersion,
|
||||
capabilities: agent.capabilities,
|
||||
runtimeStatus: agent.runtimeStatus,
|
||||
runtimeError: agent.runtimeError,
|
||||
runtimeCheckedAt: agent.runtimeCheckedAt,
|
||||
|
|
@ -635,6 +646,20 @@ function cleanString(value, maxLength) {
|
|||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function cleanStringList(value, maxItems = 80, maxLength = 120) {
|
||||
const source = Array.isArray(value)
|
||||
? value
|
||||
: String(value || "").split(/[\n,;]+/);
|
||||
const out = [];
|
||||
for (const item of source) {
|
||||
const text = cleanString(item, maxLength);
|
||||
if (!text || out.includes(text)) continue;
|
||||
out.push(text);
|
||||
if (out.length >= maxItems) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sanitizeTimeoutMs(value, fallback) {
|
||||
const numeric = Number(value || fallback);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
|
|
|
|||
Loading…
Reference in New Issue