Metadata-Version: 2.4
Name: safe-fix-harness
Version: 0.1.0
Summary: A language-agnostic, safety-governed coding agent harness.
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: PyYAML<7,>=6.0
Requires-Dist: keyring<26,>=25
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: pytest<10,>=8; extra == "dev"

# SafeFix Harness

## 项目简介

SafeFix Harness 是一个 Python 3.11+ 命令行 Coding Agent Harness。LLM 每轮只决定一个结构化 Action；本项目自己的代码负责上下文、协议解析、路径与审批治理、工具分发、检查反馈、记忆、预算和停止判断，不依赖 LangChain AgentExecutor、AutoGen、CrewAI 或其他现成 Agent runner。

P0 提供 `list_files`、`read_file`、`search_text`、`apply_patch`、`run_check`、`finish` 六个 Action。主要贡献是确定性反馈闭环：把检查失败分类并提取可信位置，去除时间、临时路径和地址等噪声后生成稳定指纹，再以 `BRIEF`、`DETAILED`、`REDIRECT` 逐级增加反馈；第三次相同失败的 `REDIRECT` 会进入下一轮上下文，只有修改后第四次仍相同才停止为 `STALLED`。

```text
任务 → Context → 单次 LLM 调用 → 严格 Action JSON
     → Guardrail → 可选一次性审批 → Dispatcher
     → Observation → FailureReport / RunState → 下一轮或确定性停止
```

核心机制均可注入 `MockLLM`、`MockApprover`、临时目录和假传输层，在无网络、无真实 key 的环境中重复验证。完整行为与验收标准见 [SPEC.md](./SPEC.md)，实现顺序与证据门见 [PLAN.md](./PLAN.md)。

## 安装

### 从源码开发与测试

每个 worktree 使用自己的 Harness 开发环境。引导阶段显式使用 `python3`，后续命令显式使用该环境的解释器：

```bash
python3 -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/python -m pytest -q
```

默认 pytest 配置排除 `@pytest.mark.network`；这条测试命令不调用真实 LLM、不读取真实 API key，也不访问模型端点。

### 从公共 PyPI 安装

`v0.1.0` 的 Trusted Publishing workflow 成功后，干净环境的固定安装命令是：

```bash
python3 -m venv safe-fix-venv
safe-fix-venv/bin/python -m pip install "safe-fix-harness==0.1.0"
safe-fix-venv/bin/safe-fix --help
```

在 `v0.1.0` 尚未成功发布前，以上公共安装命令不会被声称为可用；以 [PyPI 项目页](https://pypi.org/project/safe-fix-harness/) 和 release workflow 结果为准。源码安装仍可按上一节离线运行测试与演示。

## 运行

示例配置 [examples/safefix.yaml](./examples/safefix.yaml) 的 `workspace: "./broken-calculator"` 相对配置文件目录解析，因此目标路径始终是 `examples/broken-calculator`。先把其中的 `llm.base_url` 与 `llm.model` 改成实际 OpenAI-compatible provider，再为同一规范化端点录入 key：

```bash
.venv/bin/safe-fix key set --base-url https://api.example.com/v1
.venv/bin/safe-fix run \
  --config examples/safefix.yaml \
  --task "修复 examples/broken-calculator 的 divide 除零行为，并让必要检查通过"
```

`run` 的 stdout 是格式化 `RunResult` JSON；逐步审计写入 stderr，包含步号、Action、有限参数摘要、Observation code、耗时与审批状态，不输出完整补丁、源文件或凭据。退出码固定为：成功 `0`、运行未成功 `1`、参数/配置/凭据启动错误 `2`。

Harness 自己的 `.venv` 与目标项目的解释器是两个边界：

- `.venv/bin/python` 运行 SafeFix、开发测试与机制演示；
- `python_executable` 决定目标项目检查使用哪个 Python；
- 检查 argv 中的 `${python}` 会在启动期展开为 `python_executable` 的绝对 launcher 路径，进入 Dispatcher 后不再保留占位符；显式配置 venv 时会保留 `.venv/bin/python`，不会把该 symlink 改写成系统 Python；
- 目标项目若有独立虚拟环境，应把 `python_executable` 指向那个环境，例如 `examples/broken-calculator/.venv/bin/python`，而不是假定它与 Harness `.venv` 相同。

## 分发命令

本项目 P0 的唯一分发形态是 PyPI 包，不交付 Docker 镜像或单文件二进制。`.[dev]` 只安装测试依赖；执行分发构建前还需显式安装冻结范围内的 `build`。本地构建与 smoke test：

```bash
.venv/bin/python -m pip install "build>=1.2,<2"
PATH="$PWD/.venv/bin:$PATH" make wheel
.venv/bin/python -m build
python3 -m venv /tmp/safe-fix-wheel-smoke
/tmp/safe-fix-wheel-smoke/bin/python -m pip install dist/safe_fix_harness-0.1.0-py3-none-any.whl
/tmp/safe-fix-wheel-smoke/bin/safe-fix --help
```

正式发布只由 `.github/workflows/release.yml` 完成：

1. Task 17 经独立最终交付审计、人工验收、双 CI 并合入后，GitHub 与 NJU GitLab 指向同一 release commit；
2. 用户在 PyPI 配置 pending Trusted Publisher，精确绑定 owner `dInG-yAnWen`、repository `AgentHarness`、workflow `release.yml`、environment `pypi`；
3. GitHub 的受保护 `pypi` environment 要求人工批准；
4. 推送 annotated tag `v0.1.0` 后，workflow 从该 tag 测试、核对 tag/包版本、构建 sdist/wheel，再用 OIDC 发布；
5. 只有 `publish` job 获得 `id-token: write`，仓库、GitHub secrets 与 NJU GitLab 均不保存长期 PyPI token。

版本不可覆盖或重发；发布失败后的修复必须递增版本并使用新 tag。

## 目录结构

```text
.
├── src/safe_fix/
│   ├── models.py          # Action / Observation / RunResult 等冻结模型
│   ├── protocol.py        # 严格 Action JSON 协议
│   ├── config.py          # YAML、profile、解释器与启动期校验
│   ├── paths.py           # canonical workspace 路径与 glob 围栏
│   ├── guardrail.py       # ALLOW / DENY / REQUIRE_APPROVAL
│   ├── approval.py        # 精确 digest、一次性 token 与 HITL
│   ├── tools/             # 读取、原子单 hunk 补丁、固定 argv 检查
│   ├── feedback.py        # 分类、位置、抗噪指纹与三级反馈
│   ├── state.py           # 预算、finish 前置与第四次失败停止
│   ├── memory.py          # .safefix/memory.json 的确定性跨会话记忆
│   ├── context.py         # 数据隔离、历史压缩与上下文构建
│   ├── credentials.py     # endpoint-bound keyring 与统一脱敏
│   ├── llm.py             # MockLLM 与单次 OpenAI-compatible HTTP 适配器
│   ├── loop.py            # 自有 Agent 主循环
│   └── cli.py             # run、key set/status/clear 与审计输出
├── tests/                 # 默认离线的单元/集成测试
├── examples/
│   ├── broken-calculator/ # 每个场景复制到临时目录后运行
│   └── safefix.yaml
├── scripts/mechanism_demo.py
├── .github/workflows/ci.yml
├── .github/workflows/release.yml
├── .gitlab-ci.yml
├── SPEC.md
├── PLAN.md
├── SPEC_PROCESS.md
└── AGENT_LOG.md
```

`REFLECTION.md` 是课程要求的学生个人反思，只能由学生本人完成，不属于智能体生成的实现产物。

## 安全边界说明

- 所有工具调用先经过 Guardrail；工作区外路径、绝对路径、`..`、symlink 逃逸、`.git/`、`.safefix/`、测试、CI、配置和 lock 文件永久拒绝，审批不能覆盖。
- Action 中的路径统一使用 workspace-root 相对 POSIX 路径，`"."` 或省略可选路径表示根目录；不要把项目目录显示名重复加到路径前，也不要把项目名当作 `search_text` 的源码 query。布局未知时先对 `"."` 调用 `list_files`；空搜索仍为 `OK`，但会返回结构化 guidance，下一步应列目录/读文件或至少改变 query、path、glob 之一，不能立即原样重复。
- `apply_patch` 只修改已存在、允许写的 UTF-8 源码文件，要求单文件单 hunk 且 old block 恰好匹配一次；系统规则向真实模型公开完整补丁语法和合法 JSON 示例，格式错误在请求 HITL 前即以 `INVALID_ARGUMENTS` 拒绝。命中 `approval_required_globs` 时，交互默认拒绝，批准只绑定完整规范化 Action 的 SHA-256 且只可消费一次。
- 模型不能提供 Shell。`run_check` 只接受配置中的检查名称，使用固定 argv、`shell=False`、固定 cwd、超时、有界输出与移除凭据变量的最小环境。
- **配置的检查是可信计算基，不是沙箱。** 测试/lint/build 会以当前用户的操作系统权限运行，可能访问或修改工作区外资源；只应对你信任的仓库和检查命令启用 `run_check`。
- P0 没有安装依赖、任意网络、Git push、发布、删除、创建或重命名文件的 Action。SafeFix 自身只有真实 LLM 适配器会访问配置的 HTTPS endpoint。
- 仓库内容和工具输出以不可信 `<data>` 进入上下文；Observation 在进入状态、上下文、审计或终态前先按已知 key 与常见凭据形态脱敏，再按字节上限截断。
- 成功不能由模型自行宣称：最后一次源码修改之后，所有 required checks 必须有最新通过记录，`finish(status="success")` 才被接受。

## 凭据管理

API key 只进入操作系统钥匙串，并绑定规范化 HTTPS `base_url`；不同端点使用不同条目。P0 不读取 `.env` 或环境变量，不接受 `--api-key`，不把 key 写入 YAML、项目记忆、日志、Observation、RunResult 或 Git。

```bash
.venv/bin/safe-fix key set --base-url https://api.example.com/v1
.venv/bin/safe-fix key status --base-url https://api.example.com/v1
.venv/bin/safe-fix key clear --base-url https://api.example.com/v1
```

`set` 通过隐藏输入录入；`status` 只显示截断预览，短 key 一律显示 `configured (redacted)`；`clear` 幂等。`run` 找不到该端点的 key 时返回启动错误和对应的 `key set` 指引，而不是读取其他端点或回显明文。

macOS 使用 Keychain，属于 P0 完整支持平台。Linux 真实模型运行需要可用且已解锁的 Secret Service/keyring backend；无桌面会话的 CI 或容器通常没有该服务，只支持无需 key 的离线测试和演示。

## 机制演示

三个 A.6 场景只使用 MockLLM/MockApprover 与临时工作区；仓库中的 `examples/broken-calculator` 不会被测试原地修改：

```bash
.venv/bin/python scripts/mechanism_demo.py
# 或在已激活 .venv 后
make demo
```

1. 治理：拒绝 `../../outside.py` 与测试文件修改，源码敏感路径需精确一次性审批，拒绝不改字节，批准只执行一次；
2. 反馈闭环：`examples/broken-calculator` 首次检查失败，下一轮上下文含 `BRIEF` 与 `tests/test_calculator.py` 定位，MockLLM 改用修复补丁，复查通过后合法结束；
3. 主贡献：三次字节变化但同一失败依次产生 `BRIEF`、`DETAILED`、`REDIRECT`，模型实际看到 `REDIRECT` 后，第四次仍相同才以 `STALLED` 停止。

完整离线回归可用：

```bash
PATH="$PWD/.venv/bin:$PATH" make test
```

`pyproject.toml` 默认跳过 `@pytest.mark.network`；真实供应商联通性不属于 CI 或机制演示的通过前提。

## 第三方依赖与许可证

下表只列项目直接声明或 release workflow 直接安装的依赖。版本范围来自 `pyproject.toml`/workflow；核验版本与许可证来自当前安装发行元数据、随 wheel 安装的许可证文件以及官方上游许可证/声明，不以记忆推断。传递依赖遵循其各自许可证。

| 用途 | 依赖与声明范围 | 本地核验版本 | 许可证 | 官方项目 / 许可证 |
|---|---|---:|---|---|
| 运行时 YAML | `PyYAML>=6.0,<7` | 6.0.3 | MIT | [项目](https://github.com/yaml/pyyaml) / [LICENSE](https://github.com/yaml/pyyaml/blob/main/LICENSE) |
| 运行时钥匙串 | `keyring>=25,<26` | 25.7.0 | MIT | [项目](https://github.com/jaraco/keyring) / [SPDX 声明](https://github.com/jaraco/keyring/blob/main/pyproject.toml#L27) |
| 运行时 HTTP | `httpx>=0.27,<1` | 0.28.1 | BSD-3-Clause | [项目](https://github.com/encode/httpx) / [LICENSE](https://github.com/encode/httpx/blob/master/LICENSE.md) |
| 开发测试 | `pytest>=8,<10` | 9.1.1 | MIT | [项目](https://github.com/pytest-dev/pytest) / [LICENSE](https://github.com/pytest-dev/pytest/blob/main/LICENSE) |
| PEP 517 build backend | `setuptools>=68` | build isolation 按范围解析 | MIT | [项目](https://github.com/pypa/setuptools) / [LICENSE](https://github.com/pypa/setuptools/blob/main/LICENSE) |
| release 构建 | `build>=1.2,<2` | 1.5.0 | MIT | [项目](https://github.com/pypa/build) / [LICENSE](https://github.com/pypa/build/blob/main/LICENSE) |

仓库当前没有项目自身的 `LICENSE` 文件；不要把第三方依赖的宽松许可证解释为 SafeFix Harness 已授权再分发。

## 已知限制

- 需要 Python 3.11+；macOS arm64/x86_64 为完整保证平台，Linux x86_64 离线核心受 CI 验证但真实 key 依赖 Secret Service，Windows 的 symlink 与进程终止语义不在 P0 保证范围。
- 只有单 workspace、单进程、单 Agent；没有多 Agent 编排、通用 Shell、自动依赖安装、Git 操作或容器分发。
- 被修项目依赖必须预先安装。`python_executable` 只选择目标解释器，不替用户创建目标虚拟环境；检查进程受信任且不受 OS 沙箱隔离。
- 真实 provider 必须提供冻结的 OpenAI-compatible `POST {base_url}/chat/completions` 契约；具体 provider/model 由使用者配置，P0 单次请求不自动重试，真实网络测试默认跳过。
- P0 `apply_patch` 对 CRLF 目标文件返回 `PATCH_CONFLICT`；old block 是唯一子串匹配而非行锚定匹配；原子 `os.replace()` 前会 fsync 临时文件，但不会额外 fsync 父目录。这三项是已记录并接受的 Task 8 限制。
- 补丁只能修改已存在的 UTF-8 文件，不能创建、删除、重命名文件或整体替换大文件。
- 公共 `safe-fix-harness==0.1.0` 只有在 `v0.1.0` release workflow、PyPI Trusted Publishing 和全新 venv 安装 smoke 全部通过后才算正式可用。
