fix: remove obsolete code agent gates
This commit is contained in:
@@ -750,7 +750,7 @@ function resolveProviderPlan(env, options = {}) {
|
||||
mode: "codex-cli"
|
||||
};
|
||||
}
|
||||
if (provider === "codex-stdio" || provider === "codex-mcp-stdio") {
|
||||
if (provider === "codex-stdio") {
|
||||
return {
|
||||
provider: CODEX_STDIO_PROVIDER,
|
||||
model,
|
||||
|
||||
@@ -873,78 +873,6 @@ test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", asyn
|
||||
}
|
||||
});
|
||||
|
||||
test("Code Agent natural-language Keil and serial monitor request goes through Codex stdio", async () => {
|
||||
const calls = [];
|
||||
let providerCalled = false;
|
||||
const fakeCodex = await createFakeCodexCommand();
|
||||
const codexHome = await prepareFakeCodexHome();
|
||||
const registry = createCodeAgentSessionRegistry();
|
||||
const manager = createCodexStdioSessionManager({
|
||||
idFactory: () => "ses_freq_keil_serial_stdio",
|
||||
createRpcClient: async () => createFakeAppServerClient({
|
||||
calls,
|
||||
responses: ["已进入 F:/work/constart,完成 71-FREQ Keil build/download,并通过 serial-monitor 抓取启动日志。"]
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_freq_keil_serial_stdio",
|
||||
traceId: "trc_freq_keil_serial_stdio",
|
||||
projectId: "prj_mvp_topology",
|
||||
message: "去 f:/work/constart 找到 71-FREQ 工程,用 keil skill 编译下载,用 serial-monitor skill 抓启动日志"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-25T10:51:24.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
OPENAI_API_KEY: "test-openai-key-material",
|
||||
CODEX_HOME: codexHome,
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
},
|
||||
sessionRegistry: registry,
|
||||
codexStdioManager: manager,
|
||||
callProvider: async () => {
|
||||
providerCalled = true;
|
||||
throw new Error("text fallback must not be used");
|
||||
},
|
||||
m3IoSkillRequestJson: async () => {
|
||||
throw new Error("natural-language Keil and serial monitor prompt must not be routed to M3 IO");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
validateCodeAgentChatSchema(payload);
|
||||
assert.equal(payload.status, "completed");
|
||||
assert.equal(payload.provider, "codex-stdio");
|
||||
assert.equal(payload.responseType, undefined);
|
||||
assert.notEqual(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||||
assert.equal(providerCalled, false);
|
||||
assert.equal(payload.toolCalls.some((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io"), false);
|
||||
const turn = calls.find((call) => call.method === "turn/start");
|
||||
assert.ok(turn, "Codex stdio turn/start should be called");
|
||||
assert.match(turn.args.prompt, /f:\/work\/constart/iu);
|
||||
assert.match(turn.args.prompt, /71-FREQ/u);
|
||||
assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u);
|
||||
assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\keil/u);
|
||||
assert.match(turn.args.prompt, /keil-cli\.py/u);
|
||||
assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\serial-monitor/u);
|
||||
assert.match(turn.args.prompt, /monitor start/u);
|
||||
assert.match(turn.args.prompt, /--powershell-stdin/u);
|
||||
} finally {
|
||||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||||
await rm(codexHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -1240,97 +1168,6 @@ test("Codex app-server args pin repo-owned DEV responses egress without leaking
|
||||
assert.equal(args.join(" ").includes("/v1/responses"), false);
|
||||
});
|
||||
|
||||
test("repo-owned Codex stdio manager creates and reuses long-lived sessions with trace evidence", async () => {
|
||||
const calls = [];
|
||||
const fakeCodex = await createFakeCodexCommand();
|
||||
const codexHome = await prepareFakeCodexHome();
|
||||
const registry = createCodeAgentSessionRegistry();
|
||||
const manager = createCodexStdioSessionManager({
|
||||
idFactory: () => "ses_stdio_ready",
|
||||
createRpcClient: async () => createFakeAppServerClient({ calls })
|
||||
});
|
||||
const env = {
|
||||
PATH: process.env.PATH,
|
||||
OPENAI_API_KEY: "test-openai-key-material",
|
||||
CODEX_HOME: codexHome,
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
};
|
||||
|
||||
const first = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_stdio_ready",
|
||||
traceId: "trc_stdio_ready_1",
|
||||
message: "first"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-23T00:06:00.000Z",
|
||||
codexStdioManager: manager,
|
||||
sessionRegistry: registry,
|
||||
env
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(first);
|
||||
assert.equal(first.status, "completed");
|
||||
assert.equal(first.provider, "codex-stdio");
|
||||
assert.equal(first.runner.kind, "codex-app-server-stdio-runner");
|
||||
assert.equal(first.sessionMode, "codex-app-server-stdio-long-lived");
|
||||
assert.equal(first.implementationType, "repo-owned-codex-app-server-stdio-session");
|
||||
assert.equal(first.session.longLivedSession, true);
|
||||
assert.equal(first.session.codexStdio, true);
|
||||
assert.equal(first.runner.writeCapable, true);
|
||||
assert.equal(first.runner.durableSession, true);
|
||||
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, ["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(
|
||||
{
|
||||
conversationId: "cnv_stdio_ready",
|
||||
traceId: "trc_stdio_ready_2",
|
||||
message: "second"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-23T00:06:01.000Z",
|
||||
codexStdioManager: manager,
|
||||
sessionRegistry: registry,
|
||||
env
|
||||
}
|
||||
);
|
||||
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-app-server.thread/resume+turn/start");
|
||||
assert.equal(second.reply.content, "second stdio reply");
|
||||
const turnPathCalls = calls.filter((call) => call.method !== "initialize");
|
||||
assert.deepEqual(turnPathCalls.map((call) => call.method), ["thread/start", "turn/start", "thread/resume", "turn/start"]);
|
||||
assert.equal(turnPathCalls[0].args.cwd, process.cwd());
|
||||
assert.equal(turnPathCalls[2].args.threadId, "thread_stdio_ready");
|
||||
assert.match(turnPathCalls[3].args.prompt, /Prior session facts/u);
|
||||
assert.match(turnPathCalls[3].args.prompt, /ses_stdio_ready/u);
|
||||
assert.match(turnPathCalls[3].args.prompt, /trc_stdio_ready_1/u);
|
||||
assert.match(turnPathCalls[3].args.prompt, new RegExp(escapeRegExp(process.cwd()), "u"));
|
||||
assert.equal(second.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
|
||||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||||
await rm(codexHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("Codex stdio skills discovery returns bounded manifest facts instead of generic model text", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-stdio-skills-ready-"));
|
||||
const workspace = path.join(root, "workspace");
|
||||
|
||||
@@ -246,10 +246,10 @@ function buildSessionRunnerReadiness(codeAgent = {}) {
|
||||
return {
|
||||
status,
|
||||
ready: runnerReady,
|
||||
kind: stdioReady ? stdio.kind ?? "codex-mcp-stdio-runner" : runner.kind ?? "unknown",
|
||||
kind: stdioReady ? stdio.kind ?? "codex-app-server-stdio-runner" : runner.kind ?? "unknown",
|
||||
provider: stdioReady ? stdio.provider ?? "codex-stdio" : runner.provider ?? codeAgent?.provider ?? "unknown",
|
||||
backend: stdioReady ? stdio.backend ?? "hwlab-cloud-api/codex-mcp-stdio" : runner.backend ?? codeAgent?.backend ?? "unknown",
|
||||
mode: stdioReady ? "codex-mcp-stdio-long-lived" : runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "unknown",
|
||||
backend: stdioReady ? stdio.backend ?? "hwlab-cloud-api/codex-app-server-stdio" : runner.backend ?? codeAgent?.backend ?? "unknown",
|
||||
mode: stdioReady ? "codex-app-server-stdio-long-lived" : runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "unknown",
|
||||
capabilityLevel: stdioReady ? "long-lived-codex-stdio-session" : runner.capabilityLevel ?? codeAgent?.capabilityLevel ?? "unknown",
|
||||
longLivedSession: stdioReady || runner.longLivedSession === true,
|
||||
durableSession: stdioReady || runner.durableSession === true || runner.durable === true,
|
||||
|
||||
+10
-1288
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
@@ -10,7 +10,6 @@ import {
|
||||
runDevCloudWorkbenchLayoutSmoke,
|
||||
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
|
||||
runDevCloudWorkbenchSessionContinuityFixtureSmoke,
|
||||
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke,
|
||||
runDevCloudWorkbenchTimeoutFixtureSmoke,
|
||||
sanitizeAgentChatBody
|
||||
@@ -252,15 +251,6 @@ test("source/default smoke covers #288 gate single-table contract", () => {
|
||||
assert.equal(report.checks.some((item) => item.id === "feedback-119-gate-retains-diagnostics"), false);
|
||||
});
|
||||
|
||||
test("source/default smoke covers Code Agent quick prompt fill-only contract", () => {
|
||||
const report = runDevCloudWorkbenchStaticSmoke();
|
||||
const check = report.checks.find((item) => item.id === "code-agent-quick-prompts-contract");
|
||||
assert.equal(check?.status, "pass");
|
||||
assert.equal(check.evidence.includes("Codex:DO1=true 后读 DI1"), true);
|
||||
assert.equal(check.evidence.includes("Codex:DO1=false 复核 DI1"), true);
|
||||
assert.equal(check.evidence.some((item) => /我点击发送后才确认执行这次写入请求/u.test(item)), true);
|
||||
});
|
||||
|
||||
test("live workbench identity passes only when runtime commit or image tag matches current source", () => {
|
||||
assert.equal(
|
||||
classifyLiveDeploymentIdentity(sourceIdentity, {
|
||||
@@ -522,48 +512,6 @@ test("Code Agent readiness classifier blocks completed payloads over any non-2xx
|
||||
}
|
||||
});
|
||||
|
||||
test("Code Agent browser classifier accepts only completed HTTP success plus completed UI and real provider evidence", () => {
|
||||
const summary = sanitizeAgentChatBody({
|
||||
status: "completed",
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
traceId: "trc_completed",
|
||||
reply: {
|
||||
content: "ok"
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
}
|
||||
}, { httpStatus: 200 });
|
||||
|
||||
const pass = classifyCodeAgentBrowserJourney({
|
||||
responseSummary: summary,
|
||||
httpOk: true,
|
||||
httpStatus: 200,
|
||||
ui: {
|
||||
agentChatStatus: "DEV-LIVE 回复",
|
||||
completedMessageVisible: true,
|
||||
failedMessageVisible: false
|
||||
}
|
||||
});
|
||||
assert.equal(pass.status, "pass");
|
||||
|
||||
const blocked = classifyCodeAgentBrowserJourney({
|
||||
responseSummary: summary,
|
||||
httpOk: false,
|
||||
httpStatus: 500,
|
||||
ui: {
|
||||
agentChatStatus: "DEV-LIVE 回复",
|
||||
completedMessageVisible: true,
|
||||
failedMessageVisible: false
|
||||
}
|
||||
});
|
||||
assert.equal(blocked.status, "blocked");
|
||||
assert.equal(blocked.blocker, "provider-upstream");
|
||||
});
|
||||
|
||||
test("Code Agent browser classifier blocks completed payloads without backend evidence", () => {
|
||||
const summary = sanitizeAgentChatBody({
|
||||
status: "completed",
|
||||
@@ -612,35 +560,6 @@ test("local Code Agent timeout fixture keeps bounded timeout state, trace contex
|
||||
assert.equal(timeoutCheck.observations.ui.completedMessageVisible, false);
|
||||
});
|
||||
|
||||
test("local slow structured blocker fixture preserves trace, pending state, and retry input", async () => {
|
||||
const report = await runDevCloudWorkbenchSlowBlockerFixtureSmoke({
|
||||
responseDelayMs: 5200
|
||||
});
|
||||
|
||||
if (report.status === "skip") {
|
||||
assert.match(report.summary, /Playwright is unavailable/u);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2));
|
||||
assert.equal(report.evidenceLevel, "SOURCE");
|
||||
assert.equal(report.devLive, false);
|
||||
const blockerCheck = report.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
|
||||
assert.equal(blockerCheck?.status, "pass");
|
||||
assert.equal(blockerCheck.observations.ui.agentChatStatus, "Runner 受阻");
|
||||
assert.equal(blockerCheck.observations.ui.failedMessageVisible, true);
|
||||
assert.equal(blockerCheck.observations.ui.failedMessageHasTrace, true);
|
||||
assert.equal(blockerCheck.observations.ui.retryInputPreserved, true);
|
||||
assert.equal(blockerCheck.observations.ui.retryInputValue, "列出你能使用的所有skill");
|
||||
assert.equal(blockerCheck.observations.prompt.legacyWindow.permanentFailureAround4500ms, false);
|
||||
assert.equal(blockerCheck.observations.prompt.legacyWindow.pendingChineseVisible, true);
|
||||
assert.equal(blockerCheck.observations.prompt.response.provider, "codex-stdio");
|
||||
assert.equal(blockerCheck.observations.prompt.response.traceId, blockerCheck.observations.prompt.traceId);
|
||||
assert.equal(blockerCheck.observations.prompt.response.error.code, "skills_unavailable");
|
||||
assert.equal(blockerCheck.observations.prompt.response.skills.status, "blocked");
|
||||
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
|
||||
});
|
||||
|
||||
test("local session continuity fixture reuses Code Agent context and retries with degraded-copy guard", async () => {
|
||||
const report = await runDevCloudWorkbenchSessionContinuityFixtureSmoke();
|
||||
if (report.status === "skip") {
|
||||
|
||||
@@ -500,10 +500,7 @@ export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {
|
||||
"codex_stdio_failed",
|
||||
"codex_stdio_protocol_blocked",
|
||||
"codex_stdio_empty_response",
|
||||
"codex_stdio_command_probe_failed",
|
||||
"external_network_blocked",
|
||||
"network_tool_unavailable",
|
||||
"network_timeout"
|
||||
"codex_stdio_command_probe_failed"
|
||||
].includes(errorCode)) {
|
||||
return {
|
||||
blocked: true,
|
||||
|
||||
@@ -4471,8 +4471,6 @@ async function inspectJourneyUi(page) {
|
||||
runtimePathShowsProviderFields: /provider|runnerKind|protocol|implementationType|providerTrace\.command|providerTrace\.terminalStatus/u.test(runtimePathDialogText),
|
||||
runtimePathSummaryStaysCompact: !/providerTrace\.command|providerTrace\.terminalStatus|protocol/u.test(runtimePathText),
|
||||
runtimePathFallbackNotFull: !/(OpenAI text fallback|source-fixture|SOURCE)[\s\S]{0,120}(真实 runner|repo-owned Codex app-server stdio)/u.test(runtimePathText),
|
||||
runtimePathMcpNotFull: !/(MCP|codex-mcp-stdio-runner|mcp-jsonrpc-stdio)/u.test(runtimePathText) ||
|
||||
(/DEGRADED:MCP|MCP\/其他 runner/u.test(runtimePathText) && !/使用 Responses wire API|wire_api=responses|真实 runner:Codex app-server stdio/u.test(runtimePathText)),
|
||||
noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0,
|
||||
attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test(
|
||||
tracePanels
|
||||
@@ -4609,15 +4607,6 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchSlowBlockerFixtureSmoke(options = {}) {
|
||||
return runLocalAgentFixtureSmoke({
|
||||
mode: "local-agent-slow-blocker-fixture-browser",
|
||||
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
|
||||
agentFixtureMode: "slow-blocker",
|
||||
expectTimeout: false
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
|
||||
let chromium;
|
||||
try {
|
||||
@@ -4952,7 +4941,6 @@ async function runLocalAgentFixtureSmoke({
|
||||
mode,
|
||||
responseDelayMs = 0,
|
||||
timeoutConfigMs = null,
|
||||
agentFixtureMode = "success",
|
||||
expectTimeout = false
|
||||
} = {}) {
|
||||
let chromium;
|
||||
@@ -4992,7 +4980,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
};
|
||||
}
|
||||
|
||||
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode });
|
||||
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode: "success" });
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
@@ -5052,22 +5040,17 @@ async function runLocalAgentFixtureSmoke({
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
} else {
|
||||
const prompts = agentFixtureMode === "slow-blocker"
|
||||
? [codeAgentE2ePrompts[1]]
|
||||
: codeAgentE2ePrompts;
|
||||
for (const prompt of prompts) {
|
||||
for (const prompt of codeAgentE2ePrompts) {
|
||||
promptResults.push(await runCodeAgentPromptJourney(page, prompt, { sourceFixture: true }));
|
||||
if (agentFixtureMode === "success") {
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
|
||||
null,
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
}
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
|
||||
null,
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
const ui = await inspectJourneyUi(page);
|
||||
const mobilePending = !expectTimeout && agentFixtureMode === "success"
|
||||
const mobilePending = !expectTimeout
|
||||
? await inspectLocalAgentPendingViewport(browser, server.url, {
|
||||
width: 390,
|
||||
height: 844,
|
||||
@@ -5087,30 +5070,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
ui.timeoutActionPanelContained &&
|
||||
!ui.completedMessageVisible
|
||||
)
|
||||
: agentFixtureMode === "slow-blocker"
|
||||
? (
|
||||
promptResults.length === 1 &&
|
||||
promptResults[0]?.status === "blocked" &&
|
||||
promptResults[0]?.classification?.blocker === "runner-blocked" &&
|
||||
promptResults[0]?.legacyWindow?.permanentFailureAround4500ms === false &&
|
||||
promptResults[0]?.legacyWindow?.pendingChineseVisible === true &&
|
||||
promptResults[0]?.legacyWindow?.pendingTraceShowsEvents === true &&
|
||||
ui.agentChatStatus === "Runner 受阻" &&
|
||||
ui.failedMessageVisible &&
|
||||
ui.failedMessageHasTrace &&
|
||||
ui.retryInputPreserved &&
|
||||
ui.failedRetryActionVisible &&
|
||||
ui.failedTraceActionVisible &&
|
||||
ui.failedActionPanelContained &&
|
||||
ui.runtimePathVisible &&
|
||||
ui.runtimePathCompactSummaryVisible &&
|
||||
ui.runtimePathUsesCompactDialog &&
|
||||
ui.runtimePathShowsProviderFields &&
|
||||
ui.runtimePathSummaryStaysCompact &&
|
||||
ui.runtimePathMcpNotFull &&
|
||||
!ui.completedMessageVisible
|
||||
)
|
||||
: (
|
||||
: (
|
||||
promptResults.every((result) =>
|
||||
result.status === "pass" &&
|
||||
result.response?.status === "completed" &&
|
||||
@@ -5170,8 +5130,6 @@ async function runLocalAgentFixtureSmoke({
|
||||
status: pass ? "pass" : "blocked",
|
||||
summary: expectTimeout
|
||||
? "Local SOURCE fixture proves a bounded Code Agent timeout stays user-visible/BLOCKED in the UI, preserves trace context, and keeps the user's input for retry."
|
||||
: agentFixtureMode === "slow-blocker"
|
||||
? "Local SOURCE fixture returns a delayed structured blocker and the conversation shows the Chinese reason with trace evidence while preserving the input."
|
||||
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
|
||||
observations: expectTimeout ? {
|
||||
response: responseSummary,
|
||||
@@ -5185,7 +5143,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
}
|
||||
}
|
||||
];
|
||||
if (!expectTimeout && agentFixtureMode === "success") {
|
||||
if (!expectTimeout) {
|
||||
checks.push({
|
||||
id: "local-agent-fixture-no-4500ms-permanent-failure",
|
||||
status: promptResults.every((result) =>
|
||||
@@ -5221,18 +5179,6 @@ async function runLocalAgentFixtureSmoke({
|
||||
summary: "390x844 mobile view shows Chinese Code Agent pending status, trace/session context, and no pending-card text overflow before the delayed response arrives.",
|
||||
observations: mobilePending
|
||||
});
|
||||
} else if (!expectTimeout && agentFixtureMode === "slow-blocker") {
|
||||
checks.push({
|
||||
id: "local-agent-fixture-slow-structured-blocker",
|
||||
status: pass ? "pass" : "blocked",
|
||||
summary: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of a generic 后端失败 state, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
|
||||
observations: {
|
||||
legacyFailureWindowMs,
|
||||
fixtureDelayMs: responseDelayMs,
|
||||
prompt: promptResults[0] ?? null,
|
||||
ui
|
||||
}
|
||||
});
|
||||
}
|
||||
const blockers = checks
|
||||
.filter((check) => check.status !== "pass")
|
||||
@@ -5258,7 +5204,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
safety: {
|
||||
...staticSafety(),
|
||||
localFixtureOnly: true,
|
||||
agentFixtureMode,
|
||||
agentFixtureMode: "success",
|
||||
fixtureTrustBoundary: "SOURCE fixture replies render as SOURCE 回复 and never as DEV-LIVE 回复.",
|
||||
legacyFailureWindowMs,
|
||||
fixtureDelayMs: responseDelayMs,
|
||||
@@ -6177,8 +6123,8 @@ function startLocalAgentTrace(agentTraceStore, { traceId, conversationId, delayM
|
||||
function localAgentTraceSnapshot({ traceId, conversationId = null, startedAt = Date.now(), events = [], lastEvent = null }) {
|
||||
return {
|
||||
traceId,
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
sessionId: conversationId,
|
||||
sessionStatus: events.length > 0 ? "running" : "pending",
|
||||
status: events.length > 0 ? "running" : "pending",
|
||||
@@ -6877,145 +6823,6 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
if (options.agentFixtureMode === "slow-blocker") {
|
||||
jsonResponse(response, 200, {
|
||||
conversationId,
|
||||
sessionId: conversationId,
|
||||
messageId,
|
||||
status: "failed",
|
||||
sourceKind: "SOURCE",
|
||||
evidenceLevel: "SOURCE",
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
traceId,
|
||||
provider: "codex-stdio",
|
||||
model: "codex-stdio",
|
||||
backend: "local-source-fixture/codex-stdio-slow-structured-blocker",
|
||||
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "read-only",
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
writeCapable: true,
|
||||
codexStdio: true,
|
||||
longLivedSession: true,
|
||||
durableSession: true
|
||||
},
|
||||
capabilityLevel: "blocked",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
session: {
|
||||
sessionId: conversationId,
|
||||
conversationId,
|
||||
status: "idle",
|
||||
turn: 1,
|
||||
lastTraceId: traceId
|
||||
},
|
||||
sessionReuse: {
|
||||
conversationId,
|
||||
sessionId: conversationId,
|
||||
mapped: true,
|
||||
reused: true,
|
||||
turn: 1,
|
||||
status: "idle"
|
||||
},
|
||||
error: {
|
||||
code: "skills_unavailable",
|
||||
category: "runner_blocked",
|
||||
layer: "skill-cli",
|
||||
message: "SOURCE slow structured blocker: no readable SKILL.md files were found.",
|
||||
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
|
||||
retryable: true,
|
||||
blocker: {
|
||||
code: "skills_unavailable",
|
||||
category: "runner_blocked",
|
||||
layer: "skill-cli",
|
||||
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
|
||||
retryable: true
|
||||
}
|
||||
},
|
||||
blocker: {
|
||||
code: "skills_unavailable",
|
||||
category: "runner_blocked",
|
||||
layer: "skill-cli",
|
||||
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
|
||||
retryable: true
|
||||
},
|
||||
blockers: [
|
||||
{
|
||||
code: "skills_unavailable",
|
||||
category: "runner_blocked",
|
||||
layer: "skill-cli",
|
||||
retryable: true
|
||||
}
|
||||
],
|
||||
toolCalls: [
|
||||
{
|
||||
name: "skills.discover",
|
||||
status: "blocked",
|
||||
exitCode: 2,
|
||||
traceId,
|
||||
stderrSummary: "skills_unavailable",
|
||||
blocker: {
|
||||
code: "skills_unavailable",
|
||||
category: "runner_blocked",
|
||||
layer: "skill-cli"
|
||||
}
|
||||
}
|
||||
],
|
||||
skills: {
|
||||
status: "blocked",
|
||||
items: [],
|
||||
count: 0,
|
||||
totalCount: 0,
|
||||
blockers: [
|
||||
{
|
||||
code: "skills_unavailable",
|
||||
category: "runner_blocked",
|
||||
layer: "skill-cli"
|
||||
}
|
||||
]
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId,
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
status: "blocked",
|
||||
events: [
|
||||
{ seq: 1, traceId, type: "session", stage: "created", status: "completed", label: "session:created" },
|
||||
{ seq: 2, traceId, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent" },
|
||||
{ seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
|
||||
],
|
||||
lastEvent: { seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
|
||||
},
|
||||
conversationFacts: {
|
||||
conversationId,
|
||||
sessionId: conversationId,
|
||||
sessionStatus: "idle",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
turnCount: 1,
|
||||
latestTraceId: traceId,
|
||||
traceIds: [traceId],
|
||||
latestSkills: {
|
||||
status: "blocked",
|
||||
count: 0,
|
||||
totalCount: 0,
|
||||
names: [],
|
||||
blockers: ["skills_unavailable"]
|
||||
},
|
||||
recentToolCalls: [{ name: "skills.discover", status: "blocked" }],
|
||||
facts: [],
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
},
|
||||
providerTrace: {
|
||||
source: "SOURCE-local-browser-fixture",
|
||||
sourceKind: "SOURCE",
|
||||
responseId: "rsp_source_fixture_slow_blocker",
|
||||
redacted: true
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const fixtureSkills = isSkillListPrompt
|
||||
? {
|
||||
status: "ready",
|
||||
|
||||
@@ -301,7 +301,7 @@ export function classifyCodeAgentSkillPath(payload, { httpStatus = null, httpOk
|
||||
);
|
||||
const runnerKind = payload?.runner?.kind ?? null;
|
||||
const controlledSessionRunner =
|
||||
runnerKind === "codex-mcp-stdio-runner" ||
|
||||
runnerKind === "codex-app-server-stdio-runner" ||
|
||||
runnerKind === "hwlab-readonly-runner";
|
||||
const missing = [];
|
||||
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
|
||||
@@ -507,7 +507,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
|
||||
const routeEvidenceReady = route === HWLAB_M3_IO_API_ROUTE || route === M3_IO_CONTROL_ROUTE;
|
||||
const routePass = httpOk === true &&
|
||||
payload?.status === "completed" &&
|
||||
payload?.provider === "hwlab-skill-cli" &&
|
||||
payload?.provider === "legacy-skill-cli" &&
|
||||
skillTool &&
|
||||
["completed", "blocked"].includes(skillTool.status) &&
|
||||
routeEvidenceReady &&
|
||||
@@ -522,7 +522,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
|
||||
const missing = [];
|
||||
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
|
||||
if (payload?.status !== "completed") missing.push("payload.status=completed");
|
||||
if (payload?.provider !== "hwlab-skill-cli") missing.push("provider=hwlab-skill-cli");
|
||||
if (payload?.provider !== "legacy-skill-cli") missing.push("provider=legacy-skill-cli");
|
||||
if (!skillTool) missing.push(`${HWLAB_M3_IO_SKILL_NAME} toolCall`);
|
||||
if (!routeEvidenceReady) missing.push(`route=${HWLAB_M3_IO_API_ROUTE}`);
|
||||
if (method !== "POST") missing.push("method=POST");
|
||||
|
||||
@@ -142,12 +142,12 @@ test("Code Agent Codex stdio skill discovery passes with completed toolCalls and
|
||||
traceId: "trc_test",
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio-runner",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
session: {
|
||||
sessionId: "ses_test",
|
||||
status: "idle",
|
||||
@@ -161,17 +161,17 @@ test("Code Agent Codex stdio skill discovery passes with completed toolCalls and
|
||||
turn: 1
|
||||
},
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true
|
||||
},
|
||||
runnerTrace: {
|
||||
runnerKind: "codex-mcp-stdio-runner"
|
||||
runnerKind: "codex-app-server-stdio-runner"
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
@@ -286,139 +286,6 @@ test("Code Agent fallback-only turns cannot satisfy session checks", () => {
|
||||
assert.ok(result.observations.blockers.includes("fallback-only"));
|
||||
});
|
||||
|
||||
test("Skill CLI M3 route passes with operation/audit/evidence ids and no direct path", () => {
|
||||
const result = classifySkillCliM3Route(skillCliPayload(), { httpStatus: 200, httpOk: true });
|
||||
|
||||
assert.equal(result.status, "pass");
|
||||
assert.equal(result.observations.route, "/v1/m3/io");
|
||||
assert.equal(result.observations.method, "POST");
|
||||
assert.equal(result.observations.idsPresent, true);
|
||||
assert.equal(result.observations.accepted, true);
|
||||
assert.equal(result.observations.controlReady, true);
|
||||
assert.equal(result.observations.readback.value, true);
|
||||
assert.equal(result.observations.safety.directGatewayCalls, false);
|
||||
assert.equal(result.observations.safety.fallbackUsed, false);
|
||||
});
|
||||
|
||||
test("Skill CLI M3 route passes with structured blocker and no direct path", () => {
|
||||
const payload = skillCliPayload({
|
||||
accepted: false,
|
||||
status: "blocked",
|
||||
operationId: null,
|
||||
auditId: null,
|
||||
evidenceId: null,
|
||||
blocker: {
|
||||
code: "hwlab_api_unavailable",
|
||||
message: "HWLAB API unavailable"
|
||||
}
|
||||
});
|
||||
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
|
||||
|
||||
assert.equal(result.status, "pass");
|
||||
assert.equal(result.observations.structuredBlocker, true);
|
||||
assert.equal(result.observations.idsPresent, false);
|
||||
});
|
||||
|
||||
test("Skill CLI M3 route blocks missing route and direct gateway evidence", () => {
|
||||
const payload = skillCliPayload({
|
||||
route: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101/invoke",
|
||||
directGatewayCalls: true
|
||||
});
|
||||
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
|
||||
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.match(result.summary, /route=\/v1\/m3\/io/u);
|
||||
assert.equal(result.observations.safety.directGatewayCalls, true);
|
||||
});
|
||||
|
||||
test("Skill CLI M3 route requires explicit POST method evidence", () => {
|
||||
const result = classifySkillCliM3Route(skillCliPayload({ method: null }), { httpStatus: 200, httpOk: true });
|
||||
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.match(result.summary, /method=POST/u);
|
||||
assert.ok(result.observations.missing.includes("method=POST"));
|
||||
});
|
||||
|
||||
test("RPT-004 live report defaults to read-only M3 route checks and records no live mutation", async () => {
|
||||
const report = await buildRpt004Report({
|
||||
...parseArgs([
|
||||
"--live",
|
||||
"--url",
|
||||
"http://74.48.78.17:16666/",
|
||||
"--api-url",
|
||||
"http://74.48.78.17:16667/",
|
||||
"--expected-commit",
|
||||
fixtureCommit
|
||||
]),
|
||||
writeReport: false
|
||||
}, {
|
||||
repoRoot: "/fixture-rpt004",
|
||||
fsJson: fixtureArtifactJson(),
|
||||
now: () => "2026-05-23T00:00:00.000Z",
|
||||
httpGetJson: async (url) => ({ ok: true, status: 200, json: healthPayload(url) }),
|
||||
httpGetText: async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
contentType: "text/html; charset=utf-8",
|
||||
body: workbenchHtml()
|
||||
}),
|
||||
layoutRunner: async () => ({ status: "pass", summary: "layout ok", artifacts: { screenshots: [] } }),
|
||||
codeAgentRunner: async ({ turn, conversationId, sessionId }) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: codexSessionPayload({
|
||||
conversationId,
|
||||
sessionId,
|
||||
toolName: turn.kind === "skills" ? "skills.discover" : "pwd",
|
||||
turn: turn.kind === "pwd" ? 1 : turn.kind === "skills" ? 2 : 3,
|
||||
reused: turn.kind !== "pwd",
|
||||
skills: turn.kind === "skills" ? {
|
||||
status: "ready",
|
||||
items: [{
|
||||
name: "hwlab-agent-runtime",
|
||||
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
|
||||
}],
|
||||
count: 1
|
||||
} : undefined,
|
||||
reply: turn.kind === "context-pwd"
|
||||
? "前文第一轮你问的是 pwd;当前工作目录仍是 /workspace/hwlab。"
|
||||
: "当前工作目录是 /workspace/hwlab。"
|
||||
})
|
||||
}),
|
||||
skillCliRunner: async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: skillCliPayload()
|
||||
})
|
||||
});
|
||||
|
||||
assert.equal(report.status, "pass");
|
||||
assert.equal(report.liveMutationUsed, false);
|
||||
assert.equal(report.safety.liveMutationUsed, false);
|
||||
assert.equal(report.commanderSummary.allowedLiveMutationUsed, false);
|
||||
assert.equal(report.commanderSummary.sessionResult.status, "pass");
|
||||
assert.equal(report.commanderSummary.skillCliResult.status, "pass");
|
||||
assert.equal(report.commanderSummary.m3RouteResult.route, "/v1/m3/io");
|
||||
assert.equal(report.commanderSummary.m3RouteResult.method, "POST");
|
||||
assert.equal(report.commanderSummary.m3RouteResult.accepted, true);
|
||||
assert.equal(report.commanderSummary.m3RouteResult.controlReady, true);
|
||||
assert.equal(report.commanderSummary.m3RouteResult.readback.value, true);
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.visible, true);
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.route, "/v1/m3/io");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.method, "POST");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.operationId, "op_rpt004_skill_m3");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.traceId, "trc_rpt004_skill");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.auditId, "aud_rpt004_skill_m3");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.evidenceId, "evd_rpt004_skill_m3");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.accepted, true);
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.status, "completed");
|
||||
assert.equal(report.commanderSummary.exactRouteEvidenceContract.readback.value, true);
|
||||
assert.equal(report.commanderSummary.evidenceIds.evidenceId, "evd_rpt004_skill_m3");
|
||||
assert.equal(report.dimensions.skillCliM3Route.status, "pass");
|
||||
assert.equal(report.dimensions.m3TrustedLoop.observations.skippedWriteLoop, true);
|
||||
assert.equal(report.dimensions.durableEvidence.observations.notApplicableInDefaultReadOnlyMode, true);
|
||||
});
|
||||
|
||||
test("RPT-004 live report emits NOT_CURRENT and skips downstream checks", async () => {
|
||||
const report = await buildRpt004Report({
|
||||
...parseArgs([
|
||||
@@ -530,12 +397,12 @@ function codexSessionPayload({
|
||||
traceId: `trc_rpt004_${turn}`,
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio-runner",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
session: {
|
||||
sessionId,
|
||||
status: "idle",
|
||||
@@ -555,20 +422,20 @@ function codexSessionPayload({
|
||||
status: "idle"
|
||||
},
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
session: "codex-mcp-stdio-long-lived",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
session: "codex-app-server-stdio-long-lived",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
codexStdio: true,
|
||||
writeCapable: true,
|
||||
durableSession: true
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId: `trc_rpt004_${turn}`,
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
sessionId,
|
||||
turn,
|
||||
sessionReused: reused,
|
||||
@@ -620,128 +487,6 @@ function openAiFallbackPayload({ reply = "text fallback" } = {}) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function skillCliPayload({
|
||||
route = "/v1/m3/io",
|
||||
method = "POST",
|
||||
accepted = true,
|
||||
status = "succeeded",
|
||||
operationId = "op_rpt004_skill_m3",
|
||||
auditId = "aud_rpt004_skill_m3",
|
||||
evidenceId = "evd_rpt004_skill_m3",
|
||||
blocker = null,
|
||||
directGatewayCalls = false,
|
||||
directBoxCalls = false,
|
||||
directPatchPanelCalls = false
|
||||
} = {}) {
|
||||
return {
|
||||
status: "completed",
|
||||
conversationId: "cnv_rpt004_skill",
|
||||
sessionId: "ses_rpt004_skill",
|
||||
messageId: "msg_rpt004_skill",
|
||||
traceId: "trc_rpt004_skill",
|
||||
provider: "hwlab-skill-cli",
|
||||
model: "controlled-m3-io",
|
||||
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "hwlab-api-route-only",
|
||||
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
|
||||
sessionMode: "controlled-m3-io-skill-cli",
|
||||
implementationType: "skill-cli-hwlab-api-adapter",
|
||||
runner: {
|
||||
kind: "hwlab-m3-io-skill-cli",
|
||||
safety: {
|
||||
directGatewayCallsAllowed: false,
|
||||
directBoxSimuCallsAllowed: false,
|
||||
directPatchPanelCallsAllowed: false
|
||||
}
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId: "trc_rpt004_skill",
|
||||
runnerKind: "hwlab-m3-io-skill-cli",
|
||||
method,
|
||||
route,
|
||||
fallbackUsed: false,
|
||||
directGatewayCalls,
|
||||
directBoxCalls,
|
||||
directPatchPanelCalls
|
||||
},
|
||||
providerTrace: {
|
||||
runnerKind: "hwlab-m3-io-skill-cli",
|
||||
route,
|
||||
method,
|
||||
traceId: "trc_rpt004_skill",
|
||||
operationId,
|
||||
auditId,
|
||||
evidenceId,
|
||||
accepted,
|
||||
controlReady: accepted,
|
||||
fallbackUsed: false,
|
||||
readback: {
|
||||
status: "succeeded",
|
||||
value: true,
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
}
|
||||
},
|
||||
toolCalls: [{
|
||||
name: "hwlab-agent-runtime.m3-io",
|
||||
status: accepted ? "completed" : "blocked",
|
||||
type: "skill-cli",
|
||||
route,
|
||||
method,
|
||||
accepted,
|
||||
controlReady: accepted,
|
||||
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
|
||||
operationId,
|
||||
traceId: "trc_rpt004_skill",
|
||||
auditId,
|
||||
evidenceId,
|
||||
audit: {
|
||||
auditId,
|
||||
status
|
||||
},
|
||||
evidence: {
|
||||
evidenceId,
|
||||
status: accepted ? "blocked" : "blocked",
|
||||
sourceKind: accepted ? "BLOCKED" : "BLOCKED",
|
||||
blocker: blocker?.code ?? "runtime_durable_not_green",
|
||||
writeStatus: "written_non_durable"
|
||||
},
|
||||
durable: {
|
||||
status: "degraded",
|
||||
blocker: blocker?.code ?? "runtime_durable_not_green"
|
||||
},
|
||||
readback: {
|
||||
status: "succeeded",
|
||||
value: true,
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
},
|
||||
blocker,
|
||||
blockers: blocker ? [blocker] : [{
|
||||
code: "runtime_durable_not_green",
|
||||
message: "durable trust is not green"
|
||||
}],
|
||||
directGatewayCalls,
|
||||
directBoxCalls,
|
||||
directPatchPanelCalls,
|
||||
fallbackUsed: false
|
||||
}],
|
||||
skills: {
|
||||
status: "used",
|
||||
items: [{
|
||||
name: "hwlab-agent-runtime.m3-io",
|
||||
route
|
||||
}],
|
||||
blockers: blocker ? [blocker] : []
|
||||
},
|
||||
reply: {
|
||||
content: "M3 IO Skill CLI result"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function healthPayload(url, { revision = fixtureCommit } = {}) {
|
||||
const isWeb = String(url).includes(":16666");
|
||||
return {
|
||||
|
||||
@@ -1,493 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
HWLAB_M3_IO_API_BASE_URL_ENV,
|
||||
HWLAB_M3_IO_CAPABILITY_LEVELS,
|
||||
HWLAB_M3_IO_DEV_SERVICE_BASE_URL,
|
||||
HWLAB_M3_IO_API_ROUTE,
|
||||
HWLAB_M3_STATUS_API_ROUTE,
|
||||
runM3IoSkillCommand,
|
||||
validateCloudApiTarget
|
||||
} from "./src/m3-io-skill-client.mjs";
|
||||
|
||||
test("M3 Skill CLI posts only to HWLAB API /v1/m3/io for DO1 writes", async () => {
|
||||
const calls = [];
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"io",
|
||||
"--action",
|
||||
"do.write",
|
||||
"--value",
|
||||
"false",
|
||||
"--api-base-url",
|
||||
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"--approved",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_write_false",
|
||||
"--request-id",
|
||||
"req_skill_cli_write_false"
|
||||
],
|
||||
{
|
||||
now: () => "2026-05-23T00:08:00.000Z",
|
||||
requestJson: async (url, request) => {
|
||||
calls.push({ url, request });
|
||||
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
|
||||
assert.equal(request.method, "POST");
|
||||
assert.equal(request.body.action, "do.write");
|
||||
assert.equal(request.body.resourceId, "res_boxsimu_1");
|
||||
assert.equal(request.body.boxId, "boxsimu_1");
|
||||
assert.equal(request.body.port, "DO1");
|
||||
assert.equal(request.body.value, false);
|
||||
assert.equal(request.body.approval.approved, true);
|
||||
assert.equal(request.body.approval.policy, "hwlab-api-control-with-approval");
|
||||
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
status: "succeeded",
|
||||
accepted: true,
|
||||
traceId: "trc_skill_cli_write_false",
|
||||
operationId: "op_m3_do_write_cli",
|
||||
auditId: "aud_m3_do_write_cli_succeeded",
|
||||
evidenceId: "evd_m3_do_write_cli_succeeded",
|
||||
auditState: {
|
||||
status: "written_non_durable"
|
||||
},
|
||||
evidenceState: {
|
||||
status: "blocked",
|
||||
sourceKind: "BLOCKED",
|
||||
blocker: "runtime_durable_not_green",
|
||||
writeStatus: "written_non_durable"
|
||||
},
|
||||
durableStatus: {
|
||||
status: "degraded",
|
||||
durable: false,
|
||||
blocker: "runtime_durable_not_green"
|
||||
},
|
||||
result: {
|
||||
value: false,
|
||||
targetReadback: {
|
||||
value: false
|
||||
}
|
||||
},
|
||||
controlPath: {
|
||||
cloudApi: true,
|
||||
gatewaySimu: true,
|
||||
boxSimu: true,
|
||||
patchPanel: true,
|
||||
frontendBypass: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.route, HWLAB_M3_IO_API_ROUTE);
|
||||
assert.equal(result.method, "POST");
|
||||
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
|
||||
assert.equal(result.controlReady, true);
|
||||
assert.equal(result.readiness.status, "ready");
|
||||
assert.equal(result.accepted, true);
|
||||
assert.equal(result.status, "succeeded");
|
||||
assert.equal(result.operationId, "op_m3_do_write_cli");
|
||||
assert.equal(result.approval.approved, true);
|
||||
assert.equal(result.approval.policy, "hwlab-api-control-with-approval");
|
||||
assert.equal(result.auditId, "aud_m3_do_write_cli_succeeded");
|
||||
assert.equal(result.evidenceId, "evd_m3_do_write_cli_succeeded");
|
||||
assert.equal(result.audit.auditId, "aud_m3_do_write_cli_succeeded");
|
||||
assert.equal(result.evidence.evidenceId, "evd_m3_do_write_cli_succeeded");
|
||||
assert.equal(result.readback.value, false);
|
||||
assert.equal(result.durable.blocker, "runtime_durable_not_green");
|
||||
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
|
||||
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
|
||||
assert.equal(result.safety.directGatewayCalls, false);
|
||||
assert.equal(result.safety.directBoxCalls, false);
|
||||
assert.equal(result.safety.directPatchPanelCalls, false);
|
||||
assert.equal(result.safety.fallbackUsed, false);
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test("M3 Skill CLI rejects direct gateway, box, and patch-panel targets before request", async () => {
|
||||
for (const url of [
|
||||
"http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101",
|
||||
"http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201",
|
||||
"http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301"
|
||||
]) {
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"io",
|
||||
"--action",
|
||||
"di.read",
|
||||
"--api-base-url",
|
||||
url,
|
||||
"--trace-id",
|
||||
"trc_skill_cli_direct_blocked"
|
||||
],
|
||||
{
|
||||
requestJson: async () => {
|
||||
throw new Error("direct hardware target must be blocked before network call");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.method, "POST");
|
||||
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||||
assert.equal(result.controlReady, false);
|
||||
assert.equal(result.readiness.status, "blocked");
|
||||
assert.equal(result.accepted, false);
|
||||
assert.equal(result.blocker.code, "direct_hardware_target_blocked");
|
||||
assert.equal(result.capabilityBlocker.code, "direct_hardware_target_blocked");
|
||||
assert.equal(result.operationId, null);
|
||||
assert.equal(result.safety.directGatewayCalls, false);
|
||||
assert.equal(result.safety.directBoxCalls, false);
|
||||
assert.equal(result.safety.directPatchPanelCalls, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("M3 Skill CLI validates exact API route contract", () => {
|
||||
const invalid = validateCloudApiTarget({
|
||||
url: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/rpc/m3.io.di.read"
|
||||
});
|
||||
assert.equal(invalid.code, "invalid_hwlab_api_route");
|
||||
});
|
||||
|
||||
test("M3 Skill CLI enforces DEV/MVP HWLAB API base allowlist", async () => {
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"status",
|
||||
"--api-base-url",
|
||||
"https://example.com",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_unallowlisted"
|
||||
],
|
||||
{
|
||||
requestJson: async () => {
|
||||
throw new Error("unallowlisted API base must be blocked before network call");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.route, HWLAB_M3_STATUS_API_ROUTE);
|
||||
assert.equal(result.method, "GET");
|
||||
assert.equal(result.blocker.code, "hwlab_api_target_not_allowlisted");
|
||||
assert.equal(result.hwlabApi.redactedUrl, "https://example.com/v1/m3/status");
|
||||
});
|
||||
|
||||
test("M3 Skill CLI requires explicit approval for DO writes before request", async () => {
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"io",
|
||||
"--action",
|
||||
"do.write",
|
||||
"--value",
|
||||
"true",
|
||||
"--api-base-url",
|
||||
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_write_unapproved"
|
||||
],
|
||||
{
|
||||
requestJson: async () => {
|
||||
throw new Error("unapproved DO write must be blocked before network call");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.blocker.code, "approval_required");
|
||||
assert.equal(result.approval.required, true);
|
||||
assert.equal(result.approval.approved, false);
|
||||
assert.equal(result.approval.policy, "hwlab-api-control-with-approval");
|
||||
assert.equal(result.accepted, false);
|
||||
assert.equal(result.operationId, null);
|
||||
assert.equal(result.safety.directGatewayCalls, false);
|
||||
assert.equal(result.safety.fallbackUsed, false);
|
||||
});
|
||||
|
||||
test("M3 Skill CLI reads status only through HWLAB API /v1/m3/status", async () => {
|
||||
const calls = [];
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"status",
|
||||
"--api-base-url",
|
||||
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_status"
|
||||
],
|
||||
{
|
||||
now: () => "2026-05-23T00:08:30.000Z",
|
||||
requestJson: async (url, request) => {
|
||||
calls.push({ url, request });
|
||||
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_STATUS_API_ROUTE}`);
|
||||
assert.equal(request.method, "GET");
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
status: "live",
|
||||
sourceKind: "DEV-LIVE",
|
||||
traceId: "trc_skill_cli_status",
|
||||
gateways: [
|
||||
{ id: "gwsimu_1", online: true },
|
||||
{ id: "gwsimu_2", online: true }
|
||||
],
|
||||
boxes: [
|
||||
{
|
||||
id: "boxsimu_2",
|
||||
resourceId: "res_boxsimu_2",
|
||||
online: true,
|
||||
ports: {
|
||||
DI1: {
|
||||
value: true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
patchPanel: {
|
||||
serviceId: "hwlab-patch-panel",
|
||||
observable: true,
|
||||
connectionActive: true
|
||||
},
|
||||
trust: {
|
||||
operationId: "op_m3_status_latest",
|
||||
traceId: "trc_skill_cli_status",
|
||||
auditId: "aud_m3_status_latest",
|
||||
evidenceId: "evd_m3_status_latest",
|
||||
durableStatus: "green",
|
||||
blocker: null,
|
||||
readStatus: {
|
||||
audit: "read",
|
||||
evidence: "read"
|
||||
},
|
||||
runtime: {
|
||||
durable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.route, HWLAB_M3_STATUS_API_ROUTE);
|
||||
assert.equal(result.method, "GET");
|
||||
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.readonly);
|
||||
assert.equal(result.readonly, true);
|
||||
assert.equal(result.readback.value, true);
|
||||
assert.equal(result.operationId, "op_m3_status_latest");
|
||||
assert.equal(result.auditId, "aud_m3_status_latest");
|
||||
assert.equal(result.evidenceId, "evd_m3_status_latest");
|
||||
assert.equal(result.safety.allowedRoute, HWLAB_M3_STATUS_API_ROUTE);
|
||||
assert.equal(result.safety.directPatchPanelCalls, false);
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test("M3 Skill CLI reports status HWLAB API failures on /v1/m3/status", async () => {
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"status",
|
||||
"--api-base-url",
|
||||
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_status_unavailable"
|
||||
],
|
||||
{
|
||||
requestJson: async (url, request) => {
|
||||
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_STATUS_API_ROUTE}`);
|
||||
assert.equal(request.method, "GET");
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
body: null,
|
||||
error: "service unavailable"
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.route, HWLAB_M3_STATUS_API_ROUTE);
|
||||
assert.equal(result.method, "GET");
|
||||
assert.equal(result.blocker.code, "hwlab_api_unavailable");
|
||||
assert.equal(result.blocker.route, HWLAB_M3_STATUS_API_ROUTE);
|
||||
assert.equal(result.error.code, "hwlab_api_unavailable");
|
||||
assert.equal(result.error.route, HWLAB_M3_STATUS_API_ROUTE);
|
||||
assert.match(result.error.userMessage, /状态未读取/u);
|
||||
});
|
||||
|
||||
test("M3 Skill CLI reports missing API base as safe structured config blocker", async () => {
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"io",
|
||||
"--action",
|
||||
"di.read",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_api_base_missing"
|
||||
],
|
||||
{
|
||||
env: {
|
||||
PATH: process.env.PATH
|
||||
},
|
||||
requestJson: async () => {
|
||||
throw new Error("missing API base must be blocked before request");
|
||||
}
|
||||
}
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.method, "POST");
|
||||
assert.equal(result.accepted, false);
|
||||
assert.equal(result.blocker.code, "skill_cli_api_base_missing");
|
||||
assert.equal(result.blocker.layer, "skill-cli-config");
|
||||
assert.equal(result.blocker.retryable, false);
|
||||
assert.equal(result.error.code, "skill_cli_api_base_missing");
|
||||
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||||
assert.equal(result.accepted, false);
|
||||
assert.equal(result.operationId, null);
|
||||
assert.equal(result.audit.status, "not_written");
|
||||
assert.equal(result.hwlabApi.source, "missing-config");
|
||||
assert.equal(result.hwlabApi.redactedUrl, null);
|
||||
assert.deepEqual(result.error.missingConfig, [
|
||||
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL",
|
||||
"HWLAB_API_BASE_URL",
|
||||
"HWLAB_CLOUD_API_BASE_URL",
|
||||
"contract:hwlab-agent-runtime.m3-io.apiBaseUrl"
|
||||
]);
|
||||
assert.equal(JSON.stringify(result).includes("://"), false);
|
||||
});
|
||||
|
||||
test("M3 Skill CLI reports HWLAB API unavailable as retryable structured blocker", async () => {
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"io",
|
||||
"--action",
|
||||
"di.read",
|
||||
"--api-base-url",
|
||||
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_hwlab_api_unavailable"
|
||||
],
|
||||
{
|
||||
requestJson: async () => ({
|
||||
ok: false,
|
||||
status: 503,
|
||||
body: null,
|
||||
error: "service unavailable"
|
||||
})
|
||||
}
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.blocker.code, "hwlab_api_unavailable");
|
||||
assert.equal(result.blocker.layer, "hwlab-api");
|
||||
assert.equal(result.blocker.retryable, true);
|
||||
assert.equal(result.error.code, "hwlab_api_unavailable");
|
||||
assert.equal(result.error.retryable, true);
|
||||
assert.equal(result.error.route, HWLAB_M3_IO_API_ROUTE);
|
||||
assert.equal(result.error.toolName, "hwlab-agent-runtime.m3-io");
|
||||
});
|
||||
|
||||
test("M3 Skill CLI preserves slow structured blocker and timeout budget", async () => {
|
||||
const startedAt = Date.now();
|
||||
const result = await runM3IoSkillCommand(
|
||||
[
|
||||
"m3",
|
||||
"io",
|
||||
"--action",
|
||||
"di.read",
|
||||
"--api-base-url",
|
||||
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"--approved",
|
||||
"--trace-id",
|
||||
"trc_skill_cli_slow_blocker",
|
||||
"--request-id",
|
||||
"req_skill_cli_slow_blocker",
|
||||
"--timeout-ms",
|
||||
"30000"
|
||||
],
|
||||
{
|
||||
now: () => "2026-05-23T00:09:00.000Z",
|
||||
requestJson: async (url, request) => {
|
||||
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
|
||||
assert.equal(request.timeoutMs, 30000);
|
||||
await delay(4600);
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
status: "blocked",
|
||||
accepted: false,
|
||||
traceId: "trc_skill_cli_slow_blocker",
|
||||
operationId: "op_skill_cli_slow_blocker",
|
||||
auditId: "aud_skill_cli_slow_blocker_failed",
|
||||
evidenceId: "evd_skill_cli_slow_blocker_failed",
|
||||
blocker: {
|
||||
code: "runtime_durable_not_green",
|
||||
message: "runtime durable evidence is not green",
|
||||
zh: "runtime durable evidence 尚未为 green"
|
||||
},
|
||||
blockerClassification: {
|
||||
category: "runtime_durability"
|
||||
},
|
||||
evidenceState: {
|
||||
status: "blocked",
|
||||
sourceKind: "BLOCKED",
|
||||
blocker: "runtime_durable_not_green",
|
||||
writeStatus: "not_written"
|
||||
},
|
||||
durableStatus: {
|
||||
status: "degraded",
|
||||
durable: false,
|
||||
blocker: "runtime_durable_not_green"
|
||||
},
|
||||
controlPath: {
|
||||
cloudApi: true,
|
||||
frontendBypass: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(Date.now() - startedAt >= 4500, true);
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.status, "blocked");
|
||||
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||||
assert.equal(result.traceId, "trc_skill_cli_slow_blocker");
|
||||
assert.equal(result.requestId, "req_skill_cli_slow_blocker");
|
||||
assert.equal(result.operationId, "op_skill_cli_slow_blocker");
|
||||
assert.equal(result.blocker.code, "runtime_durable_not_green");
|
||||
assert.equal(result.blocker.category, "runtime_durability");
|
||||
assert.equal(result.capabilityBlocker.code, "runtime_durable_not_green");
|
||||
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
|
||||
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
|
||||
assert.equal(result.audit.auditId, "aud_skill_cli_slow_blocker_failed");
|
||||
assert.equal(result.evidence.evidenceId, "evd_skill_cli_slow_blocker_failed");
|
||||
assert.equal(result.durable.blocker, "runtime_durable_not_green");
|
||||
assert.equal(result.safety.fallbackUsed, false);
|
||||
assert.equal(result.safety.directGatewayCalls, false);
|
||||
});
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -3675,10 +3675,7 @@ function agentFailurePresentation(error, { result = null, traceId = null } = {})
|
||||
"codex_stdio_failed",
|
||||
"codex_stdio_protocol_blocked",
|
||||
"codex_stdio_empty_response",
|
||||
"codex_stdio_command_probe_failed",
|
||||
"external_network_blocked",
|
||||
"network_tool_unavailable",
|
||||
"network_timeout"
|
||||
"codex_stdio_command_probe_failed"
|
||||
].includes(code)) {
|
||||
return {
|
||||
category: "runner_blocked",
|
||||
|
||||
@@ -5,9 +5,6 @@ const CODEX_APP_SERVER_RUNNER = "codex-app-server-stdio-runner";
|
||||
const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived";
|
||||
const CODEX_APP_SERVER_IMPLEMENTATION = "repo-owned-codex-app-server-stdio-session";
|
||||
const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio";
|
||||
const CODEX_MCP_RUNNER = "codex-mcp-stdio-runner";
|
||||
const CODEX_MCP_SESSION_MODE = "codex-mcp-stdio-long-lived";
|
||||
const CODEX_MCP_IMPLEMENTATION = "repo-owned-codex-mcp-stdio-session";
|
||||
const MISSING_FIELD_VALUE = "字段缺失:证据不足";
|
||||
const REQUIRED_ATTRIBUTION_KEYS = Object.freeze([
|
||||
"provider",
|
||||
@@ -108,17 +105,6 @@ export function classifyCodeAgentFacts(message) {
|
||||
};
|
||||
}
|
||||
|
||||
if (isCodexMcpPath({ runnerKind, sessionMode, implementationType, protocol: runtimePath?.protocol })) {
|
||||
return {
|
||||
kind: "codex-mcp-or-other-runner",
|
||||
statusLabel: "MCP/其他 runner:非当前完整 Code Agent",
|
||||
tone: "source",
|
||||
summary: "这是 Codex stdio/MCP 或其他 runner 形态;当前完整 Code Agent 口径要求 repo-owned codex app-server --listen stdio:// 和 providerTrace 终态证据。",
|
||||
fullCodeAgent: false,
|
||||
codexSessionGatePass: false
|
||||
};
|
||||
}
|
||||
|
||||
if (runnerKind === CODEX_ONE_SHOT_RUNNER || sessionMode === "ephemeral-one-shot" || implementationType === "codex-cli-one-shot-ephemeral") {
|
||||
return {
|
||||
kind: "stateless-one-shot",
|
||||
@@ -239,7 +225,6 @@ export function codeAgentRuntimePathFromMessage(message) {
|
||||
.filter(([, value]) => !nullableText(value))
|
||||
.map(([key]) => key);
|
||||
const isAppServer = isCodexAppServerPath({ provider, runnerKind, protocol, implementationType, command });
|
||||
const isMcp = isCodexMcpPath({ runnerKind, sessionMode: message.sessionMode, implementationType, protocol });
|
||||
const isFallback = provider === "openai-responses" || runnerKind === TEXT_FALLBACK_RUNNER || message.capabilityLevel === "text-chat-only";
|
||||
const isReadonly = provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || message.sessionMode === READONLY_SESSION_MODE;
|
||||
const terminalStatusOk = terminalStatus === "completed" || terminalStatus === "succeeded";
|
||||
@@ -272,11 +257,6 @@ export function codeAgentRuntimePathFromMessage(message) {
|
||||
label = "DEGRADED:OpenAI text fallback";
|
||||
summary = "这是 OpenAI text fallback/text-chat-only;没有 workspace runner、Codex app-server session 或工具控制能力。";
|
||||
tone = blocked ? "blocked" : "warn";
|
||||
} else if (isMcp) {
|
||||
kind = "mcp-or-other-runner";
|
||||
label = "DEGRADED:MCP/其他 runner";
|
||||
summary = "这是 MCP 或其他 runner 形态,不等同于 repo-owned Codex app-server stdio。";
|
||||
tone = blocked ? "blocked" : "warn";
|
||||
} else if (isReadonly) {
|
||||
kind = "readonly-runner";
|
||||
label = "DEGRADED:read-only/source runner";
|
||||
@@ -386,7 +366,6 @@ function messageAttributionLabel(message, classification) {
|
||||
if (classification.kind === "text-chat-only") return "OpenAI fallback:只是文本回答";
|
||||
if (classification.kind === READONLY_SESSION_MODE) return "read-only-session-tools:只读长会话";
|
||||
if (classification.kind === "codex-stdio-long-lived") return "真实 runner:Codex app-server stdio 长会话";
|
||||
if (classification.kind === "codex-mcp-or-other-runner") return "MCP/其他 runner:非当前完整 Code Agent";
|
||||
if (classification.kind === "stateless-one-shot") return "一次性 runner:非长会话";
|
||||
if (classification.kind === "codex-stdio-provider-trace-missing") return "DEGRADED:providerTrace 缺失";
|
||||
if (classification.kind === "codex-stdio-blocked") return "Codex stdio 受阻:当前不能执行";
|
||||
@@ -426,15 +405,12 @@ function localizedAttributionValue(key, value, message) {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (key === "provider" && normalized === "openai-responses") return `${normalized} / OpenAI fallback:只是文本回答`;
|
||||
if (key === "runnerKind" && normalized === CODEX_APP_SERVER_RUNNER) return `${normalized} / repo-owned Codex app-server stdio`;
|
||||
if (key === "runnerKind" && normalized === CODEX_MCP_RUNNER) return `${normalized} / MCP/其他 runner`;
|
||||
if (key === "runnerKind" && normalized === TEXT_FALLBACK_RUNNER) return `${normalized} / 不是 runner 控制`;
|
||||
if (key === "runnerKind" && normalized === "hwlab-readonly-runner") return `${normalized} / 只读 runner`;
|
||||
if (key === "protocol" && normalized === CODEX_APP_SERVER_PROTOCOL) return `${normalized} / Responses wire API`;
|
||||
if (key === "implementationType" && normalized === CODEX_APP_SERVER_IMPLEMENTATION) return `${normalized} / repo-owned app-server stdio session`;
|
||||
if (key === "implementationType" && normalized === CODEX_MCP_IMPLEMENTATION) return `${normalized} / MCP stdio session`;
|
||||
if (key === "sessionMode" && normalized === READONLY_SESSION_MODE) return `${normalized} / 只读 session registry`;
|
||||
if (key === "sessionMode" && normalized === CODEX_APP_SERVER_SESSION_MODE) return `${normalized} / app-server stdio 长会话`;
|
||||
if (key === "sessionMode" && normalized === CODEX_MCP_SESSION_MODE) return `${normalized} / MCP stdio 长会话`;
|
||||
if (key === "capabilityLevel" && normalized === "text-chat-only") return "text-chat-only / 只是文本回答";
|
||||
if (key === "capabilityLevel" && normalized === "read-only-session-tools") return `${normalized} / 只读工具`;
|
||||
if (key === "toolCalls" && normalized === "none") {
|
||||
@@ -511,15 +487,6 @@ function isCodexAppServerPath({ provider, runnerKind, protocol, implementationTy
|
||||
);
|
||||
}
|
||||
|
||||
function isCodexMcpPath({ runnerKind, sessionMode, implementationType, protocol }) {
|
||||
return (
|
||||
runnerKind === CODEX_MCP_RUNNER ||
|
||||
sessionMode === CODEX_MCP_SESSION_MODE ||
|
||||
implementationType === CODEX_MCP_IMPLEMENTATION ||
|
||||
/mcp/iu.test(String(protocol ?? ""))
|
||||
);
|
||||
}
|
||||
|
||||
function parseToolStdout(stdout) {
|
||||
if (typeof stdout !== "string" || !stdout.trim()) return null;
|
||||
const text = stdout.trim();
|
||||
|
||||
@@ -130,43 +130,6 @@ test("long-lived Codex app-server stdio session fields are shown as full Code Ag
|
||||
assert.match(runtimePath.summary, /Responses wire API/u);
|
||||
});
|
||||
|
||||
test("legacy MCP-shaped Codex stdio is degraded instead of full app-server completion", () => {
|
||||
const message = {
|
||||
status: "completed",
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
codexStdio: true,
|
||||
writeCapable: true
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
},
|
||||
providerTrace: {
|
||||
transport: "stdio",
|
||||
protocol: "mcp-jsonrpc-stdio",
|
||||
command: "codex mcp-server",
|
||||
terminalStatus: "completed"
|
||||
},
|
||||
traceId: "trc_mcp"
|
||||
};
|
||||
const facts = codeAgentFactsFromMessage(message);
|
||||
const runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...message });
|
||||
|
||||
assert.equal(facts.kind, "codex-mcp-or-other-runner");
|
||||
assert.equal(facts.fullCodeAgent, false);
|
||||
assert.equal(runtimePath.kind, "mcp-or-other-runner");
|
||||
assert.equal(runtimePath.label, "DEGRADED:MCP/其他 runner");
|
||||
});
|
||||
|
||||
test("Codex app-server without providerTrace is degraded with structured missing fields", () => {
|
||||
const message = {
|
||||
role: "agent",
|
||||
|
||||
@@ -6,8 +6,8 @@ export const CODE_AGENT_M3_TRUSTED_ROUTE = Object.freeze({
|
||||
toResourceId: "res_boxsimu_2",
|
||||
toPort: "DI1"
|
||||
});
|
||||
export const CODE_AGENT_M3_SKILL_PROVIDER = "hwlab-skill-cli";
|
||||
export const CODE_AGENT_M3_SKILL_RUNNER_KIND = "hwlab-m3-io-skill-cli";
|
||||
export const CODE_AGENT_M3_SKILL_PROVIDER = "legacy-skill-cli";
|
||||
export const CODE_AGENT_M3_SKILL_RUNNER_KIND = "legacy-m3-io-skill-cli";
|
||||
export const CODE_AGENT_M3_SKILL_NAME = "hwlab-agent-runtime.m3-io";
|
||||
export const CODE_AGENT_M3_PATH_LABEL = "Code Agent -> Skill CLI -> HWLAB API";
|
||||
|
||||
|
||||
@@ -71,37 +71,6 @@ 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 legacy MCP-shaped Codex stdio as app-server long-lived session", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
latestMessage: {
|
||||
status: "completed",
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
codexStdio: true
|
||||
},
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
},
|
||||
providerTrace: {
|
||||
protocol: "mcp-jsonrpc-stdio",
|
||||
command: "codex mcp-server",
|
||||
terminalStatus: "completed"
|
||||
},
|
||||
traceId: "trc_mcp"
|
||||
}
|
||||
});
|
||||
|
||||
assert.notEqual(summary.kind, "long-lived-session");
|
||||
assert.notEqual(summary.tone, "ok");
|
||||
assert.equal(summary.runtimePathLabel, "DEGRADED:MCP/其他 runner");
|
||||
});
|
||||
|
||||
test("summarizes Codex app-server stdio thread as reusable long-lived session", () => {
|
||||
const summary = classifyCodeAgentStatusSummary({
|
||||
latestMessage: {
|
||||
@@ -150,34 +119,6 @@ test("summarizes Codex app-server stdio thread as reusable long-lived session",
|
||||
assert.equal(summary.fields.threadId, "thread_app_server");
|
||||
});
|
||||
|
||||
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: {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"m3-readonly": "node scripts/m3-readonly-contract.mjs",
|
||||
"check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs code-agent-m3-evidence.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
|
||||
"check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
|
||||
"build": "node scripts/build.mjs",
|
||||
"layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json",
|
||||
"layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
runDevCloudWorkbenchMobileSmoke,
|
||||
runDevCloudWorkbenchAuthFixtureSmoke,
|
||||
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
|
||||
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke
|
||||
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import { gateSummary } from "../gate-summary.mjs";
|
||||
@@ -100,8 +99,6 @@ const mobileHelpSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.i
|
||||
const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke();
|
||||
const localAgentNo4500Check = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-no-4500ms-permanent-failure");
|
||||
const localAgentMobilePendingCheck = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-mobile-pending-layout");
|
||||
const slowBlockerFixtureSmoke = await runDevCloudWorkbenchSlowBlockerFixtureSmoke();
|
||||
const slowBlockerCheck = slowBlockerFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
|
||||
const quickPromptsFixtureSmoke = await runDevCloudWorkbenchQuickPromptsFixtureSmoke();
|
||||
const quickPromptsFillCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-fill-input");
|
||||
const quickPromptsNoAutosendCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-write-no-autosend");
|
||||
@@ -182,8 +179,6 @@ function runMinimalWebCheck() {
|
||||
assert.equal(localAgentFixtureSmoke.devLive, false);
|
||||
assert.equal(localAgentNo4500Check?.status, "pass", "local fixture must prove no 4500ms permanent failure");
|
||||
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local fixture must preserve mobile pending layout");
|
||||
assert.equal(slowBlockerFixtureSmoke.status, "pass", JSON.stringify(slowBlockerFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(slowBlockerCheck?.status, "pass", "slow blocker fixture must stay actionable");
|
||||
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(quickPromptsFillCheck?.status, "pass", "quick prompts must fill input");
|
||||
assert.equal(quickPromptsNoAutosendCheck?.status, "pass", "write quick prompts must not auto-send");
|
||||
@@ -236,10 +231,6 @@ assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(localAgentFixtureSmoke.devLive, false);
|
||||
assert.equal(localAgentNo4500Check?.status, "pass", "local Code Agent fixture must prove the 4500ms browser timeout path is not permanent");
|
||||
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local Code Agent fixture must keep mobile pending state visible with trace/session context");
|
||||
assert.equal(slowBlockerFixtureSmoke.status, "pass", JSON.stringify(slowBlockerFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(slowBlockerFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(slowBlockerFixtureSmoke.devLive, false);
|
||||
assert.equal(slowBlockerCheck?.status, "pass", "slow structured blocker fixture must show Chinese reason, trace, retry input, and no 4500ms permanent failure");
|
||||
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
|
||||
assert.equal(quickPromptsFixtureSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(quickPromptsFixtureSmoke.devLive, false);
|
||||
|
||||
@@ -105,7 +105,7 @@ const codexStdioRest = Object.freeze({
|
||||
status: "codex-stdio-feasible",
|
||||
ready: true,
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
agentKind: "codex-stdio-feasible",
|
||||
codexStdioFeasibility: {
|
||||
@@ -824,14 +824,14 @@ test("classifies Codex stdio session facts as pass even when unrelated runtime d
|
||||
status: "ready",
|
||||
ready: true,
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
mode: "codex-stdio"
|
||||
},
|
||||
sessionRunner: {
|
||||
status: "codex_stdio_ready",
|
||||
ready: true,
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
mode: "codex-mcp-stdio-long-lived",
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
mode: "codex-app-server-stdio-long-lived",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
longLivedSession: true,
|
||||
durableSession: true,
|
||||
@@ -855,25 +855,19 @@ test("classifies Codex stdio session facts as pass even when unrelated runtime d
|
||||
status: "codex-stdio-feasible",
|
||||
ready: true,
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
runner: {
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
kind: "codex-app-server-stdio-runner",
|
||||
sessionMode: "codex-app-server-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"
|
||||
],
|
||||
runnerLimitations: ["secret-values-redacted"],
|
||||
longLivedSessionGate: {
|
||||
status: "pass",
|
||||
pass: true
|
||||
|
||||
Reference in New Issue
Block a user