a8d6dd4b87
git.jocayn.qzz.io 在 Cloudflare 后面同时有 IPv4/IPv6 地址,部分网络(如 nas)IPv4 不通、 IPv6 可用;原来用 -4 强制 IPv4 且没有连接超时,会重试到卡死。改为由 curl 自动选择协议族, 探测 8 秒连接超时、15 秒总时长且不重试,下载加 15 秒连接超时。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.8 KiB
8.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
项目
bolt 是一个单文件 MCP 服务器(src/main.rs),让 MCP 客户端在主机上执行 shell 命令、读写和传输文件。它手写实现了 JSON-RPC 2.0 + MCP(协议版本 2024-11-05 / 2025-03-26 / 2025-06-18),支持两种传输:stdio(默认)和 Streamable HTTP(--http <addr>)。依赖只有 serde_json,Unix 下再加 libc;base64、SHA-256、HMAC、百分号编码、随机数(/dev/urandom / BCryptGenRandom)都是手写的。请保持这一点:不引入新 crate,不引入 async runtime,也不引入 HTTP/MCP 框架。代码注释、日志信息和文档都用中文,新增内容请保持一致。
常用命令
cargo build --release # 产物 target/release/bolt(release 配置:lto=fat、codegen-units=1、strip)
cargo test --release # 单元测试(位于 src/main.rs 末尾的 mod tests)
cargo test --release test_policy # 按名称运行单个单元测试
cargo clippy --release --all-targets # 当前无警告,请保持
cargo check --release --all-targets --target x86_64-pc-windows-gnu # 在 Linux 上检查 Windows 分支能否编译
./target/release/bolt --help # 查看 CLI 参数;每个参数都可用对应的 BOLT_* 环境变量作为回退(CLI 优先)
./target/release/bolt --http 127.0.0.1:8080 --token t --allow "git,ls" --file-root /tmp
test_run_command_basic和test_shell_session带#[cfg(unix)](使用sh -c、printf、sleep),Windows 上会跳过。Windows 下服务器使用cmd /C。- 端到端测试在
docs/bolt_stdio_test.py(stdio)和docs/bolt_http_test.py(HTTP,会额外起一个带--token和--file-root的实例)。它们对管道使用select,需要 Unix 主机;二进制路径默认是target/release/bolt,可用BOLT_BIN覆盖,例如BOLT_BIN=/path/bolt python3 docs/bolt_http_test.py。HTTP 测试占用端口 18080 和 18081。
架构(src/main.rs,自上而下)
文件用 // ---- 横幅注释分段:常量 → 配置 → 出站通道 → 应用状态 → JSON-RPC 辅助 → 命令策略 → 编码/摘要/随机数 → run_command 执行引擎 → shell 会话 → 文件 → 工具列表 → MCP 分发 → 客户端作用域 → stdio 传输 → HTTP 传输 → 信号与清理 → main → 测试。
App/Shared = Arc<App>:全局状态。包含Config、按配置生成的tools、inflight表(作用域\n请求id→Arc<Killable>)、sessions(shell 会话,带owner作用域)、mcp_sessions(Mcp-Session-Id→ 最近活动时间)、并发/连接计数,以及file_link的签名密钥(启动时随机生成)。Ctx(作用域 + 出口):每个请求都带一个Ctx { scope, sink, base_url }。scope用来隔离取消和 shell 会话:stdio 下是"stdio";HTTP 下是Mcp-Session-Id,不带该头的请求共用匿名作用域"-"。sink决定通知和响应发往哪里:Sink::Stdio、Sink::Hub(hub, scope)(只推给该作用域的 GET SSE 流)、Sink::Chan(当前 POST 的 SSE 响应流)。base_url由Host/X-Forwarded-*推出,只有file_link用到。process_message(v, app, ctx, async_exec):两种传输共用的唯一入口。通知返回None。tools/call先通过CallSlot占用一个并发名额(超限时返回isError结果),再注册Killable。async_exec为 true(stdio)时在bolt-tool线程中执行,完成后经ctx.sink发送响应,所以 stdio 响应可能乱序;为 false(HTTP)时同步执行。notifications/exit只在 stdio 作用域生效。exec_tool_call:按工具名分发。工具执行失败返回Ok(tool_error(..))(isError: true),成功结果用tool_ok(v)包成content+structuredContent。参数错误或未知工具返回Err((code, msg)),转成 JSON-RPC 错误。参数用req_str/opt_u64等辅助函数读取。新增工具时请保持这一区分,并在build_tools(cfg)里补上 schema。文件工具只有在files_enabled时才会出现在列表中,file_link只在 HTTP 模式下出现。- 进程生命周期:Unix 下子进程放进独立进程组(
pre_exec+setpgid(0,0)),kill_tree对-pid发SIGKILL;Windows 下用taskkill /T /F。Killable处理"pid 注册前就收到取消"的情况。主进程被收割后会把 pid 清零,防止 pid 复用时误杀。主进程退出后,读取线程最多再等PIPE_DRAIN_GRACE;如果后台进程还占着管道,就标记detached直接返回,读取线程转入后台继续排空但不再保存。 - Shell 会话:
SessBuf里的偏移都是绝对字节偏移(base/read),裁剪缓冲前部不会让读取位置错位。退出信息也放在同一把锁里,Condvar不会丢唤醒。shell_read会避免切断 UTF-8 字符,每 200ms 检查一次是否被取消。janitor_loop每 30 秒回收空闲会话和过期的 MCP 会话。 - 文件:
resolve_path是所有文件访问(工具和/files)唯一的路径入口。已存在的路径用canonicalize解析符号链接;不存在的路径先规范化最近的已存在祖先,再拼上剩余部分(剩余部分不允许./..),最后检查是否在--file-root之内。write_file的 overwrite 模式写同目录临时文件后 rename。错误类型是FErr = (HTTP 状态码, 信息),工具和 HTTP 共用。 - HTTP 传输:手写 HTTP/1.1,每个连接一个线程(
conn_loop,受MAX_HTTP_CONNS限制)。检查顺序:OPTIONS(CORS)→ Origin 白名单 → Bearer token(ct_eq常量时间比较;带sig的/files请求改为在handle_files里校验 HMAC 签名)→ 路由到handle_mcp或handle_files。读取 body 之前出错一律关闭连接。请求体统一经BodyReader读取,支持 Content-Length 和 chunked。handle_*对读写端是泛型的,单元测试直接喂内存缓冲。 - 退出:Unix 下屏蔽 SIGINT 和 SIGTERM,由
sigwait线程接收信号,执行shutdown_cleanup后直接process::exit(stdio 主线程可能阻塞在读 stdin 上)。Windows 下用SetConsoleCtrlHandler做同样的事。 - 命令策略(
policy_check(cfg, command, shell)):allow/deny 都按程序名精确匹配(会去掉路径、引号、转义和.exe等扩展名)。配置了 allow 时,shell 模式禁止元字符;deny 会检查按元字符切分后每一段的程序名。作用范围:run_command、shell_open的 shell 名称、shell_write写入的每一行。配置了 allow 但没有--file-root时文件功能禁用。
各种上限和默认值是文件顶部的 const(MAX_HTTP_BODY、MAX_CONCURRENT_CALLS、SESSION_IDLE、DEFAULT_READ_LEN、DEFAULT_MAX_UPLOAD 等)。
部署(deploy/)
- 三个独立的 POSIX sh 脚本(需要 root,设计为
curl … | sudo sh直接运行,所以每个脚本都自带下载/探测函数,不能互相引用):install.sh:首次安装。自动识别 Linux(systemd:/usr/local/bin/bolt、/etc/bolt.env、bolt.service)或 macOS(launchd 标签io.jocay.bolt,plist 放在/Library/Application Support/bolt/,启用开机自启时复制到/Library/LaunchDaemons/)。已安装时拒绝执行,除非加--reinstall,因为它会按本次参数重写配置。update.sh:只替换二进制,保留配置。重启和健康检查交给独立进程执行(Linux 用systemd-run,macOS 用(trap '' HUP; sh … &);macOS 上nohup … &启动的进程会在调用返回后消失),失败时从/usr/local/bin/bolt.bak回滚。uninstall.sh:先删文件,最后才停服务,因为通过 bolt 自身执行时,停服务会结束脚本本身。
- 下载站点:IPv4 用
https://git.jocayn.qzz.io,IPv6 用https://git.draws.qzz.io(同一个 Gitea,仓库jocay/bolt已公开)。脚本先试 IPv4 站点再试 IPv6 站点(不强制-4/-6:git.jocayn.qzz.io在 Cloudflare 后面,同时有 IPv4 和 IPv6 地址,有的网络只有其中一种能通;探测带 8 秒连接超时、不重试);最新版本通过/api/v1/repos/jocay/bolt/releases/latest获取(这个 Gitea 不支持releases/latest/download/直链),附件命名为bolt-<linux|darwin>-<amd64|arm64>,可选附带SHA256SUMS。BOLT_GIT_HOST、BOLT_VERSION、BOLT_DL_URL、--binary可以覆盖;BOLT_DL_TOKEN(下载认证)与--token(服务器的 Bearer 令牌)相互独立。 - 发布用的二进制用 cargo-zigbuild 编译:
x86_64-unknown-linux-gnu.2.28(普通cargo build产物依赖构建机的 glibc 版本,较老的系统上跑不了)和aarch64-apple-darwin。 bolt.service和bolt.env.example用于手动 systemd 部署。监听地址写在 unit 文件的ExecStart里,其余配置来自/etc/bolt.env中的BOLT_*变量。- 修改 CLI 参数时,需同步更新
parse_args/print_help、install.sh(参数解析及其生成的 env/plist)、bolt.env.example和 README 中的参数表。