diff --git a/README.md b/README.md index f44838a..09de0bd 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,13 @@ **Minimal AI agent harness — wrap any LLM with tools and a streaming CLI.** -> 中文一句话:极小的 AI Agent 执行环境(harness),给任意 LLM 配上工具、对话循环、流式事件与 CLI 会话。约 1,100 行、零魔法、一个依赖,是 1998x-stack harness 家族的最小基线。 +> 中文一句话:极小的 AI Agent 执行环境(harness),给 LLM 配上工具、对话循环、流式事件与 CLI 会话。强调可读性、显式状态和可测试的执行契约,是 1998x-stack harness 家族的最小基线。 [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/) [![Python tests](https://github.com/1998x-stack/tiny-harness/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/1998x-stack/tiny-harness/actions/workflows/ci.yml) -[![Lines](https://img.shields.io/badge/code-1%2C350%20lines-lightgrey)](.) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) -`tiny-harness` is a ~1,100-line Python package that turns an LLM into an AI agent — giving it tools, a conversation loop, streaming events, and a CLI session. One dependency. One import. +`tiny-harness` is a small Python agent runtime with tools, a conversation loop, streaming events, and CLI sessions. It uses explicit contracts for tool validation, filesystem paths, and success/failure status. ```bash pip install tiny-harness @@ -49,7 +48,7 @@ tiny-harness "Create hello.py" - **Rich TUI** — optional `--tui` mode with panels, markdown rendering, color-coded messages (`pip install tiny-harness[tui]`) - **Persistence** — JSONL session history with `/save` and `/history` commands - **Filesystem guard** — workspace boundary enforcement, path traversal protection -- **1,125 lines** — readable top-to-bottom, CleanRL-inspired code style +- **Inspectable contracts** — explicit tool risk classification, workspace path declarations, and JSON Schema Draft 2020-12 validation ## Quick Start @@ -68,13 +67,13 @@ pip install tiny-harness export ANTHROPIC_API_KEY="sk-ant-..." # Run a single prompt -tiny-harness "Create a hello.py file that prints 'Hello, world!'" +tiny-harness --provider anthropic "Create a hello.py file that prints 'Hello, world!'" ``` ### Session Mode ```bash -tiny-harness --model claude-sonnet-4-20250514 --skills files +tiny-harness --provider anthropic --model claude-sonnet-4-20250514 --skills files ``` ``` @@ -96,7 +95,7 @@ Session commands: `/exit`, `/help`, `/tools`, `/clear`. ```bash pip install tiny-harness[tui] -tiny-harness --tui --model claude-sonnet-4-20250514 --skills files +tiny-harness --tui --provider anthropic --model claude-sonnet-4-20250514 --skills files ``` ``` @@ -207,7 +206,7 @@ agent.tools.register_from_def( description="Get current weather for a city.", parameters={"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, ), - handler=lambda city: f"Sunny, 22°C in {city}", + handler=lambda args: "Sunny, 22°C in " + args["city"], ) # Load built-in skills @@ -236,12 +235,14 @@ agent.on("error", lambda e: print(f"Error: {e.message}")) | `max_consecutive_errors` | `3` | Consecutive error budget | | `timeout_ms` | `30_000` | Tool execution timeout | | `max_tool_result_chars` | `50_000` | Truncate large results | +| `context_window` | `200_000` | Input + reserved output window (heuristic estimator) | +| `max_tokens` | `16_384` | Reserve for model output; must be less than context_window | ## Providers -### Anthropic (default) +### Anthropic (explicit) ```python -config = Config(model="claude-sonnet-4-20250514", api_key="...", workspace=".") +config = Config(model="claude-sonnet-4-20250514", api_key="...", workspace=".", provider="anthropic") ``` ### OpenAI @@ -290,6 +291,8 @@ Full design documentation in [`docs/`](docs/): - [cli.md](docs/cli.md) — CLI design, streaming format - [skills.md](docs/skills.md) — skill system architecture - [code-style.md](docs/code-style.md) — coding conventions +- [TOOL_FACTORY.md](docs/TOOL_FACTORY.md) — JSON Schema and filesystem path contract for custom tools +- [TOOL_OUTCOME.md](docs/TOOL_OUTCOME.md) — typed tool execution success/failure semantics - [adr/](docs/adr/) — architectural decision records ## Development @@ -314,7 +317,7 @@ The built-in Shell skill requires human approval by default. In `--no-hitl` mode ## Security -The built-in Shell skill is **not** an OS sandbox. Read [security boundaries](docs/security.md) and the [phased optimization status](docs/OPTIMIZATION_STATUS.md) before deploying to untrusted environments. +Tool arguments are checked with JSON Schema Draft 2020-12 and declared filesystem paths are normalized before tool execution. **These checks are not an OS sandbox**. Read [security boundaries](docs/security.md) and the [phased optimization status](docs/OPTIMIZATION_STATUS.md) before deploying to untrusted environments. ## License diff --git a/docs/OPTIMIZATION_STATUS.md b/docs/OPTIMIZATION_STATUS.md index 146a2f3..7b77563 100644 --- a/docs/OPTIMIZATION_STATUS.md +++ b/docs/OPTIMIZATION_STATUS.md @@ -24,6 +24,8 @@ | Phase 1 | [#16](https://github.com/1998x-stack/tiny-harness/pull/16) | Anthropic SSE 截断检测、message_stop 校验、usage/finish、可配置网关 | PR CI 通过;真实 API 冒烟仍待执行 | | Phase 2 | [#18](https://github.com/1998x-stack/tiny-harness/pull/18) | ToolOutcome 显式成功/失败契约,文件、搜索、Shell 异常传给 ErrorBudget | PR CI 通过;低层直接调用保留字符串兼容 | | Phase 1 | [#19](https://github.com/1998x-stack/tiny-harness/pull/19) | OpenAI/Anthropic 流式请求在首次输出前重试 429/5xx/部分网络错误,Retry-After 上限;输出后禁重试 | PR CI 通过;真实 API 冒烟仍待执行 | +| Phase 4 | [#21](https://github.com/1998x-stack/tiny-harness/pull/21) | ToolDef 显式 path_args、所有声明路径规范化、第三方兼容字段全部检查 | PR CI 通过;仍非 OS 沙箱 | +| Phase 4 | [#22](https://github.com/1998x-stack/tiny-harness/pull/22) | Draft 2020-12 JSON Schema(嵌套/组合/数组),注册时检查 Schema,拒绝远程引用 | PR CI 通过;引入 jsonschema 运行时依赖 | ## 待完成事项(不应标记为已修复) @@ -38,7 +40,7 @@ ### P2:持续质量提升 - 文件工具:有界读取、二进制识别、原子写、拒绝默认覆盖与 edit_file。 -- ToolRegistry:完整 JSON Schema 与 path 参数契约;消除静态白名单。 +- ToolRegistry:JSON Schema 与显式 path 参数契约已落地;进一步清理过渡期遗留工具名称兼容映射,完善工具能力策略与插件迁移。 - EventBus:事件成功状态、耗时与稳定的 finish/usage 契约。 - 错误预算与循环检测:区分失败重复与成功的合法重复,避免错误的长期封禁。 - Session:旧格式兼容迁移、增量写入、CLI `/resume`、大结果 sidecar 与索引。 diff --git a/docs/TOOL_FACTORY.md b/docs/TOOL_FACTORY.md new file mode 100644 index 0000000..9aa37b8 --- /dev/null +++ b/docs/TOOL_FACTORY.md @@ -0,0 +1,69 @@ +# Tool Factory Contract / 工具定义与执行契约 + +工具不是可信任的函数调用。模型输出、工具参数、Skill 文本以及外部内容都应被视作不可信输入。 + +## 1. 工具注册 + +以下示例展示如何为插件定义类型、文件路径和失败语义: + +```python +from tiny_harness import ToolDef +from tiny_harness._tool_outcome import ToolOutcome + +def inspect_manifest(args: dict) -> ToolOutcome: + # args["manifest_path"] 已由 ToolExecutor 规范化为 workspace 内绝对路径。 + with open(args["manifest_path"], encoding="utf-8") as f: + return ToolOutcome.ok(f.read()) + +agent.tools.register_from_def( + ToolDef( + name="inspect_manifest", + description="Read a manifest from this agent's workspace", + risk_level="read_only", + path_args=("manifest_path",), + parameters={ + "type": "object", + "additionalProperties": False, + "properties": { + "manifest_path": {"type": "string", "minLength": 1}, + }, + "required": ["manifest_path"], + }, + ), + inspect_manifest, +) +``` + +`path_args` 用于声明路径语义,不限制字段名称为 `path`。多个位置必须全部列出,例如 `("source", "destination")`。每个声明字段必须是参数 Schema 中的字符串属性。旧插件的 `path/source/destination/cwd` 习惯字段仍进行兼容性检查;新代码不要依赖工具名称白名单。 + +## 2. JSON Schema(Draft 2020-12) + +- ToolDef 创建时验证 Schema 是否合法,并要求根 Schema 表示对象。 +- ToolExecutor 执行前使用标准 JSON Schema 校验:嵌套对象、数组元素、`required`、`additionalProperties`、`minLength`、范围、枚举、`oneOf` 等均生效。 +- 用户审批处理函数若返回修改后的参数,会重新执行 Schema 与路径守卫。 +- 支持同一 Schema 内的 `#/$defs/...` 引用;拒绝外部 URL 引用,避免执行过程中隐式获取不可信远程 Schema。 +- 不会自动类型转换:`"42"` 不等于整数 `42`,布尔值不当作整数。 + +## 3. 风险分类与结果 + +| 风险等级 | 默认行为 | +|---|---| +| `safe`、`read_only` | 通常无需 HITL,但路径守卫仍生效 | +| `mutation` | 需要 HITL | +| `destructive`、`dangerous` | 每次都需要 HITL,不保留会话级批准 | +| 未指定 | 默认 `mutation` | +| 非法/未知等级 | 注册即失败 | + +返回 `ToolOutcome.ok(text)` / `ToolOutcome.error(text)`,让错误预算可靠区分成功与失败。现有直接返回字符串的 handler 依旧兼容,但 **字符串内容不用于推断执行成功与否**。详见 [TOOL_OUTCOME.md](TOOL_OUTCOME.md)。 + +## 4. 不提供的安全保证 + +`FilesystemGuard` 通过规范化路径阻止显式的工作区外路径;**这不是 OS 沙箱**:不能阻止工具直接访问其他系统资源、TOCTOU 符号链接竞态,也不能限制任意 Shell 的网络/文件系统访问。部署于多租户或不可信环境时,必须另配进程、文件系统和网络隔离。 + +## 5. Regression checklist + +- 参数缺少必需字段、数组/嵌套类型不符,handler 完全不执行。 +- 受保护路径的相对值按 `AgentConfig.workspace` 解释,不能按调用 Python 进程的当前工作目录解释。 +- 任意多路径中只要有一个越界,整个工具调用即失败。 +- HITL 修改过的参数重新校验,不允许绕过风险和路径守卫。 +- 任何新工具注册时应具备明确风险分类、Schema、`path_args`(如涉及路径)及成功/失败结果契约。