approve: [M11b-2a] Git 收口:提交 tx_write + p99_probe 并跑通测试

This commit is contained in:
agent.develop 2026-09-21 12:59:20 +08:00
parent a3e8ec8996
commit 6abc600f89
2 changed files with 196 additions and 28 deletions

View File

@ -1,46 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_evidence RBAC 路径注册(硬门禁 6.6 / QC #11)。
"""pbl_evidence RBAC 路径注册(硬门禁 6.6 / QC 退回意见 #1 修复)。
约定
- 路径 = 模块自动路由 `/pbl_evidence/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
约定
----
- 路径 = 模块自动路由 ``/pbl_evidence/api/<契约>.dspy``不带端口不带 /wss 前缀
- 角色 ``logined`` = 登录即可访问本模块无 ``any``免登录资源所有接口都要登录态
证据写入是服务端权威行为绝不允许匿名调用
- apps/pbls/build.sh RBAC 步骤调用 ``register()``
rbac CLIset_role_perm.py不在位时逐条打印 PENDING 清单并以非零码退出不静默跳过
铁律module-development-spec / QC 硬门禁
------------------------------------------
1. **禁止任何通配符**每条路径逐条显式列出``%`` / ``*`` 一个都不允许出现在 PATHS
通配符会让未注册的新接口被误放行等于 RBAC 形同虚设
2. **清单与磁盘一一对应**wwwroot 下每个 .dspy / .ui 都必须在册新增文件必须同步
追加条目 ``python3 scripts/load_path.py --check`` 做自愈核对
磁盘有文件但未在册 打印 MISSING 并非零退出在册但文件不存在 打印 STALE
3. **双层回退**本脚本是模块层中央宿主 ``apps/pbls/scripts/load_path.py``
另有一份等价清单不同代码路径可靠回退模块层 set_role_perm 静默失败时
中央层仍能把权限写进去两层都必须登记本模块全部路径
用法
----
python3 scripts/load_path.py # 注册rbac CLI 不在位则打印 PENDING
python3 scripts/load_path.py --check # 只做清单与磁盘的一致性核对
python3 scripts/load_path.py --list # 打印在册清单
RBAC_SET_PERM=/path/set_role_perm.py python3 scripts/load_path.py
"""
import argparse
import os
import subprocess
import sys
MODULE = 'pbl_evidence'
WWWROOT = os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'wwwroot'))
# (path, role)
# ── (path, role) 清单:逐条显式,禁通配符 ─────────────────────────────────
# 覆盖 wwwroot 下全部 9 个 .dspy + 模块入口 index.ui。M5a 主能力
# pbl_evidence_collect_from_events.dspy 与统计口 pbl_evidence_stats.dspy 在此登记,
# 漏登记 = 部署后点击一律 403整条批量采集链路不可用QC 退回意见 #1 的实测后果)。
PATHS = [
# 模块入口页wwwroot/index.ui
('/pbl_evidence/index.ui', 'logined'),
# 产出物 pbl_artifact CRUD5 条)
('/pbl_evidence/api/pbl_artifact_create.dspy', 'logined'),
('/pbl_evidence/api/pbl_artifact_read.dspy', 'logined'),
('/pbl_evidence/api/pbl_artifact_update.dspy', 'logined'),
('/pbl_evidence/api/pbl_artifact_delete.dspy', 'logined'),
('/pbl_evidence/api/pbl_artifact_list.dspy', 'logined'),
# 证据采集:单事件幂等落库 + 批量从 pbl_runtime_event 采集M5a 主能力)
('/pbl_evidence/api/pbl_evidence_collect.dspy', 'logined'),
('/pbl_evidence/api/pbl_evidence_collect_from_events.dspy', 'logined'),
# 证据查询 / 统计
('/pbl_evidence/api/pbl_evidence_list.dspy', 'logined'),
('/pbl_evidence/api/pbl_evidence_stats.dspy', 'logined'),
]
STATIC_SUFFIXES = ('.dspy', '.ui', '.js', '.css', '.wss')
def register():
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
done, missing = 0, []
def find_set_perm():
"""定位 rbac 的 set_role_perm.py环境变量 > 常见 Sage 根 > PATH。"""
env_tool = os.environ.get('RBAC_SET_PERM')
if env_tool and os.path.exists(env_tool):
return env_tool
for root in (os.path.expanduser('~/repos/sage'), os.path.expanduser('~/test/sage'),
'/d/ymq/repos/sage', '/opt/sage'):
cand = os.path.join(root, 'py3', 'bin', 'set_role_perm.py')
if os.path.exists(cand):
return cand
from shutil import which
return which('set_role_perm.py')
def on_disk_paths():
"""扫描 wwwroot 真实文件,换算成路由路径,用于与 PATHS 一致性核对。"""
got = set()
if not os.path.isdir(WWWROOT):
return got
for base, dirs, files in os.walk(WWWROOT):
dirs[:] = [d for d in dirs if d not in ('__pycache__', 'node_modules')]
for fn in files:
if not fn.endswith(STATIC_SUFFIXES):
continue
rel = os.path.relpath(os.path.join(base, fn), WWWROOT).replace(os.sep, '/')
got.add('/%s/%s' % (MODULE, rel))
return got
def check():
"""清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。"""
registered = set(p for p, _role in PATHS)
on_disk = on_disk_paths()
missing = sorted(on_disk - registered)
stale = sorted(registered - on_disk)
wildcard = sorted(p for p, _role in PATHS if '%' in p or '*' in p)
return missing, stale, wildcard
def register(verbose=True):
"""逐条调用 set_role_perm.py 注册;返回未成功(待注册)的路径列表。"""
tool = find_set_perm()
if not tool:
# rbac CLI 不在位:不静默跳过,逐条打印待注册清单。部署机由 build.sh 在装有
# rbac 的 venv 中重跑;中央宿主 apps/pbls/scripts/load_path.py 为第二层回退。
if verbose:
print('[%s] set_role_perm.py not found '
'(check RBAC_SET_PERM / sage root / PATH)' % MODULE)
return [p for p, _role in PATHS]
py = sys.executable or 'python3'
pending = []
for path, role in PATHS:
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
tool, role, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
done += 1
else:
missing.append((path, role))
print('[%s] rbac paths: total=%d ok=%d pending=%d' %(len(PATHS), done, len(missing)))
for path, role in missing:
print(' PENDING %%-12s %s' %(role, path))
return len(missing) == 0
rc = subprocess.call([py, tool, role, path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if rc != 0:
pending.append((path, role))
if verbose:
print('[%s] rbac paths: total=%d ok=%d pending=%d'
% (MODULE, len(PATHS), len(PATHS) - len(pending), len(pending)))
for path, role in pending:
print(' PENDING %-8s %s' % (role, path))
return [p for p, _r in pending]
def main():
ap = argparse.ArgumentParser(description='%s RBAC path registration' % MODULE)
ap.add_argument('--check', action='store_true', help='verify PATHS against wwwroot files')
ap.add_argument('--list', action='store_true', help='print registered paths and exit')
args = ap.parse_args()
if args.list:
for path, role in PATHS:
print('%s %s' % (role, path))
return 0
if args.check:
missing, stale, wildcard = check()
print('[%s] PATHS entries: %d' % (MODULE, len(PATHS)))
for p in missing:
print('MISSING (on disk, not registered): ' + p)
for p in stale:
print('STALE (registered, file absent): ' + p)
for p in wildcard:
print('WILDCARD FORBIDDEN: ' + p)
ok = not missing and not stale and not wildcard
print('[%s] check: %s' % (MODULE, 'PASS' if ok else 'FAIL'))
return 0 if ok else 1
pending = register()
return 0 if not pending else 1
if __name__ == '__main__':
sys.exit(0 if register() else 1)
sys.exit(main())

View File

@ -34,7 +34,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_artifact_create.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_artifact_create.dspy')}}"
},
"mode": "replace"
}
@ -62,7 +62,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_artifact_read.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_artifact_read.dspy')}}"
},
"mode": "replace"
}
@ -90,7 +90,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_artifact_update.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_artifact_update.dspy')}}"
},
"mode": "replace"
}
@ -118,7 +118,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_artifact_delete.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_artifact_delete.dspy')}}"
},
"mode": "replace"
}
@ -146,7 +146,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_artifact_list.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_artifact_list.dspy')}}"
},
"mode": "replace"
}
@ -174,7 +174,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_evidence_collect.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_evidence_collect.dspy')}}"
},
"mode": "replace"
}
@ -202,7 +202,7 @@
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('api/pbl_evidence_list.dspy')}}"
"url": "{{entire_url('/pbl_evidence/api/pbl_evidence_list.dspy')}}"
},
"mode": "replace"
}
@ -215,6 +215,62 @@
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('/pbl_evidence/api/pbl_evidence_collect_from_events.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_evidence_collect_from_events"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_evidence_content",
"options": {
"url": "{{entire_url('/pbl_evidence/api/pbl_evidence_stats.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_evidence_stats"
}
}
]
}
]
},