Skip to content

Commit 25cf719

Browse files
Pigbibicodex
andcommitted
fix: harden M0 publisher source trust
Co-Authored-By: Codex <noreply@openai.com>
1 parent 34baea4 commit 25cf719

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

.github/workflows/publish-m0-research-ledger.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ concurrency:
2121

2222
jobs:
2323
publish-verified-research-ledger:
24+
# Environment secrets and the publisher endpoint are deployable only from
25+
# the protected main branch. A manual dispatch on another ref is skipped.
26+
if: github.ref == 'refs/heads/main'
2427
runs-on: ubuntu-latest
28+
environment: m0-research-publisher
2529
timeout-minutes: 10
2630
env:
2731
QAR_REPOSITORY: QuantStrategyLab/QuantAdvisorResearch
@@ -72,6 +76,13 @@ jobs:
7276
repository = metadata.get("repository")
7377
if not isinstance(repository, dict) or repository.get("full_name") != expected_repository:
7478
raise SystemExit("QAR run repository mismatch")
79+
head_repository = metadata.get("head_repository")
80+
if not isinstance(head_repository, dict) or head_repository.get("full_name") != expected_repository:
81+
raise SystemExit("QAR run head repository mismatch")
82+
if metadata.get("head_branch") != "main":
83+
raise SystemExit("QAR run must originate from the main branch")
84+
if metadata.get("event") not in {"schedule", "workflow_dispatch"}:
85+
raise SystemExit("QAR run event is not trusted for M0 publication")
7586
revision = metadata.get("head_sha")
7687
if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
7788
raise SystemExit("QAR run head revision is invalid")

docs/m0_research_publisher_envelope_contract.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ header;它从不写进封套、标准输出、错误信息或日志。该工
9393
必须输入一个**已经成功完成**`QuantStrategyLab/QuantAdvisorResearch`「Weekly
9494
Intelligent Advisory Review」run ID;它不会检索、猜测或自动采用最新 run。
9595

96+
该 job 必须在 `QuantRuntimeSettings``main` 分支运行(`github.ref` 必须为
97+
`refs/heads/main`),并绑定专用 GitHub Environment `m0-research-publisher`。环境的
98+
deployment branch 也必须只允许 `main`;从其他 ref 手动 dispatch 时 job 会跳过,不能
99+
取得任何环境配置或发布研究台账。
100+
96101
该入口固定只读以下来源:
97102

98103
- repository:`QuantStrategyLab/QuantAdvisorResearch`
@@ -101,7 +106,8 @@ Intelligent Advisory Review」run ID;它不会检索、猜测或自动采用
101106
- artifact 内唯一命名为 `m0_research_source_snapshot_YYYY-MM-DD.json` 的文件。
102107

103108
在下载前,workflow 用专用的 `QAR_ARTIFACT_READ_TOKEN` 验证 run ID、成功状态、
104-
workflow 身份、来源仓库、immutable `head_sha`,以及 artifact 与该 run 的绑定。下载后,
109+
workflow 身份、来源仓库和 `head_repository``head_branch=main`、可信 event(仅
110+
`schedule``workflow_dispatch`)、immutable `head_sha`,以及 artifact 与该 run 的绑定。下载后,
105111
它拒绝不安全 ZIP 路径、多个或缺失 snapshot、超过 2 MiB 的 snapshot、错误 schema/source
106112
ID 或无效 report digest,并计算**原始 snapshot 字节**的 SHA-256。该 SHA、QAR revision、
107113
run ID 和 artifact ID 都作为 `source_artifact` metadata 显式传给构建器,构建器会再次验证
@@ -114,6 +120,11 @@ run ID 和 artifact ID 都作为 `source_artifact` metadata 显式传给构建
114120
| variable `M0_RESEARCH_SYNC_URL` | `QSL_M0_RESEARCH_LEDGER_PUBLISH_URL` | HTTPS 研究台账接收地址 |
115121
| secret `M0_RESEARCH_SYNC_TOKEN` | `QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN` | 接收端专用 Bearer token |
116122

123+
`QAR_ARTIFACT_READ_TOKEN``M0_RESEARCH_SYNC_TOKEN``M0_RESEARCH_SYNC_URL` 都必须配置
124+
`m0-research-publisher` Environment 中,而不是 repository-level 默认作用域。两个 token
125+
必须是不同的值和不同的最小权限用途:前者只能读取固定 QAR repository 的 Actions run/artifact,
126+
后者只能向 M0 接收端发布封套;不得复用、互相授予或写入运行时/平台配置。
127+
117128
URL、发布 token 和 QAR 读取 token 不会写进封套、`GITHUB_STEP_SUMMARY` 或 workflow 输出。该
118129
workflow 不读取运行时、平台、selector、策略或券商配置;其唯一网络写入是构建器在
119130
`--publish` 明确指定时,对上述研究接收地址发送经过校验的 no-order 封套。

python/tests/test_manual_m0_research_publisher_workflow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def test_workflow_is_manual_and_binds_one_explicit_successful_qar_artifact(self)
1515

1616
self.assertIn("workflow_dispatch:", workflow)
1717
self.assertNotRegex(workflow, r"(?m)^ (?:push|pull_request|schedule|repository_dispatch):")
18+
self.assertIn("if: github.ref == 'refs/heads/main'", workflow)
19+
self.assertIn("environment: m0-research-publisher", workflow)
1820
self.assertRegex(
1921
workflow,
2022
r"(?s)qar_run_id:\n.*?required: true\n.*?type: string",
@@ -28,6 +30,13 @@ def test_workflow_is_manual_and_binds_one_explicit_successful_qar_artifact(self)
2830
self.assertIn('"repos/${QAR_REPOSITORY}/actions/runs/${QAR_RUN_ID}"', workflow)
2931
self.assertIn("QAR run must already be completed successfully", workflow)
3032
self.assertIn("QAR run is not the fixed Weekly Intelligent Advisory Review workflow", workflow)
33+
self.assertIn("QAR run head repository mismatch", workflow)
34+
self.assertIn("QAR run must originate from the main branch", workflow)
35+
self.assertIn("QAR run event is not trusted for M0 publication", workflow)
36+
self.assertIn('head_repository = metadata.get("head_repository")', workflow)
37+
self.assertIn('head_repository.get("full_name") != expected_repository', workflow)
38+
self.assertIn('metadata.get("head_branch") != "main"', workflow)
39+
self.assertIn('metadata.get("event") not in {"schedule", "workflow_dispatch"}', workflow)
3140
self.assertIn("QAR artifact workflow-run binding mismatch", workflow)
3241
self.assertIn("QAR artifact must contain exactly one dated M0 source snapshot", workflow)
3342
self.assertIn("m0_research_source_snapshot_[0-9]{4}-[0-9]{2}-[0-9]{2}", workflow)

0 commit comments

Comments
 (0)