diff --git a/backend/app/engine/inquiry.py b/backend/app/engine/inquiry.py index 1556e6f5..19610299 100644 --- a/backend/app/engine/inquiry.py +++ b/backend/app/engine/inquiry.py @@ -49,8 +49,9 @@ 正文里写出某人的年龄遮不住,只靠 SQL 里的等值比较和按列遮;同一个短的数在 SQL 里被换掉了,别的列上撞上它的阈值、 LIMIT 的行数照原样(age = 30 AND visits >= 30)。够长的数在回答正文和回答引用的格子里按值遮,碰巧和某个遮罩值相同的 数(访问次数正好等于某一笔金额)也遮掉,这是产品取舍,不按上下文去猜;工具边界不按值换数值格(存下的是证据),交给 - agent 的、存下的查询快照里这样的数照原样,只在回答和质询工件里遮。占位的写法(未知、暂无……,masking.PLACEHOLDERS) - 不当遮罩值 + agent 的、存下的查询快照里这样的数照原样,只在回答和质询工件里遮。反过来,没设遮罩的列里存成数的号码(别的库里的 + 联系方式表把手机号存成整数)工具边界也不按值换,agent 看得到:要遮就在那个数据源上把那一列也设成遮罩。存成文字的照换。 + 占位的写法(未知、暂无……,masking.PLACEHOLDERS)不当遮罩值 - 遮罩数值列的 SUM / AVG / MIN / MAX 算派生自遮罩列,整列遮(COUNT 不算,见 _value_columns),CASE 里有一支取的是 遮罩列的也算:MIN / MAX 就是某一行的原值,只有一行的分组 SUM / AVG 也是,按列认不出哪些汇总值安全,宁可多遮。 核查 agent 的查询在工具边界照这个办法遮,所以这样的指标双盲复算不了(原 SQL 里认得出的,派盲算之前就记为无法 diff --git a/backend/app/engine/masking.py b/backend/app/engine/masking.py index 2e5283dc..30490636 100644 --- a/backend/app/engine/masking.py +++ b/backend/app/engine/masking.py @@ -28,8 +28,9 @@ 认它带不带同名的列(_schema_tables),不知道表结构的按带得出算(多遮)。分档写在派生来源里、外层按它分组的(一档 一行,_grouped_labels)同当分组键的 CASE 一样照常显示(_shown_labels) - 哪些值(_Secrets):遮罩列里出现过的值,按值认有门槛(SECRET_MIN、SECRET_DIGITS),占位的写法(PLACEHOLDERS:未知、 - 暂无……)不收;换的时候先按词、按开头几个字筛出正文里真有的那几个值,不现编上万个分支的正则;正文、文字格、SQL 的字符串和注释里换了写法的长号码(199-1474-2785、 - +86 …、全角数字)按纯数字写法在集合里查(loose_spans);已经摆在读者眼前的值免遮,之后并进来的照样免(protected) + 暂无……)不收;换的时候先按词、按开头几个字筛出正文里真有的那几个值,不现编上万个分支的正则;正文、文字格、SQL 的 + 字符串和注释里换了写法的长号码(199-1474-2785、+86 …、全角数字)按纯数字写法在集合里查(loose_spans);已经摆在 + 读者眼前的值免遮,之后并进来的照样免(protected) - 快照遮好的样子(_masked_view),回答时才认得出、事后要照位置重放的地方(view_edits、replay_edits) - 一次核查里认出的遮罩值(SeenValues):调用方交进来的和 agent 查询时遮掉的分开记,合起来时 agent 查到的在前 @@ -1263,9 +1264,9 @@ def _visible_sources(select: Any, node: Any) -> list[tuple[str, Any]]: from sqlglot import exp sources = _from_sources(select) - child = node + child, below = node, None while child.parent is not None and child.parent is not select: - child = child.parent + child, below = child.parent, child if child.parent is not select: return sources if isinstance(child, exp.From): @@ -1273,7 +1274,10 @@ def _visible_sources(select: Any, node: Any) -> list[tuple[str, Any]]: if isinstance(child, exp.Join): joins = list(select.args.get("joins") or []) at = next((i for i, join in enumerate(joins) if join is child), None) - return sources[:1 + at] if at is not None else sources + if at is None: + return sources + # 在连接的来源本身里(LATERAL)只看得见前面的;在 ON 条件里连它自己也看得见 + return sources[:1 + at] if below is child.this else sources[:2 + at] return sources @@ -2403,8 +2407,9 @@ def mask_query_result(payload: dict[str, Any], masked: Iterable[str], kind: str - 整列遮:列名直接对上遮罩列的,加上按 SQL 认出值取自遮罩列的(_derived_hidden:别名、函数、子查询、WITH、 SELECT *(展开的列换了名字的也算)、表值函数、整行引用和函数参数里的 *、列名表、TABLE x;COUNT、只在条件里用到、 - 分档当分组键的不算);SQL 解析不了、又写着遮罩列(或者 *、TABLE x)的,整份结果的列都遮。这几列的格子换成 - 「已遮罩」,类型记成文本 + 分档当分组键的不算,分档写在派生来源里、外层按它分组的也不算);认不全的(SQL 解析不了又写着遮罩列、*、TABLE x, + (表达式).* 读到遮罩列,表名、SQL 写在字符串里交给函数,分析出意外、超出工作量上限)整份结果的列都遮。这几列的 + 格子换成「已遮罩」,类型记成文本 - 别的列按值换,只换文字格:整格就是遮罩值的换掉,文字格里夹着的那一截换掉(换了写法的号码也认, _Secrets.loose_spans),换过的格子记下来(QueryMask.cells)。遮罩值是这份结果里的加上 known:先查号码、再查写着 号码的备注、没设遮罩的联系方式表(别的库里的也一样)时,后一份同样遮好。门槛同按值遮(SECRET_DIGITS、SECRET_MIN);