Skip to content

Commit 7d3b159

Browse files
committed
graph: add AI-owned routing and repair loop
1 parent 326f6b4 commit 7d3b159

38 files changed

Lines changed: 1353 additions & 503 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# AI 判断规则与 learner memory 边界
2+
3+
- **日期**:2026-04-28
4+
- **类型**:规则收紧
5+
- **影响范围**:规则文档、策略配置、intent routing、learner memory
6+
- **决策者**:用户
7+
8+
## 背景
9+
10+
用户要求意图分流、运维语义判断和具体操作方法不得在 Python 代码中写死,应该由模型运行时判断;成功执行的方法应脱敏后进入 learner memory,供后续推荐和相似命令检索。
11+
12+
## 新决策
13+
14+
- Python 中禁止新增业务/意图关键词表、固定运维方法答案、产品特定操作流程分支。
15+
- 意图分流由 `prompts/intent_router.md` 的 LLM 路由决定,Python 只解析结构化协议。
16+
- 默认安全策略数据从 `configs/policy.default.yaml` 加载;Python 只负责 Pydantic 校验、token 化、匹配和决策。
17+
- learner memory 记录脱敏后的完整成功命令模式,不再只记录第一个 token。
18+
- R-SEC / R-HITL 红线仍保持确定性策略,不能交给模型自由记忆或自由放行。
19+
20+
## 影响
21+
22+
- **受影响文档**
23+
- `.work/rule/baseline.md`
24+
- `README.md`
25+
- `docs/en/README.md`
26+
- `docs/zh/README.md`
27+
- **受影响代码**
28+
- `src/linuxagent/policy/builtin_rules.py`
29+
- `src/linuxagent/policy/engine.py`
30+
- `src/linuxagent/policy/models.py`
31+
- `src/linuxagent/intelligence/command_learner.py`
32+
- `src/linuxagent/graph/intent.py`
33+
34+
## 是否向后兼容
35+
36+
是。安全策略输出保持 `SAFE` / `CONFIRM` / `BLOCK`,但规则数据改为 YAML 单一真源;learner memory 的 key 从命令头升级为脱敏后的完整命令模式。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# 日常聊天直答分流
2+
3+
- **日期**:2026-04-28
4+
- **类型**:设计变更
5+
- **影响范围**:Graph intent parsing、HITL 触发条件
6+
- **决策者**:用户
7+
8+
## 背景
9+
10+
此前 `parse_intent` 除少数能力问题外,默认要求模型返回 `CommandPlan`。这会让普通解释、闲聊、how-to 问题也进入命令计划路径,并在确认面板中展示 `Command / Goal / Purpose / Safety / Rule / Source / Risk / Preflight / Verify / Rollback` 等执行字段。
11+
12+
## 新决策
13+
14+
`parse_intent` 前置 LLM 意图路由:
15+
16+
- 运行时先调用 `prompts/intent_router.md`,由模型返回 `DIRECT_ANSWER` / `COMMAND_PLAN` / `CLARIFY`
17+
- `DIRECT_ANSWER``CLARIFY` 直接输出模型给出的文本,不生成 `pending_command`,因此不会触发 HITL 确认面板。
18+
- `COMMAND_PLAN` 才进入现有 CommandPlan / policy / HITL / execute 流程。
19+
- Python 中禁止维护意图关键词表;路由判断由 prompt + 模型承担。
20+
21+
## 影响
22+
23+
- **受影响文档**:无
24+
- **受影响代码**
25+
- `prompts/intent_router.md`
26+
- `src/linuxagent/prompts_loader.py`
27+
- `src/linuxagent/graph/intent.py`
28+
- `tests/unit/graph/test_agent_graph.py`
29+
30+
## 是否向后兼容
31+
32+
是。实际操作请求仍走原有 policy / HITL;普通聊天减少不必要的确认面板。
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# 失败计划自动修复循环
2+
3+
- **日期**:2026-04-28
4+
- **类型**:设计变更
5+
- **影响范围**:Graph 路由、CommandPlan 失败恢复、交互命令识别
6+
- **决策者**:用户
7+
8+
## 背景
9+
10+
多步骤计划虽然已经能顺序执行所有原始步骤,但当计划中的命令本身写错或不适合无 shell 执行环境时,Graph 会在计划耗尽后进入分析并结束当前 turn。用户要求不能在任务未完成时直接结束,应根据失败结果继续排查并生成修复命令,直到完成或由人类干预停止。
11+
12+
MySQL 场景还暴露出另一个问题:`mysql -e ...` 属于非交互批处理命令,但策略把所有 `mysql` 都标记为 `INTERACTIVE`,导致执行结果无法被正常捕获。
13+
14+
## 新决策
15+
16+
当当前 `CommandPlan` 已耗尽且本轮计划结果包含非零退出码时,Graph 进入 `repair_plan` 节点。该节点把原始用户目标和失败命令结果交给 LLM,要求返回后续修复 `CommandPlan`,再继续走统一的 safety / HITL / execute 流程。每个修复命令仍需要策略检查和必要的人类确认;用户拒绝确认即停止。
17+
18+
为避免旧失败导致修复成功后继续重复重规划,状态中记录 `plan_result_start_index`,只检查当前计划对应的执行结果。
19+
20+
`mysql``psql` 等交互客户端在带 `-e``--execute``-B``--batch` 等非交互标志时,不再按 `INTERACTIVE` 执行。
21+
22+
## 影响
23+
24+
- **受影响文档**
25+
- `prompts/system.md`
26+
- **受影响代码**
27+
- `src/linuxagent/graph/replanning.py`
28+
- `src/linuxagent/graph/agent_graph.py`
29+
- `src/linuxagent/graph/routing.py`
30+
- `src/linuxagent/graph/state.py`
31+
- `src/linuxagent/graph/nodes.py`
32+
- `src/linuxagent/graph/intent.py`
33+
- `src/linuxagent/policy/engine.py`
34+
35+
## 是否向后兼容
36+
37+
是。成功计划仍直接分析并结束;失败计划会继续请求修复命令,且所有命令仍受现有策略和 HITL 保护。
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# 多步骤计划不得因单步失败提前结束
2+
3+
- **日期**:2026-04-28
4+
- **类型**:设计变更
5+
- **影响范围**:Graph 路由、多步骤 CommandPlan prompt
6+
- **决策者**:用户
7+
8+
## 背景
9+
10+
用户执行“安装 MySQL 并修改密码”这类多目标任务时,当前流程可能在安装步骤执行后直接结束当前对话,后续配置或改密步骤未继续执行。原因是 Graph 在任一命令返回非 0 时直接进入分析节点,即使 `CommandPlan.commands` 中仍有后续步骤。
11+
12+
## 新决策
13+
14+
多步骤 `CommandPlan` 只要还有后续步骤,就继续推进到下一步的 safety / HITL / execute 流程;非 0 退出码会保留在步骤结果中,最终分析时一起呈现。只有计划已耗尽、命令被策略 BLOCK、或用户拒绝 HITL 时,当前 turn 才终止。
15+
16+
Prompt 同步强调多目标请求必须覆盖安装、配置、改密、服务启动和验证等完整结果,不得只停在下载安装。
17+
18+
## 影响
19+
20+
- **受影响文档**
21+
- `prompts/system.md`
22+
- **受影响代码**
23+
- `src/linuxagent/graph/routing.py`
24+
- `src/linuxagent/graph/intent.py`
25+
- `tests/unit/graph/test_agent_graph.py`
26+
27+
## 是否向后兼容
28+
29+
是。单命令计划行为不变;多步骤计划更严格地执行完整计划,仍复用每一步现有 policy 与 HITL。

.work/rule/baseline.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# baseline.md · 项目级编码约定
2+
3+
> **状态**:有效
4+
> **适用范围**:所有在本仓库提交的 Python 代码
5+
> 规则可增不可删;删除或放宽需在 `change/` 中记录原因并获得评审。
6+
7+
---
8+
9+
## R-SEC:安全规则(不可妥协)
10+
11+
### R-SEC-01 禁止 `shell=True`
12+
所有 `subprocess` 调用**必须**使用列表参数:
13+
```python
14+
# ✅ 正确
15+
subprocess.run(["ls", "-la", path], capture_output=True, timeout=30)
16+
17+
# ❌ 禁止
18+
subprocess.run(f"ls -la {path}", shell=True)
19+
```
20+
**零例外**。管道组合必须用 `subprocess.run([...], stdin=..., stdout=PIPE)` 串联两个进程,或用 Python 标准库(`glob``pathlib``gzip` 等)替代。历史上的「硬编码常量」豁免无法被 grep 门禁机械验证,已取消。
21+
22+
### R-SEC-02 命令安全检测必须使用 token 级分析
23+
```python
24+
import shlex, re
25+
26+
def is_dangerous(cmd: str) -> bool:
27+
try:
28+
tokens = shlex.split(cmd)
29+
except ValueError:
30+
return True # 解析失败视为危险
31+
# 检测命令名 + 参数组合
32+
...
33+
```
34+
**禁止**`if pattern in command`(字符串包含)用于安全判断。
35+
36+
### R-SEC-03 SSH 必须验证主机密钥
37+
```python
38+
# ✅ 正确
39+
client.set_missing_host_key_policy(paramiko.RejectPolicy())
40+
client.load_system_host_keys()
41+
42+
# ❌ 禁止
43+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
44+
```
45+
46+
### R-SEC-04 敏感配置只走 config.yaml + 强权限
47+
密钥(API Key、SSH 密码等)**只能**写在 `config.yaml` 中,禁止:
48+
- 写入日志(含 DEBUG 级别)
49+
- 出现在 `__repr__` / `__str__`
50+
- 提交到 git(`./config.yaml` 默认 gitignore)
51+
- 通过 `.env` / 命令行参数 / 环境变量承载实际值
52+
53+
加载时必须同时满足:
54+
1. 文件权限 `0o600`,非 `0o600``ConfigPermissionError` 拒绝启动
55+
2. 文件所有者为当前用户(`stat().st_uid == os.getuid()`
56+
3. Pydantic 模型用 `SecretStr`,取值通过 `.get_secret_value()` 显式调用
57+
58+
```python
59+
# ✅ 正确
60+
class APIConfig(BaseModel):
61+
api_key: SecretStr
62+
...
63+
64+
def load_config(path: Path) -> AppConfig:
65+
mode = path.stat().st_mode & 0o777
66+
if mode != 0o600:
67+
raise ConfigPermissionError(f"{path} must be chmod 600, got {oct(mode)}")
68+
...
69+
```
70+
71+
环境变量**只用于指定配置路径**`LINUXAGENT_CONFIG``LINUXAGENT_PROFILE`),不承载值。例外:LangSmith 追踪相关变量(第三方框架原生要求)。
72+
73+
### R-SEC-05 历史文件权限
74+
写入 `~/.linuxagent_*.json` 时必须 `chmod 0o600`
75+
```python
76+
path.touch(mode=0o600, exist_ok=True)
77+
```
78+
79+
---
80+
81+
## R-HITL:Human-in-the-Loop 规则(不可妥协)
82+
83+
运维 Agent 的 HITL 是一等原则。下述规则定义**何时必须问人****如何问****可否降级**。违反任一条视为与 R-SEC 同级的红线。
84+
85+
### R-HITL-01 LLM 输出默认不可信
86+
任何 LLM 生成的命令字符串首次出现时,**必须**经过一次人工 CONFIRM,即使 token 级安全检测判定为 SAFE。经用户批准后可加入**会话级白名单**(归一化命令 + 参数模式),仅在当前进程生命周期内降级为 SAFE;进程退出即失效。**禁止**跨会话持久化白名单。
87+
88+
### R-HITL-02 批量操作强制确认,不可降级
89+
SSH 集群操作的目标主机数 ≥ `cluster.batch_confirm_threshold`(默认 `2`)时,**必须**以下述两种模式之一获得确认:
90+
91+
- **全部同意一次**:预览所有主机 + 命令,用户一次批准
92+
- **逐台确认**:逐台弹出 confirm,用户可在任意一台中止
93+
94+
本规则**不受** `--yes` / 会话白名单影响。
95+
96+
### R-HITL-03 破坏性命令永不进白名单
97+
命中 policy 配置中 `never_whitelist: true` 规则的命令,**每次**执行都必须 CONFIRM,即使已在会话白名单中。破坏性模式清单定义在 `configs/policy.default.yaml` 或用户配置的 policy YAML 中,Python 代码只负责加载、校验和执行这些规则;修改默认策略需走 `change/`
98+
99+
### R-HITL-04 `--yes` 仅对会话级生效
100+
`--yes` / `--no-confirm` / `--batch` 仅对**无直接副作用**的对话级确认生效(如"是否加载历史"、"是否进入多轮对话")。对以下**一律无效**
101+
102+
- 命令级 CONFIRM / BLOCK
103+
- R-HITL-02 批量操作
104+
- R-HITL-03 破坏性命令
105+
106+
**非交互环境(无 TTY)** 中遇到 CONFIRM 请求:**默认拒绝**(记录为 `decision: non_tty_auto_deny`),禁止静默通过。
107+
108+
### R-HITL-05 使用 LangGraph `interrupt()` 原语
109+
confirm 节点**必须**通过 `langgraph.types.interrupt()` 实现,中断点由 `MemorySaver` 持久化。**禁止**在图节点内同步调用 `input()` / `prompt_async()` / `click.confirm()`。理由:
110+
- 中断点可序列化,支持 Ctrl-C 恢复(`Command(resume=...)`
111+
- 非 CLI 前端(Web / API / LangSmith Studio)可直接对接同一流程,无需双实现
112+
113+
### R-HITL-06 所有人工决策留痕
114+
每次 HITL 事件(请求 + 决策 + 执行结果)追加到审计日志 `~/.linuxagent/audit.log`,JSONL 格式,文件权限 `0o600`**不轮转不截断**。字段至少:
115+
116+
```json
117+
{
118+
"ts": "2026-04-23T14:30:00+08:00",
119+
"session_id": "...",
120+
"checkpoint_id": "...",
121+
"command": "rm -rf /tmp/foo",
122+
"command_source": "llm|user|whitelist",
123+
"safety_level": "CONFIRM",
124+
"matched_rule": "DESTRUCTIVE_RM",
125+
"batch_hosts": ["host-a", "host-b"],
126+
"decision": "yes|no|non_tty_auto_deny|timeout",
127+
"latency_ms": 4280,
128+
"exit_code": 0
129+
}
130+
```
131+
132+
敏感值(已知密钥字段、Authorization header 等)写入前脱敏为 `***redacted***`;命令原文本身**不脱敏**(审计需要可追溯)。磁盘容量管理由用户负责,`logrotate` 归档不得覆盖 / 删除当前文件。
133+
134+
---
135+
136+
## R-ARCH:架构规则
137+
138+
### R-ARCH-01 Agent 类行数上限 300 行
139+
`src/linuxagent/app/agent.py` 只做协调(组合服务调用),**禁止**在此实现业务逻辑。
140+
业务逻辑放对应的 `src/linuxagent/services/` 模块。
141+
142+
### R-ARCH-02 服务间依赖必须通过接口
143+
服务类只依赖 `src/linuxagent/interfaces/` 中的抽象类,不得直接 `import` 具体实现。
144+
145+
### R-ARCH-03 统一使用相对导入
146+
同一包内一律使用相对导入:
147+
```python
148+
# ✅ 正确
149+
from .config import AppConfig
150+
from ..interfaces import LLMProvider
151+
152+
# ❌ 禁止
153+
from src.config import AppConfig
154+
```
155+
156+
### R-ARCH-04 Config 必须 fail-fast
157+
启动时用 Pydantic `model_validate` 解析全部配置节;任何字段类型错误立即抛出,不得使用 `getattr(config, 'key', default)` 绕过验证。
158+
159+
### R-ARCH-05 禁止全局可变状态
160+
禁止模块级可变变量(除 `logger`)。所有状态通过实例属性或显式传参管理。
161+
162+
### R-ARCH-06 禁止在 Python 中硬编码业务判断规则
163+
意图分流、运维语义判断、命令生成策略、故障恢复策略等可变业务规则不得用 Python 关键词表、字符串包含、分支枚举等方式写死在代码中。此类规则必须放在单一真源:
164+
165+
- Prompt 模板:`prompts/`
166+
- 策略配置:`configs/policy.default.yaml` 或用户配置的 policy YAML
167+
- Runbook:`runbooks/`
168+
- Pydantic 模型/枚举:仅用于承载结构化协议,不承载业务规则
169+
170+
Python 代码只负责解析结构化输出、执行状态机、调用 policy/runbook/prompt,以及 fail-fast 校验。若确需新增硬编码安全不变量,必须先在 `.work/change/` 记录原因,并且仅限 R-SEC / R-HITL 红线级约束。
171+
172+
测试中也不得把具体运维方法(如某产品改密 SQL、安装流程)作为固定答案写死;只能验证协议、状态流转、安全拦截、脱敏和记忆持久化等行为。具体方法由模型在运行时生成,执行成功后由 learner memory 记录脱敏后的成功命令模式。
173+
174+
---
175+
176+
## R-QUAL:代码质量规则
177+
178+
### R-QUAL-01 裸 `except` 禁止
179+
```python
180+
# ✅ 正确
181+
except (ValueError, KeyError) as e:
182+
logger.warning("...", exc_info=e)
183+
184+
# ❌ 禁止
185+
except:
186+
pass
187+
except Exception:
188+
pass # 不记录的吞掉异常
189+
```
190+
191+
### R-QUAL-02 函数行数上限 50 行
192+
超出时必须拆分。生成器、复杂流式处理可申请豁免,需在 `change/` 记录。
193+
194+
### R-QUAL-03 禁止方法内 import
195+
`import` 语句只写在文件顶部。
196+
197+
### R-QUAL-04 魔法数字必须命名
198+
```python
199+
# ✅ 正确
200+
MAX_CHAT_HISTORY = 20
201+
STREAM_CHUNK_TIMEOUT = 30.0
202+
203+
# ❌ 禁止
204+
if len(history) > 20: ...
205+
```
206+
207+
### R-QUAL-05 注释只写 WHY,不写 WHAT
208+
代码本身表达 WHAT;注释说明不明显的约束、绕过的 bug、或反直觉的实现原因。
209+
210+
---
211+
212+
## R-TEST:测试规则
213+
214+
### R-TEST-01 每个公共函数/方法至少一个单元测试
215+
新增代码的测试覆盖率不得低于 **80%**`pytest-cov` 检查)。
216+
217+
### R-TEST-02 不得 mock 文件系统和子进程用于安全测试
218+
安全相关测试(命令过滤、SSH 策略)必须使用真实逻辑路径,不得用 mock 替代关键安全检查。
219+
220+
### R-TEST-03 测试文件与源文件一一对应
221+
`src/linuxagent/services/command_service.py``tests/unit/services/test_command_service.py`
222+
223+
### R-TEST-04 集成测试隔离
224+
集成测试放 `tests/integration/`,默认不在 CI 主流程运行(需 `--integration` 标志)。
225+
226+
---
227+
228+
## R-DEP:依赖规则
229+
230+
### R-DEP-01 运行时依赖必须实际使用
231+
新增依赖前确认无法用标准库实现。运行时依赖的**唯一真源**`pyproject.toml``[project.dependencies]`;每次 PR 需 `pipreqs src/linuxagent/` 与 pyproject 对齐,pyproject 列出但未被 import 的包视为幽灵依赖,需在 `change/` 说明或删除。**不使用 `requirements.txt` 作为真源**(如存在应由 `pip-compile` 生成为 lockfile)。
232+
233+
### R-DEP-02 构建与开发工具不进运行时依赖
234+
`pyinstaller``build``mypy``ruff``pytest` 等放 `[project.optional-dependencies.dev]` 或专门的 extras group,不进 `[project.dependencies]`
235+
236+
### R-DEP-03 固定主版本号
237+
```
238+
pydantic>=2.0,<3.0
239+
paramiko>=3.0,<4.0
240+
```
241+
禁止裸版本(`pydantic`)或过宽范围(`pydantic>=1.0`)。

0 commit comments

Comments
 (0)