feat: add codex stdio code agent session gate
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const trustedLiveProviders = new Set(["openai-responses", "codex-cli", "codex-readonly-runner"]);
|
||||
const trustedRunnerProviders = new Set(["codex-readonly-runner"]);
|
||||
const trustedLiveProviders = new Set(["openai-responses", "codex-cli", "codex-readonly-runner", "codex-stdio"]);
|
||||
const trustedRunnerProviders = new Set(["codex-stdio"]);
|
||||
const partialRunnerProviders = new Set(["codex-readonly-runner"]);
|
||||
const readonlySessionCapabilityLevels = new Set(["read-only-tools", "read-only-session-tools"]);
|
||||
const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
|
||||
const blockedCodeAgentUiLabels = new Set([
|
||||
@@ -115,27 +116,38 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
const missing = [];
|
||||
if (payload?.status !== "completed") missing.push("completed status");
|
||||
if (!trustedRunnerProviders.has(provider)) missing.push("runner provider");
|
||||
if (runnerKind !== "hwlab-readonly-runner") missing.push("runner.kind");
|
||||
if (!readonlySessionCapabilityLevels.has(capabilityLevel)) missing.push("capabilityLevel=read-only-session-tools");
|
||||
if (runnerKind !== "codex-mcp-stdio-runner") missing.push("runner.kind=codex-mcp-stdio-runner");
|
||||
if (capabilityLevel !== "long-lived-codex-stdio-session") missing.push("capabilityLevel=long-lived-codex-stdio-session");
|
||||
if (!workspace) missing.push("workspace");
|
||||
if (sandbox !== "read-only") missing.push("sandbox=read-only");
|
||||
if (sessionMode !== "controlled-readonly-session-registry") missing.push("sessionMode=controlled-readonly-session-registry");
|
||||
if (implementationType !== "controlled-readonly-session-registry") missing.push("implementationType=controlled-readonly-session-registry");
|
||||
if (sandbox !== "workspace-write") missing.push("sandbox=workspace-write");
|
||||
if (sessionMode !== "codex-mcp-stdio-long-lived") missing.push("sessionMode=codex-mcp-stdio-long-lived");
|
||||
if (implementationType !== "repo-owned-codex-mcp-stdio-session") missing.push("implementationType=repo-owned-codex-mcp-stdio-session");
|
||||
if (!session) missing.push("session");
|
||||
if (session && !["idle", "ready", "busy"].includes(session.status)) missing.push("session.status active/idle");
|
||||
if (session && typeof session.idleTimeoutMs !== "number") missing.push("session.idleTimeoutMs");
|
||||
if (session && !session.lastTraceId) missing.push("session.lastTraceId");
|
||||
if (session && session.longLivedSession !== true) missing.push("session.longLivedSession=true");
|
||||
if (session && session.codexStdio !== true) missing.push("session.codexStdio=true");
|
||||
if (!sessionReuse) missing.push("sessionReuse");
|
||||
if (toolCalls.length === 0) missing.push("toolCalls");
|
||||
if (!runnerTrace) missing.push("runnerTrace");
|
||||
if (!skills) missing.push("skills");
|
||||
if (!runnerLimitations.includes("not-codex-stdio")) missing.push("runnerLimitations.not-codex-stdio");
|
||||
if (payload?.runner?.codexStdio !== false) missing.push("runner.codexStdio=false");
|
||||
if (payload?.runner?.writeCapable !== false) missing.push("runner.writeCapable=false");
|
||||
if (payload?.runner?.durableSession !== false) missing.push("runner.durableSession=false");
|
||||
if (payload?.runner?.codexStdio !== true) missing.push("runner.codexStdio=true");
|
||||
if (payload?.runner?.writeCapable !== true) missing.push("runner.writeCapable=true");
|
||||
if (payload?.runner?.durableSession !== true) missing.push("runner.durableSession=true");
|
||||
if (!longLivedSessionGate) missing.push("longLivedSessionGate");
|
||||
if (longLivedSessionGate?.status !== "pass" || longLivedSessionGate?.pass !== true) missing.push("longLivedSessionGate=pass");
|
||||
|
||||
if (missing.length > 0) {
|
||||
if (partialRunnerProviders.has(provider) && runnerKind === "hwlab-readonly-runner") {
|
||||
return {
|
||||
status: "blocked",
|
||||
level: "BLOCKED/controlled-readonly-session-registry",
|
||||
blocker: "controlled-readonly-not-long-lived",
|
||||
capabilityPass: false,
|
||||
reason: "controlled-readonly-session-registry can expose bounded read-only tools, but it cannot pass the long-lived Codex stdio capability gate."
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "blocked",
|
||||
level: "BLOCKED/runner-capability",
|
||||
@@ -145,16 +157,6 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
};
|
||||
}
|
||||
|
||||
if (longLivedSessionGate?.status === "pass") {
|
||||
return {
|
||||
status: "blocked",
|
||||
level: "BLOCKED/runner-gate",
|
||||
blocker: "unexpected-long-lived-green",
|
||||
capabilityPass: false,
|
||||
reason: "read-only session registry responses must not claim the full long-lived Codex stdio gate."
|
||||
};
|
||||
}
|
||||
|
||||
if (toolCalls.some((tool) => tool.status !== "completed")) {
|
||||
return {
|
||||
status: "blocked",
|
||||
@@ -167,11 +169,11 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
|
||||
return {
|
||||
status: "pass",
|
||||
level: "#317 read-only session registry partial pass",
|
||||
level: "#275 long-lived Codex stdio session skeleton pass",
|
||||
blocker: null,
|
||||
capabilityPass: true,
|
||||
longLivedSession: false,
|
||||
reason: "completed read-only session-registry response includes session, workspace, sandbox, toolCalls, skills, runnerTrace, and a blocked long-lived Codex stdio gate"
|
||||
longLivedSession: true,
|
||||
reason: "completed repo-owned Codex stdio response includes reusable session, workspace-write sandbox, toolCalls, skills, runnerTrace, and a passed long-lived session gate"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -538,6 +540,15 @@ export function inspectRealCompletionEvidence(payload) {
|
||||
if (provider === "codex-readonly-runner" && payload?.longLivedSessionGate?.status === "pass") {
|
||||
return { ok: false, reason: "read-only runner cannot claim long-lived session gate pass" };
|
||||
}
|
||||
if (provider === "codex-readonly-runner") {
|
||||
return { ok: false, reason: "controlled read-only runner is partial capability only and cannot be reported as full Code Agent completion" };
|
||||
}
|
||||
if (provider === "openai-responses" && payload?.longLivedSessionGate?.status !== "pass") {
|
||||
return { ok: false, reason: "OpenAI Responses fallback is text-chat-only and cannot be reported as full Code Agent completion" };
|
||||
}
|
||||
if (provider === "codex-stdio" && payload?.longLivedSessionGate?.status !== "pass") {
|
||||
return { ok: false, reason: "codex-stdio completion is missing long-lived session gate pass" };
|
||||
}
|
||||
if (untrustedProviderPattern.test(`${provider} ${backend}`)) {
|
||||
return { ok: false, reason: `provider/backend looks non-real: ${payload.provider}/${payload.backend}` };
|
||||
}
|
||||
@@ -675,6 +686,7 @@ function summarizeSession(value) {
|
||||
turn: typeof value.turn === "number" ? value.turn : null,
|
||||
longLivedSession: value.longLivedSession === true,
|
||||
codexStdio: value.codexStdio === true,
|
||||
writeCapable: value.writeCapable === true,
|
||||
secretMaterialStored: value.secretMaterialStored === true
|
||||
};
|
||||
}
|
||||
@@ -687,6 +699,20 @@ function summarizeCodexStdioFeasibility(value) {
|
||||
currentImplementation: stringOrNull(value.currentImplementation),
|
||||
implementationRequired: stringOrNull(value.implementationRequired),
|
||||
binaryOnPath: value.binaryOnPath === true,
|
||||
workspace: stringOrNull(value.workspace),
|
||||
sandbox: stringOrNull(value.sandbox),
|
||||
ready: value.ready === true,
|
||||
sessionLifecycle: value.sessionLifecycle && typeof value.sessionLifecycle === "object"
|
||||
? {
|
||||
create: value.sessionLifecycle.create === true,
|
||||
reuse: value.sessionLifecycle.reuse === true,
|
||||
cancel: value.sessionLifecycle.cancel === true,
|
||||
reap: value.sessionLifecycle.reap === true,
|
||||
idleTimeout: value.sessionLifecycle.idleTimeout === true,
|
||||
traceCapture: value.sessionLifecycle.traceCapture === true,
|
||||
idleTimeoutMs: typeof value.sessionLifecycle.idleTimeoutMs === "number" ? value.sessionLifecycle.idleTimeoutMs : null
|
||||
}
|
||||
: null,
|
||||
blockers: Array.isArray(value.blockers)
|
||||
? value.blockers.map((blocker) => ({
|
||||
code: stringOrNull(blocker.code),
|
||||
|
||||
@@ -377,6 +377,10 @@ function validateCloudApiCodeAgentSource(ctx, env) {
|
||||
"$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
"cloud API Code Agent DEV egress/proxy base URL"
|
||||
);
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED, "1", "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED", "cloud API Codex stdio adapter enabled flag");
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR, "repo-owned", "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR", "cloud API Codex stdio supervisor mode");
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_CODEX_WORKSPACE, "/workspace/hwlab", "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_WORKSPACE", "cloud API Codex stdio workspace mount contract");
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, "workspace-write", "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_SANDBOX", "cloud API Codex stdio sandbox contract");
|
||||
expect(
|
||||
ctx,
|
||||
env.HWLAB_CODE_AGENT_OPENAI_BASE_URL !== contract.egress.forbiddenDirectBaseUrl,
|
||||
@@ -532,6 +536,10 @@ function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
||||
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
"cloud API workload Code Agent DEV egress/proxy base URL"
|
||||
);
|
||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED")?.value, "1", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED", "cloud API workload Codex stdio adapter enabled flag");
|
||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR")?.value, "repo-owned", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR", "cloud API workload Codex stdio supervisor mode");
|
||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_CODEX_WORKSPACE")?.value, "/workspace/hwlab", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_WORKSPACE", "cloud API workload Codex stdio workspace mount contract");
|
||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_CODEX_SANDBOX")?.value, "workspace-write", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_CODEX_SANDBOX", "cloud API workload Codex stdio sandbox contract");
|
||||
expect(
|
||||
ctx,
|
||||
env.get("HWLAB_CODE_AGENT_OPENAI_BASE_URL")?.value !== contract.egress.forbiddenDirectBaseUrl,
|
||||
|
||||
@@ -211,7 +211,10 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"message",
|
||||
"providerTrace",
|
||||
"openai-responses",
|
||||
"codex-stdio",
|
||||
"codex-cli",
|
||||
"codex-mcp-stdio-long-lived",
|
||||
"repo-owned-codex-mcp-stdio-session",
|
||||
"controlled-readonly-session-registry",
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
@@ -491,7 +494,7 @@ function runStaticSmoke() {
|
||||
evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider blockers without exposing credential internals and only real completed replies can become DEV-LIVE.", {
|
||||
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
|
||||
});
|
||||
@@ -506,7 +509,7 @@ function runStaticSmoke() {
|
||||
evidence: ["rail-button min-width/min-height", "state-tag wrapping", "side-tab stable labels", "mobile message evidence column"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed Code Agent replies expose provider/model/trace/conversation evidence and reject echo/mock/stub completions.", {
|
||||
addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed full Code Agent replies expose Codex stdio provider/model/trace/conversation/session evidence and reject echo/mock/stub, OpenAI fallback, and read-only runner completions.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: requiredCodeAgentEvidenceTerms
|
||||
});
|
||||
@@ -2228,7 +2231,7 @@ function hasCodeAgentChatContract({ html, app }) {
|
||||
);
|
||||
}
|
||||
|
||||
function hasCodeAgentReadinessVisibility({ html, app }) {
|
||||
function hasCodeAgentReadinessVisibility({ html, app, liveStatus = "" }) {
|
||||
const source = `${html}\n${app}`;
|
||||
const userFacingBodies = [
|
||||
functionBody(app, "codeAgentStatusMessage"),
|
||||
@@ -2256,6 +2259,9 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
|
||||
/isRealCompletedChatMessage/u.test(app) &&
|
||||
/isRealCompletedChatResult/u.test(app) &&
|
||||
/hasRealCodeAgentEvidence/u.test(app) &&
|
||||
/codeAgentBlockerDetail/u.test(app) &&
|
||||
/codex_stdio_blocked_readonly_available/u.test(liveStatus) &&
|
||||
/codex_stdio_supervisor_disabled/u.test(app) &&
|
||||
!/status === "failed" \? "dev-live"/u.test(app) &&
|
||||
!/tone:\s*state\.conversationId\s*\?\s*"dev-live"/u.test(app) &&
|
||||
!/provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu.test(source) &&
|
||||
@@ -2297,14 +2303,18 @@ function hasCodeAgentCompletedEvidenceVisibility({ app }) {
|
||||
/isTrustedCodeAgentProvider\(value\?\.provider\)/u.test(realEvidenceBody) &&
|
||||
/isCodexRunnerCapableEvidence\(value\)/u.test(realEvidenceBody) &&
|
||||
/CODEX_RUNNER_CAPABLE_PROVIDERS/u.test(app) &&
|
||||
/controlled-readonly-session-registry/u.test(runnerEvidenceBody) &&
|
||||
/read-only-session-tools/u.test(runnerEvidenceBody) &&
|
||||
/CODEX_READONLY_PARTIAL_PROVIDERS/u.test(app) &&
|
||||
/codex-mcp-stdio-long-lived/u.test(runnerEvidenceBody) &&
|
||||
/repo-owned-codex-mcp-stdio-session/u.test(runnerEvidenceBody) &&
|
||||
/long-lived-codex-stdio-session/u.test(runnerEvidenceBody) &&
|
||||
/isReadOnlyRunnerPartialEvidence/u.test(app) &&
|
||||
/isTextFallbackChatResult/u.test(app) &&
|
||||
/value\?\.session\?\.status === "idle"/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.longLivedSessionGate\?\.status === "blocked"/u.test(runnerEvidenceBody) &&
|
||||
/not-codex-stdio/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.runner\?\.codexStdio === false/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.runner\?\.writeCapable === false/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.runner\?\.durableSession === false/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.longLivedSessionGate\?\.status === "pass"/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.session\?\.codexStdio === true/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.runner\?\.codexStdio === true/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.runner\?\.writeCapable === true/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.runner\?\.durableSession === true/u.test(runnerEvidenceBody) &&
|
||||
/TRUSTED_CODE_AGENT_PROVIDERS/u.test(app) &&
|
||||
/UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/u.test(realEvidenceBody) &&
|
||||
/value\?\.conversationId \|\| value\?\.sessionId/u.test(realEvidenceBody) &&
|
||||
|
||||
Reference in New Issue
Block a user