Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/technical/02-Orchestrator执行循环.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@
7. 若有工具调用:逐个执行,写入 tool 消息,再进入下一步。
8. 达到步数上限返回 `step limit reached`。

### 3.1 推荐实现分层(重构目标)

`RunTurn` 应只保留流程编排职责,具体逻辑下沉为 pipeline step:

1. `prepareTurnState`:输入入栈、上下文刷新、回合状态初始化。
2. `chatStep`:调用 provider(含流式输出控制)。
3. `toolLoopStep`:工具调用循环(策略、审批、执行、回写)。
4. `autoVerifyStep`:自动验证与重试注入。
5. `persistStep`:session 落盘与收尾同步。

要求:

- 每个 step 尽量可单测。
- step 之间通过显式状态结构传递,不直接读写过多 orchestrator 字段。

## 4. 工具调用执行顺序
1. Agent 工具开关检查。
2. Policy 决策(`allow/ask/deny`)。
Expand All @@ -30,6 +45,11 @@
5. 执行工具。
6. 结果写入 `tool` 消息并更新运行状态。

补充约束:

- 审批链路应支持“策略层 ask + 工具层 approval request”聚合为一次交互。
- tool 执行失败要标准化写回(`{"ok":false,"error":"..."}`)并继续后续流程判定。

## 5. 模式行为矩阵
- `build`
- 目标:交付改动。
Expand Down Expand Up @@ -111,6 +131,11 @@
- 若无可执行白名单命令:显式提示“未执行自动验证”。
- 可重试失败时注入修复提示继续回合;最多 `max_verify_attempts` 次。

工程约束:

- 自动验证开关与重试默认值应来源于配置层,orchestrator 不维护重复硬编码默认值。
- 是否触发验证的判定逻辑与命令选择逻辑应解耦,分别可测试。

## 9. 复杂任务判定
按需求固定规则执行:
1. 显式多目标枚举命中即复杂。
Expand All @@ -122,8 +147,20 @@
- `/new`、`/resume` 会切换当前 session 上下文。
- 维护回合级 undo 快照栈(仅保留最近若干回合),供 `/undo` 精确回滚使用。

## 10.1 错误与取消语义

- `context canceled/deadline exceeded`:优先返回上下文错误,不包装为业务错误。
- provider/tool/approval 等非上下文错误:按链路分别包装,保留根因信息。
- Esc 取消后不自动回滚副作用,保持“已执行即生效”。

## 11. 运行态取消(Esc)
- REPL 在执行 `RunInput` 时提供可取消的 `context`;Esc 触发 `context cancel`。
- `RunTurn` 在模型调用、审批、tool 执行、自动验证链路中检测 `context canceled` 并立即终止,不继续后续自动化步骤。
- 审批等待场景中 Esc 语义为全局 Cancel(不是 `N`)。
- 取消不做回滚;已完成副作用保持,todo 状态维持最后一次持久化结果。

## 12. 兼容策略(重构期间)

- 对外行为保持稳定:`RunInput`、`RunTurn`、`/` 与 `!` 命令契约不变。
- 允许的小行为修复(例如错误文案更准确、边界值处理修复)需在变更说明中列出。
- 任何行为变化必须配套回归测试(优先 orchestrator 层单测)。
126 changes: 71 additions & 55 deletions docs/technical/10-配置加载与覆盖.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,95 @@
# 10. 配置加载与覆盖(目标态)

## 1. 首次启动初始化项目配置
## 1. 设计目标

当在某个工作目录下首次运行 agent 二进制时,如果当前目录下:
- 以 `config.Default()` 作为**默认值单一来源(SSOT)**。
- `Load()` 仅负责“读取 + 合并 + 归一化 + 环境变量覆盖”。
- 业务层(如 orchestrator/bootstrap)不再复制配置默认值常量。

- 不存在 `./.coder/config.json`,且
- 不存在 `./agent.config.json`
## 2. 首次启动初始化项目配置

程序会自动
当首次在项目目录运行时,若不存在 `./.coder/config.json`,程序会

1. 创建 `./.coder/` 目录(如有需要)。
2. 将内置默认配置(`config.Default()`)序列化为 JSON,写入 `./.coder/config.json`(缩进 2 空格)
1. 创建 `./.coder/`(若不存在)。
2. `config.Default()` 序列化输出模板到 `./.coder/config.json`。

若目录无写权限或写入失败,仅在 stderr 打印警告,不会中断后续配置加载流程
若目录无写权限或写入失败,仅输出 warning,不阻断后续加载流程

## 2. 加载顺序
## 3. 加载顺序

`config.Load()` 实际流程
`config.Load()` 的确定性顺序

1. 内置默认配置(`config.Default()`)。
2. 全局配置:`~/.coder/config.json`(若存在则合并)。
3. 项目配置(按顺序自动发现并合并):
- 当前路径下的 `agent.config.json`;
- 当前路径下的 `.coder/config.json`。
4. 归一化:
- 补默认值;
- 路径展开(如 `~`);
- 列表去重与去空等。
5. 环境变量覆盖(如 `AGENT_BASE_URL`、`AGENT_MODEL` 等)。
6. 二次归一化(保证覆盖后仍满足约束)。
1. 内置默认配置:`config.Default()`。
2. 全局配置:`~/.coder/config.json`(存在则 merge)。
3. 项目配置:`./.coder/config.json`(存在则 merge)。
4. `normalize`(补缺省、路径展开、去重与约束校验)。
5. 环境变量覆盖(如 `AGENT_BASE_URL`、`AGENT_MODEL`)。
6. 二次 `normalize`(确保 env 覆盖后仍满足约束)。

## 3. JSONC 支持
- 解析前去除行注释与块注释。
- 使用标准 JSON 反序列化。
## 4. JSONC 支持

## 4. merge 规则
- 标量字段:非空/有效值覆盖。
- 列表字段:新列表整体覆盖旧列表。
- map 字段:按配置块整体替换。
- 读取后先移除行注释 `//` 与块注释 `/* ... */`。
- 再使用标准 JSON 反序列化。

## 5. normalize 规则
- 补默认值:`max_steps`、`timeout`、`token_limit` 等。
- 路径展开:`~` -> home,统一绝对路径。
- 去重与去空:模型列表、技能路径、命令列表。
- 空白字段清理。
## 5. merge 责任边界

- `types`:声明配置结构,不包含 merge 行为。
- `loader`:文件读取、JSONC 清洗、反序列化。
- `merge`:字段级覆盖策略(标量、list、map 规则)。
- `normalize`:约束收敛与路径归一化。
- `env`:环境变量读取与覆盖。

merge 规则:

- 标量:override 为非空/有效值时覆盖 base。
- list:采用“整体替换”或“显式追加”策略,禁止隐式拼接。
- map:按配置块语义处理(例如 `permission.bash` 为整体替换)。

## 6. normalize 规则

- 补默认值:来自 `config.Default()`。
- 路径展开:`~` 展开为 home,统一绝对路径。
- 去重与去空:模型列表、命令列表、路径列表。
- 约束修正:如阈值上下界、最小重试次数等。

## 7. 环境变量覆盖

## 6. 环境变量覆盖
- `AGENT_BASE_URL`
- `AGENT_MODEL`
- `AGENT_API_KEY`(可回退 `DASHSCOPE_API_KEY`)
- `AGENT_WORKSPACE_ROOT`
- `AGENT_MAX_STEPS`
- `AGENT_CACHE_PATH`

非法值处理:
- 例如 `AGENT_MAX_STEPS<=0` 直接报错。
错误处理:

- 非法值立即返回错误(如 `AGENT_MAX_STEPS<=0`)。

## 8. 关键配置块

## 7. 关键配置块
- `provider`:私有模型地址、默认模型、超时、模型列表。
- `provider`:模型地址/默认模型/超时/模型列表。
- `runtime`:workspace、最大步数、上下文上限。
- `safety`:命令超时、输出截断。
- `compaction`:压缩阈值与保留条数。
- `workflow`:复杂任务 todo、自动验证开关与命令、模式轮转。
- `workflow`:复杂任务 todo、自动验证开关与命令。
- `permission`:工具权限与 bash pattern。
- `agents`:agent 定义与默认 agent。
- `skills`:skill 扫描路径(默认开启)。
- `storage`:SQLite 路径。

## 8. 预设与运行规则
- `/permissions` 预设:`build`、`plan`(与 `/mode` 联动)。
- `/model <name>` 持久化仅写 `./.coder/config.json`。
- 主题/字体不提供配置项:固定 `OpenCode Dark` + 终端默认等宽字体。
- `safety`:命令超时、输出上限。
- `compaction`:压缩开关、阈值、保留消息数。
- `workflow`:todo 约束、自动验证、重试次数、验证命令。
- `approval`:交互审批与自动放行策略。
- `permission`:工具权限、bash 策略、allowlist。
- `agent/agents`:模式与 agent profile 定义。
- `skills`:skill 搜索路径。
- `storage`:持久化目录与缓存策略。
- `lsp`:语言服务配置。
- `fetch`:抓取超时、大小限制、默认请求头。

## 9. 兼容与行为变更记录(重构要求)

- 配置重构应保持外部字段兼容,不改 JSON key。
- 若存在行为变化(如默认值修正),必须:
- 在 PR 描述列出“Before/After”;
- 在本文件追加兼容说明与迁移建议;
- 为变化点补充回归测试。

## 10. 运行规则

## 9. 离线默认建议
- `workflow.verify_commands` 配置白名单命令。
- token 估算默认启发式,精确计数仅作可选增强。
- 不配置 MCP 相关项。
- `/permissions` 预设:`build`、`plan`(与 `/mode` 联动)。
- `/model <name>` 持久化仅写入 `./.coder/config.json`。
- 默认离线模式不要求 MCP 配置。
120 changes: 120 additions & 0 deletions internal/bootstrap/approval_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package bootstrap

import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"

"golang.org/x/term"

"coder/internal/config"
"coder/internal/permission"
"coder/internal/tools"
)

func buildApprovalFunc(cfg config.Config, policy *permission.Policy, workspaceRoot string) func(context.Context, tools.ApprovalRequest) (bool, error) {
return func(ctx context.Context, req tools.ApprovalRequest) (bool, error) {
isTTY := term.IsTerminal(int(os.Stdin.Fd()))
isBash := strings.EqualFold(strings.TrimSpace(req.Tool), "bash")
reason := strings.TrimSpace(req.Reason)

// 非交互环境:为安全起见,继续拒绝执行,避免静默放行破坏性操作。
if !isTTY {
return false, nil
}

// 解析 bash 命令文本(仅在需要展示或加入 allowlist 时使用)。
reader := bufio.NewReader(os.Stdin)
bashCommand := ""
if isBash && strings.TrimSpace(req.RawArgs) != "" {
var in struct {
Command string `json:"command"`
}
if err := json.Unmarshal([]byte(req.RawArgs), &in); err == nil {
bashCommand = strings.TrimSpace(in.Command)
}
}

// 区分策略层 ask 与工具层危险命令风险审批。
isPolicyAsk := strings.Contains(reason, "policy requires approval")
isDangerous := strings.Contains(reason, "dangerous") ||
strings.Contains(reason, "overwrite") ||
strings.Contains(reason, "substitution") ||
strings.Contains(reason, "parse failed") ||
strings.Contains(reason, "matches dangerous command policy")

// 非交互模式配置:策略层 ask 可自动放行;危险命令仍需显式 y/n。
if !cfg.Approval.Interactive && !isDangerous {
// 仅策略层 ask 走 auto_approve_ask;危险命令一律不在此路径放行。
if cfg.Approval.AutoApproveAsk || isPolicyAsk {
return true, nil
}
}

if prompter, ok := approvalPrompterFromContext(ctx); ok {
decision, err := prompter.PromptApproval(ctx, req, ApprovalPromptOptions{
AllowAlways: !isDangerous,
BashCommand: bashCommand,
})
if err != nil {
return false, err
}
switch decision {
case ApprovalDecisionAllowOnce:
return true, nil
case ApprovalDecisionAllowAlways:
if !isDangerous && isBash && bashCommand != "" {
name := config.NormalizeCommandName(bashCommand)
if name != "" && policy.AddToCommandAllowlist(name) {
_ = config.WriteCommandAllowlist(workspaceRoot, name)
}
}
return true, nil
default:
return false, nil
}
}

// 交互式审批:策略层 ask 支持 y/n/always;危险命令仅 y/n。
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, "[approval required] tool=%s reason=%s\n", req.Tool, req.Reason)
if isBash && bashCommand != "" {
_, _ = fmt.Fprintf(os.Stdout, "[command] %s\n", bashCommand)
}

if isDangerous {
// 危险命令风险审批:始终仅 y/n。
_, _ = fmt.Fprint(os.Stdout, "允许执行?(y/N): ")
line, _ := reader.ReadString('\n')
ans := strings.ToLower(strings.TrimSpace(line))
if ans != "y" && ans != "yes" {
return false, nil
}
return true, nil
}

// 策略层 ask:支持 y/n/always。
_, _ = fmt.Fprint(os.Stdout, "允许执行?(y/N/always): ")
line, _ := reader.ReadString('\n')
ans := strings.ToLower(strings.TrimSpace(line))
switch ans {
case "y", "yes":
return true, nil
case "always", "a":
// 仅针对 bash 记录 allowlist;按命令名归一化。
if isBash && bashCommand != "" {
name := config.NormalizeCommandName(bashCommand)
if name != "" && policy.AddToCommandAllowlist(name) {
// best-effort 持久化到项目配置;失败不影响本次放行。
_ = config.WriteCommandAllowlist(workspaceRoot, name)
}
}
return true, nil
default:
return false, nil
}
}
}
Loading
Loading