feat: discover codex app-server capabilities

This commit is contained in:
AI Bot
2026-05-31 03:44:02 +08:00
parent 4800352e22
commit f333676c36
16 changed files with 662 additions and 5 deletions

View File

@@ -58,6 +58,25 @@ function normalizeTimeoutMs(value) {
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 120_000;
}
function normalizePositiveInteger(value, fallback) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : fallback;
}
function boolFromConfigOrEnv(configValue, envValue, fallback) {
if (configValue === true || configValue === false) {
return configValue;
}
const normalized = trimToDefined(envValue)?.toLowerCase();
if (normalized === "1" || normalized === "true" || normalized === "yes") {
return true;
}
if (normalized === "0" || normalized === "false" || normalized === "no") {
return false;
}
return fallback;
}
function normalizeTransport(value) {
const normalized = trimToDefined(value)?.toLowerCase();
return normalized === "ws" || normalized === "websocket"
@@ -117,6 +136,19 @@ export function getCodexAppServerRunnerConfig(env = process.env, config = {}) {
authTokenFile:
trimToDefined(config.codexAppServerAuthTokenFile) ||
trimToDefined(env.BOSS_CODEX_APP_SERVER_AUTH_TOKEN_FILE),
discoveryEnabled: boolFromConfigOrEnv(
config.codexAppServerDiscoveryEnabled,
env.BOSS_CODEX_APP_SERVER_DISCOVERY_ENABLED,
true,
),
discoveryTtlMs: normalizePositiveInteger(
config.codexAppServerDiscoveryTtlMs ?? env.BOSS_CODEX_APP_SERVER_DISCOVERY_TTL_MS,
300_000,
),
discoveryLimit: normalizePositiveInteger(
config.codexAppServerDiscoveryLimit ?? env.BOSS_CODEX_APP_SERVER_DISCOVERY_LIMIT,
20,
),
};
}
@@ -727,6 +759,226 @@ function buildServerRequestFallbackResponse(message) {
};
}
function normalizeDiscoveryModel(model) {
const id = trimToDefined(model?.id) || trimToDefined(model?.model);
if (!id) {
return null;
}
return {
id,
model: trimToDefined(model?.model) || id,
displayName: trimToDefined(model?.displayName) || id,
description: safeProgressText(model?.description, 160),
hidden: Boolean(model?.hidden),
isDefault: Boolean(model?.isDefault),
supportsPersonality: Boolean(model?.supportsPersonality),
supportedReasoningEfforts: asArray(model?.supportedReasoningEfforts).map(String).slice(0, 8),
inputModalities: asArray(model?.inputModalities).map(String).slice(0, 8),
};
}
function pickFastModelId(models) {
const fast = models.find((model) =>
/mini|fast|flash|lite|haiku/i.test(`${model.id} ${model.displayName} ${model.description}`),
);
return fast?.id || models[0]?.id || "";
}
function normalizeDiscoverySkills(result) {
return asArray(result?.data)
.flatMap((entry) => asArray(entry?.skills))
.map((skill) => {
const name = trimToDefined(skill?.name);
if (!name) return null;
return {
name,
description: safeProgressText(skill?.description || skill?.shortDescription, 180),
scope: trimToDefined(skill?.scope),
enabled: skill?.enabled !== false,
};
})
.filter(Boolean);
}
function normalizeDiscoveryPlugins(result) {
return asArray(result?.marketplaces)
.flatMap((marketplace) => asArray(marketplace?.plugins))
.map((plugin) => {
const id = trimToDefined(plugin?.id) || trimToDefined(plugin?.name);
if (!id) return null;
return {
id,
name: trimToDefined(plugin?.name) || id,
installed: Boolean(plugin?.installed),
enabled: plugin?.enabled !== false,
localVersion: trimToDefined(plugin?.localVersion),
};
})
.filter(Boolean);
}
function normalizeDiscoveryApps(result) {
return asArray(result?.data)
.map((app) => {
const id = trimToDefined(app?.id) || trimToDefined(app?.name);
if (!id) return null;
return {
id,
name: trimToDefined(app?.name) || id,
description: safeProgressText(app?.description, 160),
isAccessible: app?.isAccessible !== false,
isEnabled: app?.isEnabled !== false,
pluginDisplayNames: asArray(app?.pluginDisplayNames).map(String).slice(0, 8),
};
})
.filter(Boolean);
}
async function withCodexAppServerRpcSession(runnerConfig, callback) {
const cwd = runnerConfig.cwd || process.cwd();
let closed = false;
let rpcTransport;
let nextId = 1;
const pending = new Map();
const timeout = setTimeout(() => {
for (const { reject } of pending.values()) {
reject(new Error("CODEX_APP_SERVER_DISCOVERY_TIMEOUT"));
}
pending.clear();
rpcTransport?.close?.("SIGKILL");
}, Math.min(runnerConfig.timeoutMs, 10_000));
const request = (method, params = {}) =>
new Promise((resolveRequest, rejectRequest) => {
if (closed) {
rejectRequest(new Error("CODEX_APP_SERVER_CLOSED"));
return;
}
const id = nextId++;
pending.set(id, { resolve: resolveRequest, reject: rejectRequest });
rpcTransport.send(JSON.stringify({ method, id, params }), (error) => {
if (error) {
pending.delete(id);
rejectRequest(error);
}
});
});
const notify = (method, params = {}) => {
if (!closed) {
rpcTransport.send(JSON.stringify({ method, params }));
}
};
try {
rpcTransport = await openCodexAppServerTransport(runnerConfig, cwd, {
onLine(line) {
if (!line.trim()) return;
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
if (!Object.hasOwn(message, "id")) return;
const pendingRequest = pending.get(message.id);
if (!pendingRequest) return;
pending.delete(message.id);
if (message.error) {
pendingRequest.reject(new Error(message.error.message || JSON.stringify(message.error)));
} else {
pendingRequest.resolve(message.result ?? {});
}
},
onError(error) {
closed = true;
for (const { reject } of pending.values()) {
reject(error);
}
pending.clear();
},
onClose({ code, message }) {
closed = true;
const error = new Error(message || `CODEX_APP_SERVER_EXITED:${code ?? "unknown"}`);
for (const { reject } of pending.values()) {
reject(error);
}
pending.clear();
},
});
await request("initialize", {
clientInfo: {
name: runnerConfig.clientName,
title: runnerConfig.clientTitle,
version: runnerConfig.clientVersion,
},
});
notify("initialized", {});
return await callback(request);
} finally {
clearTimeout(timeout);
rpcTransport?.close?.("SIGTERM");
}
}
export async function discoverCodexAppServerCapabilities(runnerConfig) {
if (!runnerConfig?.enabled || runnerConfig.discoveryEnabled === false) {
return undefined;
}
const safeRequest = async (request, method, params = {}) => {
try {
return await request(method, params);
} catch (error) {
return { __bossError: error instanceof Error ? error.message : String(error) };
}
};
return withCodexAppServerRpcSession(runnerConfig, async (request) => {
const limit = runnerConfig.discoveryLimit ?? 20;
const [modelResult, providerCapabilities, skillsResult, pluginResult, appsResult] = await Promise.all([
safeRequest(request, "model/list", { includeHidden: false, limit }),
safeRequest(request, "modelProvider/capabilities/read", {}),
safeRequest(request, "skills/list", { cwds: [runnerConfig.cwd || process.cwd()], forceReload: false }),
safeRequest(request, "plugin/list", { cwds: [runnerConfig.cwd || process.cwd()] }),
safeRequest(request, "app/list", { limit }),
]);
const models = asArray(modelResult?.data)
.map(normalizeDiscoveryModel)
.filter(Boolean)
.slice(0, limit);
const defaultModelId = models.find((model) => model.isDefault)?.id || models[0]?.id || "";
const fastModelId = pickFastModelId(models);
const deepModelId = models.find((model) => model.id !== fastModelId)?.id || defaultModelId;
return {
version: trimToDefined(runnerConfig.version),
discoveredAt: new Date().toISOString(),
models,
defaultModelId,
fastModelId,
deepModelId,
providerCapabilities: {
namespaceTools: Boolean(providerCapabilities?.namespaceTools),
imageGeneration: Boolean(providerCapabilities?.imageGeneration),
webSearch: Boolean(providerCapabilities?.webSearch),
},
skills: normalizeDiscoverySkills(skillsResult).slice(0, limit),
plugins: normalizeDiscoveryPlugins(pluginResult).slice(0, limit),
apps: normalizeDiscoveryApps(appsResult).slice(0, limit),
errors: [
modelResult?.__bossError ? `model/list:${modelResult.__bossError}` : undefined,
providerCapabilities?.__bossError
? `modelProvider/capabilities/read:${providerCapabilities.__bossError}`
: undefined,
skillsResult?.__bossError ? `skills/list:${skillsResult.__bossError}` : undefined,
pluginResult?.__bossError ? `plugin/list:${pluginResult.__bossError}` : undefined,
appsResult?.__bossError ? `app/list:${appsResult.__bossError}` : undefined,
].filter(Boolean),
};
});
}
function createProgressCollector() {
const steps = [];
const artifacts = [];