feat: gate Code Agent session registry
Implements #317 session-gated read-only runner phase with structured long-lived Codex stdio blockers.
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 readonlySessionCapabilityLevels = new Set(["read-only-tools", "read-only-session-tools"]);
|
||||
const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
|
||||
const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]);
|
||||
const timeoutPattern = /\b(?:timeout|timed out|abort|aborted|超时|请求超过)\b/iu;
|
||||
@@ -86,6 +87,10 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
? payload.runner.runnerLimitations.filter((item) => typeof item === "string")
|
||||
: [];
|
||||
const sessionReuse = payload?.sessionReuse && typeof payload.sessionReuse === "object" ? payload.sessionReuse : null;
|
||||
const session = payload?.session && typeof payload.session === "object" ? payload.session : null;
|
||||
const longLivedSessionGate = payload?.longLivedSessionGate && typeof payload.longLivedSessionGate === "object"
|
||||
? payload.longLivedSessionGate
|
||||
: null;
|
||||
|
||||
if (provider === "openai-responses" || runnerKind === "openai-responses-fallback") {
|
||||
return {
|
||||
@@ -101,11 +106,15 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
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 (capabilityLevel !== "read-only-tools") missing.push("capabilityLevel=read-only-tools");
|
||||
if (!readonlySessionCapabilityLevels.has(capabilityLevel)) missing.push("capabilityLevel=read-only-session-tools");
|
||||
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 (!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 (!sessionReuse) missing.push("sessionReuse");
|
||||
if (toolCalls.length === 0) missing.push("toolCalls");
|
||||
if (!runnerTrace) missing.push("runnerTrace");
|
||||
@@ -114,6 +123,7 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
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 (!longLivedSessionGate) missing.push("longLivedSessionGate");
|
||||
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
@@ -125,6 +135,16 @@ 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",
|
||||
@@ -137,10 +157,11 @@ export function classifyCodexRunnerCapability(payload, { httpStatus = null } = {
|
||||
|
||||
return {
|
||||
status: "pass",
|
||||
level: "#275 read-only Codex runner capability pass",
|
||||
level: "#317 read-only session registry partial pass",
|
||||
blocker: null,
|
||||
capabilityPass: true,
|
||||
reason: "completed read-only runner response includes workspace, sandbox, toolCalls, skills, and runnerTrace evidence"
|
||||
longLivedSession: false,
|
||||
reason: "completed read-only session-registry response includes session, workspace, sandbox, toolCalls, skills, runnerTrace, and a blocked long-lived Codex stdio gate"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -240,9 +261,11 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
|
||||
runnerTrace: null,
|
||||
sessionMode: null,
|
||||
sessionReuse: null,
|
||||
session: null,
|
||||
implementationType: null,
|
||||
runnerLimitations: [],
|
||||
codexStdioFeasibility: null
|
||||
codexStdioFeasibility: null,
|
||||
longLivedSessionGate: null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -285,9 +308,11 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
|
||||
runnerTrace: summarizeRunnerTrace(payload.runnerTrace),
|
||||
sessionMode: stringOrNull(payload.sessionMode),
|
||||
sessionReuse: summarizeSessionReuse(payload.sessionReuse),
|
||||
session: summarizeSession(payload.session),
|
||||
implementationType: stringOrNull(payload.implementationType),
|
||||
runnerLimitations: summarizeStringList(payload.runnerLimitations),
|
||||
codexStdioFeasibility: summarizeCodexStdioFeasibility(payload.codexStdioFeasibility)
|
||||
codexStdioFeasibility: summarizeCodexStdioFeasibility(payload.codexStdioFeasibility),
|
||||
longLivedSessionGate: summarizeLongLivedSessionGate(payload.longLivedSessionGate)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -310,9 +335,11 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
|
||||
runnerTrace: summarizeRunnerTrace(payload.runnerTrace),
|
||||
sessionMode: stringOrNull(payload.sessionMode),
|
||||
sessionReuse: summarizeSessionReuse(payload.sessionReuse),
|
||||
session: summarizeSession(payload.session),
|
||||
implementationType: stringOrNull(payload.implementationType),
|
||||
runnerLimitations: summarizeStringList(payload.runnerLimitations),
|
||||
codexStdioFeasibility: summarizeCodexStdioFeasibility(payload.codexStdioFeasibility)
|
||||
codexStdioFeasibility: summarizeCodexStdioFeasibility(payload.codexStdioFeasibility),
|
||||
longLivedSessionGate: summarizeLongLivedSessionGate(payload.longLivedSessionGate)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -409,6 +436,9 @@ export function inspectRealCompletionEvidence(payload) {
|
||||
if (provider === "openai-responses" && payload?.capabilityLevel && payload.capabilityLevel !== "text-chat-only") {
|
||||
return { ok: false, reason: "openai-responses cannot claim runner capabilityLevel" };
|
||||
}
|
||||
if (provider === "codex-readonly-runner" && payload?.longLivedSessionGate?.status === "pass") {
|
||||
return { ok: false, reason: "read-only runner cannot claim long-lived session gate pass" };
|
||||
}
|
||||
if (untrustedProviderPattern.test(`${provider} ${backend}`)) {
|
||||
return { ok: false, reason: `provider/backend looks non-real: ${payload.provider}/${payload.backend}` };
|
||||
}
|
||||
@@ -521,7 +551,32 @@ function summarizeSessionReuse(value) {
|
||||
reused: value.reused === true,
|
||||
turn: typeof value.turn === "number" ? value.turn : null,
|
||||
previousTurns: typeof value.previousTurns === "number" ? value.previousTurns : null,
|
||||
workspace: stringOrNull(value.workspace)
|
||||
workspace: stringOrNull(value.workspace),
|
||||
status: stringOrNull(value.status),
|
||||
idleTimeoutMs: typeof value.idleTimeoutMs === "number" ? value.idleTimeoutMs : null,
|
||||
lastTraceId: stringOrNull(value.lastTraceId)
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeSession(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return {
|
||||
sessionId: stringOrNull(value.sessionId),
|
||||
conversationId: stringOrNull(value.conversationId),
|
||||
status: stringOrNull(value.status),
|
||||
workspace: stringOrNull(value.workspace),
|
||||
sandbox: stringOrNull(value.sandbox),
|
||||
runnerKind: stringOrNull(value.runnerKind),
|
||||
capabilityLevel: stringOrNull(value.capabilityLevel),
|
||||
implementationType: stringOrNull(value.implementationType),
|
||||
createdAt: stringOrNull(value.createdAt),
|
||||
updatedAt: stringOrNull(value.updatedAt),
|
||||
idleTimeoutMs: typeof value.idleTimeoutMs === "number" ? value.idleTimeoutMs : null,
|
||||
lastTraceId: stringOrNull(value.lastTraceId),
|
||||
turn: typeof value.turn === "number" ? value.turn : null,
|
||||
longLivedSession: value.longLivedSession === true,
|
||||
codexStdio: value.codexStdio === true,
|
||||
secretMaterialStored: value.secretMaterialStored === true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -542,6 +597,26 @@ function summarizeCodexStdioFeasibility(value) {
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeLongLivedSessionGate(value) {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
return {
|
||||
status: stringOrNull(value.status),
|
||||
pass: value.pass === true,
|
||||
requiredCapability: stringOrNull(value.requiredCapability),
|
||||
currentCapability: stringOrNull(value.currentCapability),
|
||||
sessionId: stringOrNull(value.sessionId),
|
||||
sessionStatus: stringOrNull(value.sessionStatus),
|
||||
runnerKind: stringOrNull(value.runnerKind),
|
||||
provider: stringOrNull(value.provider),
|
||||
blockers: Array.isArray(value.blockers)
|
||||
? value.blockers.map((blocker) => ({
|
||||
code: stringOrNull(blocker.code),
|
||||
sourceIssue: stringOrNull(blocker.sourceIssue)
|
||||
}))
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeStringList(value) {
|
||||
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user