feat(ai-workspace): support SEO codex provider bridge
This commit is contained in:
@@ -9,7 +9,7 @@ import { Pool } from "pg";
|
||||
import { handleAssistantCallerRequest } from "../../ontology-core/src/assistant-action-caller.mjs";
|
||||
import { loadCatalog } from "../../ontology-core/src/catalog.mjs";
|
||||
|
||||
const SUPPORTED_SURFACES = new Set(["engine", "ops", "global"]);
|
||||
const SUPPORTED_SURFACES = new Set(["engine", "ops", "seo-mode", "global"]);
|
||||
const SUPPORTED_EXECUTOR_TYPES = new Set(["codex-remote", "ndc-agent-core"]);
|
||||
const SUPPORTED_CONNECTION_MODES = new Set(["hub", "direct"]);
|
||||
const SUPPORTED_EXECUTOR_STATUSES = new Set(["unknown", "online", "offline", "checking", "error"]);
|
||||
@@ -100,6 +100,24 @@ const APP_ROUTING_CATALOG = [
|
||||
requiredScopes: ["ops:project:read"],
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
},
|
||||
{
|
||||
appId: "seo-mode",
|
||||
appTitle: "NDC SEO Mode",
|
||||
surface: "seo-mode",
|
||||
skillId: "seo-mode-context",
|
||||
whenToUse: [
|
||||
"SEO model-provider tasks",
|
||||
"keyword cleaning proposals",
|
||||
"SERP interpretation proposals",
|
||||
"strategy synthesis proposals",
|
||||
"strategy quality review",
|
||||
],
|
||||
actionNamespaces: ["seo.*"],
|
||||
actionIdPrefixes: ["seo."],
|
||||
mcpServerNames: [],
|
||||
requiredScopes: ["seo:model-provider:run"],
|
||||
deniedText: ACCESS_DENIED_TEXT,
|
||||
},
|
||||
];
|
||||
const ASSISTANT_ACTION_CACHE_TTL_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_ACTION_CACHE_TTL_MS || 30_000);
|
||||
const ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_TIMEOUT_MS || 25_000);
|
||||
@@ -422,7 +440,8 @@ app.post("/api/ai-workspace/assistant/v1/executors/:executorId/agent/setup-comma
|
||||
port: installerPort,
|
||||
appMcpServers,
|
||||
});
|
||||
const command = buildBridgeSetupCommand(setupCode.code);
|
||||
const bridgePackageSpec = bridgePackageSpecForSetupCode(setupCode.code);
|
||||
const command = buildBridgeSetupCommand(setupCode.code, bridgePackageSpec);
|
||||
res.status(201).json({
|
||||
ok: true,
|
||||
owner: publicOwner(owner),
|
||||
@@ -430,7 +449,7 @@ app.post("/api/ai-workspace/assistant/v1/executors/:executorId/agent/setup-comma
|
||||
install: {
|
||||
command,
|
||||
packageName: config.bridgePackageName,
|
||||
packageSpec: config.bridgePackageSpec,
|
||||
packageSpec: bridgePackageSpec,
|
||||
gatewayUrl: config.setupGatewayUrl,
|
||||
expiresAt: setupCode.expiresAt,
|
||||
},
|
||||
@@ -980,12 +999,12 @@ function bridgeSetupPayload(executor, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildBridgeSetupCommand(code) {
|
||||
function buildBridgeSetupCommand(code, packageSpec = config.bridgePackageSpec) {
|
||||
const parts = [
|
||||
"npx",
|
||||
"--yes",
|
||||
"--package",
|
||||
config.bridgePackageSpec,
|
||||
packageSpec,
|
||||
AI_WORKSPACE_BRIDGE_PACKAGE_BIN,
|
||||
"setup",
|
||||
code,
|
||||
@@ -995,6 +1014,13 @@ function buildBridgeSetupCommand(code) {
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function bridgePackageSpecForSetupCode(code) {
|
||||
const packageSpec = String(config.bridgePackageSpec || "");
|
||||
if (!/^https?:\/\//i.test(packageSpec)) return packageSpec;
|
||||
const separator = packageSpec.includes("?") ? "&" : "?";
|
||||
return `${packageSpec}${separator}setup=${encodeURIComponent(setupCodeSuffix(code))}`;
|
||||
}
|
||||
|
||||
async function getBridgePackageTarball() {
|
||||
if (cachedBridgePackageTarball) return cachedBridgePackageTarball;
|
||||
|
||||
@@ -1217,7 +1243,7 @@ function mergeOwnerActiveContext(current, incoming) {
|
||||
};
|
||||
}
|
||||
|
||||
if ((surface === "engine" || surface === "ops") && !isPlainObject(incomingContexts[surface])) {
|
||||
if ((surface === "engine" || surface === "ops" || surface === "seo-mode") && !isPlainObject(incomingContexts[surface])) {
|
||||
const { contexts: _contexts, ...surfaceContext } = incomingContext;
|
||||
nextContexts[surface] = {
|
||||
...(isPlainObject(nextContexts[surface]) ? nextContexts[surface] : {}),
|
||||
@@ -2770,15 +2796,17 @@ function mergeSurfaceContexts(...contexts) {
|
||||
}
|
||||
const surface = normalizeKey(context.surface);
|
||||
const mode = normalizeKey(context.modeId);
|
||||
const inferredSurface = surface === "engine" || surface === "ops"
|
||||
const inferredSurface = surface === "engine" || surface === "ops" || surface === "seo-mode"
|
||||
? surface
|
||||
: mode === "ndc-agent-core"
|
||||
? "engine"
|
||||
: mode === "ops"
|
||||
? "ops"
|
||||
: mode === "seo-model-task"
|
||||
? "seo-mode"
|
||||
: "";
|
||||
if (inferredSurface) {
|
||||
const { contexts: _contexts, engineContext: _engineContext, opsContext: _opsContext, ...plain } = context;
|
||||
const { contexts: _contexts, engineContext: _engineContext, opsContext: _opsContext, seoModeContext: _seoModeContext, ...plain } = context;
|
||||
out[inferredSurface] = mergeBridgeContextObject(out[inferredSurface], plain);
|
||||
}
|
||||
}
|
||||
@@ -3373,7 +3401,7 @@ async function migrate() {
|
||||
constraint ai_workspace_threads_owner_check
|
||||
check (owner_user_id is not null or owner_email is not null),
|
||||
constraint ai_workspace_threads_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'global')),
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global')),
|
||||
constraint ai_workspace_threads_lifecycle_check
|
||||
check (lifecycle_state in ('active', 'archived'))
|
||||
)
|
||||
@@ -3420,13 +3448,27 @@ async function migrate() {
|
||||
constraint ai_workspace_runs_owner_check
|
||||
check (owner_user_id is not null or owner_email is not null),
|
||||
constraint ai_workspace_runs_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'global')),
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global')),
|
||||
constraint ai_workspace_runs_status_check
|
||||
check (status in ('running', 'completed', 'failed', 'timeout')),
|
||||
unique (owner_key, thread_id, request_id)
|
||||
)
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
alter table ai_workspace_threads
|
||||
drop constraint if exists ai_workspace_threads_surface_check,
|
||||
add constraint ai_workspace_threads_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global'))
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
alter table ai_workspace_runs
|
||||
drop constraint if exists ai_workspace_runs_surface_check,
|
||||
add constraint ai_workspace_runs_surface_check
|
||||
check (origin_surface in ('engine', 'ops', 'seo-mode', 'global'))
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
create table if not exists ai_workspace_run_events (
|
||||
id uuid primary key,
|
||||
@@ -4203,6 +4245,11 @@ function parseEntitlementAdapters() {
|
||||
token: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN,
|
||||
required: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED,
|
||||
});
|
||||
collectEntitlementAdapter(adapters, "seo-mode", {
|
||||
url: process.env.AI_WORKSPACE_SEO_MODE_ENTITLEMENT_URL || process.env.NDC_AI_WORKSPACE_SEO_MODE_ENTITLEMENT_URL,
|
||||
token: process.env.AI_WORKSPACE_SEO_MODE_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_SEO_MODE_ENTITLEMENT_TOKEN,
|
||||
required: process.env.AI_WORKSPACE_SEO_MODE_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_SEO_MODE_ENTITLEMENT_REQUIRED,
|
||||
});
|
||||
collectEntitlementAdapter(adapters, "launcher", {
|
||||
url:
|
||||
process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL ||
|
||||
@@ -4228,6 +4275,7 @@ function parseEntitlementAdapters() {
|
||||
const order = new Map([
|
||||
["ops", 10],
|
||||
["engine", 20],
|
||||
["seo-mode", 30],
|
||||
["launcher", 90],
|
||||
]);
|
||||
return Array.from(byAppId.values())
|
||||
|
||||
Reference in New Issue
Block a user