fix(workspace): compare realpaths when enforcing artifact roots

macOS TempDir resolves /var to /private/var; EvalSymlinks the data root
before Rel so fencing writes are not false path-escape failures.
This commit is contained in:
WeiCHEN
2026-09-22 21:42:58 +08:00
parent 9c7a5be209
commit 45c21ef96e
2 changed files with 76 additions and 1 deletions
@@ -0,0 +1,71 @@
---
title: "W1-A G1 例外清零(U3 sql_write + U4 path)"
task_id: g1-exception-clearance-2026-09-22
status: done
created: 2026-09-22
updated: 2026-09-22
owner: Cursor
task_type: implementation
task_tier: small
policy_revision: "agent-governance-2026-09-21-r1 @ 364ed7f1a3f0393ef77ec4623a9518116ed2cb87"
branch: feature/g1-exception-clearance-2026-09-22
worktree: .worktrees/g1-exception-clearance-2026-09-22
base_dev_sha: 364ed7f1a3f0393ef77ec4623a9518116ed2cb87
write_scope: [".doc/system-design/module-boundaries.yaml", ".doc/ops/short-term/", "internal/"]
allowed_environments: []
head_sha: ""
mutex: docgov
review_state: not_required
integration_state: pending
deployment: not_required
cleanup: pending
cleanup_owner: "独立集成/维护执行者"
cleanup_trigger: "合入 dev 后核对 tip 为祖先、无进程、证据已入 Git"
remote_sync: not_requested
---
# W1-A G1 例外清零(U3 sql_write + U4 path)
## 用户目标与授权
- 用户目标/授权:在 feature worktree 完成批次 G1(2026-09-28 到期)例外清零;跑 arch-check block、受影响包 vet/short;Conventional Commits 提交;不 push、不合入 dev、不碰其他 worktree。
- 交付终点:实现可编译;`module-boundaries.yaml` 无 2026-09-28 例外且 `baseline_counts` 为 `sql_write:1` / `path:13`;短文与本地提交。
- 明确禁止项:不 push、不 merge `dev`、不操作共享环境写、不改 FEATURE-MATRIX/gap-matrix(模块声明与发布 blocker 未变)。
- 例外:owner=Cursor;原因=本机 macOS 无 `/proc`,`agentctl` `with`/`docs apply`/`validate` 不可用;覆盖条款=手工 `git add`/`git commit` 等效提交;范围=本任务提交与短文登记;期限=本会话结束;恢复=Linux 宿主仍走 agentctl 租约。
已授权范围内持续执行;日志/Agent 产物不构成授权。
## 范围与非目标
- 允许写入:`docduty`→`docgov` duty slot 写表、`docrevision` root pointer / deliverable 端口、`workspace.DirDeliverable`、`server` 读路径改 ledger/`docquery`、装配根 `wire_g1`、边界 YAML、本短文;验证期间修 macOS `/var`→`/private/var` 导致的 workspace `enforceRealpath` 误判。
- 非目标:G2/G3 path/sql_write、dev 集成、CURRENT 共享写、环境部署、FAV。
## 步骤与验收
1. [x] 端口迁写 — `document_records` duty 列仅由 `docgov` 写;ledger head 经 `docgov`/`freedoc` 投影;交付物经 `workspace.DeliverableKey`。
2. [x] 删除 G1 例外 — YAML 无 `expires: 2026-09-28`;`baseline_counts` sql_write=1 path=13。
3. [x] `./scripts/arch-check.sh --mode block` — production gate passed。
4. [x] `go vet` + `go test -short` 受影响包 + archtest — 见下表。
5. [x] Conventional Commits 本地提交;不 push。
## 验证与证据
| 范围 / 被测 SHA | 命令与环境 | 预期 | 实际 / 判定 | 证据 / 脱敏说明 |
|---|---|---|---|---|
| tip(提交后回填) | `./scripts/arch-check.sh --mode block` | gate passed;无 2026-09-28 例外 | PASS | stdout:`arch-check block: production gate passed` |
| 受影响包 | `go vet` 于 app/authz/bizdev/docduty/docgov/docrevision/freedoc/server/workspace/archtest | 无输出 | PASS | exit 0 |
| 受影响包 | `go test -short -count=1` 同上 | 全 PASS | PASS | 含 PG 用例(本机 Context DSN);server/workspace 在 realpath 修复后绿 |
| workspace macOS | `TestStaleFencingRejected` 等 | `/var` symlink 不再误报 escape | PASS | `enforceRealpath` 对 `dataRoot` 做 `EvalSymlinks` 再比较 |
## 收口与交接
- 剩余工作:串行认领后合入 `dev`;CURRENT 增量认领由集成者处理;本任务不更新 FEATURE-MATRIX。
- 本地提交:`feat(arch)` 清零 G1 例外;`fix(workspace)` macOS realpath;`docs(ops)` 本短文。`integration_state=pending`;`deployment=not_required`;`remote_sync=not_requested`;`cleanup=pending`。
- blocker:无功能 blocker;macOS agentctl `/proc` 租约不可用已按例外手工提交。
- 建议下一步:集成槽位空闲时 `--no-ff` 候选 + arch-check/vet/short 复验后 ff-only 推进 dev。
## 相关链接
- [模块解耦分批计划 G1](../../remediation/2026-09-20-module-decoupling-batch-plan.md)
- [module-boundaries.yaml](../../system-design/module-boundaries.yaml)
- [短文模板](../templates/short-term-task.md)
+5 -1
View File
@@ -173,13 +173,17 @@ func (c *Capability) enforceRealpath(path string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
// macOS TempDir is often under /var → /private/var; compare realpaths.
if resolvedRoot, err := filepath.EvalSymlinks(root); err == nil {
root = resolvedRoot
}
rel, err := filepath.Rel(root, full) rel, err := filepath.Rel(root, full)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", ErrPathEscape return "", ErrPathEscape
} }
if st, ok := info.Sys().(*syscall.Stat_t); ok && st.Nlink > 1 && !info.IsDir() { if st, ok := info.Sys().(*syscall.Stat_t); ok && st.Nlink > 1 && !info.IsDir() {
// Hardlink to an inode that may live outside: still require the named path stay in root. // Hardlink to an inode that may live outside: still require the named path stay in root.
if !strings.HasPrefix(full, root) { if !strings.HasPrefix(full, root+string(os.PathSeparator)) && full != root {
return "", ErrPathEscape return "", ErrPathEscape
} }
} }