mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:51:57 +00:00
使用者的原始运行记录只给问数据的运行,对账运行经证据接口读(复核 M3)
复核发现:使用者(开登录后的 user 角色)发起的口径对账运行带 MASK_RESULTS、记在本人名下,run_events_readable 只看这两条,
于是他能经 /api/runs/{id}、/events、/graph 读到对账运行的原始图和事件,里面原样是口径取数节点的 SQL 模板。这一波正是以
「口径的 SQL 可能写着遮罩值」为由不让使用者在问数据里看口径、use_caliber,这条读法绕开了证据面板按遮罩换 SQL 的那一层。
做法:api/deps.run_events_readable 给 user 角色再加一条 origin_kind == "chat"——原始记录、事件、运行时的图、实时流只给他
自己的、查询结果在工具边界遮过的问数据运行(原来的说明里就写着「实际上满足的就是它自己的问数据运行」,这次照实收紧)。
对账运行他仍然能经账本和证据接口读(can_read_run 不变,那里展示的 SQL 按遮罩换过),对账的发起也不变。改了
api/deps.py 的这一个函数(没分给哪个包)和 ledger/reconcile 的模块说明。
测试:开账号密码登录,使用者对自己的问数据答案经接口发起对账,对账运行的 /api/runs/{id}、/events、/graph 都回 403,证据
接口 200;他自己的问数据运行这三样照旧 200。去掉这一条回跑,新用例失败;test_auth_access、test_personal_apps 照旧过。
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
- can_read_run / can_read_artifact:运行、工件的读权限(user 角色只能读有授权的应用视图运行、自己参与的实例);
|
||||
- ensure_role / ensure_can_publish:接口内部按请求内容判定的权限(MCP 的 stdio、受管级发布、版本退役,Q8);
|
||||
- restricted():这次请求是不是开登录后的 user 角色(按请求内容收窄的接口用:问数据只能发起探索运行、工件按白名单给);
|
||||
- run_events_readable(principal, run):运行的原始记录、事件、实时流能不能读(user 只读查询结果在工具边界遮过的);
|
||||
- run_events_readable(principal, run):运行的原始记录、事件、实时流能不能读(user 只读查询结果在工具边界遮过的问数据运行);
|
||||
- ensure_conversation_access(session, conversation_id):接受 conversation_id 的接口核对对话归属(对话按人隔离);
|
||||
- run_scope / evidence_scope / chat_scope:main.py 挂在运行、证据、问数据几组路由上的依赖,按 run_id 判断能不能读。
|
||||
run_scope 同样管运行的 WebSocket,握手时按运行判;
|
||||
@@ -213,15 +213,18 @@ async def run_events_readable(principal: Principal, run: Any) -> bool:
|
||||
"""能不能读这次运行的原始记录、事件和实时流(GET /api/runs/{id}、/events、/graph,WebSocket /stream)。
|
||||
|
||||
关登录:能。builder 及以上:这次运行它能读就能(can_read_run:别人的个人应用刷新、个人蓝图试运行读不了)。
|
||||
user:这次运行它能读(can_read_run),而且查询结果在工具边界就遮过(masked_at_boundary)。两个条件缺一不可:
|
||||
没在边界遮过的运行,事件里 tool.end 的预览、运行状态里是查询结果的原文,遮罩管不到——自己发起的应用实例、
|
||||
有授权的应用视图运行经应用页和证据接口读(那两处按派生遮罩给)。实际上满足两条的就是它自己开登录后发起的
|
||||
问数据运行;关登录时自报同名留下的老运行没在边界遮过,开了登录也读不到。"""
|
||||
user:问数据的运行(origin_kind=chat),这次它能读(can_read_run),而且查询结果在工具边界就遮过(masked_at_boundary)。
|
||||
三个条件缺一不可:没在边界遮过的运行,事件里 tool.end 的预览、运行状态里是查询结果的原文,遮罩管不到——自己发起的
|
||||
应用实例、有授权的应用视图运行经应用页和证据接口读(那两处按派生遮罩给)。服务端替他派生的运行也带着边界遮罩,但图
|
||||
不是他的助手搭的:口径对账(ledger/reconcile)的图里原样是口径取数节点的 SQL 模板(口径的 SQL 可能写着遮罩值,所以
|
||||
问数据里使用者不看口径),同样经证据接口读(展示的 SQL 里写着的遮罩值换成「已遮罩」,同应用视图运行)。实际上满足
|
||||
三条的就是它自己开登录后发起的问数据运行;关登录时自报同名留下的老运行没在边界遮过,开了登录也读不到。"""
|
||||
if principal.mode == "off":
|
||||
return True
|
||||
if principal.at_least("builder"):
|
||||
return await can_read_run(principal, run)
|
||||
if run is None or not principal.username or not masked_at_boundary(run):
|
||||
if run is None or not principal.username or getattr(run, "origin_kind", None) != "chat" \
|
||||
or not masked_at_boundary(run):
|
||||
return False
|
||||
return await can_read_run(principal, run)
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
不同时不猜,照实拒绝)。维度列是遮罩列、或者这次的结果里合并键不唯一时照实拒绝:前者对齐要用到遮住的值,后者对不准
|
||||
是哪一行。被引用的列是遮罩列的同样不对账。
|
||||
|
||||
只读:派生图只查库、不写;使用者发起时带上 MASK_RESULTS / READ_ONLY(同问数据),搭建者也带 READ_ONLY。
|
||||
只读:派生图只查库、不写;使用者发起时带上 MASK_RESULTS / READ_ONLY(同问数据),搭建者也带 READ_ONLY。对账运行记在
|
||||
使用者名下,但它的原始记录、事件、运行时的图不给使用者(api/deps.run_events_readable 只给问数据的运行):图里原样是口径
|
||||
取数节点的 SQL 模板,他经账本和证据接口看结果(展示的 SQL 按遮罩换过)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -214,3 +214,37 @@ async def test_users_reconcile_their_own_answers_read_only_and_masked(client, mo
|
||||
assert run.options[MASK_RESULTS_OPTION] is True and run.options[READ_ONLY_OPTION] is True
|
||||
done = await S.wait(run.id)
|
||||
assert done.status == "succeeded", done.error
|
||||
|
||||
|
||||
async def test_users_read_their_reconcile_runs_only_through_the_evidence(client, monkeypatch):
|
||||
"""使用者发起的对账运行带 MASK_RESULTS、记在他名下,但派生图里有口径取数节点的 SQL 模板(口径的 SQL 可能写着遮罩值,
|
||||
所以问数据里使用者不看口径、不能 use_caliber):运行的原始记录、事件、运行时的图(/api/runs/{id}、/events、/graph)只给
|
||||
他自己的问数据运行,对账运行经证据接口读(那里按遮罩渲染)。"""
|
||||
from app.tools.datasource import MASK_RESULTS_OPTION
|
||||
from tests.fakes import auth_env
|
||||
|
||||
wf = await S.publish()
|
||||
row = await S.explore(monkeypatch, S.WITH_REFUNDS, origin="chat")
|
||||
# 当作开登录后使用者 carol 问的:问数据运行在工具边界遮
|
||||
async with SessionLocal() as session:
|
||||
source = await session.get(Run, row.id)
|
||||
source.started_by, source.options = "carol", {MASK_RESULTS_OPTION: True}
|
||||
await session.commit()
|
||||
[action] = [a for a in (await ledger_of(client, row.id))["actions"] if a["kind"] == "reconcile"]
|
||||
await auth_env.reset()
|
||||
try:
|
||||
admin = await auth_env.turn_on_local()
|
||||
await auth_env.make_user("carol", "user")
|
||||
assert (await admin.put("/api/auth/settings", json={"user_can_chat": True})).status_code == 200
|
||||
carol = await auth_env.login("carol")
|
||||
res = await carol.post(f"/api/ledger/answers/{row.id}/reconcile", json={
|
||||
"query": action["query"], "workflow_id": wf, "version": 1, "inputs": CHECKED, "confirm": True})
|
||||
assert res.status_code == 201, res.text
|
||||
done = await S.wait(res.json()["run_id"])
|
||||
assert done.status == "succeeded" and done.started_by == "carol" and done.options[MASK_RESULTS_OPTION] is True
|
||||
for rest in ("", "/events", "/graph"):
|
||||
assert (await carol.get(f"/api/runs/{done.id}{rest}")).status_code == 403, rest
|
||||
assert (await carol.get(f"/api/runs/{row.id}{rest}")).status_code == 200, f"自己的问数据运行照旧:{rest}"
|
||||
assert (await carol.get(f"/api/runs/{done.id}/evidence")).status_code == 200
|
||||
finally:
|
||||
await auth_env.reset()
|
||||
|
||||
Reference in New Issue
Block a user