) => {
+ event.preventDefault();
+ setLocalError("");
+ if (!reviewed || requestJSON.trim() === "") return;
+ try {
+ JSON.parse(requestJSON);
+ } catch (caught) {
+ setLocalError(humanError(caught));
+ return;
+ }
+ start.mutate();
+ };
+
+ const download = async (artifact: UIEvidenceArtifactMetadata) => {
+ setLocalError("");
+ try {
+ const content = await client.downloadUIEvidenceArtifact(artifact.attempt_id, artifact);
+ const objectURL = URL.createObjectURL(content);
+ const anchor = document.createElement("a");
+ anchor.href = objectURL;
+ anchor.download = artifactFilename(artifact);
+ anchor.rel = "noopener";
+ anchor.click();
+ URL.revokeObjectURL(objectURL);
+ } catch (caught) {
+ setLocalError(humanError(caught));
+ }
+ };
+
+ const current = bundle.data?.attempt;
+ const mutationError = start.error || cancel.error;
+ return
+
+
+
+ {t("证据没有授权能力", "Evidence carries no authority")}
+ {t("页面内容与下载产物均不可信;它们不能启动进程、访问凭证或自动判定验证通过。只有精确的 passed 状态显示为通过,not_run 始终保持中性。",
+ "Page content and downloads are untrusted. They cannot start processes, access credentials, or grant a verification pass. Only the exact passed state is successful; not_run remains neutral.")}
+
+
+ {attempts.isLoading &&
}
+ {attempts.isError &&
}
+ {attempts.data?.length === 0 &&
{t("尚未创建 UI 验证 Attempt", "No UI evidence attempt has been created")} }
+
+ {attempts.data && attempts.data.length > 0 &&
+
+ {attempts.data.map((attempt) => setSelectedID(attempt.manifest.attempt_id)}
+ selected={attempt.manifest.attempt_id === activeID} />)}
+
+
+ {bundle.isLoading && }
+ {bundle.isError && }
+ {bundle.data && }
+
+
}
+
+ {current?.status === "running" && client.hasUIEvidence &&
cancel.mutate(current.manifest.attempt_id)} type="button">
+ {cancel.isPending ? :
+ }
+ {t("取消并清理 Attempt", "Cancel and clean up attempt")}
+ }
+
+
+ {t("审阅并启动精确清单", "Review and start an exact manifest")}
+ {t(
+ "模板面向本仓库的 Vite UI。提交前必须逐字段核对 Workspace 相对命令、loopback 端口、fixture、交互步骤、遮罩与失败策略。原始输入仅用于当前请求,不会写入证据清单。",
+ "The template targets this repository's Vite UI. Before submission, review every Workspace-relative command, loopback port, fixture, interaction, mask, and failure rule. Raw typed input is used only for the current request and is not persisted in the evidence manifest.",
+ )}
+ {
+ setRequestJSON(JSON.stringify(templateRequest(), null, 2));
+ setReviewed(false);
+ setLocalError("");
+ }} type="button">
+ {t("载入本仓库模板", "Load repository template")}
+
+ {!client.hasUIEvidence && {t(
+ "当前连接为只读;启动和取消需要 UI evidence、Run execution、命令运行时与受限 CDP 控制能力。",
+ "This connection is read-only. Start and cancel require UI evidence, Run execution, command-runtime, and restricted-CDP control capabilities.",
+ )}
}
+
+ {(localError || mutationError) &&
+ {localError || humanError(mutationError)}
}
+
;
+}
+
+function AttemptButton({ attempt, onSelect, selected }: {
+ attempt: UIEvidenceAttempt;
+ onSelect: () => void;
+ selected: boolean;
+}) {
+ const { t } = useLocale();
+ return
+ {attempt.manifest.route}
+ {shortID(attempt.manifest.attempt_id)} · {formatDate(attempt.created_at)}
+
+ ;
+}
+
+function AttemptDetail({ artifacts, attempt, onDownload, steps }: {
+ artifacts: UIEvidenceArtifactMetadata[];
+ attempt: UIEvidenceAttempt;
+ onDownload: (artifact: UIEvidenceArtifactMetadata) => void;
+ steps: Array<{
+ step_id: string;
+ sequence: number;
+ kind: string;
+ status: string;
+ failure_stage: string;
+ message?: string;
+ completed_at: string;
+ }>;
+}) {
+ const { t } = useLocale();
+ const manifest = attempt.manifest;
+ const cleanupComplete = Object.values(attempt.cleanup).every(Boolean);
+ const sourceLabel = `${shortID(manifest.source.commit)}${manifest.source.dirty ? " + dirty" : ""}`;
+ return <>
+
+
+
{t("源码", "Source")} {sourceLabel}
+
{t("Dirty digest", "Dirty digest")} {manifest.source.dirty_digest}
+
{t("浏览器", "Browser")} {manifest.browser.product} {manifest.browser.version}
+
{t("驱动", "Driver")} {manifest.browser.driver_protocol}
+
URL / route {manifest.url} · {manifest.route}
+
{t("视口", "Viewport")} {manifest.environment.viewport.width} × {manifest.environment.viewport.height} @ {manifest.environment.viewport.dpr}x
+
{t("呈现环境", "Presentation")} {manifest.environment.locale} · {manifest.environment.theme} · {manifest.environment.reduced_motion ? "reduced motion" : "full motion"}
+
Fixture / seed {manifest.fixture.name} · {manifest.fixture.seed}
+
{t("页面状态", "Page state")} {manifest.fixture.page_state}
+
{t("产物", "Artifacts")} {attempt.artifact_count} · {formatBytes(attempt.artifact_bytes)}
+
{t("清理", "Cleanup")} {cleanupComplete ? t("完整", "complete") : t("不完整", "incomplete")}
+
{t("失败阶段", "Failure stage")} {attempt.failure_stage}
+
+ {attempt.failure_message &&
+ {attempt.failure_code} {attempt.failure_message}
}
+
+ {Object.entries(attempt.diagnostics).map(([key, value]) =>
+ {key.replaceAll("_", " ")} {value} )}
+
+ {t("精确源码、命令与清单绑定", "Exact source, recipe, and manifest binding")}
+ {JSON.stringify({ source: manifest.source, build: manifest.build,
+ start: manifest.start, readiness: manifest.readiness, fixture: manifest.fixture,
+ capture: manifest.capture, failure_policy: manifest.failure_policy,
+ authority: manifest.authority, fingerprint: manifest.fingerprint }, null, 2)}
+
+
+ {t("步骤收据", "Step receipts")}
+ {steps.length === 0 ? {t("尚无已执行步骤。", "No step has executed.")}
: steps.map((step) =>
+ {step.sequence}. {step.step_id}
+ {step.kind} · {formatDate(step.completed_at)}
+ {step.failure_stage !== "none" &&
+ {step.failure_stage}{step.message ? ` · ${step.message}` : ""} }
)}
+
+
+ {t("哈希验证的不可信产物", "Hash-verified untrusted artifacts")}
+ {artifacts.length === 0 ? {t("尚无产物。", "No artifacts.")}
: artifacts.map((artifact) =>
+ {artifact.kind}
+ {artifact.sha256}{artifact.mime} · {formatBytes(artifact.bytes)} · {artifact.step_id} · {formatDate(artifact.created_at)} · {artifact.retention_policy} · {artifact.redacted ? t("已脱敏", "redacted") : t("未标记脱敏", "not marked redacted")}
+ void onDownload(artifact)} type="button">
+
)}
+
+ >;
+}
+
+function artifactFilename(artifact: UIEvidenceArtifactMetadata): string {
+ const extension = artifact.mime === "image/png" ? "png" :
+ artifact.mime === "application/json" ? "json" : "txt";
+ const safeID = artifact.id.replace(/[^a-zA-Z0-9._-]/gu, "-");
+ return `untrusted-${artifact.kind}-${safeID}.${extension}`;
+}
+
+function humanError(value: unknown): string {
+ return value instanceof Error ? value.message : String(value || "Unknown error");
+}
diff --git a/web/src/lib/desktop-bridge.test.ts b/web/src/lib/desktop-bridge.test.ts
index 6b4de0da..78131df6 100644
--- a/web/src/lib/desktop-bridge.test.ts
+++ b/web/src/lib/desktop-bridge.test.ts
@@ -43,6 +43,7 @@ const bootstrap = {
skill_installation_enabled: false,
evidence_attachment_enabled: false,
verification_evidence_enabled: false,
+ ui_evidence_control_enabled: false,
embedded_analyzer_execution_enabled: false,
workspace_checkpoint_control_enabled: false,
batch_delivery_control_enabled: false,
diff --git a/web/src/lib/desktop-bridge.ts b/web/src/lib/desktop-bridge.ts
index a14f8b38..f9909de9 100644
--- a/web/src/lib/desktop-bridge.ts
+++ b/web/src/lib/desktop-bridge.ts
@@ -67,6 +67,7 @@ export interface DesktopConnectionBootstrap {
workspace_checkpoint_control_enabled: boolean;
batch_delivery_control_enabled: boolean;
batch_delivery_host_validation_enabled: boolean;
+ ui_evidence_control_enabled: boolean;
user_terminal_enabled: boolean;
agent_terminal_input_default: false;
workspace_open_enabled: boolean;
@@ -704,7 +705,7 @@ function validBootstrap(value: unknown): value is DesktopConnectionBootstrap {
"evidence_attachment_enabled",
"verification_evidence_enabled", "embedded_analyzer_execution_enabled",
"workspace_checkpoint_control_enabled", "batch_delivery_control_enabled",
- "batch_delivery_host_validation_enabled",
+ "batch_delivery_host_validation_enabled", "ui_evidence_control_enabled",
"user_terminal_enabled", "agent_terminal_input_default",
"file_edit_proposal_enabled", "file_edit_review_enabled", "model_control_enabled",
"provider_credential_enabled", "process_execution_enabled",
@@ -756,6 +757,7 @@ function validBootstrap(value: unknown): value is DesktopConnectionBootstrap {
typeof value.workspace_checkpoint_control_enabled === "boolean" &&
typeof value.batch_delivery_control_enabled === "boolean" &&
typeof value.batch_delivery_host_validation_enabled === "boolean" &&
+ typeof value.ui_evidence_control_enabled === "boolean" &&
typeof value.docker_execution_enabled === "boolean" &&
typeof value.user_terminal_enabled === "boolean" &&
value.agent_terminal_input_default === false &&
@@ -777,6 +779,7 @@ function validBootstrap(value: unknown): value is DesktopConnectionBootstrap {
value.evidence_attachment_enabled || value.verification_evidence_enabled ||
value.embedded_analyzer_execution_enabled || value.workspace_checkpoint_control_enabled ||
value.batch_delivery_control_enabled || value.batch_delivery_host_validation_enabled ||
+ value.ui_evidence_control_enabled ||
value.docker_execution_enabled ||
value.user_terminal_enabled) &&
(value.control_token === "" || validToken(value.control_token)) &&
@@ -794,6 +797,9 @@ function validBootstrap(value: unknown): value is DesktopConnectionBootstrap {
value.danger_full_access_enabled)) &&
(!value.full_cdp_debug_enabled ||
(value.browser_cdp_permission_control_enabled && value.debug_maximum_access_enabled)) &&
+ (!value.ui_evidence_control_enabled ||
+ (value.command_runtime_enabled && value.browser_cdp_permission_control_enabled &&
+ value.run_execution_enabled)) &&
value.command_runtime_enabled ===
(value.run_execution_enabled && value.danger_full_access_enabled) &&
value.read_only_default === !(value.control_enabled || value.run_creation_enabled ||
@@ -812,6 +818,7 @@ function validBootstrap(value: unknown): value is DesktopConnectionBootstrap {
value.evidence_attachment_enabled || value.verification_evidence_enabled ||
value.embedded_analyzer_execution_enabled || value.workspace_checkpoint_control_enabled ||
value.batch_delivery_control_enabled || value.batch_delivery_host_validation_enabled ||
+ value.ui_evidence_control_enabled ||
value.docker_execution_enabled ||
value.user_terminal_enabled) &&
value.process_execution_enabled ===
diff --git a/web/src/state/connection.ts b/web/src/state/connection.ts
index f10ba9e5..e6d00091 100644
--- a/web/src/state/connection.ts
+++ b/web/src/state/connection.ts
@@ -39,6 +39,7 @@ interface ConnectionState {
skillInstallationEnabled: boolean;
evidenceAttachmentEnabled: boolean;
verificationEvidenceEnabled: boolean;
+ uiEvidenceControlEnabled: boolean;
embeddedAnalyzerExecutionEnabled: boolean;
dockerExecutionEnabled: boolean;
agentCodeToolsEnabled: boolean;
@@ -90,6 +91,7 @@ export const useConnectionStore = create((set) => ({
skillInstallationEnabled: false,
evidenceAttachmentEnabled: false,
verificationEvidenceEnabled: false,
+ uiEvidenceControlEnabled: false,
embeddedAnalyzerExecutionEnabled: false,
dockerExecutionEnabled: false,
agentCodeToolsEnabled: false,
@@ -134,6 +136,8 @@ export const useConnectionStore = create((set) => ({
evidenceAttachmentEnabled: present && (capabilities.evidenceAttachmentEnabled ?? true),
verificationEvidenceEnabled: present &&
(capabilities.verificationEvidenceEnabled ?? false),
+ uiEvidenceControlEnabled: present &&
+ (capabilities.uiEvidenceControlEnabled ?? false),
embeddedAnalyzerExecutionEnabled: present &&
(capabilities.embeddedAnalyzerExecutionEnabled ?? false),
dockerExecutionEnabled: present &&
@@ -159,6 +163,7 @@ export const useConnectionStore = create((set) => ({
skillInstallationEnabled: false,
evidenceAttachmentEnabled: false,
verificationEvidenceEnabled: false,
+ uiEvidenceControlEnabled: false,
embeddedAnalyzerExecutionEnabled: false,
dockerExecutionEnabled: false,
agentCodeToolsEnabled: false,
diff --git a/web/src/styles.css b/web/src/styles.css
index 56ff1dd4..0fd00ba8 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -270,7 +270,7 @@ code, pre { font-family: var(--prayu-font-mono); }
.danger-button:disabled { cursor: default; opacity: .6; }
.status-badge { max-width: 150px; display: inline-flex; align-items: center; padding: 2px 6px; overflow: hidden; border: 1px solid #cdd5dc; border-radius: 4px; background: #f4f6f7; color: #52606c; font-size: 10px; line-height: 1.3; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; }
-.status-running, .status-active, .status-completed, .status-live { border-color: #9ccdbb; background: #e9f7f1; color: #146c4e; }
+.status-running, .status-active, .status-completed, .status-live, .status-passed { border-color: #9ccdbb; background: #e9f7f1; color: #146c4e; }
.status-failed, .status-cancelled, .status-denied { border-color: #e3b1ac; background: #fff0ee; color: var(--danger); }
.status-critical, .status-high, .status-rejected, .status-aborted { border-color: #e3b1ac; background: #fff0ee; color: var(--danger); }
.status-medium, .status-partial { border-color: #e4c68e; background: #fff7e5; color: #855000; }
@@ -768,6 +768,48 @@ td code { font-size: 11px; }
.operator-action-list code, .evidence-inventory-list code { overflow: hidden; color: var(--muted); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.operator-action-list time, .evidence-inventory-list time { color: var(--muted); font-size: 10px; text-align: right; }
.evidence-inventory-list > div { min-height: 58px; display: grid; grid-template-columns: 22px minmax(180px, 1fr) 150px 34px; align-items: center; gap: 10px; padding: 8px 2px; border-bottom: 1px solid var(--border); }
+.ui-evidence-panel { min-width: 0; }
+.ui-evidence-boundary { display: flex; align-items: flex-start; gap: 9px; margin: 12px 0; padding: 11px 12px; border: 1px solid #d4932c; border-radius: 5px; background: #fff8e9; color: #714800; font-size: 11px; line-height: 1.45; }
+.ui-evidence-boundary > span { display: grid; gap: 2px; }
+.ui-evidence-layout { display: grid; grid-template-columns: minmax(210px, .32fr) minmax(0, 1fr); min-height: 360px; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
+.ui-evidence-attempts { max-height: 620px; overflow: auto; border-right: 1px solid var(--border); background: var(--surface-subtle); }
+.ui-evidence-attempts > button { width: 100%; min-height: 62px; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; gap: 8px; padding: 9px 10px; border: 0; border-bottom: 1px solid var(--border); background: transparent; color: var(--ink); text-align: left; cursor: pointer; }
+.ui-evidence-attempts > button:hover, .ui-evidence-attempts > button.selected { background: var(--accent-soft); }
+.ui-evidence-attempts span, .ui-evidence-detail > header > div, .ui-evidence-artifacts span, .ui-evidence-steps span { min-width: 0; display: grid; gap: 3px; }
+.ui-evidence-attempts strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ui-evidence-attempts small, .ui-evidence-detail small { color: var(--muted); font-size: 10px; }
+.ui-evidence-detail { min-width: 0; padding: 12px 14px; overflow: auto; }
+.ui-evidence-detail > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
+.ui-evidence-detail > header code { overflow-wrap: anywhere; color: var(--muted); font-size: 10px; }
+.ui-evidence-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; }
+.ui-evidence-facts > div { min-width: 0; padding: 8px 5px; border-bottom: 1px solid var(--border); }
+.ui-evidence-facts dt { color: var(--muted); font-size: 10px; }
+.ui-evidence-facts dd { min-width: 0; margin: 3px 0 0; overflow-wrap: anywhere; font-size: 11px; }
+.ui-evidence-facts code, .ui-evidence-artifacts code { overflow-wrap: anywhere; font-size: 9px; }
+.ui-evidence-diagnostics { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
+.ui-evidence-diagnostics span { padding: 4px 6px; border: 1px solid var(--border); border-radius: 4px; color: var(--muted); font-size: 9px; }
+.ui-evidence-diagnostics strong { color: var(--ink); }
+.ui-evidence-failure { display: grid; gap: 3px; margin-top: 10px; padding: 9px 10px; border-left: 3px solid var(--danger); background: #fff0ee; color: var(--danger); font-size: 11px; }
+.ui-evidence-detail details { margin-top: 10px; }
+.ui-evidence-detail details summary { color: var(--muted); cursor: pointer; font-size: 11px; }
+.ui-evidence-detail pre { max-height: 280px; margin: 7px 0 0; padding: 9px; overflow: auto; border: 1px solid var(--border); background: var(--surface-subtle); font: 9px/1.5 "Cascadia Code", Consolas, monospace; }
+.ui-evidence-steps, .ui-evidence-artifacts { margin-top: 14px; }
+.ui-evidence-steps h3, .ui-evidence-artifacts h3 { margin: 0 0 6px; font-size: 12px; }
+.ui-evidence-steps > div, .ui-evidence-artifacts > div { min-height: 48px; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; gap: 8px; padding: 7px 2px; border-bottom: 1px solid var(--border); }
+.ui-evidence-steps > div > small { grid-column: 1 / -1; color: var(--danger); }
+.ui-evidence-artifacts strong { font-size: 11px; }
+.ui-evidence-launch { margin-top: 16px; padding: 11px 12px; border: 1px solid var(--border); border-radius: 6px; }
+.ui-evidence-launch > summary { cursor: pointer; font-size: 12px; font-weight: 600; }
+.ui-evidence-launch > p { color: var(--muted); font-size: 11px; line-height: 1.5; }
+.ui-evidence-launch form { display: grid; gap: 10px; margin-top: 10px; }
+.ui-evidence-launch textarea { width: 100%; min-height: 300px; padding: 10px; resize: vertical; border: 1px solid var(--border-strong); border-radius: 5px; background: var(--surface); color: var(--ink); font: 10px/1.5 "Cascadia Code", Consolas, monospace; }
+.ui-evidence-launch label { display: flex; align-items: flex-start; gap: 8px; color: var(--muted); font-size: 11px; line-height: 1.45; }
+.ui-evidence-launch label input { flex: 0 0 auto; width: 15px; height: 15px; margin: 0; accent-color: var(--accent); }
+@media (max-width: 860px) {
+ .ui-evidence-layout { grid-template-columns: 1fr; }
+ .ui-evidence-attempts { max-height: 220px; border-right: 0; border-bottom: 1px solid var(--border); }
+ .ui-evidence-facts { grid-template-columns: 1fr; }
+}
.command-palette-backdrop { position: fixed; inset: 0; z-index: 40; display: grid; place-items: start center; padding: min(14vh, 110px) 18px 18px; background: rgb(17 27 34 / 42%); }
.command-palette { width: min(620px, 100%); max-height: min(560px, calc(100dvh - 140px)); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border-strong); border-radius: 7px; background: var(--surface); box-shadow: 0 20px 54px rgb(10 18 24 / 24%); }
.command-palette > header { min-height: 52px; display: grid; grid-template-columns: 22px minmax(0, 1fr) 34px; align-items: center; gap: 8px; padding: 8px 10px 8px 14px; border-bottom: 1px solid var(--border); }
@@ -4911,6 +4953,7 @@ samp {
.status-active,
.status-completed,
.status-live,
+.status-passed,
.status-approved,
.status-applied,
.status-instructed,