fix: align Code Agent session facts
Align Code Agent chat, live health, and workbench UI session facts for Codex stdio vs fallback paths.
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
} from "./code-agent-contract.mjs";
|
||||
import {
|
||||
CODEX_STDIO_BACKEND,
|
||||
CODEX_STDIO_CAPABILITY_LEVEL,
|
||||
CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
CODEX_STDIO_PROVIDER,
|
||||
CODEX_STDIO_RUNNER_KIND,
|
||||
@@ -78,6 +79,19 @@ const READONLY_LIMITATION_FLAGS = Object.freeze([
|
||||
"not-write-capable",
|
||||
"process-local-session-registry"
|
||||
]);
|
||||
const CODEX_STDIO_LIMITATION_FLAGS = Object.freeze([
|
||||
"hardware-control-via-cloud-api-only",
|
||||
"no-direct-gateway-link",
|
||||
"no-direct-box-simu-link",
|
||||
"no-direct-patch-panel-link",
|
||||
"secret-values-redacted"
|
||||
]);
|
||||
const OPENAI_FALLBACK_LIMITATION_FLAGS = Object.freeze([
|
||||
"openai-responses-fallback",
|
||||
"text-chat-only",
|
||||
"not-workspace-tools",
|
||||
"not-session-runner"
|
||||
]);
|
||||
const SKIPPED_READONLY_DIRS = new Set([
|
||||
".git",
|
||||
".hg",
|
||||
@@ -271,12 +285,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
|
||||
sessionMode: providerResult.sessionMode ?? "provider-text-request",
|
||||
sessionReuse: providerResult.sessionReuse ?? null,
|
||||
implementationType: providerResult.implementationType ?? OPENAI_FALLBACK_RUNNER_KIND,
|
||||
runnerLimitations: providerResult.runnerLimitations ?? [
|
||||
"text-chat-only",
|
||||
"not-codex-stdio",
|
||||
"not-workspace-tools",
|
||||
"not-session-runner"
|
||||
],
|
||||
runnerLimitations: providerResult.runnerLimitations ?? [...OPENAI_FALLBACK_LIMITATION_FLAGS],
|
||||
codexStdioFeasibility: providerResult.codexStdioFeasibility ?? await inspectCodexStdioFeasibility(options.env ?? process.env, options),
|
||||
longLivedSessionGate: providerLongLivedGate,
|
||||
toolCalls: Array.isArray(providerResult.toolCalls) ? providerResult.toolCalls : [],
|
||||
@@ -499,10 +508,15 @@ export async function describeCodeAgentAvailability(env = process.env, options =
|
||||
sessionRegistry
|
||||
});
|
||||
const codexStdio = await inspectCodexStdioFeasibility(env, options);
|
||||
const codexStdioReady = codexStdioLongLivedReady(codexStdio);
|
||||
const codexStdioActive = providerPlan.mode !== "openai" && codexStdioReady;
|
||||
const activeRunnerAvailability = codexStdioActive
|
||||
? codexStdioRunnerAvailability(codexStdio)
|
||||
: runnerAvailability;
|
||||
const blocked = providerPlan.mode === "openai"
|
||||
? !providerContract.ready
|
||||
: providerPlan.mode === "codex-stdio"
|
||||
? !codexStdio.ready
|
||||
? !codexStdioReady
|
||||
: missingEnv.length > 0;
|
||||
const fallbackAvailability = {
|
||||
provider: providerPlan.provider,
|
||||
@@ -520,29 +534,29 @@ export async function describeCodeAgentAvailability(env = process.env, options =
|
||||
safety: providerContract.safety
|
||||
};
|
||||
const longLivedGate = longLivedSessionGate({
|
||||
provider: codexStdio.ready ? CODEX_STDIO_PROVIDER : runnerAvailability.provider,
|
||||
runnerKind: codexStdio.ready ? CODEX_STDIO_RUNNER_KIND : runnerAvailability.kind,
|
||||
sessionMode: codexStdio.ready ? CODEX_STDIO_SESSION_MODE : READONLY_SESSION_MODE,
|
||||
implementationType: codexStdio.ready ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE,
|
||||
provider: codexStdioActive ? CODEX_STDIO_PROVIDER : runnerAvailability.provider,
|
||||
runnerKind: codexStdioActive ? CODEX_STDIO_RUNNER_KIND : runnerAvailability.kind,
|
||||
sessionMode: codexStdioActive ? CODEX_STDIO_SESSION_MODE : READONLY_SESSION_MODE,
|
||||
implementationType: codexStdioActive ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE,
|
||||
codexStdioFeasibility: codexStdio
|
||||
});
|
||||
const agentKind = codexStdio.ready
|
||||
const agentKind = codexStdioActive
|
||||
? "codex-stdio-feasible"
|
||||
: runnerAvailability.ready
|
||||
? "controlled-readonly-session-registry"
|
||||
: blocked
|
||||
? "openai-fallback-blocked"
|
||||
: "openai-fallback";
|
||||
const capabilityStatus = codexStdio.ready
|
||||
const capabilityStatus = codexStdioActive
|
||||
? "codex-stdio-feasible"
|
||||
: runnerAvailability.ready
|
||||
? "controlled-readonly-session-registry"
|
||||
: blocked
|
||||
? "blocked"
|
||||
: "openai-fallback";
|
||||
const codeAgentReady = codexStdio.ready;
|
||||
const status = codexStdio.ready ? "codex-stdio-feasible" : blocked && !runnerAvailability.ready ? "blocked" : "partial";
|
||||
const capabilityLevel = codexStdio.ready
|
||||
const codeAgentReady = codexStdioActive;
|
||||
const status = codexStdioActive ? "codex-stdio-feasible" : blocked && !runnerAvailability.ready ? "blocked" : "partial";
|
||||
const capabilityLevel = codexStdioActive
|
||||
? "long-lived-codex-stdio-session"
|
||||
: runnerAvailability.ready
|
||||
? READONLY_SESSION_CAPABILITY_LEVEL
|
||||
@@ -585,7 +599,7 @@ export async function describeCodeAgentAvailability(env = process.env, options =
|
||||
"error.missingEnv",
|
||||
"availability.status"
|
||||
],
|
||||
runner: runnerAvailability,
|
||||
runner: activeRunnerAvailability,
|
||||
fallback: fallbackAvailability,
|
||||
codexStdio,
|
||||
status,
|
||||
@@ -601,10 +615,14 @@ export async function describeCodeAgentAvailability(env = process.env, options =
|
||||
egress: providerContract.egress,
|
||||
safety: providerContract.safety,
|
||||
capabilityLevel,
|
||||
sessionRegistry: runnerAvailability.sessionRegistry,
|
||||
sessionMode: runnerAvailability.sessionMode,
|
||||
implementationType: runnerAvailability.implementationType,
|
||||
runnerLimitations: runnerAvailability.runnerLimitations,
|
||||
workspace: activeRunnerAvailability.workspace,
|
||||
sandbox: activeRunnerAvailability.sandbox,
|
||||
session: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0] ?? null,
|
||||
sessionStatus: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0]?.status ?? null,
|
||||
sessionRegistry: activeRunnerAvailability.sessionRegistry ?? runnerAvailability.sessionRegistry,
|
||||
sessionMode: activeRunnerAvailability.sessionMode,
|
||||
implementationType: activeRunnerAvailability.implementationType,
|
||||
runnerLimitations: activeRunnerAvailability.runnerLimitations,
|
||||
codexStdioFeasibility: codexStdio,
|
||||
longLivedSessionGate: longLivedGate,
|
||||
blockers: [
|
||||
@@ -632,6 +650,78 @@ function fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio })
|
||||
return "凭证缺口";
|
||||
}
|
||||
|
||||
function codexStdioLongLivedReady(codexStdio = {}) {
|
||||
return Boolean(
|
||||
(codexStdio.ready === true || codexStdio.canStartLongLivedCodexStdio === true) &&
|
||||
codexStdio.commandProbe?.ready === true
|
||||
);
|
||||
}
|
||||
|
||||
function codexStdioRunnerAvailability(codexStdio = {}) {
|
||||
const workspace = codexStdio.workspace ?? repoRoot;
|
||||
const sandbox = codexStdio.sandbox ?? "workspace-write";
|
||||
return {
|
||||
kind: CODEX_STDIO_RUNNER_KIND,
|
||||
backend: CODEX_STDIO_BACKEND,
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
workspace,
|
||||
sandbox,
|
||||
mode: CODEX_STDIO_SESSION_MODE,
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
session: CODEX_STDIO_SESSION_MODE,
|
||||
status: "available",
|
||||
ready: true,
|
||||
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
longLivedSession: true,
|
||||
durableSession: true,
|
||||
durable: true,
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
readOnly: false,
|
||||
runnerLimitations: [...CODEX_STDIO_LIMITATION_FLAGS],
|
||||
codexStdioFeasibility: codexStdio,
|
||||
longLivedSessionGate: longLivedSessionGate({
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
runnerKind: CODEX_STDIO_RUNNER_KIND,
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
codexStdioFeasibility: codexStdio
|
||||
}),
|
||||
sessionRegistry: codexStdioSessionRegistrySummary(codexStdio),
|
||||
safety: {
|
||||
secretsRead: false,
|
||||
secretValuesPrinted: false,
|
||||
kubeconfigRead: false,
|
||||
hardwareControlViaCloudApiOnly: true,
|
||||
directGatewayCallsAllowed: false,
|
||||
directBoxSimuCallsAllowed: false,
|
||||
directPatchPanelCallsAllowed: false,
|
||||
m3m4m5AcceptanceClaimsAllowed: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function codexStdioSessionRegistrySummary(codexStdio = {}) {
|
||||
return {
|
||||
kind: "codex-stdio-session-registry",
|
||||
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
|
||||
status: "available",
|
||||
sessionCount: codexStdio.sessionLifecycle?.activeSessions ?? codexStdio.lifecycleSupervisor?.activeSessions ?? null,
|
||||
maxSessions: codexStdio.sessionLifecycle?.maxSessions ?? codexStdio.lifecycleSupervisor?.maxSessions ?? null,
|
||||
idleTimeoutMs: codexStdio.sessionLifecycle?.idleTimeoutMs ?? codexStdio.lifecycleSupervisor?.idleTimeoutMs ?? null,
|
||||
statuses: ["creating", "ready", "busy", "idle", "interrupted", "expired", "failed"],
|
||||
sessionMode: CODEX_STDIO_SESSION_MODE,
|
||||
recentSessions: Array.isArray(codexStdio.recentSessions) ? codexStdio.recentSessions : [],
|
||||
longLivedCodexStdio: true,
|
||||
longLivedSession: true,
|
||||
durable: true,
|
||||
codexStdio: true,
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }) {
|
||||
if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].summary;
|
||||
if (blocked && !runnerAvailability.ready) return providerPlan.mode === "openai" ? providerContract.blocker : "凭证缺口";
|
||||
@@ -2844,12 +2934,7 @@ function openAiFallbackRunnerEvidence({ providerResult, traceId, conversationId,
|
||||
codexRunner: false,
|
||||
reason: "OpenAI Responses fallback is text chat only and cannot satisfy the Codex runner capability gate."
|
||||
},
|
||||
runnerLimitations: [
|
||||
"text-chat-only",
|
||||
"not-codex-stdio",
|
||||
"not-workspace-tools",
|
||||
"not-session-runner"
|
||||
],
|
||||
runnerLimitations: [...OPENAI_FALLBACK_LIMITATION_FLAGS],
|
||||
traceId
|
||||
};
|
||||
}
|
||||
@@ -2899,12 +2984,7 @@ async function callOpenAiResponses({ providerPlan, message, conversationId, trac
|
||||
usage: null,
|
||||
capabilityLevel: "text-chat-only",
|
||||
implementationType: OPENAI_FALLBACK_RUNNER_KIND,
|
||||
runnerLimitations: [
|
||||
"text-chat-only",
|
||||
"not-codex-stdio",
|
||||
"not-workspace-tools",
|
||||
"not-session-runner"
|
||||
],
|
||||
runnerLimitations: [...OPENAI_FALLBACK_LIMITATION_FLAGS],
|
||||
providerTrace: {
|
||||
fallbackUsed: true,
|
||||
fallbackKind: OPENAI_FALLBACK_RUNNER_KIND,
|
||||
|
||||
@@ -389,6 +389,13 @@ test("OpenAI fallback and codex one-shot do not pass the long-lived session gate
|
||||
assert.equal(fallback.status, "completed");
|
||||
assert.equal(fallback.runner.kind, "openai-responses-fallback");
|
||||
assert.equal(fallback.capabilityLevel, "text-chat-only");
|
||||
assert.deepEqual(fallback.runnerLimitations, [
|
||||
"openai-responses-fallback",
|
||||
"text-chat-only",
|
||||
"not-workspace-tools",
|
||||
"not-session-runner"
|
||||
]);
|
||||
assert.equal(fallback.runnerLimitations.includes("not-codex-stdio"), false);
|
||||
assert.equal(fallback.blocker.code, "text_chat_only_fallback");
|
||||
assert.equal(fallback.blocker.layer, "provider");
|
||||
assert.equal(fallback.blocker.retryable, false);
|
||||
@@ -1388,6 +1395,10 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
|
||||
assert.equal(first.capabilityLevel, "long-lived-codex-stdio-session");
|
||||
assert.equal(first.longLivedSessionGate.status, "pass");
|
||||
assert.equal(first.longLivedSessionGate.pass, true);
|
||||
assert.deepEqual(first.runnerLimitations, ["hardware-control-via-cloud-api-only", "secret-values-redacted"]);
|
||||
assert.equal(first.runnerLimitations.includes("not-codex-stdio"), false);
|
||||
assert.equal(first.runnerLimitations.includes("not-write-capable"), false);
|
||||
assert.equal(first.runnerLimitations.includes("process-local-session-registry"), false);
|
||||
assert.equal(classifyCodexRunnerCapability(first, { httpStatus: 200 }).capabilityPass, true);
|
||||
|
||||
const second = await handleCodeAgentChat(
|
||||
@@ -1404,8 +1415,13 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
|
||||
);
|
||||
assert.equal(second.status, "completed");
|
||||
assert.equal(second.sessionId, first.sessionId);
|
||||
assert.equal(second.conversationId, first.conversationId);
|
||||
assert.equal(second.sessionReuse.reused, true);
|
||||
assert.equal(second.sessionReuse.turn, 2);
|
||||
assert.equal(second.runnerTrace.sessionId, first.sessionId);
|
||||
assert.equal(second.runnerTrace.sessionReused, true);
|
||||
assert.equal(second.workspace, first.workspace);
|
||||
assert.equal(second.sandbox, first.sandbox);
|
||||
assert.equal(second.toolCalls[0].name, "codex-reply");
|
||||
assert.equal(second.reply.content, "second stdio reply");
|
||||
assert.equal(calls[0].name, "codex");
|
||||
|
||||
@@ -1811,6 +1811,61 @@ test("OpenAI fallback text chat does not satisfy Codex stdio capability gate", a
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api health does not pass long-lived session gate for explicit OpenAI fallback even if Codex stdio is feasible", async () => {
|
||||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-openai-health-"));
|
||||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-openai-health-codex-home-"));
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
CODEX_HOME: codexHome,
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
OPENAI_API_KEY: "test-openai-key-material",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||||
},
|
||||
codexStdioManager: {
|
||||
describe() {
|
||||
return codexStdioReadyFixture({ workspace, codexHome });
|
||||
},
|
||||
async probe() {
|
||||
return codexStdioReadyFixture({ workspace, codexHome });
|
||||
},
|
||||
async chat(params = {}) {
|
||||
return codexStdioChatFixture({ workspace, codexHome, params });
|
||||
},
|
||||
cancel() {},
|
||||
reapIdle() {}
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json();
|
||||
assert.equal(payload.codeAgent.provider, "openai-responses");
|
||||
assert.equal(payload.codeAgent.mode, "openai");
|
||||
assert.equal(payload.codeAgent.ready, false);
|
||||
assert.equal(payload.codeAgent.capabilityLevel, "read-only-session-tools");
|
||||
assert.equal(payload.codeAgent.runner.kind, "hwlab-readonly-runner");
|
||||
assert.equal(payload.codeAgent.runner.codexStdio, false);
|
||||
assert.equal(payload.codeAgent.longLivedSessionGate.status, "blocked");
|
||||
assert.equal(payload.readiness.sessionRunner.status, "codex_stdio_ready");
|
||||
assert.equal(payload.readiness.codeAgent.ready, false);
|
||||
assert.equal(payload.readiness.codeAgent.provider.provider, "openai-responses");
|
||||
assert.equal(payload.readiness.codeAgent.sessionRunner.codexStdio, true);
|
||||
assert.equal(JSON.stringify(payload.codeAgent).includes("test-openai-key-material"), false);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
await rm(codexHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api /v1/agent/chat parse and params errors use structured blocker envelope", async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
@@ -1934,6 +1989,83 @@ test("cloud api /v1/agent/chat runs Codex stdio pwd with session and workspace e
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api health reports Codex stdio runner facts without readonly limitations", async () => {
|
||||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-health-stdio-"));
|
||||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-health-codex-home-"));
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
CODEX_HOME: codexHome,
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
OPENAI_API_KEY: "test-openai-key-material",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||||
},
|
||||
codexStdioManager: {
|
||||
describe() {
|
||||
return codexStdioReadyFixture({ workspace, codexHome });
|
||||
},
|
||||
async probe() {
|
||||
return codexStdioReadyFixture({ workspace, codexHome });
|
||||
},
|
||||
async chat(params = {}) {
|
||||
return codexStdioChatFixture({ workspace, codexHome, params });
|
||||
},
|
||||
cancel() {},
|
||||
reapIdle() {}
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json();
|
||||
assert.equal(payload.codeAgent.status, "codex-stdio-feasible");
|
||||
assert.equal(payload.codeAgent.ready, true);
|
||||
assert.equal(payload.codeAgent.provider, "codex-stdio");
|
||||
assert.equal(payload.codeAgent.runner.kind, "codex-mcp-stdio-runner");
|
||||
assert.equal(payload.codeAgent.runner.sessionMode, "codex-mcp-stdio-long-lived");
|
||||
assert.equal(payload.codeAgent.runner.codexStdio, true);
|
||||
assert.equal(payload.codeAgent.runner.writeCapable, true);
|
||||
assert.equal(payload.codeAgent.runner.durableSession, true);
|
||||
assert.equal(payload.codeAgent.workspace, workspace);
|
||||
assert.equal(payload.codeAgent.sandbox, "workspace-write");
|
||||
assert.equal(payload.codeAgent.sessionMode, "codex-mcp-stdio-long-lived");
|
||||
assert.equal(payload.codeAgent.sessionRegistry.kind, "codex-stdio-session-registry");
|
||||
assert.deepEqual(payload.codeAgent.sessionRegistry.statuses, ["creating", "ready", "busy", "idle", "interrupted", "expired", "failed"]);
|
||||
assert.equal(payload.codeAgent.capabilityLevel, "long-lived-codex-stdio-session");
|
||||
assert.equal(payload.codeAgent.longLivedSessionGate.status, "pass");
|
||||
assert.equal(payload.readiness.sessionRunner.status, "codex_stdio_ready");
|
||||
assert.equal(payload.readiness.sessionRunner.kind, "codex-mcp-stdio-runner");
|
||||
assert.equal(payload.readiness.sessionRunner.codexStdio, true);
|
||||
assert.equal(payload.readiness.sessionRunner.writeCapable, true);
|
||||
assert.equal(payload.readiness.sessionRunner.capabilityLevel, "long-lived-codex-stdio-session");
|
||||
assert.equal(payload.readiness.codeAgent.sessionRunner.codexStdio, true);
|
||||
assert.equal(payload.readiness.codeAgent.sessionRunner.writeCapable, true);
|
||||
assert.deepEqual(payload.codeAgent.runnerLimitations, [
|
||||
"hardware-control-via-cloud-api-only",
|
||||
"no-direct-gateway-link",
|
||||
"no-direct-box-simu-link",
|
||||
"no-direct-patch-panel-link",
|
||||
"secret-values-redacted"
|
||||
]);
|
||||
const serialized = JSON.stringify(payload.codeAgent);
|
||||
assert.equal(serialized.includes("not-codex-stdio"), false);
|
||||
assert.equal(serialized.includes("not-write-capable"), false);
|
||||
assert.equal(serialized.includes("process-local-session-registry"), false);
|
||||
assert.equal(serialized.includes("test-openai-key-material"), false);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
await rm(codexHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api /v1/agent/chat runs Codex stdio pwd through controlled command path without OpenAI fallback", async () => {
|
||||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-pwd-"));
|
||||
const codexHome = path.join(workspace, "codex-home");
|
||||
|
||||
@@ -276,10 +276,18 @@ function hasBlockingSkillCliSignal(payload) {
|
||||
|
||||
function isLongLivedReady(payload) {
|
||||
return (
|
||||
payload.longLivedSessionGate?.status === "pass" ||
|
||||
payload.longLivedSessionGate?.pass === true ||
|
||||
payload.codexStdioFeasibility?.canStartLongLivedCodexStdio === true ||
|
||||
payload.capabilityLevel === "long-lived-codex-stdio-session" ||
|
||||
(
|
||||
payload.longLivedSessionGate?.status === "pass" ||
|
||||
payload.longLivedSessionGate?.pass === true ||
|
||||
payload.codexStdioFeasibility?.canStartLongLivedCodexStdio === true
|
||||
) &&
|
||||
(
|
||||
payload.provider === "codex-stdio" ||
|
||||
payload.runner?.kind === "codex-mcp-stdio-runner" ||
|
||||
payload.runner?.codexStdio === true ||
|
||||
payload.session?.codexStdio === true
|
||||
) &&
|
||||
payload.capabilityLevel === "long-lived-codex-stdio-session" &&
|
||||
payload.sessionMode === "codex-mcp-stdio-long-lived"
|
||||
) && payload.status !== "failed" && payload.status !== "blocked" && payload.ready !== false;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,34 @@ test("summarizes long-lived Code Agent as green with current deployment revision
|
||||
assert.equal(Object.keys(summary.fields).includes("sourceMain"), false);
|
||||
});
|
||||
|
||||
test("does not mark Codex stdio-shaped fields green without runner and gate evidence", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
availability: {
|
||||
status: "partial",
|
||||
ready: true,
|
||||
provider: "openai-responses",
|
||||
mode: "openai",
|
||||
backend: "hwlab-cloud-api/openai-responses",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
runner: {
|
||||
kind: "openai-responses-fallback",
|
||||
codexStdio: false,
|
||||
writeCapable: false
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "blocked",
|
||||
pass: false,
|
||||
blockers: [{ code: "openai_responses_fallback_not_session" }]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.notEqual(summary.kind, "long-lived-session");
|
||||
assert.notEqual(summary.tone, "ok");
|
||||
assert.equal(summary.readinessBlockers.includes("openai_responses_fallback_not_session"), true);
|
||||
});
|
||||
|
||||
test("maps degraded readonly session tools to warning, not green", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
availability: {
|
||||
|
||||
@@ -397,7 +397,7 @@ function classifyStructuredCodeAgentProbe(readiness = {}) {
|
||||
const provider = readiness.provider?.provider ?? "not_observed";
|
||||
const backend = readiness.provider?.backend ?? "not_observed";
|
||||
const capability = readiness.sessionRunner?.capabilityLevel ?? "not_observed";
|
||||
if (readiness.ready === true && readiness.codexStdioFeasible === true) {
|
||||
if (structuredCodexStdioReady(readiness)) {
|
||||
return passProbe({
|
||||
serviceId,
|
||||
apiPath,
|
||||
@@ -425,6 +425,17 @@ function classifyStructuredCodeAgentProbe(readiness = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function structuredCodexStdioReady(readiness = {}) {
|
||||
return Boolean(
|
||||
readiness.sessionRunnerReady === true &&
|
||||
readiness.codexStdioFeasible === true &&
|
||||
readiness.sessionRunner?.codexStdio === true &&
|
||||
readiness.sessionRunner?.writeCapable === true &&
|
||||
readiness.sessionRunner?.longLivedSession === true &&
|
||||
readiness.sessionRunner?.capabilityLevel === "long-lived-codex-stdio-session"
|
||||
);
|
||||
}
|
||||
|
||||
function currentStructuredBlockers(readiness = {}) {
|
||||
return Array.isArray(readiness.currentBlockers)
|
||||
? readiness.currentBlockers.filter(Boolean)
|
||||
|
||||
@@ -789,6 +789,120 @@ test("keeps ready durable runtime green while attributing read-only mode to Code
|
||||
assert.doesNotMatch(healthLiveProbe.reason, /blocked/u);
|
||||
});
|
||||
|
||||
test("classifies Codex stdio session facts as pass even when unrelated runtime durability is blocked", () => {
|
||||
const status = classifyWorkbenchLiveStatus({
|
||||
healthLive: {
|
||||
ok: true,
|
||||
status: 200,
|
||||
data: {
|
||||
serviceId: "hwlab-cloud-api",
|
||||
status: "degraded",
|
||||
ready: false,
|
||||
readiness: {
|
||||
status: "degraded",
|
||||
ready: false,
|
||||
durability: {
|
||||
status: "blocked",
|
||||
ready: false,
|
||||
blocker: "runtime_durable_adapter_query_blocked"
|
||||
},
|
||||
codeAgent: {
|
||||
status: "blocked",
|
||||
ready: false,
|
||||
providerReady: true,
|
||||
durableDbReady: false,
|
||||
sessionRunnerReady: true,
|
||||
codexStdioFeasible: true,
|
||||
currentBlocker: "runtime_durable_adapter_query_blocked",
|
||||
currentBlockers: ["runtime_durable_adapter_query_blocked"],
|
||||
provider: {
|
||||
status: "ready",
|
||||
ready: true,
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
mode: "codex-stdio"
|
||||
},
|
||||
sessionRunner: {
|
||||
status: "codex_stdio_ready",
|
||||
ready: true,
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
mode: "codex-mcp-stdio-long-lived",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
longLivedSession: true,
|
||||
durableSession: true,
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
blocker: null
|
||||
},
|
||||
codexStdio: {
|
||||
status: "ready",
|
||||
ready: true,
|
||||
feasible: true,
|
||||
blocker: null,
|
||||
blockerCodes: []
|
||||
},
|
||||
secretMaterialRead: false,
|
||||
valuesRedacted: true
|
||||
}
|
||||
},
|
||||
codeAgent: {
|
||||
endpoint: "POST /v1/agent/chat",
|
||||
status: "codex-stdio-feasible",
|
||||
ready: true,
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true
|
||||
},
|
||||
runnerLimitations: [
|
||||
"hardware-control-via-cloud-api-only",
|
||||
"no-direct-gateway-link",
|
||||
"no-direct-box-simu-link",
|
||||
"no-direct-patch-panel-link",
|
||||
"secret-values-redacted"
|
||||
],
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
},
|
||||
codexStdioFeasibility: {
|
||||
ready: true,
|
||||
canStartLongLivedCodexStdio: true,
|
||||
commandProbe: { ready: true },
|
||||
blockerCodes: []
|
||||
}
|
||||
},
|
||||
runtime: {
|
||||
status: "degraded",
|
||||
durable: true,
|
||||
ready: false,
|
||||
blocker: "runtime_durable_adapter_query_blocked"
|
||||
}
|
||||
}
|
||||
},
|
||||
restIndex: okRest,
|
||||
health: okApi,
|
||||
adapter: okApi,
|
||||
m3Control: okM3Control,
|
||||
m3Status: okM3Status
|
||||
});
|
||||
|
||||
const codeAgentProbe = status.probes.find((probe) => probe.apiPath === "POST /v1/agent/chat");
|
||||
assert.equal(codeAgentProbe.kind, "pass");
|
||||
assert.equal(codeAgentProbe.reasonCode, "codex_stdio_ready");
|
||||
assert.match(codeAgentProbe.reason, /capability=long-lived-codex-stdio-session/u);
|
||||
assert.doesNotMatch(codeAgentProbe.reason, /not-codex-stdio|not-write-capable|process-local-session-registry/u);
|
||||
assert.notEqual(status.reasonCode, "codex_stdio_blocked_readonly_session_available");
|
||||
});
|
||||
|
||||
test("fixture matrix keeps durable_readiness_ready green and attributes read-only mode to Code Agent", () => {
|
||||
const restPayload = {
|
||||
serviceId: "hwlab-cloud-api",
|
||||
|
||||
Reference in New Issue
Block a user