recall离线项目记忆
§ · Nested detail page

捕获、路径限制与 git hardening 深度解析

本页只展开一个模块,把导入契约、核心实现、流程和失败模式拆成可回溯行号。

§ 00 · IMPORT CHAIN

导入链与契约

入口代码说明该模块接受什么输入、输出什么结构,以及它依赖哪些相邻模块。

▸入口片段

src/scripts/common.py · L26-61 repo: scripts/common.pyPython
  26def output_dir(cwd, cfg):
  27    """Resolve the output dir, confined to within cwd, or None if none is safe.
  28
  29    A project-local config is itself untrusted (it ships in the repo), so it must
  30    not be able to redirect writes outside the project via an absolute path or
  31    `..`. A pre-planted symlink at the default `.recall` is just as untrusted: an
  32    escaping config value falls back to `.recall`, but if even that resolves
  33    outside cwd (because `.recall` is a symlink), we return None and refuse to
  34    write rather than land outside the project. Callers treat None as "skip".
  35    """
  36    base = os.path.realpath(cwd)
  37    requested = cfg.get("output_dir", ".recall") or ".recall"
  38    for candidate in (requested, ".recall"):
  39        target = os.path.realpath(os.path.join(cwd, candidate))
  40        if target == base or target.startswith(base + os.sep):
  41            return target
  42    return None
  43
  44
  45def _in_output_dir(cwd, cfg, name):
  46    d = output_dir(cwd, cfg)
  47    return os.path.join(d, name) if d else None
  48
  49
  50def context_path(cwd, cfg):
  51    return _in_output_dir(cwd, cfg, "context.md")
  52
  53
  54def history_path(cwd, cfg):
  55    return _in_output_dir(cwd, cfg, "history.md")
  56
  57
  58def state_path(cwd, cfg):
  59    return _in_output_dir(cwd, cfg, ".capture.json")
  60
  61

源码索引:src/scripts/common.py:26 · src/scripts/common.py:27 · src/scripts/common.py:28 · src/scripts/common.py:29 · src/scripts/common.py:30 · src/scripts/common.py:31 · src/scripts/common.py:32 · src/scripts/common.py:33 · src/scripts/common.py:34 · src/scripts/common.py:35 · src/scripts/common.py:36 · src/scripts/common.py:37 · src/scripts/common.py:38 · src/scripts/common.py:39 · src/scripts/common.py:40 · src/scripts/common.py:41 · src/scripts/common.py:42 · src/scripts/common.py:43 · src/scripts/common.py:44 · src/scripts/common.py:45 · src/scripts/common.py:46 · src/scripts/common.py:47 · src/scripts/common.py:48 · src/scripts/common.py:49 · src/scripts/common.py:50 · src/scripts/common.py:51 · src/scripts/common.py:52 · src/scripts/common.py:53 · src/scripts/common.py:54 · src/scripts/common.py:55 · src/scripts/common.py:56 · src/scripts/common.py:57 · src/scripts/common.py:58 · src/scripts/common.py:59 · src/scripts/common.py:60 · src/scripts/common.py:61 · src/scripts/common.py:62 · src/scripts/common.py:63 · src/scripts/common.py:64 · src/scripts/common.py:65 · src/scripts/common.py:66 · src/scripts/common.py:67 · src/scripts/common.py:68 · src/scripts/common.py:69 · src/scripts/common.py:70

§ 01 · CORE IMPL

核心实现,逐段拆

▸中段状态转换

这一段通常体现最关键的状态转换:解析、选择、打分、写入或安全检查。

src/scripts/common.py · L62-101 repo: scripts/common.pyPython
  62def pause_path(cwd, cfg):
  63    return _in_output_dir(cwd, cfg, ".capture-paused")
  64
  65
  66def read_text(path):
  67    if not path:
  68        return ""
  69    try:
  70        with open(path, "r", encoding="utf-8") as fh:
  71            return fh.read()
  72    except OSError:
  73        return ""
  74
  75
  76def ensure_output_dir(cwd, cfg):
  77    d = output_dir(cwd, cfg)
  78    if not d:
  79        return None
  80    os.makedirs(d, exist_ok=True)
  81    return d
  82
  83
  84def _open_nofollow(path, append):
  85    """Open for writing without following a symlink at the final path component.
  86
  87    Prevents a pre-planted symlink (e.g. history.md -> /etc/something) from
  88    redirecting our writes. Raises OSError (ELOOP) if the target is a symlink.
  89    """
  90    flags = os.O_WRONLY | os.O_CREAT | (os.O_APPEND if append else os.O_TRUNC)
  91    if hasattr(os, "O_NOFOLLOW"):
  92        flags |= os.O_NOFOLLOW
  93    return os.fdopen(os.open(path, flags, 0o644), "w", encoding="utf-8")
  94
  95
  96def write_text(path, data):
  97    with _open_nofollow(path, append=False) as fh:
  98        fh.write(data)
  99
 100
 101def append_text(path, data):

▸末段收束

收束段决定失败是否显式返回、结果是否稳定落盘,以及调用者能否继续恢复。

src/scripts/common.py · L159-194 repo: scripts/common.pyPython
 159        )
 160        return out.stdout.strip() if out.returncode == 0 else ""
 161    except (OSError, subprocess.SubprocessError):
 162        return ""
 163
 164
 165def _is_git_repo(cwd):
 166    return _git(cwd, ["rev-parse", "--is-inside-work-tree"]) == "true"
 167
 168
 169def git_info(cwd):
 170    if not _is_git_repo(cwd):
 171        return ""
 172    parts = []
 173    stat = _git(cwd, ["diff", "--no-ext-diff", "--stat"])
 174    if stat:
 175        parts.append("Uncommitted changes (git diff --stat):\n" + stat)
 176    log = _git(cwd, ["log", "--oneline", "-8"])
 177    if log:
 178        parts.append("Recent commits:\n" + log)
 179    return "\n\n".join(parts)
 180
 181
 182def git_uncommitted(cwd):
 183    """List of paths with uncommitted changes (porcelain), or []."""
 184    if not _is_git_repo(cwd):
 185        return []
 186    out, seen = [], set()
 187    for line in _git(cwd, ["status", "--porcelain"]).splitlines():
 188        path = line[3:].strip()  # "XY <path>" (rename/copy shows "old -> new")
 189        if " -> " in path:
 190            path = path.split(" -> ", 1)[1].strip()
 191        if path and path not in seen:
 192            seen.add(path)
 193            out.append(path)
 194    return out
§ 02 · KEY FLOW

关键流程

▸流程拆解

input → normalize → validate → output
failure → explicit fallback / error record → caller decision

如果后续改动触碰这些行,主页面对应的风险判断也要重跑。

§ 03 · EDGE CASES

边界 / 失败模式

▸边界列表

  • 空输入或缺失字段必须被显式处理。
  • 外部命令、模型调用或文件写入失败不能覆盖原始上下文。
  • 路径别名仅用于索引兼容;实际源码位置见每个代码块的 repo 标注。
Sub-page · 2026-06-29Source · raiyanyahya/recall↑ recall 主页面