mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 07:11:57 +00:00
可验证报告:查询快照只给哈希时照实说明原因,样本页和浏览器检查补「遮罩源上的汇总查询可核对」
核对页对只给哈希的查询快照原来写「含遮罩列,未随文件附带」。现在没碰遮罩列的快照照常附带,只给哈希的是这条查询 用到或可能用到设了遮罩的列(条件里写到的、解析不了的也算),原文改成「这条查询用到或可能用到设了遮罩的列,快照未随 文件附带,只能核对到哈希」;口径卡的说法不变。核对逻辑不变,只是按工件种类挑说法(verifier.js 的 maskedQuery); 内联脚本的 CSP 哈希由 render_html 按脚本现算,不用另改。顺手改正 verifier.js 开头一句过时的注释:口径卡现在会因遮罩 只给哈希,导出时不会出现的是报告文档标着遮罩。 tests/bundle_samples.py 加样本页 masked-source:会员库设了号码遮罩(查询当时记下了、数据源现在也设着),报告引用会员 按地区汇总的查询和选出了号码的那份。check-bundle 断网打开它:汇总快照在 ③ 通过、引用它的两个数字在 ⑤ 通过、点开看得 到汇总的 SQL 和被引用的那一行;选出了号码的快照和引用它的数字无法核对并写明原因;总览「可核对的项全部通过」,页面里 找不到号码。另在这一页上篡改:汇总快照改一个字节 ③ 变红,改它的数字的引用值并重算全部哈希 ⑤ 变红。base 页上的原因 文案跟着改。test_bundle_build 守住样本页的形状。
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
* engine/expressions.py 的 cell_value / same_value;数字怎么显示是服务端渲染好的,这里不重新排版;
|
||||
* ⑥ 运行信息:运行结果、运行类别、出具档位拿封存范围内已核对的事件对照。工作流名、版本、起止时间、节点名称
|
||||
* 绑不进封存,页面上写明它们取自运行记录。
|
||||
* 每项给出通过 / 不通过 / 无法核对和原因。服务端导出时不会出现的状态(被引用的工件不在清单里、口径卡标着遮罩)
|
||||
* 每项给出通过 / 不通过 / 无法核对和原因。服务端导出时不会出现的状态(被引用的工件不在清单里、报告文档标着遮罩)
|
||||
* 一律算不通过:只可能是文件被改过。这些只证明内部一致,真伪要拿封存哈希去系统里对照,页面上写明。
|
||||
*
|
||||
* SHA-256 优先用浏览器的 crypto.subtle;拿不到(非安全上下文、某些浏览器打开本地文件时)用下面的纯 JS 实现,
|
||||
@@ -105,6 +105,9 @@
|
||||
artifactMismatch: '内容与工件 id(哈希)不一致',
|
||||
artifactUndecodable: '内容无法解码',
|
||||
artifactUnparsable: '哈希一致,但内容无法解析',
|
||||
/** 查询快照只给哈希:服务端证明不了这条查询没碰设了遮罩的列(没碰的照常随文件附带) */
|
||||
maskedQuery: '这条查询用到或可能用到设了遮罩的列,快照未随文件附带,只能核对到哈希',
|
||||
/** 口径卡只给哈希:卡上有输入取自遮罩列(卡上记着输入的值和代入式) */
|
||||
masked: '含遮罩列,未随文件附带,只能核对到哈希',
|
||||
maskedMismatch: '导出时服务端的这份文件与工件 id(哈希)不一致,可能被改过;按含遮罩列处理,未随文件附带',
|
||||
maskedUnparsable: '导出时服务端的这份文件无法解析,认不出是否含遮罩列,按含遮罩列处理,未随文件附带',
|
||||
@@ -487,7 +490,8 @@
|
||||
}
|
||||
}
|
||||
} else if (a.masked) {
|
||||
// 查询快照带遮罩列;口径卡上有输入取自遮罩列(卡上记着输入的值和代入式)。两种都只给 id,报告文档不会。
|
||||
// 查询快照:这条查询用到或可能用到设了遮罩的列;口径卡:卡上有输入取自遮罩列(卡上记着输入的值和代入式)。
|
||||
// 两种都只给 id,报告文档不会。
|
||||
// 内容不随文件附带,浏览器算不出它对不对得上:导出时服务端看到自己的文件和 id 对不上(mismatch)、解析不出来
|
||||
// (unparsable)的照实说,对不上的判不通过——不然被改过的文件看起来和正常的遮罩一样,篡改的证据就丢了
|
||||
if (a.kind !== 'query' && a.kind !== 'metric_set') {
|
||||
@@ -497,7 +501,7 @@
|
||||
item.result = FAIL
|
||||
item.reason = T.maskedMismatch
|
||||
} else {
|
||||
item.reason = a.unparsable ? T.maskedUnparsable : T.masked
|
||||
item.reason = a.unparsable ? T.maskedUnparsable : a.kind === 'query' ? T.maskedQuery : T.masked
|
||||
}
|
||||
} else if (a.missing) {
|
||||
item.reason = T.artifactMissing
|
||||
@@ -980,7 +984,7 @@
|
||||
if (snap && typeof snap.sql === 'string' && snap.sql) parts.push(h('p', { class: 'sub' }, T.dSql), h('pre', { class: 'sql' }, snap.sql))
|
||||
const record = snap && Number.isInteger(number.row) && Array.isArray(snap.rows) ? snap.rows[number.row] : undefined
|
||||
if (Array.isArray(record) || isObj(record)) {
|
||||
// 被引用的那一行:快照随文件附带(没有遮罩)才走到这里,整行照原样列出,引用的那一格高亮
|
||||
// 被引用的那一行:快照随文件附带(没碰设了遮罩的列)才走到这里,整行照原样列出,引用的那一格高亮
|
||||
let columns = (Array.isArray(snap.columns) ? snap.columns : []).map(String)
|
||||
if (isObj(record) && !columns.length) columns = Object.keys(record)
|
||||
const cellAt = (c, i) => (isObj(record) ? own(record, c) : record[i])
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
- missing-doc / missing-card:导出时服务端已没有报告文档 / 口径卡的文件
|
||||
- masked-card:口径卡上一个指标的输入取自会员消费的号码列(设了遮罩),卡上记着号码原值:整张卡只给 id,引用它的
|
||||
数字无法核对
|
||||
- masked-source:会员库设了号码遮罩(查询当时记下了,数据源现在也设着)。报告引用会员按地区汇总的查询(没碰遮罩列:
|
||||
快照随文件附带,数字核对得过)和选出了号码的那份会员消费(只给哈希)
|
||||
- nan:运行开始事件和门店流水快照里有 json.dumps 写出的裸 NaN(快照里那一列报告没有引用)
|
||||
- stray:报告自查记下的文档是另一次运行写的
|
||||
"""
|
||||
@@ -39,8 +41,8 @@ RUN = {"id": RUN_ID, "workflow_id": "wf-sample-weekly", "workflow_name": "门店
|
||||
"started_at": "2026-09-07T01:00:00+00:00", "finished_at": "2026-09-07T01:02:03+00:00"}
|
||||
GRAPH = {"nodes": [{"id": n, "type": t, "data": {"label": label}} for n, t, label in [
|
||||
("start", "input", "开始"), ("fetch", "tool", "门店流水"), ("members", "tool", "会员消费"),
|
||||
("card", "metrics", "周报口径"), ("write", "report", "写周报"), ("monthly", "report", "写月报"),
|
||||
("out", "output", "成果")]]}
|
||||
("regions", "tool", "会员地区汇总"), ("card", "metrics", "周报口径"), ("write", "report", "写周报"),
|
||||
("monthly", "report", "写月报"), ("out", "output", "成果")]]}
|
||||
EXPORTED_AT = "2026-10-05T08:00:00+00:00"
|
||||
|
||||
STORES = [["青石店", 1280.5], ["白沙店", "960.00"], ["临川店", 732], ["望江店", 1105.25], ["枫桥店", 418],
|
||||
@@ -48,6 +50,11 @@ STORES = [["青石店", 1280.5], ["白沙店", "960.00"], ["临川店", 732], ["
|
||||
CRM = {"columns": ["name", "phone", "spend"], "column_types": {"name": "text", "phone": "text", "spend": "number"},
|
||||
"rows": [["林一", "000-0001", 320.0], ["周二", "000-0002", 188.5]], "source": "crm",
|
||||
"sql": "SELECT name, phone, spend FROM members ORDER BY id", "truncated": False, "mask_columns": ["phone"]}
|
||||
#: 会员按地区汇总:同一个设了号码遮罩的会员库,SQL 没碰号码列
|
||||
REGIONS = {"columns": ["region", "spend"], "column_types": {"region": "text", "spend": "number"},
|
||||
"rows": [["华东", 320.0], ["华南", 188.5]], "source": "crm",
|
||||
"sql": "SELECT region, SUM(spend) AS spend FROM members GROUP BY region ORDER BY region", "truncated": False,
|
||||
"mask_columns": ["phone"]}
|
||||
CARD = {"kind": "metric_set", "caliber": "周报口径", "caliber_version": "v2",
|
||||
"metrics": [{"id": "gmv", "name": "销售额", "value": 8381.25, "unit": "元", "decimals": 2, "format": "thousands"}]}
|
||||
|
||||
@@ -55,6 +62,9 @@ WEEKLY = ("## 门店周报\n\n本周销售额 [[m:gmv]],*按发生周统计*
|
||||
"**会员消费** [[v:Q2.r0.spend]],另有 42 笔退款待入账。[[see:Q1]]\n\n- 云岭店流水最高\n- 枫桥店流水最低\n\n"
|
||||
"[[table:Q1 cols=store,amount rows=0-7]]")
|
||||
MONTHLY = "## 门店月报\n\n本月销售额 [[m:gmv]],青石店流水 [[v:Q1.r0.amount]]。[[see:Q1]]"
|
||||
#: 会员周报:Q3 是会员按地区汇总(没碰遮罩列),Q2 是选出了号码的会员消费
|
||||
MEMBERS = ("## 会员周报\n\n华东会员消费 [[v:Q3.r0.spend]],华南 [[v:Q3.r1.spend]]。[[see:Q3]]\n\n"
|
||||
"会员林一消费 [[v:Q2.r0.spend]]。")
|
||||
|
||||
|
||||
def stored(obj: Any) -> tuple[str, bytes]:
|
||||
@@ -83,21 +93,29 @@ def masked_card(crm_id: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
class Sample:
|
||||
"""一次虚构的已封存运行:工件(原字节)、证据目录、报告文档、事件。masked=True 时口径卡带着取自遮罩列的输入。"""
|
||||
"""一次虚构的已封存运行:工件(原字节)、证据目录、报告文档、事件。masked=True 时口径卡带着取自遮罩列的输入;
|
||||
regions=True 时会员库另有一次按地区汇总的查询(目录里排在 Q3)。"""
|
||||
|
||||
def __init__(self, *, nan: bool = False, masked: bool = False) -> None:
|
||||
def __init__(self, *, nan: bool = False, masked: bool = False, regions: bool = False) -> None:
|
||||
self.files: dict[str, bytes] = {}
|
||||
self.shop_id = self.put(shop(nan))
|
||||
self.crm_id = self.put(CRM)
|
||||
self.card = masked_card(self.crm_id) if masked else CARD
|
||||
self.card_id = self.put(self.card)
|
||||
self.ledger = [
|
||||
{"kind": "query", "node_id": "fetch", "exec": 1, "artifact": self.shop_id, "tool": "db_query__shop",
|
||||
"source": "shop", "columns": ["store", "amount", "ratio"], "rows": len(STORES), "truncated": False},
|
||||
{"kind": "query", "node_id": "members", "exec": 1, "artifact": self.crm_id, "tool": "db_query__crm",
|
||||
"source": "crm", "columns": CRM["columns"], "rows": len(CRM["rows"]), "truncated": False},
|
||||
{"kind": "metric_set", "node_id": "card", "artifact": self.card_id},
|
||||
]
|
||||
self.entries = {
|
||||
"fetch": {"kind": "query", "node_id": "fetch", "exec": 1, "artifact": self.shop_id,
|
||||
"tool": "db_query__shop", "source": "shop", "columns": ["store", "amount", "ratio"],
|
||||
"rows": len(STORES), "truncated": False},
|
||||
"members": {"kind": "query", "node_id": "members", "exec": 1, "artifact": self.crm_id,
|
||||
"tool": "db_query__crm", "source": "crm", "columns": CRM["columns"], "rows": len(CRM["rows"]),
|
||||
"truncated": False},
|
||||
}
|
||||
if regions:
|
||||
self.entries["regions"] = {"kind": "query", "node_id": "regions", "exec": 1,
|
||||
"artifact": self.put(REGIONS), "tool": "db_query__crm", "source": "crm",
|
||||
"columns": REGIONS["columns"], "rows": len(REGIONS["rows"]), "truncated": False}
|
||||
self.entries["card"] = {"kind": "metric_set", "node_id": "card", "artifact": self.card_id}
|
||||
self.ledger = list(self.entries.values())
|
||||
self.catalog = build_catalog(nodes={"card": self.card}, ledger=self.ledger)
|
||||
|
||||
def put(self, obj: Any) -> str:
|
||||
@@ -131,12 +149,17 @@ class Sample:
|
||||
"preview": json.dumps({"columns": ["store", "amount"], "rows": STORES[:2]},
|
||||
ensure_ascii=False)})
|
||||
add("node.finished", "fetch", {"duration_ms": 14, "attempt": 1, "preview": "{\"rows\": …}",
|
||||
"evidence": [self.ledger[0]]})
|
||||
"evidence": [self.entries["fetch"]]})
|
||||
add("tool.end", "members", {"tool": "db_query__crm", "duration_ms": 9, "query_artifact": self.crm_id,
|
||||
"preview": json.dumps(CRM["rows"][:1], ensure_ascii=False)})
|
||||
add("node.finished", "members", {"duration_ms": 10, "attempt": 1, "evidence": [self.ledger[1]]})
|
||||
add("node.finished", "members", {"duration_ms": 10, "attempt": 1, "evidence": [self.entries["members"]]})
|
||||
if "regions" in self.entries:
|
||||
add("tool.end", "regions", {"tool": "db_query__crm", "duration_ms": 7,
|
||||
"query_artifact": self.entries["regions"]["artifact"],
|
||||
"preview": json.dumps(REGIONS["rows"], ensure_ascii=False)})
|
||||
add("node.finished", "regions", {"duration_ms": 8, "attempt": 1, "evidence": [self.entries["regions"]]})
|
||||
add("node.finished", "card", {"duration_ms": 3, "attempt": 1, "preview": {"kind": "metric_set"},
|
||||
"evidence": [self.ledger[2]]})
|
||||
"evidence": [self.entries["card"]]})
|
||||
for node_id, doc_id in reports.items():
|
||||
add("llm.end", node_id, {"model": "mock", "output": "(写作草稿)"})
|
||||
add("report.checked", node_id, {"doc_artifact": doc_id, "ok": True})
|
||||
@@ -167,12 +190,12 @@ class Sample:
|
||||
|
||||
|
||||
def render(sample: Sample, events: list[tuple[int, str, str | None, Any]], bound: int, *, run: dict[str, Any] | None = None,
|
||||
legacy: bool = False, drop: tuple[str, ...] = ()) -> str:
|
||||
"""真实封存哈希的页面。drop:导出时服务端已没有文件的工件。"""
|
||||
legacy: bool = False, drop: tuple[str, ...] = (), source_masks: dict[str, list[str]] | None = None) -> str:
|
||||
"""真实封存哈希的页面。drop:导出时服务端已没有文件的工件;source_masks:数据源现在设的遮罩。"""
|
||||
sealed = [e for e in events if e[0] <= bound]
|
||||
files = {k: v for k, v in sample.files.items() if k not in drop}
|
||||
bundle = build_bundle(run or RUN, events, manifest_seq=bound, manifest_hash=manifest_hash(sealed), read=files.get,
|
||||
exported_at=EXPORTED_AT, legacy=legacy, graph=GRAPH, source_masks={})
|
||||
exported_at=EXPORTED_AT, legacy=legacy, graph=GRAPH, source_masks=source_masks or {})
|
||||
return render_html(bundle)
|
||||
|
||||
|
||||
@@ -215,6 +238,10 @@ def pages() -> dict[str, str]:
|
||||
m = Sample(masked=True)
|
||||
events, bound = m.events({"write": m.doc(WEEKLY, "write")})
|
||||
out["masked-card"] = render(m, events, bound)
|
||||
|
||||
r = Sample(regions=True)
|
||||
events, bound = r.events({"write": r.doc(MEMBERS, "write")})
|
||||
out["masked-source"] = render(r, events, bound, source_masks={"crm": ["phone"]})
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -461,9 +461,10 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
|
||||
本身:封存哈希是对事件真算的(浏览器的拼法重算得出),各页的形状是 check-bundle 的断言预期的那样。"""
|
||||
from tests.bundle_samples import pages
|
||||
|
||||
got = {name: json.loads(data_block(page)) for name, page in pages().items()}
|
||||
rendered = pages()
|
||||
got = {name: json.loads(data_block(page)) for name, page in rendered.items()}
|
||||
assert set(got) == {"base", "two-reports", "legacy", "failed", "continued", "degraded", "missing-doc",
|
||||
"missing-card", "nan", "stray", "masked-card"}
|
||||
"missing-card", "nan", "stray", "masked-card", "masked-source"}
|
||||
for name, bundle in got.items():
|
||||
assert browser_manifest(bundle["events"]) == bundle["meta"]["manifest_hash"], name
|
||||
|
||||
@@ -502,6 +503,13 @@ def test_the_sample_pages_for_the_browser_check_are_sealed_for_real():
|
||||
# 口径卡上有输入取自遮罩列:整张卡只给 id,页面里找不到那个号码
|
||||
card = next(a for a in got["masked-card"]["artifacts"] if a["kind"] == "metric_set")
|
||||
assert card.get("masked") is True and "b64" not in card
|
||||
# 会员库设了号码遮罩:按地区汇总的那份没碰号码列,随文件附带;选出了号码的那份只给 id,页面里找不到号码
|
||||
source = got["masked-source"]
|
||||
shapes = {(a["kind"], a.get("node_id")): {k for k in a if k in ("b64", "masked", "missing")}
|
||||
for a in source["artifacts"]}
|
||||
assert shapes == {("report_doc", "write"): {"b64"}, ("query", "regions"): {"b64"}, ("query", "members"): {"masked"}}
|
||||
assert [n for n in segments(source) if n.get("cite", {}).get("alias") == "Q3"]
|
||||
assert not any(phone in rendered["masked-source"] for phone in ("000-0001", "000-0002"))
|
||||
nan = got["nan"]
|
||||
assert "NaN" in next(e["canonical"] for e in nan["events"] if e["type"] == "run.started")
|
||||
assert b"NaN" in base64.b64decode(next(a["b64"] for a in nan["artifacts"] if a.get("node_id") == "fetch"))
|
||||
|
||||
Reference in New Issue
Block a user