Skip to main content
Glama
Asaad-Suliman

safe-mcp-suite

safe-mcp-suite

两个 MCP 服务器 —— 一个终端和一个文件整理器 —— 共享同一个安全核心,两者都不允许绕过它。

Python 3.12+ License: MIT CI MCP


这是什么

在 GitHub 上搜索一个运行 shell 命令的 MCP 服务器,你会一遍又一遍地看到同一个文件:一个用 @mcp.tool() 装饰的工具,一个对 subprocess.run(command, shell=True) 的调用,然后把结果直接交回给模型。文件系统类的也是同样的形状 —— 一个循环里的 os.rename,也许外面包一个 try/except。

它们能用。这就是问题所在。它们一直能用,直到模型产生出某个没人预料到的东西,而到那时删除已经发生了,而且没有任何记录说明运行了什么、为什么被允许。

这个仓库就是把这两个服务器重建,让安全就是设计本身,而不是事后加上的包装。一个单一的 safety/ 包拥有每一个可能伤害你的决策 —— 什么被允许、边界在哪里、什么被记录下来、什么被隐藏。底下的两个服务器只是接线。两者都无法越过核心,因为两者都没有实现自己的任何规则。

它背后的赌注是:确定性策略比模型判断更值得信赖。 提示词可以说服模型不再小心。它无法说服一个路径包含检查返回 True。


Related MCP server: Safe Terminal MCP Server

快速开始

你需要 Python 3.12+ 和 uv。

git clone https://github.com/Asaad-Suliman/safe-mcp-suite.git safe-mcp-suite
cd safe-mcp-suite
uv sync
./scripts/make_demo_sandbox.sh

最后一个脚本会播种 sandbox/terminal、sandbox/files 和 state/,这样两个服务器都有合法的操作空间。然后启动你想要的那个:

uv run safe-mcp terminal --config policy.example.toml
uv run safe-mcp files --config policy.example.toml

关于那个 --config 标志

它不是可选的,也没有回退。没有隐式的 ./policy.toml 查找,没有 .env 加载,没有悄悄指向你主目录的默认根目录。如果既没有设置 --config 也没有设置 SAFE_MCP_POLICY_FILE,服务器会打印原因然后退出。

这是故意的,也是整个设置中最有主见的一点。一个默默默认到某个方便位置的沙箱,总有一天会默默默认到某个昂贵的位置。拒绝启动是最便宜的失败方式。

仓库附带两个策略文件,它们不可互换:

文件

它是什么

会启动吗?

policy.example.toml

可用的示例,根目录在 ./sandbox

会 —— 现在就能运行

policy.toml

带注释的模板,根目录被注释掉了

不会,故意如此

policy.toml 拒绝启动,直到你自己填上 jail_root 和 workspace_root。这种拒绝是一个特性,而且有一个回归测试在维持它。复制它、编辑它、准备好后指向真实目录。

根目录也可以来自环境变量,如果你更喜欢的话:

SAFE_MCP_JAIL_ROOT=/path/to/jail
SAFE_MCP_WORKSPACE_ROOT=/path/to/workspace

注册到 MCP 客户端

{
  "mcpServers": {
    "safe-mcp terminal": {
      "command": "uv",
      "args": ["run", "safe-mcp", "terminal"],
      "env": {
        "SAFE_MCP_POLICY_FILE": "/srv/safe-mcp/policy.example.toml",
        "SAFE_MCP_JAIL_ROOT": "/srv/safe-mcp/sandbox"
      }
    },
    "safe-mcp files": {
      "command": "uv",
      "args": ["run", "safe-mcp", "files"],
      "env": {
        "SAFE_MCP_POLICY_FILE": "/srv/safe-mcp/policy.example.toml",
        "SAFE_MCP_WORKSPACE_ROOT": "/srv/safe-mcp/inbox"
      }
    }
  }
}

演示

下面的一切都是真实捕获的输出。这里没有任何手写、修剪或事后美化的内容 —— 这些是实时 MCP 客户端与两个服务器对话时返回的实际 OperationResult 信封,运行在 ./scripts/make_demo_sandbox.sh 播种的沙箱上,使用 policy.example.toml。

阅读每一个中的 code 字段。那个分类法就是全部要点:每一个结果,无论成功还是拒绝,都以相同的形状返回。

终端服务器

一个被允许的命令:

>>> tool: run_command  args: {"command": "cat notes.txt"}
{
  "action": "run_command",
  "code": "OK",
  "detail": {
    "exit_code": 0,
    "stderr": "",
    "stdout": "demo file\n"
  },
  "duration_ms": 1,
  "ok": true,
  "reason": "'cat' is allowed"
}

一个被拒绝的命令:

>>> tool: run_command  args: {"command": "rm -rf /"}
{
  "action": "run_command",
  "code": "POLICY_DENIED",
  "detail": {},
  "duration_ms": 0,
  "ok": false,
  "reason": "'rm' is on the denylist"
}

注意拒绝不是什么:不是回溯、不是抛出的异常、也不是模型必须猜测的字符串。它是一个带原因的类型化代码。

文件服务器

这个序列运行在一个播种的工作区上,其中包含普通文件、一个安装程序、一个点文件和一条符号链接 —— 策略区别对待的每一种东西各一个。

plan_organize 提议移动并列出跳过项:

>>> tool: plan_organize  args: {}
{
  "action": "plan_organize",
  "code": "OK",
  "detail": {
    "created": "2026-08-07T16:49:00.048899+00:00",
    "move_count": 2,
    "moves": [
      {
        "category": "Images",
        "dest": "Images/photo.png",
        "size": 41,
        "src": "photo.png"
      },
      {
        "category": "Documents",
        "dest": "Documents/report.pdf",
        "size": 16,
        "src": "report.pdf"
      }
    ],
    "plan_id": "7dcf2cff-399d-4af5-bb9a-a4131e1d5288",
    "skip_count": 3,
    "skips": [
      {
        "code": "NEEDS_EXPLICIT_REQUEST",
        "name": ".bashrc",
        "reason": "dotfiles are configuration, not clutter to be filed",
        "rule": "organize"
      },
      {
        "code": "POLICY_DENIED",
        "name": "link.pdf",
        "reason": "'organize' is not permitted by any rule (deny by default)",
        "rule": null
      },
      {
        "code": "NEEDS_EXPLICIT_REQUEST",
        "name": "setup.exe",
        "reason": "installers, executables and application folders are left where the user put them",
        "rule": "organize"
      }
    ],
    "truncated": false
  },
  "duration_ms": 0,
  "ok": true,
  "reason": "proposed 2 move(s), skipped 3"
}

apply_plan 执行同一个计划:

>>> tool: apply_plan  args: {"plan_id": "7dcf2cff-399d-4af5-bb9a-a4131e1d5288"}
{
  "action": "apply_plan",
  "code": "OK",
  "detail": {
    "moved": 2,
    "moves": [
      {
        "dest": "Images/photo.png",
        "src": "photo.png"
      },
      {
        "dest": "Documents/report.pdf",
        "src": "report.pdf"
      }
    ],
    "plan_id": "7dcf2cff-399d-4af5-bb9a-a4131e1d5288",
    "planned": 2
  },
  "duration_ms": 2,
  "ok": true,
  "reason": "moved 2 file(s)"
}

现在是有趣的一对。同一个工具,两个具名目标,两个不同的答案。

move_file 被 PROTECTION 拒绝 —— 就是上面 plan_organize 跳过的同一条符号链接,被显式命名:

>>> tool: move_file  args: {"src": "link.pdf", "dest": "Documents/link.pdf"}
{
  "action": "move_file",
  "code": "POLICY_DENIED",
  "detail": {},
  "duration_ms": 0,
  "ok": false,
  "reason": "'organize' is not permitted by any rule (deny by default)"
}

move_file 成功移动了 plan_organize 以 NEEDS_EXPLICIT_REQUEST 推迟的安装程序,现在被显式命名:

>>> tool: move_file  args: {"src": "setup.exe", "dest": "Documents/setup.exe"}
{
  "action": "move_file",
  "code": "OK",
  "detail": {
    "dest": "Documents/setup.exe",
    "moved": 1,
    "src": "setup.exe"
  },
  "duration_ms": 0,
  "ok": true,
  "reason": "moved setup.exe"
}

规划器拒绝了两者。直接询问时,移动器拒绝了一个、做了另一个。这个差异不是不一致 —— 它是两层模型,下面有专门的一节讲它。


系统设计

服务器是接线,不是策略

两个服务器文件都不包含安全规则。每一条都存在于 safety/ 中,两个服务器都调用相同的函数来获得相同的答案。

flowchart TD
    A["MCP client"] --> B["terminal server"]
    A --> C["files server"]
    B --> D["safety/policy.py<br/>allow or deny"]
    C --> D
    D --> E["safety/paths.py<br/>PathJail containment"]
    E --> F["execute or move"]
    F --> G["safety/redact.py<br/>secrets out, then truncate"]
    G --> H["safety/audit.py<br/>append-only JSONL"]
    H --> I["OperationResult"]
    I --> A

这不是为了整洁而整洁。它意味着一个安全修复只落在一个地方,也意味着审查这个仓库的审阅者读 safety/ 就够了。没有第二个实现藏在服务器模块里,悄悄与第一个漂移。

请求实际如何流动

  1. 解析。 命令字符串被拆分成 argv 列表。Shell 元字符在这里被拒绝,在任何东西被解释之前 —— 包括引号内的。最后一点是刻意保守的,它是一个记录在案的天花板,不是疏忽。

  2. 评估。 策略引擎回答允许或拒绝。拒绝永远获胜。任何未被明确允许的东西都被拒绝,所以一个空策略是一个无用的服务器,而不是一个开放的服务器。

  3. 包含。 每个路径都被解析并对照监狱根目录检查。符号链接作为链接被检查,永远不会被穿透。

  4. 执行。 subprocess.run 使用 shell=False、一个被擦洗的环境(恰好是 PATH、HOME、LANG)、一个超时和一个输出上限。或者在文件侧,一个记录在日志中的受保护移动。

  5. 脱敏,然后截断。 按这个顺序,总是。

  6. 记录。 追加到审计日志。如果那次写入失败,操作也随之失败。

六个值得偷走的决策

错误是数据,不是异常。 每一个结果都是一个带有类型化 ResultCode 的 OperationResult。没有回溯会到达客户端。收到堆栈跟踪的模型会试图绕过它;收到 POLICY_DENIED 的模型被告知了有用且明确的东西。

拒绝永远获胜。 允许规则和拒绝规则不会被加权或排序成平局决胜。如果拒绝匹配,答案就是否。参数规则按顺序无关的方式匹配令牌,所以重新排列标志不是绕过。

没有显式根目录就不启动。 在快速开始中讲过,但它也属于设计清单,因为"合理的默认值"是大多数沙箱获得第一次逃逸的方式。

审计日志被允许阻止你。 默认故障关闭:如果日志无法写入,操作就不会发生。你可以用 SAFE_MCP_AUDIT_FAIL_MODE 把它翻成故障开放,而那是你故意做出的决定,在一个配置文件里,有人能看到它。

先脱敏再截断。 把这两个反过来,一个 64KB 的输出上限可以把一个秘密切成两半并发出第一个片段,没有匹配任何模式。这是一个两行的排序选择,关闭了整整一类泄漏。

基于熵的秘密检测默认关闭。 它会对哈希、UUID 和 base64 负载不断触发。一个喊狼来了的脱敏器会被它自己的用户禁用,这比一个事先承认自己局限性的更糟。

PROTECTION 与 RESTRAINT

跳过规则分成两层,每条规则都声明自己属于哪一层。

PROTECTION 被每个工具强制执行,无一例外。其中只有一部分是声明的规则 —— unsafe-name 覆盖文件名中的控制字符。其余是结构性的:路径逃逸在 safety/paths.py 中失败包含检查,被占用的目标在 servers/files/apply.py 中失败 lstat 检查,符号链接根本不匹配任何允许规则,落入默认拒绝 —— 而 evaluate_layered 刻意将其归类为 PROTECTION。见下面的注释。无论哪种方式,都没有标志、没有覆盖、没有"我知道我在做什么"的参数。这些是不变量。

RESTRAINT 只由 plan_organize 强制执行。安装程序、应用程序文件夹、系统文件、隐藏文件、目录。这些并不危险 —— 它们是自动分类器不应该替你猜测的东西。规划器将它们报告为 NEEDS_EXPLICIT_REQUEST 然后继续。

结果就是上面的演示对。move_file 会移动你自己命名的安装程序,因为拒绝那是家长式作风,不是安全。它不会移动符号链接,无论你多么明确地要求,因为那是包含。

一个值得明确说明的细微差别:符号链接的拒绝原因读作 "'organize' is not permitted by any rule (deny by default)" 而不是 任何提到"符号链接"的东西。符号链接在任一层都不匹配任何允许规则, 所以它落入默认拒绝 —— 而 safety.policy.evaluate_layered 刻意将未匹配的默认拒绝回退归类为 PROTECTION, 作为对策略没有词汇描述的东西的最严格解读。通用措辞不是更弱的保证;上面 move_file 直接命名符号链接,就是证明。

一个我会在任何地方重复的实现细节:PROTECTION 集合是通过过滤完整规则集派生的,永远不会作为自己的列表组装。两个手工维护的列表会漂移,而这里漂移的失败模式是一条保护规则悄悄消失。过滤器不会忘记。

关于测试

661 个测试,全部封闭。没有测试写入真实状态:任何执行的东西都针对 tmp_path 运行,两个读取随附的 policy.toml 和 policy.example.toml 的套件会先把它们复制到 tmp_path。真实的审计日志或工作区永远不会被触碰。CI 在每次推送和拉取请求时运行套件加上 gitleaks 扫描。

这个数字在我添加测试的那一刻就会过时。重要的是隔离性,不是数量。


保证

每一行都指名执行它的模块和函数。如果这里的声明没有你可以打开的代码支持,它就不应该出现在表格里。

保证

执行方式

默认拒绝,两个服务器均如此

safety/policy.py evaluate() — 无匹配规则即拒绝

匹配的拒绝始终优先于匹配的允许

safety/policy.py evaluate() 扫描所有匹配;拒绝短路

终端服务器中无 shell

servers/terminal/execute.py — subprocess.run(argv, shell=False)

在任何策略检查之前拒绝 shell 元字符

servers/terminal/parse.py + safety/patterns.py(; | & < > `` $()

命令被限制在单个目录内

safety/paths.py PathJail.resolve,在 cwd 上检查

文件移动被限制在单个工作区内,最终组件处符号链接安全

safety/paths.py PathJail.resolve_for_write — 仅解析父目录,绝不跟随正在写入的名称处的链接

两层文件策略:安全不变量 vs 避免猜测

safety/policy.py PROTECTION / RESTRAINT,evaluate_layered()

目标位置永远不会被静默覆盖

servers/files/apply.py move_one() — 每次重命名前均经 lstat 检查

不存在删除工具,无论是否受保护

servers/files/server.py — 六个工具,均非删除;任何参数都不会产生删除工具

每次移动都可撤销,包括整个已应用的方案

servers/files/journal.py + undo_last_action / redo_last_action

已过期的方案不会移动任何文件

servers/files/plan.py verify() — 整个方案被拒绝,而非部分子集

挂起的命令在截止时间被终止

servers/terminal/execute.py — subprocess.run(timeout=...)

输出在脱敏之后才被截断,绝不在此之前

safety/redact.py redact_and_truncate()

子进程获得经过清理的环境

servers/terminal/execute.py — ENV_ALLOWLIST = (PATH, HOME, LANG)

机密信息从输出和审计字段中一并脱敏

safety/redact.py BUILTIN_PATTERNS,通过一个共享脱敏器应用

审计跟踪默认故障关闭

safety/audit.py AuditLogger.record() — 日志不可写则抛出异常,操作不会发生

每次拒绝都被记录,并附有原因

servers/*/server.py _serve() — 每个工具唯一的结果写入点


威胁模型 — 明确不在范围内

  • 你已加入白名单的危险命令。 如果你允许解释器或类 shell 工具(bash、python、sh、find -exec、awk、env 等),模型就能做该工具能做的任何事情。策略强度完全取决于操作者的白名单。

  • 内核/沙箱逃逸。 该限制是路径包含检查,而非内核沙箱 — 没有命名空间、cgroups 或 seccomp。参见"诚实说明"。

  • 对状态文件的主机访问。 审计日志和撤销日志对服务器而言是防篡改的,但对任何拥有主机文件系统访问权限的人来说并非防篡改。

  • 脱敏完整性。 基于模式且尽力而为;模式无法识别的机密形态会直接通过。

  • 多租户身份或速率限制。 两个服务器内部均无按调用方进行的身份验证 — 信任边界是"谁能启动此进程",由 MCP 客户端通过启动它来执行,而非此代码。

  • 验证路径与执行操作之间的竞态(TOCTOU)。 参见下方"诚实说明"。


与朴素的 MCP 服务器相比

许多快速的 MCP 服务器用 subprocess.run(cmd, shell=True) 包装终端访问,用 os.rename 包装文件移动。两者都方便但不安全。此表是事实陈述,并非声称完美安全。

关注点

朴素的 MCP 服务器

safe-mcp-suite

命令执行

subprocess.run(cmd, shell=True) — shell 能解析的任何内容

仅 argv,shell=False,默认拒绝的白名单,拒绝列表优先

Shell 元字符

被解释(;、|、$()、重定向)

在任何策略检查之前被拒绝

文件移动

os.rename 到进程可到达的任何位置

限制在单个工作区内;任一端出现符号链接均被拒绝,绝不跟随

覆盖文件

通常静默 — POSIX rename 替换目标

始终拒绝;绝不发明编号变体(report(1).pdf)

撤销

无

每次移动都被记录;undo_last_action / redo_last_action

删除文件

通常存在,通常无保护

此服务器中不存在删除工具,仅此而已

提供给子进程的环境

完整的父环境,包含机密信息

清理为 PATH / HOME / LANG

输出或日志中的机密信息

直接通过

在截断之前脱敏,响应和审计跟踪中均如此

可审计性

默认无

追加式 JSONL,默认故障关闭

自动 vs 明确请求的操作

同一代码路径对两者一视同仁

plan_organize(未提示)受两层规则约束;move_file(具名请求)仅受安全不变量约束

两个工具均返回 OperationResult:

OperationResult {
  ok: bool
  code: ResultCode
  action: str
  reason: str
  detail: dict            # stdout, stderr, exit_code — empty when nothing ran
  duration_ms: int
}
  • run_command(command: str, cwd: str | None = None) — 根据策略评估 command,若允许则沙箱运行。cwd 可选,必须解析到限制根目录内;越界的遍历、符号链接或绝对路径返回 PATH_ESCAPE 且不运行任何内容。每次调用均被审计;在故障关闭审计下,日志不可写时返回 AUDIT_UNAVAILABLE 而非未记录地执行。非零退出码仍为 ok: true — 命令已运行;是否成功是它自己的事。

  • explain_command(command: str) — 干运行。与 run_command 走相同的解析和评估,在执行器之前返回,因此 detail 绝不携带 stdout、stderr 或退出码 — 未运行任何内容。

此服务器可返回的结果码:OK、POLICY_DENIED、INVALID_REQUEST、PATH_ESCAPE、TIMEOUT、OUTPUT_TRUNCATED、OPERATION_FAILED、AUDIT_UNAVAILABLE、INTERNAL_ERROR。

六个工具,这个列表就是设计本身 — 没有第七个,且没有一个会删除。

  • list_files(subdir: str | None = None) — 只读。报告每个条目的名称、大小、类别、整理器是否会移动它,以及不会移动时的原因。

  • plan_organize() — 提出移动和跳过方案。不改变任何内容,连目标文件夹也不创建。返回一个 plan_id 以传给 apply_plan。

  • apply_plan(plan_id: str) — 执行方案。每个文件都先被重新检查;如果自规划以来有任何文件被更改、移动或消失,则整个方案被拒绝。一次性使用 — id 不可重放。

  • move_file(src: str, dest: str) — 移动一个具名文件。dest 是完整的目标路径,而非文件夹。目标已存在则被拒绝,绝不覆盖也绝不改名绕开。仅遵守 PROTECTION 规则 — 参见上文"两层模型"。

  • undo_last_action() — 将最近一次移动或已应用的方案作为一个操作撤销。不会覆盖任何内容来为恢复的文件腾出空间。

  • redo_last_action() — 重新应用最近被撤销的操作。一旦记录新工作,重做栈即清空。

此服务器可额外返回的结果码:NEEDS_EXPLICIT_REQUEST(通过了所有安全不变量,仅因未提示的行动会是猜测而被拒绝 — 直接指名目标并提出请求)。

一个文件 policy.toml,两个服务器均读取:

audit_log = "audit.jsonl"          # shared
audit_fail_mode = "closed"         # shared: "closed" or "open"

[redaction]                        # shared
enabled = true
entropy_fallback = false
extra_patterns = []                # [{ name = "...", regex = "..." }]

[terminal]
# jail_root = "/srv/safe-mcp/sandbox"   # REQUIRED — here or via env

[terminal.limits]
timeout_seconds = 30
max_output_bytes = 65536

[terminal.allowlist]
commands = ["ls", "cat", "echo", "pwd", "git"]

[terminal.denylist]
commands = ["rm", "shutdown", "reboot", "curl", "wget", "chmod", "sudo"]

[[terminal.rules]]
command = "git"
deny_args = ["push --force", "push -f"]
reason = "force-push rewrites shared history"

[files]
# workspace_root = "/srv/safe-mcp/inbox"   # REQUIRED — here or via env
journal = "organizer-journal.json"
max_plan_moves = 500

[files.categories]
Documents = [".pdf", ".doc", ".docx", "..."]
# ...

[[files.skip]]
layer = "protection"   # or "restraint" — required, no default
when = ["unsafe-name"]
reason = "..."

策略文件本身没有默认位置。用 --config 指向它:

safe-mcp terminal --config /path/to/policy.toml
safe-mcp files --config /path/to/policy.toml

环境变量仍然受支持,且每个都会覆盖对应的 policy.toml 键。SAFE_MCP_POLICY_FILE 是值得特别指出的一个例外:它是 --config 的替代方案,而非对它的覆盖 — 两者同时给出时 --config 优先,两者均未给出时启动拒绝。

变量

含义

默认值

SAFE_MCP_POLICY_FILE

policy.toml 的路径(必填,此处或 --config)

无 — 拒绝启动

SAFE_MCP_JAIL_ROOT

终端沙箱目录(必填,此处或 jail_root)

无 — 拒绝启动

SAFE_MCP_WORKSPACE_ROOT

文件工作区目录(必填,此处或 files.workspace_root)

无 — 拒绝启动

SAFE_MCP_FILES_JOURNAL

撤销/重做日志路径(必须位于工作区之外)

organizer-journal.json

SAFE_MCP_AUDIT_LOG

共享审计跟踪路径(必须位于两个沙箱之外)

audit.jsonl

SAFE_MCP_AUDIT_FAIL_MODE

closed 或 open

closed

启动失败时会大声报错 — 打印 fatal: 消息并以非零退出码退出 — 当出现以下情况时:完全没有提供策略文件路径(既没有 --config 也没有 SAFE_MCP_POLICY_FILE)、policy.toml 缺失或无效、沙箱/工作区根目录未设置或不是目录、操作员脱敏正则表达式无效、存在无标签的 [[files.skip]] 条目,或审计日志/日志文件位于它随后能够移动或伪造的沙箱内部。


坦诚的注意事项

这是一个加固层,不是保险库。在部署任一服务器之前,请先阅读以下内容。

  • 沙箱是路径包含检查,不是内核沙箱。 没有命名空间、cgroups 或 seccomp。内核漏洞或可从白名单二进制文件触达的逃逸通道不受限制。

  • 审计日志是可检测篡改的,不是防篡改的,并且没有轮换。 追加写入并逐条刷新 + fsync 意味着它不会因崩溃而丢失记录,但任何对主机文件系统有 audit.jsonl 访问权限的人都可以读取、修改或删除它 — 而且文件会无限增长;没有内置轮换或保留策略。

  • 脱敏是基于模式的,尽力而为。 它能捕获常见的秘密形态;新颖或不寻常的格式会未经脱敏地通过。可选的熵回退默认关闭,因为它在 git SHA、UUID 和 base64 数据上会产生噪音,而不是因为它弱。

  • 终端元字符即使在引号内也会被拒绝 — 这是一个已知的天花板。 echo "a;b" 会被拒绝,即使 ; 在引号内是惰性的,因为扫描是原始子字符串检查,不识别引号。这是安全的错误方向 — 没有任何引号技巧能让操作员绕过一开始就忽略引号的扫描 — 但这确实意味着一些合法输入会被拒绝。

  • TOCTOU:先验证后操作的路径可能在两者之间发生变化。 safety/paths.py 和 servers/files/apply.py 都先检查包含或占用,然后在单独的系统调用上操作;在该间隙中交换的符号链接或创建的文件不在覆盖范围内。在代码中记录为有意的、命名的天花板(两个文件中的 # NOTE: 注释),并注明了升级路径(O_NOFOLLOW 加上 dir-fd 相对操作),以备将来需要。

  • 孙进程不会被回收。 被杀死或超时的命令自身的子进程不在单独的进程组中;执行器只杀死直接子进程,因此该命令产生的任何东西都可能比它活得更久。


相关工作


许可证

MIT — 见 LICENSE。

Related MCP Connectors

Related MCP Servers