"""Describe recorded provenance without turning a source into a verified assertion.""" import hashlib from uuid import UUID import json SOURCE_KINDS = {"android", "computer", "terminal", "api", "file", "other"} def android_action_source(device_id: object) -> dict | None: """Stamp a native action at dispatch, using its actual selected device.""" if not isinstance(device_id, str) or not device_id or device_id == "computer:local": return None return {"kind": "android", "target_id": device_id, "label": "安卓屏幕"} def _android_screenshot_records(step: dict) -> list[dict]: """Project explicit capture receipts; never infer provenance from tool args.""" records = [] seen = set() for tool in step.get("generic_tools") or []: if not isinstance(tool, dict) or ( tool.get("type"), tool.get("name"), tool.get("status"), ) != ("tool", "android_file", "success"): continue payload = tool.get("payload") receipt = payload.get("result") if isinstance(payload, dict) else None if not isinstance(receipt, dict) or ( receipt.get("ok") is not True or receipt.get("verified") is not True or receipt.get("action") != "screenshot" ): continue serial, path, digest = ( receipt.get("device_serial"), receipt.get("trace_path"), receipt.get("sha256"), ) if not all(isinstance(value, str) and value for value in (serial, path, digest)): continue captured_at = receipt.get("captured_at") captured_at = captured_at if isinstance(captured_at, str) else None key = (serial, path, digest, captured_at) if key in seen: # Nested tool decorators can record the same completed capture twice. continue seen.add(key) records.append( { "source": {"kind": "android", "target_id": serial, "label": "安卓屏幕"}, "trace_id": tool.get("trace_id"), "artifact_path": path, "sha256": digest, "captured_at": captured_at, } ) return records def _step_reference(step: dict) -> dict: value = step.get("step_id") return { "step_id": str(value) if isinstance(value, (str, UUID)) else None, "step_number": step.get("step_number"), } def _represented_capture(action: dict, captures: list[dict]) -> bool: if action.get("action") != "android_file": return False args = action.get("args") or {} if not isinstance(args, dict) or args.get("action") != "screenshot": return False filename = args.get("filename") return any( capture["artifact_path"].replace("\\", "/").rsplit("/", 1)[-1] == filename for capture in captures ) def recorded_sources(steps: list[dict], *, limit: int = 100) -> dict: sources = {} unattributed = [] for step in steps: result = step.get("last_execution_result") execution = result.get("execution") if isinstance(result, dict) else None records = execution if isinstance(execution, list) else step.get("action_taken") records = records if isinstance(records, list) else [records] if records else [] captures = _android_screenshot_records(step) records = [ record for record in records if not isinstance(record, dict) or ( record.get("action") not in {"save_note", "read_note", "list_notes", "report_task_status"} and not _represented_capture(record, captures) ) ] records = [*records, *captures] if not records: # A planning or completion turn with no recorded action is not a # missing action source. Screenshots alone do not supply provenance. continue found = False missing = False for record in records: if not isinstance(record, dict) or not isinstance(record.get("source"), dict): missing = True continue source = record["source"] raw_kind = source.get("kind") kind = raw_kind if isinstance(raw_kind, str) and raw_kind in SOURCE_KINDS else "other" target = source.get("target_id") if not isinstance(target, str) or not target: missing = True continue found = True plugin = str(record.get("plugin_id") or "")[:100] label = str(source.get("label") or "")[:500] key = (plugin, kind, target[:256]) if key not in sources: sources[key] = { "id": hashlib.sha256(json.dumps(key).encode()).hexdigest()[:20], "kind": kind, "plugin_id": plugin, "target_id": target[:256], "label": label, "category": "interface" if kind in {"android", "computer"} else "auxiliary", "references": [], "reference_count": 0, } reference = _step_reference(step) for field in ( "call_id", "source_id", "trace_id", "artifact_path", "sha256", "captured_at", ): if isinstance(record.get(field), str) and record[field]: reference[field] = record[field][: 2048 if field == "artifact_path" else 256] if isinstance(record.get("read_only"), bool): reference["read_only"] = record["read_only"] entry = sources[key] if not entry.get("label_varies") and entry["label"] != label: # The same window can navigate between tabs/pages. Its first # title is not a valid label for all later evidence. entry["label"] = "" entry["label_varies"] = True entry["reference_count"] += 1 if len(entry["references"]) < 8: entry["references"].append(reference) if not found or missing: # A screenshot or a model-authored URL does not supply missing provenance. unattributed.append(_step_reference(step)) values = list(sources.values()) return { "schema_version": 1, "sources": values[:limit], "total_sources": len(values), "sources_omitted": max(0, len(values) - limit), "unattributed_step_count": len(unattributed), "unattributed_references": unattributed[:8], "note": "来源清单仅来自执行记录,不代表断言已验证。界面证据与接口、终端、文件来源分开;界面结果不能单独证明后端持久状态。逐项结论及未验证边界以正式验收依据为准。缺失来源不按网址或业务名称推断执行平台。", }