Files
bolt/CLAUDE.md
T
jocay a8d6dd4b87 deploy: 站点探测不再强制 IPv4/IPv6 并加超时
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>
2026-10-18 10:57:56 +08:00

51 lines
8.8 KiB
Markdown

# 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 框架。代码注释、日志信息和文档都用中文,新增内容请保持一致。
## 常用命令
```bash
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 中的参数表。