Skip to content

Commit dece267

Browse files
committed
Document application policy ownership and MCP compatibility
1 parent 751ca8c commit dece267

2 files changed

Lines changed: 38 additions & 65 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# app/request_policy.py — request safety policy
2+
3+
<!-- verified-against: 2026-09-26 -->
4+
5+
This headless application module validates untrusted read-only requests for
6+
the SDK, MCP transport, and reserved-run worker. It derives a new `TaskSpec`
7+
from raw input at reservation and repeats validation in the child after
8+
reading `request.json`; persisted input alone is not an authority.
9+
10+
`enforce_mcp_policy` permits only imported `READ_ONLY_KINDS`, forces
11+
`post=False`, limits repositories to the configured allowlist, checks positive
12+
PR/issue numbers, validates full expected-head SHAs, and strips unknown
13+
params. A bounded `deterministic_signals` list applies only to quality review;
14+
carried findings are validated for PR review. `authorize_repo_path` verifies
15+
both checkout origin identity and containment under allowed roots.
16+
17+
`enforce_strict_review_policy` accepts only PR review, forces eco mode, and
18+
rejects explicit posting. `enforce_quality_review_policy` accepts only PR
19+
quality, forces eco mode, and rejects posting. Neither path can create an
20+
outward write. Policy errors are `PolicyError`; the module does not execute a
21+
run, call a model, or access knowledge. It imports only task-specification
22+
primitives at module load; repository identity and carried-finding helpers
23+
are loaded inside the relevant validation path.
24+
25+
The old `infermatrix_copilot.mcp_policy` module re-exports these callables for
26+
compatibility. Keep policy changes here so the application and transports
27+
cannot diverge. `test_mcp.py`, `test_sdk_v1.py`, and `test_contract.py` cover
28+
the read-only, snapshot, identity, and compatibility behavior.
Lines changed: 10 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,13 @@
1-
# mcp_policy.py —— 规范
1+
# mcp_policy.py — compatibility boundary
22

3-
<!-- verified-against: 2026-09-22 -->
3+
<!-- verified-against: 2026-09-26 -->
44

5-
`LOC ~254 · 安全原语(MCP 结构性门) · refactor-status: ok`
5+
This module preserves the existing imports for `PolicyError`,
6+
`authorize_repo_path`, `enforce_mcp_policy`,
7+
`enforce_strict_review_policy`, and `enforce_quality_review_policy`.
8+
It contains no independent validation rules. The headless application owns
9+
those rules in [`app/request_policy.md`](app/request_policy.md); SDK, MCP,
10+
and reserved-run execution use the same implementation.
611

7-
## 职责
8-
从原始 MCP 输入**重新推导**出一个*安全的* `TaskSpec`,拒绝 MCP 面不被允许做的任何事
9-
—— 这就是"宿主无法扩大服务端权限"的**结构性**保证。
10-
11-
## 公开契约
12-
`enforce_mcp_policy(raw) -> TaskSpec`(拒绝时抛错);
13-
`enforce_strict_review_policy(raw) -> TaskSpec`(Strict 兼容路径:限
14-
`pr_review`、强制 `mode="eco"`);
15-
`enforce_quality_review_policy(raw) -> TaskSpec`(质量路径:限
16-
`pr_quality`、强制 `mode="eco"`、永不发布);
17-
`authorize_repo_path(repo, raw_path, settings) -> str`(对调用方提供的
18-
checkout 路径做**身份 + 圈禁**双重校验,返回 canonical 路径)。
19-
20-
## 不变量(**C2**、**C3**、**A2**)
21-
- **它跑两次,这是设计如此。** 一次在**边界**(server,工具被调用时),一次在
22-
**子进程**(**权威**,就在它读完 `request.json` 之后)。子进程这次复检之所以存在,
23-
是因为这条保证**不能依赖 `request.json` 未被篡改**:同用户的宿主进程完全可以在预约
24-
与执行之间把它改写。
25-
- `kind` 必须 ∈ `READ_ONLY_KINDS`;`post` 被**硬置为 `False`**;`repo` 必须在 allowlist
26-
内;`pr`/`issue` 必须为正;未知 params 被**剥除**而不是透传。
27-
- `pr_quality` 唯一可多带一个 `deterministic_signals`:至多 12 条、每条至多
28-
500 字符。它们只是供模型核验的可错假设,不能直接成为质量裁决。
29-
- **Strict 路径上 `post` 是被拒绝,不是被恢复。** 这条路径曾经"共享门里
30-
强制 `post=False`、事后把调用方原值放回去",使 Strict 成为唯一能发布的
31-
MCP 面。显式 `post=True` 现在直接抛 `PolicyError` —— 移除这个恢复不是
32-
制造异常,而是消灭一个异常(`mcp_server` 侧的 `ALLOW_POST="0"` 环境闸
33-
与此呼应,双闸同向)。
34-
- **`expected_head_sha` 只收全长 SHA**:经 `task_spec.FULL_SHA_RE`
35-
(import,绝不重述正则)校验后随 `TaskSpec` 冻结;短 SHA / 非十六进制
36-
被拒绝,空串 = 不钉快照。
37-
- **`authorize_repo_path` 双重校验、fail-closed**:(1) **身份** ——
38-
checkout 的 `origin` remote 必须解析为该 alias 配置的 GitHub 全名
39-
(经 `intent` 的 remote/identity helper);(2) **圈禁** —— canonical
40-
路径必须落在 `settings.allowed_repo_roots` 之下(默认 = 只有已配置的
41-
checkout 本身,最小权限)。身份无法验证 = 拒绝,绝不假定。它是
42-
`TaskSpec.repo_path` 的一等入口 —— **不要**把它塞进 `_ALLOWED_PARAMS`。
43-
- **允许集是 import 进来的,绝不重述。** 它直接引用 `task_spec.READ_ONLY_KINDS`,
44-
因此策略永远不会与任务模型漂移 —— 新增一个具备写能力的 kind,**无法**静默地把 MCP
45-
面放宽。
46-
- MCP 宿主是**非交互**的:这里没有 `[y/N]`,所以本文件里任何逻辑都不得退化成"问用户"。
47-
48-
## 边界 —— 不属于这里
49-
不执行 run;不调模型;不访问知识。
50-
51-
## 依赖(允许)
52-
`..task_spec` + stdlib;外加 `authorize_repo_path` 内部**函数级**引入的
53-
`.intent`(remote→GitHub 身份解析)—— 与 `config.md` 记录函数级 import 的
54-
方式一致的刻意例外:身份判定必须与 intent 的 URL 路由用同一套解析,
55-
否则两处各养一份就会漂移。模块导入期仍然只依赖 task_spec + stdlib。
56-
57-
## 测试
58-
`test_mcp.py`(篡改防御、只读工具集、`expected_head_sha` 贯穿与短 SHA 拒绝、
59-
Strict 显式 post 被拒);`test_contract.py`(repo_path 身份不符 / 圈外 /
60-
身份不可验证均拒绝;冻结 repo_path 直达解析与执行、压过环境路径)。
61-
62-
## 重构备注
63-
和 `push.guard_push`、`scopes` 一样,这是一个纯权限原语 —— 保持它无依赖。任何新的
64-
"MCP 可达能力"都必须表达为对 `READ_ONLY_KINDS` 的修改,**而不是这里的一个特例**。
65-
66-
## Carried finding rechecks
67-
68-
Direct 1.1 / Strict 1.3 accept typed `CarriedFinding` inputs (unique IDs, source head, severity, location, title and evidence). Direct binds the carried set into its issued context and validates `FindingRecheck` answers. Strict passes the set through the policy allowlist, performs a read-only recheck against the frozen PR head, and persists answers for the public structured result. Every carried ID needs an explicit outcome: `fixed`, `still_affected`, or `unverified`. Missing evidence is never a fix.
12+
Do not add a second policy decision here. Existing callers importing this
13+
module retain the same callable objects and exception type.

0 commit comments

Comments
 (0)