fix: show code agent completion evidence
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
|
||||
const defaultLiveUrl = "http://74.48.78.17:16666/";
|
||||
const defaultLiveMessage = "请用一句话回复 HWLAB Code Agent readiness。";
|
||||
const trustedLiveProviders = new Set(["openai-responses", "codex-cli"]);
|
||||
const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
@@ -68,6 +70,19 @@ async function runLocalContractSmoke() {
|
||||
assert.equal(sourceReadiness.devLiveReplyPass, false);
|
||||
logOk("local stub completion is not #143 DEV-LIVE pass");
|
||||
|
||||
const echoReadiness = classifyCodeAgentChatReadiness({
|
||||
...completed,
|
||||
provider: "echo-mock",
|
||||
model: "mock-model",
|
||||
backend: "hwlab-cloud-api/echo-mock"
|
||||
}, { realDevLive: true, httpStatus: 200 });
|
||||
assert.equal(echoReadiness.status, "blocked");
|
||||
assert.equal(echoReadiness.level, "BLOCKED/untrusted-completion");
|
||||
assert.equal(echoReadiness.blocker, "untrusted-completion");
|
||||
assert.equal(echoReadiness.devLiveReplyPass, false);
|
||||
assert.match(echoReadiness.reason, /echo\/mock\/stub/u);
|
||||
logOk("echo/mock completion is not real DEV-LIVE pass");
|
||||
|
||||
const failed = await handleCodeAgentChat(
|
||||
{
|
||||
traceId: "trc_code-agent-chat-smoke-failed",
|
||||
@@ -186,6 +201,16 @@ async function runLiveReadinessSmoke(args) {
|
||||
function classifyCodeAgentChatReadiness(payload, { realDevLive = false, httpStatus = null } = {}) {
|
||||
const assistantReply = typeof payload?.reply?.content === "string" ? payload.reply.content.trim() : "";
|
||||
if (payload?.status === "completed" && assistantReply) {
|
||||
const evidence = inspectRealCompletionEvidence(payload);
|
||||
if (!evidence.ok) {
|
||||
return {
|
||||
status: "blocked",
|
||||
level: realDevLive ? "BLOCKED/untrusted-completion" : "SOURCE",
|
||||
blocker: "untrusted-completion",
|
||||
devLiveReplyPass: false,
|
||||
reason: `completed response is missing real provider/model/trace/conversation evidence or looks like echo/mock/stub: ${evidence.reason}`
|
||||
};
|
||||
}
|
||||
if (realDevLive) {
|
||||
return {
|
||||
status: "pass",
|
||||
@@ -224,6 +249,28 @@ function classifyCodeAgentChatReadiness(payload, { realDevLive = false, httpStat
|
||||
};
|
||||
}
|
||||
|
||||
function inspectRealCompletionEvidence(payload) {
|
||||
const provider = String(payload?.provider ?? "").trim().toLowerCase();
|
||||
const backend = String(payload?.backend ?? "").trim().toLowerCase();
|
||||
const missing = [];
|
||||
if (!payload?.conversationId && !payload?.sessionId) missing.push("conversationId");
|
||||
if (!payload?.traceId) missing.push("traceId");
|
||||
if (!payload?.messageId) missing.push("messageId");
|
||||
if (!payload?.provider) missing.push("provider");
|
||||
if (!payload?.model) missing.push("model");
|
||||
if (!payload?.backend) missing.push("backend");
|
||||
if (missing.length > 0) {
|
||||
return { ok: false, reason: `missing ${missing.join(",")}` };
|
||||
}
|
||||
if (!trustedLiveProviders.has(provider)) {
|
||||
return { ok: false, reason: `provider ${payload.provider} is not an accepted real provider` };
|
||||
}
|
||||
if (untrustedProviderPattern.test(`${provider} ${backend}`)) {
|
||||
return { ok: false, reason: `provider/backend looks non-real: ${payload.provider}/${payload.backend}` };
|
||||
}
|
||||
return { ok: true, reason: "provider/model/trace/conversation evidence present" };
|
||||
}
|
||||
|
||||
function summarizePayload(payload) {
|
||||
const assistantReply = typeof payload?.reply?.content === "string" ? payload.reply.content.trim() : "";
|
||||
const error = payload?.error
|
||||
|
||||
@@ -83,6 +83,7 @@ const requiredTrustedRecordTerms = Object.freeze([
|
||||
"audit.event.query + SOURCE 回退",
|
||||
"evidence.record.query + SOURCE 回退",
|
||||
"conversationId、traceId 和 messageId",
|
||||
"provider/model/trace/conversation",
|
||||
"失败原因=",
|
||||
"只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定",
|
||||
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
|
||||
@@ -119,6 +120,20 @@ const workbenchMarkers = Object.freeze([
|
||||
"command-form"
|
||||
]);
|
||||
|
||||
const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"provider",
|
||||
"model",
|
||||
"backend",
|
||||
"conversation",
|
||||
"trace",
|
||||
"message",
|
||||
"providerTrace",
|
||||
"openai-responses",
|
||||
"codex-cli",
|
||||
"echo/mock/stub",
|
||||
"untrusted_completion"
|
||||
]);
|
||||
|
||||
const forbiddenWritePatterns = Object.freeze([
|
||||
/callRpc\(\s*["']hardware\./u,
|
||||
/hardware\.operation\.request/u,
|
||||
@@ -271,6 +286,11 @@ function runStaticSmoke() {
|
||||
evidence: ["BLOCKED 凭证缺口", "provider_unavailable", "OPENAI_API_KEY", "hwlab-code-agent-provider/openai-api-key", "completed -> dev-live guard"]
|
||||
});
|
||||
|
||||
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.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: requiredCodeAgentEvidenceTerms
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "no-hardware-write-api", noForbiddenWriteSurface(source), "Workbench source does not expose hardware write APIs or live mutation commands.", {
|
||||
blocker: "safety_blocker",
|
||||
evidence: forbiddenWritePatterns.map(String)
|
||||
@@ -657,10 +677,14 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
|
||||
/hwlab-code-agent-provider\/openai-api-key/u.test(app) &&
|
||||
/只有真实 completed 回复才标 DEV-LIVE/u.test(app) &&
|
||||
/不能因为只有 conversationId 标为开发实况/u.test(app) &&
|
||||
/Boolean\(message\.provider\)/u.test(app) &&
|
||||
/Boolean\(message\.model\)/u.test(app) &&
|
||||
/Boolean\(message\.backend\)/u.test(app) &&
|
||||
/status === "completed" \? "dev-live"/u.test(app) &&
|
||||
/isTrustedCodeAgentProvider/u.test(app) &&
|
||||
/Boolean\(value\?\.model\)/u.test(app) &&
|
||||
/Boolean\(value\?\.backend\)/u.test(app) &&
|
||||
/Boolean\(value\?\.traceId\)/u.test(app) &&
|
||||
/Boolean\(value\?\.conversationId \|\| value\?\.sessionId\)/u.test(app) &&
|
||||
/isRealCompletedChatMessage/u.test(app) &&
|
||||
/isRealCompletedChatResult/u.test(app) &&
|
||||
/hasRealCodeAgentEvidence/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) &&
|
||||
@@ -668,6 +692,32 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
|
||||
);
|
||||
}
|
||||
|
||||
function hasCodeAgentCompletedEvidenceVisibility({ app }) {
|
||||
const messageEvidenceBody = functionBody(app, "messageEvidenceFields");
|
||||
const realEvidenceBody = functionBody(app, "hasRealCodeAgentEvidence");
|
||||
return (
|
||||
requiredCodeAgentEvidenceTerms.every((term) => app.includes(term)) &&
|
||||
/conversationId:\s*result\.conversationId \|\| result\.sessionId \|\| state\.conversationId/u.test(app) &&
|
||||
/providerTrace:\s*result\.providerTrace/u.test(app) &&
|
||||
/function\s+providerTraceSummary\s*\(/u.test(app) &&
|
||||
/recordField\("provider",\s*message\.provider\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("model",\s*message\.model\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("backend",\s*message\.backend\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("conversation",\s*message\.conversationId\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("trace",\s*message\.traceId\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("message",\s*message\.messageId\)/u.test(messageEvidenceBody) &&
|
||||
/providerTraceSummary\(message\.providerTrace\)/u.test(messageEvidenceBody) &&
|
||||
/isTrustedCodeAgentProvider\(value\?\.provider\)/u.test(realEvidenceBody) &&
|
||||
/TRUSTED_CODE_AGENT_PROVIDERS/u.test(app) &&
|
||||
/UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/u.test(realEvidenceBody) &&
|
||||
/value\?\.conversationId \|\| value\?\.sessionId/u.test(realEvidenceBody) &&
|
||||
/isRealCompletedChatResult\(result\)/u.test(app) &&
|
||||
/realCompleted \? "completed" : "failed"/u.test(app) &&
|
||||
/Code Agent 完成证据不足/u.test(app) &&
|
||||
/本次不会标记为真实完成/u.test(app)
|
||||
);
|
||||
}
|
||||
|
||||
function noForbiddenWriteSurface(source) {
|
||||
return forbiddenWritePatterns.every((pattern) => !pattern.test(source));
|
||||
}
|
||||
@@ -848,7 +898,18 @@ async function fetchText(url) {
|
||||
}
|
||||
|
||||
function liveHtmlLooksLikeWorkbench(html) {
|
||||
return /HWLAB 云工作台/u.test(html) && labelsPresent(html, workbenchMarkers) && !/<body[^>]*>\s*<(?:main|section)[^>]*(?:gate|diagnostics|status|help)/iu.test(html);
|
||||
const workspaceTag = firstTagForDataView(html, "workspace");
|
||||
const gateTag = firstTagForDataView(html, "gate");
|
||||
const helpTag = firstTagForDataView(html, "help");
|
||||
return (
|
||||
/HWLAB 云工作台/u.test(html) &&
|
||||
labelsPresent(html, workbenchMarkers) &&
|
||||
workspaceTag !== null &&
|
||||
!/\bhidden\b/u.test(workspaceTag) &&
|
||||
gateTag !== null &&
|
||||
/\bhidden\b/u.test(gateTag) &&
|
||||
(helpTag === null || /\bhidden\b/u.test(helpTag))
|
||||
);
|
||||
}
|
||||
|
||||
async function inspectLiveDom(url) {
|
||||
@@ -892,7 +953,7 @@ async function inspectLiveDom(url) {
|
||||
dom.htmlOverflow === "hidden" &&
|
||||
dom.workspaceHidden === false &&
|
||||
dom.gateHidden === true &&
|
||||
dom.diagnosticsHidden === true &&
|
||||
(dom.diagnosticsHidden === true || dom.diagnosticsHidden === null) &&
|
||||
dom.outerScrollLocked &&
|
||||
dom.labelsPresent;
|
||||
return {
|
||||
|
||||
+71
-14
@@ -48,6 +48,8 @@ const M3_TRUSTED_ROUTE = Object.freeze({
|
||||
const LIVE_M3_ID_FIELDS = Object.freeze(["operationId", "traceId", "auditId", "evidenceId"]);
|
||||
const LIVE_M3_PASS_STATUSES = Object.freeze(["pass", "passed", "verified", "succeeded", "trusted", "completed"]);
|
||||
const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]);
|
||||
const TRUSTED_CODE_AGENT_PROVIDERS = Object.freeze(["openai-responses", "codex-cli"]);
|
||||
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
|
||||
|
||||
const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view));
|
||||
let rpcSequence = 0;
|
||||
@@ -281,27 +283,37 @@ function initCommandBar() {
|
||||
const result = await sendAgentMessage(value, state.conversationId, traceId);
|
||||
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
|
||||
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
|
||||
const status = result.status === "completed" ? "completed" : "failed";
|
||||
const realCompleted = isRealCompletedChatResult(result);
|
||||
const status = realCompleted ? "completed" : "failed";
|
||||
state.chatMessages[index] = {
|
||||
...pendingMessage,
|
||||
title: result.status === "completed" ? sourceTitle(result) : "Code Agent 返回失败",
|
||||
text: result.status === "completed"
|
||||
title: realCompleted ? sourceTitle(result) : result.status === "completed" ? "Code Agent 完成证据不足" : "Code Agent 返回失败",
|
||||
text: realCompleted
|
||||
? result.reply?.content || "Code Agent 没有返回文本。"
|
||||
: result.status === "completed"
|
||||
? untrustedCompletionMessage(result)
|
||||
: failureMessage(result),
|
||||
status,
|
||||
traceId: result.traceId,
|
||||
conversationId: result.conversationId || result.sessionId || state.conversationId,
|
||||
messageId: result.messageId,
|
||||
provider: result.provider,
|
||||
model: result.model,
|
||||
backend: result.backend,
|
||||
providerTrace: result.providerTrace,
|
||||
updatedAt: result.updatedAt,
|
||||
error: result.error,
|
||||
error: result.error ?? (result.status === "completed" && !realCompleted
|
||||
? {
|
||||
code: "untrusted_completion",
|
||||
message: "completed 回复缺少真实 provider/model/trace/conversation 证据,或 provider 属于 echo/mock/stub。"
|
||||
}
|
||||
: undefined),
|
||||
availability: result.availability
|
||||
};
|
||||
if (result.availability) {
|
||||
state.codeAgentAvailability = result.availability;
|
||||
}
|
||||
if (result.status !== "completed") {
|
||||
if (!realCompleted) {
|
||||
el.commandInput.value = value;
|
||||
}
|
||||
renderAgentChatStatus(status, result);
|
||||
@@ -1320,14 +1332,33 @@ function messageCard(message) {
|
||||
article.append(textSpan(roleLabel(message.role), "message-role"));
|
||||
article.append(textSpan(message.title, "message-title"));
|
||||
article.append(textSpan(message.text, "message-copy"));
|
||||
const meta = [message.provider, message.model, message.backend, message.traceId].filter(Boolean).join(" / ");
|
||||
const messageMeta = [message.messageId, meta].filter(Boolean).join(" / ");
|
||||
const messageMeta = messageEvidenceFields(message).join(" / ");
|
||||
if (messageMeta) {
|
||||
article.append(textSpan(messageMeta, "message-meta"));
|
||||
}
|
||||
return article;
|
||||
}
|
||||
|
||||
function messageEvidenceFields(message) {
|
||||
return [
|
||||
recordField("provider", message.provider),
|
||||
recordField("model", message.model),
|
||||
recordField("backend", message.backend),
|
||||
recordField("conversation", message.conversationId),
|
||||
recordField("trace", message.traceId),
|
||||
recordField("message", message.messageId),
|
||||
providerTraceSummary(message.providerTrace)
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function providerTraceSummary(providerTrace) {
|
||||
if (!providerTrace || typeof providerTrace !== "object") return null;
|
||||
const responseId = providerTrace.responseId ?? providerTrace.id;
|
||||
if (responseId) return recordField("providerTrace", responseId);
|
||||
if (providerTrace.command) return "providerTrace=codex-cli";
|
||||
return null;
|
||||
}
|
||||
|
||||
function statusCard(item) {
|
||||
const article = document.createElement("article");
|
||||
article.className = "status-card";
|
||||
@@ -1461,8 +1492,8 @@ function codeAgentPromptText(availability) {
|
||||
function codeAgentControlSummary(availability) {
|
||||
if (latestCompletedAgentMessage()) {
|
||||
return state.conversationId
|
||||
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口。`
|
||||
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口。";
|
||||
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。`
|
||||
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。";
|
||||
}
|
||||
if (availability?.status === "blocked") {
|
||||
return codeAgentBlockedSummary(availability);
|
||||
@@ -1474,6 +1505,11 @@ function codeAgentBlockedSummary(availability) {
|
||||
return availability?.summary ?? "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。";
|
||||
}
|
||||
|
||||
function untrustedCompletionMessage(result) {
|
||||
const provider = result?.provider ?? "unknown";
|
||||
return `Code Agent 返回 completed,但缺少真实 provider/model/trace/conversation 证据,或 provider=${provider} 属于 echo/mock/stub;本次不会标记为真实完成。`;
|
||||
}
|
||||
|
||||
function codeAgentAvailabilitySummary() {
|
||||
if (state.codeAgentAvailability?.status === "blocked") {
|
||||
return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。";
|
||||
@@ -1487,14 +1523,35 @@ function codeAgentAvailabilitySummary() {
|
||||
function latestCompletedAgentMessage() {
|
||||
return [...state.chatMessages].reverse().find((message) =>
|
||||
message.role === "agent" &&
|
||||
message.status === "completed" &&
|
||||
!message.error &&
|
||||
Boolean(message.provider) &&
|
||||
Boolean(message.model) &&
|
||||
Boolean(message.backend)
|
||||
isRealCompletedChatMessage(message)
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
function isRealCompletedChatResult(result) {
|
||||
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
|
||||
return result?.status === "completed" && assistantReply.length > 0 && hasRealCodeAgentEvidence(result);
|
||||
}
|
||||
|
||||
function isRealCompletedChatMessage(message) {
|
||||
return message?.status === "completed" && !message.error && hasRealCodeAgentEvidence(message);
|
||||
}
|
||||
|
||||
function hasRealCodeAgentEvidence(value) {
|
||||
return (
|
||||
isTrustedCodeAgentProvider(value?.provider) &&
|
||||
Boolean(value?.model) &&
|
||||
Boolean(value?.backend) &&
|
||||
Boolean(value?.traceId) &&
|
||||
Boolean(value?.conversationId || value?.sessionId) &&
|
||||
!UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN.test(`${value?.provider ?? ""} ${value?.backend ?? ""}`)
|
||||
);
|
||||
}
|
||||
|
||||
function isTrustedCodeAgentProvider(provider) {
|
||||
const normalized = String(provider ?? "").trim().toLowerCase();
|
||||
return TRUSTED_CODE_AGENT_PROVIDERS.includes(normalized);
|
||||
}
|
||||
|
||||
function latestChatResult() {
|
||||
return [...state.chatMessages].reverse().find((message) => message.role === "agent") ?? null;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ const requiredTrustedRecordTerms = Object.freeze([
|
||||
"audit.event.query + SOURCE 回退",
|
||||
"evidence.record.query + SOURCE 回退",
|
||||
"conversationId、traceId 和 messageId",
|
||||
"provider/model/trace/conversation",
|
||||
"失败原因=",
|
||||
"只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定",
|
||||
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
|
||||
@@ -283,9 +284,23 @@ assert.match(app, /OPENAI_API_KEY/);
|
||||
assert.match(app, /hwlab-code-agent-provider\/openai-api-key/);
|
||||
assert.match(app, /只有真实 completed 回复才标 DEV-LIVE/);
|
||||
assert.match(app, /不能因为只有 conversationId 标为开发实况/);
|
||||
assert.match(app, /Boolean\(message\.provider\)/);
|
||||
assert.match(app, /Boolean\(message\.model\)/);
|
||||
assert.match(app, /Boolean\(message\.backend\)/);
|
||||
assert.match(app, /TRUSTED_CODE_AGENT_PROVIDERS/);
|
||||
assert.match(app, /UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/);
|
||||
assert.match(app, /isRealCompletedChatResult/);
|
||||
assert.match(app, /isRealCompletedChatMessage/);
|
||||
assert.match(app, /hasRealCodeAgentEvidence/);
|
||||
assert.match(app, /isTrustedCodeAgentProvider\(value\?\.provider\)/);
|
||||
assert.match(app, /Boolean\(value\?\.model\)/);
|
||||
assert.match(app, /Boolean\(value\?\.backend\)/);
|
||||
assert.match(app, /Boolean\(value\?\.traceId\)/);
|
||||
assert.match(app, /Boolean\(value\?\.conversationId \|\| value\?\.sessionId\)/);
|
||||
assert.match(app, /providerTrace:\s*result\.providerTrace/);
|
||||
assert.match(app, /providerTraceSummary\(message\.providerTrace\)/);
|
||||
assert.match(app, /recordField\("conversation", message\.conversationId\)/);
|
||||
assert.match(app, /recordField\("provider", message\.provider\)/);
|
||||
assert.match(app, /Code Agent 完成证据不足/);
|
||||
assert.match(app, /本次不会标记为真实完成/);
|
||||
assert.match(app, /untrusted_completion/);
|
||||
assert.match(app, /status === "completed" \? "dev-live"/);
|
||||
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
|
||||
assert.doesNotMatch(`${html}\n${app}`, /provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu);
|
||||
|
||||
Reference in New Issue
Block a user