feat: add AI Workspace npm bridge setup

This commit is contained in:
Codex
2026-06-24 20:40:12 +03:00
parent 92feff97e6
commit 999fe4d906
7 changed files with 4479 additions and 2 deletions
+254 -2
View File
@@ -1,5 +1,5 @@
import express from "express";
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { Pool } from "pg";
@@ -14,6 +14,8 @@ const SUPPORTED_THREAD_STATES = new Set(["active", "archived"]);
const SUPPORTED_MESSAGE_ROLES = new Set(["user", "assistant", "system", "tool"]);
const SUPPORTED_TOOL_PACKS = new Set(["engine", "ops", "ndc-agent-core", "deploy", "docs"]);
const SUPPORTED_RUN_STATUSES = new Set(["running", "completed", "failed", "timeout"]);
const AI_WORKSPACE_BRIDGE_PACKAGE_NAME = "@nodedc/ai-workspace-bridge";
const AI_WORKSPACE_SETUP_CODE_PREFIX = "ndcaws";
const ASSISTANT_ACTION_TOOL_PROFILE_BASE = {
schemaVersion: "ai-workspace.assistant-actions.v1",
endpoint: "/api/ai-workspace/assistant/v1/actions",
@@ -321,6 +323,59 @@ app.get("/api/ai-workspace/assistant/v1/executors/:executorId/agent/windows.ps1"
res.send(source);
}));
app.post("/api/ai-workspace/assistant/v1/executors/:executorId/agent/setup-command", requireInternalApi, asyncRoute(async (req, res) => {
const owner = getRequestOwner(req);
const executorId = sanitizeUuid(req.params.executorId, "executorId");
const executor = await getExecutor(owner, executorId);
if (!executor) {
res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" });
return;
}
if (!cleanPairingCode(executor.pairingCode || "")) {
res.status(400).json({ ok: false, error: "pairing_code_required" });
return;
}
const installerPort = installerPortFromQuery(req.body?.port || req.query?.port);
if (!installerPort) {
res.status(400).json({ ok: false, error: "installer_port_invalid" });
return;
}
const ownerSettings = await getOwnerSettings(owner);
const appMcpServers = installerMcpServersFromSettings(ownerSettings, isPlainObject(req.body) ? req.body : {});
const setupCode = await createAiWorkspaceSetupCode(owner, executor, {
port: installerPort,
appMcpServers,
});
const command = buildBridgeSetupCommand(setupCode.code);
res.status(201).json({
ok: true,
owner: publicOwner(owner),
executorId: executor.id,
install: {
command,
packageName: config.bridgePackageName,
gatewayUrl: config.setupGatewayUrl,
expiresAt: setupCode.expiresAt,
},
setupCode: {
suffix: setupCode.suffix,
expiresAt: setupCode.expiresAt,
},
});
}));
app.post("/api/ai-workspace/assistant/v1/setup-codes/redeem", requireInternalApi, asyncRoute(async (req, res) => {
const setupCode = optionalString(req.body?.setupCode || req.body?.setup_code || req.query?.setupCode || req.query?.setup_code);
if (!setupCode) {
res.status(400).json({ ok: false, error: "setup_code_required" });
return;
}
const setup = await redeemAiWorkspaceSetupCode(setupCode);
res.json({ ok: true, setup });
}));
app.get("/api/ai-workspace/assistant/v1/threads", requireInternalApi, asyncRoute(async (req, res) => {
const owner = getRequestOwner(req);
const surface = req.query.surface ? sanitizeSurface(req.query.surface) : null;
@@ -709,6 +764,160 @@ async function getExecutor(owner, executorId) {
return result.rows[0] ? toExecutor(result.rows[0]) : null;
}
async function createAiWorkspaceSetupCode(owner, executor, options = {}) {
const code = makeAiWorkspaceSetupCode();
const expiresAt = new Date(Date.now() + config.setupCodeTtlSeconds * 1000);
const metadata = {
packageName: config.bridgePackageName,
setupGatewayUrl: config.setupGatewayUrl,
bridge: bridgeSetupPayload(executor, options),
};
const client = await pool.connect();
try {
await client.query("begin");
await client.query(
`update ai_workspace_setup_codes
set status = 'revoked'
where owner_key = $1 and executor_id = $2 and status = 'active'`,
[owner.key, executor.id]
);
await client.query(
`insert into ai_workspace_setup_codes (
id,
owner_key,
owner_user_id,
owner_email,
executor_id,
code_hash,
code_suffix,
expires_at,
metadata
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`,
[
randomUUID(),
owner.key,
owner.userId,
owner.email,
executor.id,
hashSetupCode(code),
setupCodeSuffix(code),
expiresAt,
JSON.stringify(metadata),
]
);
await client.query("commit");
} catch (error) {
await client.query("rollback");
throw error;
} finally {
client.release();
}
return {
code,
suffix: setupCodeSuffix(code),
expiresAt: expiresAt.toISOString(),
};
}
async function redeemAiWorkspaceSetupCode(code) {
const client = await pool.connect();
let committed = false;
try {
await client.query("begin");
const result = await client.query(
`select c.*, e.name as executor_name
from ai_workspace_setup_codes c
left join ai_workspace_executors e on e.owner_key = c.owner_key and e.id = c.executor_id
where c.code_hash = $1
for update of c`,
[hashSetupCode(code)]
);
const row = result.rows[0];
if (!row) throw badRequest("setup_code_invalid");
if (row.status !== "active") throw httpError("setup_code_not_active", 410);
if (new Date(row.expires_at).getTime() <= Date.now()) {
await client.query(
"update ai_workspace_setup_codes set status = 'expired' where id = $1",
[row.id]
);
await client.query("commit");
committed = true;
throw httpError("setup_code_expired", 410);
}
await client.query(
"update ai_workspace_setup_codes set status = 'used', used_at = now() where id = $1",
[row.id]
);
await client.query("commit");
committed = true;
const metadata = isPlainObject(row.metadata) ? row.metadata : {};
const bridge = isPlainObject(metadata.bridge) ? metadata.bridge : null;
if (!bridge?.pairingCode || !Array.isArray(bridge?.hubUrls) || !bridge.hubUrls.length) {
throw httpError("setup_payload_invalid", 500);
}
return {
service: "NDC AI Workspace Bridge",
packageName: optionalString(metadata.packageName) || config.bridgePackageName,
executor: {
id: row.executor_id,
name: optionalString(row.executor_name) || optionalString(bridge.machineName) || "Codex worker",
},
expiresAt: toIso(row.expires_at),
bridge,
};
} catch (error) {
if (!committed) {
try {
await client.query("rollback");
} catch {}
}
throw error;
} finally {
client.release();
}
}
function bridgeSetupPayload(executor, options = {}) {
const hubUrls = uniqueStrings([config.hubWebSocketUrl, ...(config.hubFallbackWebSocketUrls || [])]);
return {
protocol: "ai-workspace-bridge/v1",
hubUrl: hubUrls[0] || "",
hubUrls,
pairingCode: cleanPairingCode(executor.pairingCode || ""),
machineName: optionalString(executor.name) || "Codex worker",
workspace: optionalString(executor.workspacePath) || "",
port: sanitizeInteger(options.port, 8787, 1, 65535),
appMcpServers: Array.isArray(options.appMcpServers) ? options.appMcpServers : [],
};
}
function buildBridgeSetupCommand(code) {
const parts = [
"npx",
"--yes",
config.bridgePackageName,
"setup",
code,
"--gateway",
config.setupGatewayUrl,
];
return parts.join(" ");
}
function makeAiWorkspaceSetupCode() {
return `${AI_WORKSPACE_SETUP_CODE_PREFIX}_${randomBytes(18).toString("base64url")}`;
}
function hashSetupCode(code) {
return createHash("sha256").update(String(code || ""), "utf8").digest("hex");
}
function setupCodeSuffix(code) {
return String(code || "").replace(/[^A-Za-z0-9]/g, "").slice(-6).toUpperCase();
}
async function selectExecutor(owner, executorId) {
const executor = await getExecutor(owner, executorId);
if (!executor) return null;
@@ -2769,6 +2978,27 @@ async function migrate() {
)
`);
await pool.query(`
create table if not exists ai_workspace_setup_codes (
id uuid primary key,
owner_key text not null,
owner_user_id text,
owner_email text,
executor_id uuid not null references ai_workspace_executors(id) on delete cascade,
code_hash text not null unique,
code_suffix text not null,
status text not null default 'active',
expires_at timestamptz not null,
used_at timestamptz,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
constraint ai_workspace_setup_codes_owner_check
check (owner_user_id is not null or owner_email is not null),
constraint ai_workspace_setup_codes_status_check
check (status in ('active', 'used', 'expired', 'revoked'))
)
`);
await pool.query("create index if not exists ai_workspace_executors_owner_idx on ai_workspace_executors(owner_key, updated_at desc)");
await pool.query("create index if not exists ai_workspace_executors_pairing_code_idx on ai_workspace_executors(pairing_code) where pairing_code is not null");
await pool.query("create index if not exists ai_workspace_threads_owner_surface_idx on ai_workspace_threads(owner_key, origin_surface, updated_at desc)");
@@ -2777,6 +3007,8 @@ async function migrate() {
await pool.query("create index if not exists ai_workspace_runs_request_idx on ai_workspace_runs(owner_key, request_id)");
await pool.query("create index if not exists ai_workspace_run_events_run_idx on ai_workspace_run_events(run_id, occurred_at asc, created_at asc)");
await pool.query("create unique index if not exists ai_workspace_run_events_hub_event_idx on ai_workspace_run_events(run_id, hub_event_id) where hub_event_id is not null");
await pool.query("create index if not exists ai_workspace_setup_codes_owner_executor_idx on ai_workspace_setup_codes(owner_key, executor_id, created_at desc)");
await pool.query("create index if not exists ai_workspace_setup_codes_expires_idx on ai_workspace_setup_codes(status, expires_at)");
}
function sanitizeExecutorCommand(payload, { partial }) {
@@ -3412,8 +3644,12 @@ function toIso(value) {
}
function badRequest(message) {
return httpError(message, 400);
}
function httpError(message, status = 500) {
const error = new Error(message);
error.status = 400;
error.status = status;
return error;
}
@@ -3585,6 +3821,10 @@ function readConfig() {
optionalString(process.env.NDC_AI_WORKSPACE_HUB_HTTP_URL) ||
optionalString(process.env.AI_WORKSPACE_HUB_HTTP_URL) ||
httpUrlFromWebSocketUrl(hubWebSocketUrl);
const setupGatewayUrl =
optionalString(process.env.AI_WORKSPACE_SETUP_GATEWAY_URL) ||
optionalString(process.env.NDC_AI_WORKSPACE_SETUP_GATEWAY_URL) ||
httpUrlFromWebSocketUrl(hubWebSocketUrl);
const explicitHubAccessToken =
optionalString(process.env.AI_WORKSPACE_HUB_TOKEN) ||
optionalString(process.env.NDC_AI_WORKSPACE_HUB_TOKEN) ||
@@ -3633,6 +3873,18 @@ function readConfig() {
databasePoolSize: Number(process.env.AI_WORKSPACE_ASSISTANT_DATABASE_POOL_SIZE || "10"),
hubWebSocketUrl,
hubInternalHttpUrl: hubInternalHttpUrl.replace(/\/+$/, ""),
setupGatewayUrl: setupGatewayUrl.replace(/\/+$/, ""),
bridgePackageName:
optionalString(process.env.AI_WORKSPACE_BRIDGE_PACKAGE_NAME) ||
optionalString(process.env.NDC_AI_WORKSPACE_BRIDGE_PACKAGE_NAME) ||
AI_WORKSPACE_BRIDGE_PACKAGE_NAME,
setupCodeTtlSeconds: sanitizeInteger(
process.env.AI_WORKSPACE_SETUP_CODE_TTL_SECONDS ||
process.env.NDC_AI_WORKSPACE_SETUP_CODE_TTL_SECONDS,
15 * 60,
60,
24 * 60 * 60
),
hubInternalAccessToken:
explicitHubAccessToken || (isDeployedPublicHubUrl(hubInternalHttpUrl) ? "" : sharedInternalAccessToken),
ontologyLauncherBaseUrl: ontologyLauncherBaseUrl.replace(/\/+$/, ""),