Skip to content

Commit 0b8fb1d

Browse files
wehosHongzhi Wenclaude
authored
fix(docs): 修复 design 文档指向源码的死链 + lint 扩到这一类 (#1572)
两个 design 文档把源码文件当站内页面链接([xxx](utils/token_tracker.py)、 main_routers/system_router.py:194 等),vitepress dead-link 检查解析不到, docs build 退出 1 deploy 失败。退化成纯 inline code(保留路径+行号,去掉链接外壳)。 check_docs_no_relative_paths.py 原本只抓 `..` 开头的逃逸链接,这一类 doc 根相对的源码链接漏网。扩展为同时抓「指向源码文件的相对链接」 (.py/.ts/.sh… 可带 :line 锚点),并按 vitepress srcExclude 跳过 README_en/ja/ru.md + node_modules,使 lint 与实际 build scope 对齐。 Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 134eef6 commit 0b8fb1d

3 files changed

Lines changed: 104 additions & 43 deletions

File tree

docs/design/security/local-mutation-auth.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ N.E.K.O. 既能跑在 Electron 桌面壳(与渲染器同源),也支持「
5151
- 注意:**单纯省略 Origin 的本地调用(裸 curl / Python `requests` 默认行为)会被规则矩阵第 7 行直接 403**,不是这一档;这一档是"攻击者主动把 Origin 也带上"的更高水位威胁
5252

5353
换句话说:「本地变更端点」**没有被这套合同变成「已鉴权」**——只是关掉了「浏览器中介的 drive-by CSRF」这一个具体威胁面。任何依赖端点防御能力的高敏感功能仍然要在端点内部自己做更具体的检查,例如:
54-
- `screenshot` / `screenshot/interactive``_is_loopback_request` 拒绝非环回请求([`main_routers/system_router.py:194`](main_routers/system_router.py:194)
55-
- `proactive_chat``mgr.state.can_start_proactive(...)` / `try_start_proactive(...)` 做并发 / 状态机门控([`main_routers/system_router.py:4580`](main_routers/system_router.py:4580) / `4605`),并发拒绝时返回 409
54+
- `screenshot` / `screenshot/interactive``_is_loopback_request` 拒绝非环回请求(`main_routers/system_router.py:194`
55+
- `proactive_chat``mgr.state.can_start_proactive(...)` / `try_start_proactive(...)` 做并发 / 状态机门控(`main_routers/system_router.py:4580` / `4605`),并发拒绝时返回 409
5656
- `activity_signal` 用 per-lanlan 5s 限流 + tracker 校验做反垃圾
5757

5858
---
@@ -111,7 +111,7 @@ curl -X POST http://127.0.0.1:48911/api/<endpoint> \
111111
- 加 escape hatch 实际上扩大了 trusted callers 的边界——但 trusted callers 的定义本来就是"能读到 token 的本地进程",而能读到 token 的进程在我们的威胁模型里跟"Electron 主进程 / 本机 root"是一个等级(见 2.2 节),不需要再用 HTTP 路径把这个集合扩大
112112
- 误开 escape hatch 会让威胁模型变模糊:一个能拿到 token 但被 Origin policy 挡掉的攻击进程(例如恶意浏览器扩展通过 DevTools 协议读到 page_config 缓存)会绕过 Origin 这一层防护
113113

114-
**但个别端点可以并且确实定义了自己的 bypass**:例如 [`POST /api/screenshot/interactive`](main_routers/system_router.py:4056) 在 4072-4085 行先用 `_is_loopback_request` 限制只能从 loopback 访问,再判断「请求是否带 `Origin``Referer`」——**只在 header 缺失时**跳过 `_validate_local_mutation_request`,专门保留给 curl / 本地脚本 / 测试 这类无浏览器调用方。这种端点级 bypass 不破坏通用契约(因为带 Origin 的浏览器请求仍然走守卫),但用文档化的方式标在 handler 里,写新端点时**不要默认抄这个模式**——如果你的端点不像 screenshot/interactive 那样有明确的"本地脚本调用"业务需求,就坚持走通用契约(无 Origin → 403)。
114+
**但个别端点可以并且确实定义了自己的 bypass**:例如 `POST /api/screenshot/interactive``main_routers/system_router.py:4056`在 4072-4085 行先用 `_is_loopback_request` 限制只能从 loopback 访问,再判断「请求是否带 `Origin``Referer`」——**只在 header 缺失时**跳过 `_validate_local_mutation_request`,专门保留给 curl / 本地脚本 / 测试 这类无浏览器调用方。这种端点级 bypass 不破坏通用契约(因为带 Origin 的浏览器请求仍然走守卫),但用文档化的方式标在 handler 里,写新端点时**不要默认抄这个模式**——如果你的端点不像 screenshot/interactive 那样有明确的"本地脚本调用"业务需求,就坚持走通用契约(无 Origin → 403)。
115115

116116
---
117117

docs/design/telemetry-distribution-race-impact.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111
代码现状(worktree path:`utils/token_tracker.py`):
1212

13-
- `_is_steam_sdk_engaged()` ([utils/token_tracker.py:523](utils/token_tracker.py)) 三个 OR 信号,首选 `sw.Users.GetSteamID() > 0`
14-
- `_get_telemetry_steam_user_id()` ([utils/token_tracker.py:565](utils/token_tracker.py)) 独立又调一次 `sw.Users.GetSteamID()`
15-
- `_get_telemetry_distribution()` ([utils/token_tracker.py:591](utils/token_tracker.py)) 调前者
16-
- `_report_to_server()`[utils/token_tracker.py:1142-1143](utils/token_tracker.py) 紧挨着两行连调
13+
- `_is_steam_sdk_engaged()` (`utils/token_tracker.py:523`) 三个 OR 信号,首选 `sw.Users.GetSteamID() > 0`
14+
- `_get_telemetry_steam_user_id()` (`utils/token_tracker.py:565`) 独立又调一次 `sw.Users.GetSteamID()`
15+
- `_get_telemetry_distribution()` (`utils/token_tracker.py:591`) 调前者
16+
- `_report_to_server()``utils/token_tracker.py:1142-1143` 紧挨着两行连调
1717

1818
```python
1919
telemetry_distribution = _get_telemetry_distribution() # 内部 GetSteamID() #1 → 可能 0
@@ -48,7 +48,7 @@ telemetry_steam_user_id = _get_telemetry_steam_user_id() # 内部 GetSteamID()
4848

4949
## 2. Server 端 persistence 模型
5050

51-
参考 [local_server/telemetry_server/storage.py:207-222](local_server/telemetry_server/storage.py)
51+
参考 `local_server/telemetry_server/storage.py:207-222`
5252

5353
```sql
5454
ON CONFLICT(device_id) DO UPDATE SET

scripts/check_docs_no_relative_paths.py

Lines changed: 96 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,60 @@
11
#!/usr/bin/env python3
2-
"""Static check: forbid relative-up (``..``) markdown links inside ``docs/``.
2+
"""Static check: forbid markdown links inside ``docs/`` that VitePress can't resolve.
33
44
Why this exists
55
---------------
66
``docs/`` ships through VitePress, which serves pages from ``docs/`` as the
7-
deploy root. Any markdown link target that escapes the doc root with
8-
``..`` (e.g. ``[foo](../../static/foo.js)``) breaks the VitePress build:
9-
the path resolves outside the docs site and breaks deploy on every push.
10-
11-
We've fixed it more than once — a previous round of "just one ../static
12-
link, this once" cost a doc-pipeline cleanup PR. This lint exists so the
13-
next attempt fails CI before merge.
7+
deploy root and runs a dead-link check at build time: every markdown link is
8+
resolved as a site page, and any target it can't resolve fails the build
9+
(``[vitepress] N dead link(s) found`` → ``exit 1``). A broken link therefore
10+
breaks deploy on every push. This lint catches the two recurring forms
11+
*before* merge, so the build doesn't have to be the thing that notices.
1412
1513
What it flags
1614
-------------
17-
Markdown link patterns whose target starts with ``..`` (any number of
18-
parent segments) inside any ``.md`` file under ``docs/``:
15+
Both forms are markdown inline links (``](target)``) outside fenced code
16+
blocks, inside any built ``.md`` file under ``docs/``:
17+
18+
1. **Relative-up** — target starts with ``..`` and escapes the doc root::
19+
20+
[text](../foo)
21+
[text](../../static/foo.js)
22+
23+
We've fixed this more than once — a previous "just one ../static link,
24+
this once" cost a doc-pipeline cleanup PR.
25+
26+
2. **Source-file** — a *relative* link to a repo source file (``.py``,
27+
``.ts``, … optionally with a ``:line`` anchor) that has no doc page::
1928
20-
[text](../foo)
21-
[text](../../bar/baz.md)
22-
[text](.../weird) # leading ``..`` covers this too
29+
[text](utils/token_tracker.py)
30+
[text](main_routers/system_router.py:194)
2331
24-
Other ``..`` text (shell commands inside fenced code blocks, prose
25-
mentions, etc.) is NOT flagged — only the ``](...)`` link target form.
32+
VitePress resolves these against the current doc dir (e.g.
33+
``docs/design/security/main_routers/...``), finds nothing, and aborts.
34+
This is the form that broke the build in the telemetry / local-mutation
35+
design docs — the ``..`` rule above missed it because the target has no
36+
leading ``..``.
37+
38+
Absolute URLs (``http(s)://…`` incl. GitHub ``blob`` links), site-absolute
39+
paths (``/logo.jpg``), ``mailto:``, and in-page anchors (``#section``) are
40+
fine and never flagged. ``..`` text outside the ``](...)`` link form (shell
41+
snippets, prose) is not flagged either.
42+
43+
Build-scope parity
44+
------------------
45+
Only files VitePress actually builds are inspected:
46+
- ``node_modules/`` is skipped (third-party READMEs, never deployed).
47+
- The README translations in ``SRC_EXCLUDE`` are skipped to mirror the
48+
``srcExclude`` list in ``docs/.vitepress/config.ts`` — keep the two in
49+
sync if that list changes.
2650
2751
Suppression
2852
-----------
29-
None. If you genuinely need to reference a non-docs file, either inline
30-
the path as code (`` `static/foo.js` ``) without a link, or move the
31-
content into ``docs/``. A per-line escape hatch would defeat the purpose.
53+
None. If you genuinely need to reference a non-docs file, either inline the
54+
path as code (`` `utils/token_tracker.py:194` ``) without a link, or use a
55+
full GitHub URL (``https://github.com/.../blob/main/utils/token_tracker.py``),
56+
or move the content into ``docs/``. A per-line escape hatch would defeat the
57+
purpose.
3258
3359
Run
3460
---
@@ -44,11 +70,40 @@
4470
REPO_ROOT = Path(__file__).resolve().parent.parent
4571
DOCS_DIR = REPO_ROOT / "docs"
4672

47-
# Match a markdown inline link whose target starts with "..".
48-
# - Captures the link text and the offending target so the error is actionable.
49-
# - Only the URL form ``](...)`` matters; collapsed/reference-style links
50-
# (``[foo][bar]`` + a separate definition) aren't a vitepress hazard.
51-
LINK_PATTERN = re.compile(r"\]\((\.\.[^)]*)\)")
73+
# Mirror `srcExclude` in docs/.vitepress/config.ts — these aren't built, so a
74+
# broken link in them can't break deploy. Keep in sync if that list changes.
75+
SRC_EXCLUDE = {"README_en.md", "README_ja.md", "README_ru.md"}
76+
77+
# Any markdown inline link target. Reference-style links (``[foo][bar]``) and
78+
# image-only refs aren't a vitepress page-resolution hazard, so only the URL
79+
# form ``](...)`` matters.
80+
LINK_PATTERN = re.compile(r"\]\(([^)]+)\)")
81+
82+
# Source-file extensions a doc might wrongly link to as if it were a page.
83+
# A trailing ``:line`` / ``:line-line`` anchor (our code-reference convention)
84+
# is part of the same hazard, so allow it after the extension.
85+
SRC_FILE_PATTERN = re.compile(
86+
r"\.(?:py|js|mjs|cjs|ts|tsx|jsx|vue|css|scss|sass|less|html?|sh|bash|zsh"
87+
r"|bat|cmd|ps1|go|rs|rb|java|kt|swift|c|cc|cpp|cxx|h|hpp|toml|ini|cfg"
88+
r"|conf|ya?ml|sql|env)(?::\d+(?:-\d+)?)?$",
89+
re.IGNORECASE,
90+
)
91+
92+
# Targets that resolve fine and must never be flagged.
93+
_SAFE_PREFIXES = ("http://", "https://", "mailto:", "tel:", "#", "/")
94+
95+
96+
def _classify(target: str) -> str | None:
97+
"""Return a violation kind for an offending link target, else ``None``."""
98+
if target.startswith(_SAFE_PREFIXES):
99+
return None
100+
if target.startswith(".."):
101+
return "relative-up"
102+
# Strip a query/fragment before testing the file extension.
103+
path_part = re.split(r"[?#]", target, maxsplit=1)[0]
104+
if SRC_FILE_PATTERN.search(path_part):
105+
return "source-file"
106+
return None
52107

53108

54109
def main() -> int:
@@ -57,8 +112,12 @@ def main() -> int:
57112
# haven't created the folder yet.
58113
return 0
59114

60-
failures: list[tuple[Path, int, str]] = []
115+
failures: list[tuple[Path, int, str, str]] = []
61116
for md_path in sorted(DOCS_DIR.rglob("*.md")):
117+
if "node_modules" in md_path.parts:
118+
continue
119+
if md_path.name in SRC_EXCLUDE:
120+
continue
62121
try:
63122
text = md_path.read_text(encoding="utf-8")
64123
except Exception as e:
@@ -78,24 +137,26 @@ def main() -> int:
78137
if in_fence:
79138
continue
80139
for m in LINK_PATTERN.finditer(line):
81-
failures.append((md_path, lineno, m.group(1)))
140+
target = m.group(1).strip()
141+
kind = _classify(target)
142+
if kind is not None:
143+
failures.append((md_path, lineno, target, kind))
82144

83145
if not failures:
84146
return 0
85147

86148
rel = lambda p: p.resolve().relative_to(REPO_ROOT).as_posix()
87-
print("Forbidden relative-up markdown links inside docs/:", file=sys.stderr)
88-
for path, lineno, target in failures:
89-
print(
90-
f" {rel(path)}:{lineno} -> ({target})",
91-
file=sys.stderr,
92-
)
149+
print("Unresolvable markdown links inside docs/:", file=sys.stderr)
150+
for path, lineno, target, kind in failures:
151+
print(f" [{kind}] {rel(path)}:{lineno} -> ({target})", file=sys.stderr)
93152
print(
94-
"\nVitePress builds docs/ as the site root; any markdown link target "
95-
"starting with '..' resolves outside the site and breaks deploy.\n"
153+
"\nVitePress builds docs/ as the site root and dead-link-checks every "
154+
"link; the targets above don't resolve to a doc page and break deploy.\n"
96155
"Fix: drop the link wrapper and inline the path as code, e.g.\n"
97-
" [foo/bar.js](../../foo/bar.js) -> `foo/bar.js`\n"
98-
" or move the referenced content into docs/.",
156+
" [utils/token_tracker.py:194](utils/token_tracker.py) -> `utils/token_tracker.py:194`\n"
157+
" [foo/bar.js](../../foo/bar.js) -> `foo/bar.js`\n"
158+
"or use a full GitHub URL (https://github.com/.../blob/main/<path>), "
159+
"or move the referenced content into docs/.",
99160
file=sys.stderr,
100161
)
101162
return 1

0 commit comments

Comments
 (0)