config

package
v0.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package config 负责 handoff 配置的加载、默认值填充与访问令牌生成。

职责:

  • 读取 ~/.handoff/config.yaml(或指定路径)并解析为 Config
  • 首次运行(文件不存在)时生成默认配置与随机 Token 并写盘
  • 旧文件顶层 update 段先剥再严格解码,避免 KnownFields 拒启动
  • 提供 DefaultPath 默认配置路径

边界:

  • 不做网络请求,不校验 Target 可达性(由上层调用方负责)
  • 不依赖 logx,仅使用 slog 默认 logger 输出关键节点日志

listenclass.go —— listen 地址的三档归类与 loopback 变体推导(B85)。

职责:

  • 把 listen 的 host 归为 loopback / 通配 / 单点三档
  • 对通配与单点给出 "127.0.0.1:<同端口>" 的 loopback 变体地址

边界:

  • 纯函数,不做网络请求、不校验地址可绑性
  • CLI(cmd/root.go 拨号改写)与 agentd(cmd/agentd.go 辅助监听)共用同一 口径——两处一旦发散就会出现「CLI 改写了、agentd 没绑」的连接拒绝,判定 必须唯一,这正是本文件存在的理由
  • 与 cmd/init.go 的 listenKind 语义不同(那是 init 交互的预选口径,端口也 参与归类),刻意不合并(spec §3.1)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultPath

func DefaultPath() string

DefaultPath 返回默认配置文件路径(~/.handoff/config.yaml)。

func Save

func Save(path string, cfg *Config) error

Save 把配置以 YAML 写盘,自动创建父目录,文件权限 0600。

参数:

  • path: 目标路径
  • cfg: 要写入的配置

返回:

  • 错误信息:建目录、序列化或写盘失败时返回

注意:

  • 0600 是硬要求:配置里含 token,组内可读就等于把令牌给了同机其他账号

Types

type ApproverConfig

type ApproverConfig struct {
	Executor  string
	Model     string
	Timeout   time.Duration
	Blacklist []string
}

ApproverConfig 描述审批链的廉价模型审批者。

参数语义:

  • Executor:审批者执行者名(如 opencode/claude/grok);空=不启用审批链
  • Model:审批者模型名;空=用执行者自身默认模型
  • Timeout:单次裁决超时,超时按 escalate 处理(fail-closed)
  • Blacklist:自定义黑名单正则;命中即跳过审批者直接升级人工协调者

type Config

type Config struct {
	Listen  string
	Token   string
	DataDir string
	// Relay 是 executor 出站 relay 配置;nil 表示不启用 relay 出站。
	Relay *RelayConfig `yaml:"relay,omitempty"`
	// RepoRoot 是自动登记(B62)的 clone 落点根目录:首次派发到某台机器、而
	// 那台机器上还没有该项目时,agentd 会把仓库 clone 到这里,实际落点为
	// RepoRoot/<登记名>。空=未配置,Load 会补 <DataDir>/repos。
	//
	// 为什么放顶层而不是放进 Target:Target 是在**协调者本地**被读取的
	//(见 cmd/pull.go 的 cfg.Targets[task.Target]),放那儿会让「仓库放哪」
	// 变成协调者的本地状态,换一台协调者机接管就得重配。放顶层的语义是
	// 「每台执行机自己决定它的仓库放在哪」。
	// yaml:"repo_root":strict 解码器(KnownFields)按 tag 匹配键名,不加 tag 时
	// yaml.v3 会把 RepoRoot 映射成 reporoot,与 README/设计文档里的 repo_root 不符。
	RepoRoot string `yaml:"repo_root"`
	// PathDirs 是本机额外的可执行文件搜索目录:agentd 启动时按序追加到 PATH 末尾
	// (见 internal/pathenv)。内置已知目录表没覆盖到的安装位置写在这里。
	//
	// 为什么放顶层而不是放进 Executor:它描述的是「**这台机器**上工具装在哪」,
	// 不是执行者的属性——与 RepoRoot 同一个道理。
	//
	// omitempty 是硬要求,不是风格:配置以 KnownFields(true) 严格解析,未知键让
	// agentd **启动失败**。没有 omitempty 时,新版 Save 会把 path_dirs: [] 写进
	// 每一台机器的 config.yaml,而一台还没换版的旧 agentd 读到它就再也起不来了
	//(B59 spec D7 同款,方向相反)。
	PathDirs []string `yaml:"path_dirs,omitempty"`
	// Proxy 是 handoff **自身**出网时使用的代理地址,形如 http://host:port
	// https://host:port、socks5://host:port、socks5h://host:port。
	// 空 = 不配,沿用 HTTPS_PROXY/HTTP_PROXY/NO_PROXY 环境变量(现行为不变)。
	//
	// 作用范围只有两处:更新链路的 HTTP 出网(查 release、下资产)与 agentd 的
	// git clone/fetch。**不作用于协调者↔agentd 链路**——那是 LAN/loopback 地址,
	// 代理化轻则每次请求多绕一跳,重则 socks5 代理解析不了 100.x.y.z 直接断链,
	// 而这条链路的可达性是 handoff 的命根子。也**不作用于 executor**:executor
	// 的出网归 env 段(B19),两者故障域不交叉——代理挂了只影响升级,不影响任务执行。
	//
	// 为什么放顶层而不是放进 Target:它描述的是「**这台机器**怎么出网」,
	// 与 RepoRoot / PathDirs 同一个道理。
	//
	// omitempty 是硬要求,不是风格:配置以 KnownFields(true) 严格解析,未知键让
	// agentd **启动失败**。没有 omitempty 时,新版 Save 会把 proxy: "" 写进
	// 每一台机器的 config.yaml,而一台还没换版的旧 agentd 读到它就再也起不来了
	//(PathDirs 同款)。
	Proxy string `yaml:"proxy,omitempty"`
	// EnvForward 是要转发进终端会话的环境变量名单(见 internal/ptyhost)。
	//
	// 它解决的是 PathDirs 解决不了的**另一类**问题:SSH_AUTH_SOCK 这类变量由
	// launchd / ssh-agent **按会话注入**,不来自任何 dotfile,因此 login shell
	// 的 rc 链**无法**像恢复 PATH 那样把它恢复出来。agentd 以服务形态托管时,
	// 终端里的 ssh / git push 会因此全部失败。
	//
	// 三态语义(**不要**在 Load 里填默认值):
	//   nil        → 用内置默认清单 ptyhost.DefaultEnvForward()(当前是 SSH_AUTH_SOCK)
	//   非 nil     → 完全以配置为准
	//   [](显式) → 一个都不转发
	// 一旦 Load 把默认值填进结构体,下一次 Save 就会把 env_forward 落进
	// config.yaml,omitempty 形同虚设,旧 agentd 照样被顶死。
	//
	// omitempty 是硬要求,理由同 PathDirs(B59 spec D7)。
	EnvForward   []string `yaml:"env_forward,omitempty"`
	StallTimeout time.Duration
	Targets      map[string]Target
	// Approver 是分级审批链的廉价模型审批者配置。Executor 空=不启用审批链
	//(二期前的现行为:权限请求直接走人工协调者)。
	Approver ApproverConfig
	// Executor 是任务的缺省执行者选择配置。
	Executor ExecutorConfig
	// Terminal 是 dispatch 成功后是否弹终端实况的配置(Auto 默认 false,见 TerminalConfig)。
	Terminal TerminalConfig
	// Sync 是任务结束后自动同步远程任务分支到本地的配置。
	Sync SyncConfig
	// Env 是 agent(executor)名 → env 文件名的映射:该 agent 启动时注入该文件里的
	// 环境变量。文件名必须是 <DataDir>/env/ 下的纯文件名(含路径分隔符会被拒绝)。
	// 未配置的 agent 不注入。任务执行者与审批者共用同一份(见 B19 spec §4)。
	Env map[string]string
	// Discipline 是 executor 名 → 纪律块文件名的映射:派发该 executor 的任务时,
	// 把该文件的内容作为「执行纪律」注入首回合 prompt。文件名必须是
	// <DataDir>/discipline/ 下的纯文件名(含路径分隔符会被拒绝)。
	//
	// 三档语义(与 Env 刻意不同的是第三档):有非空值用该文件;显式空串关闭注入;
	// 未出现该键用内置默认。Env 在未出现该键时是不注入。
	//
	// 为什么第三档不同:env 内容是机器特有的,猜错不如不猜;纪律块内容是 handoff
	// 通用的,不给默认等于让用户退回人工粘贴到 plan 头部(见 B129 spec §2.4)。
	Discipline map[string]string
	// ProcFence 是 executor 进程围栏配置。默认启用、保留 10%。
	ProcFence ProcFenceConfig `yaml:"proc_fence,omitempty"`
	// Web 是浏览器控制台相关配置。
	Web WebConfig
}

Config 是 handoff 的顶层配置。

Listen 为本地 agentd 监听地址;Token 为本机 agentd 的访问令牌; DataDir 为数据目录;StallTimeout 为卡住会话的判定超时; Targets 为可配对远端主机的地址与令牌表(供 --target 换算)。

func Defaults added in v0.3.0

func Defaults() *Config

Defaults 返回一份「出厂默认 + 随机 token」的配置,**不落盘**。

与 Load 的分工:Load 在文件不存在时会生成 token 并把默认配置写盘(firstRun), 这是给 CLI/agentd 用的。桌面壳的首次引导不能调 Load——向导问答中途崩溃、 被杀或取消时,磁盘上绝不允许出现会让 Resolve 判为「已配置」的 config.yaml (SIGKILL 实测原样复现过这个坑,且回滚法依赖进程还活着,封不死)。Defaults 只构造内存里的配置,落盘由调用方在问答成功后一次性 config.Save。

func Load

func Load(path string) (*Config, error)

Load 加载配置:文件不存在时返回带默认值的 Config 并自动生成随机 Token 写盘。

参数:

  • path: 配置文件路径

返回:

  • 解析后的配置;文件不存在时返回默认配置
  • 错误信息:读/解析/校验失败时返回。剥 update 后回写失败不返回错误

注意:

  • 首次运行生成的 Token 需要人工同步到配对主机的 Targets 中
  • 旧文件的顶层 update 必须先剥再 KnownFields,否则 v0.1.x 机器升级即砖

与 Defaults 的分工:Load 在文件不存在时会生成 token 并把默认配置写盘(firstRun), 这是给 CLI/agentd 用的;桌面壳的首次引导走 Defaults(见其 doc 注释)。

type ExecutorConfig

type ExecutorConfig struct {
	Default string
	Model   string
}

ExecutorConfig 描述 dispatch 未显式指定执行者时的缺省选择。

type ListenClass

type ListenClass int

ListenClass 是 listen 地址的三档归类。

const (
	// ListenLoopback:host 已是回环(127.x/::1/localhost),或 listen 解析失败——
	// 错的 listen 让 net.Listen 自己去报,归类函数不抢这个错误。
	ListenLoopback ListenClass = iota
	// ListenWildcard:通配(0.0.0.0/::/空 host),监听面已含 loopback。
	ListenWildcard
	// ListenSingle:单网卡 IP 或主机名——需要辅助监听的档位。
	ListenSingle
)

func ClassifyListen

func ClassifyListen(listen string) (cls ListenClass, loopback string)

ClassifyListen 把 listen 的 host 归为三档,并推导 loopback 变体地址。

参数:

  • listen: 形如 "host:port" 的监听地址

返回:

  • cls: 三档归类;解析失败归 ListenLoopback(即调用方什么都不做)
  • loopback: 通配/单点档为 "127.0.0.1:<同端口>";loopback 档(含解析失败) 原样返回 listen,调用方可无条件使用返回值

type ProcFenceConfig

type ProcFenceConfig struct {
	Disabled     bool    `yaml:"disabled"`
	ReserveRatio float64 `yaml:"reserve_ratio"`
	// TaskBudget 是**单个任务**名下的进程数告警线,超过即发一次
	// task_proc_pressure 事件唤醒审核者。0 = 关掉这一档。
	//
	// 为什么不是把围栏值调小:RLIMIT_NPROC 的内核判定是「该 uid 当前进程总数
	// 是否超过调用者软限」,不是「这棵进程树的后代数」。给每个 shim 装 300 的
	// 效果是「uid 总数一过 300 所有 shim 一起 fork 失败」,第二个任务会被第一个
	// 饿死——表达不了每任务额度,只能换成 watchdog 按任务点名(B93 spec §2.2)
	TaskBudget int `yaml:"task_budget"`
	// TaskHardLimit 是单个任务的进程数硬上限,超过即强制清扫并落 failed。
	// 0 = 关掉这一档。
	//
	// 两档的分工:TaskBudget 是「叫醒人」,TaskHardLimit 是「不等人了」。
	// 只有一档要么太吵(每次都杀)要么太晚(人没醒机器就没了)。
	TaskHardLimit int `yaml:"task_hard_limit"`
}

ProcFenceConfig 描述 executor 进程围栏(RLIMIT_NPROC)的策略。

字段说明:

  • Disabled: true 时完全不装围栏。逃生开关,正常不该用——2026-08-12 的 整机 fork 瘫痪就是无围栏状态下发生的
  • ReserveRatio: 保留给 agentd/sshd/登录 shell 的名额占系统上限的比例; 0 或越界时取默认 0.1。这是「救护车道」的宽度,不是给 executor 的节流 旋钮——调小它不增加安全性,只会让 executor 更早撞墙

注意:yaml tag 必须写全。strict 解码器(KnownFields)按 tag 匹配键名, 不加 tag 时 yaml.v3 会把 ReserveRatio 映射成 reserveratio,与 README 里的 reserve_ratio 对不上(RepoRoot 同款教训)。

type RelayConfig added in v0.3.2

type RelayConfig struct {
	URL        string `yaml:"url"`
	Credential string `yaml:"credential"`
	Node       string `yaml:"node"`
}

RelayConfig describes the executor's outbound relay registration. Credential is only sent during the WSS control exchange; it is separate from the E2E key derived from Token.

func (*RelayConfig) Validate added in v0.3.2

func (r *RelayConfig) Validate() error

Validate validates the executor relay configuration. ws:// is accepted for local tests; production deployments should use wss://.

type SyncConfig

type SyncConfig struct {
	Auto bool
}

SyncConfig 描述任务结束(completed/failed)后 wait 是否自动把远程任务分支 同步到本地仓库。Auto 默认 true;关闭后仍可用 handoff pull 手动同步。

type Target

type Target struct {
	Addr       string `yaml:"addr,omitempty"`
	Token      string `yaml:"token,omitempty"` // relay 形态下额外用作 E2E PSK 源(HKDF 派生),relay 不可见。
	User       string `yaml:"user,omitempty"`
	Relay      string `yaml:"relay,omitempty"`      // relay WSS URL;与 Addr 互斥。
	Credential string `yaml:"credential,omitempty"` // coordinator 的 CONNECT 凭证。
	Node       string `yaml:"node,omitempty"`       // relay 上的 executor 节点名。
}

Target 描述一个可配对远端主机:Addr 为 agentd 地址,Token 为其访问令牌, User 为可选的 ssh 用户名(非空时 attach/pull 的 ssh 目标换算为 user@host, 空=保持历史行为只用 host)。

func (Target) IsRelay added in v0.3.2

func (t Target) IsRelay() bool

IsRelay reports whether this target uses the relay form.

func (Target) Validate added in v0.3.2

func (t Target) Validate() error

Validate validates a target. Relay and direct targets are mutually exclusive: relay targets require Relay, Credential, Node, and Token, while direct targets require Addr.

type TerminalConfig

type TerminalConfig struct {
	Auto bool
}

TerminalConfig 描述 dispatch 成功后的终端弹窗行为。

Auto 默认 **false**(不弹);仅当置 true 时才在 darwin 下用 osascript 弹 Terminal.app 进实况;其余平台无论配置如何都降级为打印 「实况: handoff attach <id>」提示行。

为什么默认不弹:dispatch 的 stdout 有「单行任务 JSON」契约,弹窗与提示行都 不该干扰它;且逐次开关由 --no-terminal 承担,默认不弹才不会让老脚本因为 多出一行提示而解析错乱。

type WebConfig added in v0.3.0

type WebConfig struct {
	AllowedHosts []string `yaml:"allowed_hosts"`
}

WebConfig 是浏览器控制台相关配置。

AllowedHosts 是 Host 白名单的扩展项——回环地址(127.0.0.1 / localhost / ::1) 与 Listen 的 host 恒在白名单内,无需重复配置。它为将来的域名/中转场景预留: agentd 部署在 handoff.example.com 后面时,不配这一项所有请求都会被 403。

yaml:"allowed_hosts":strict 解码器(KnownFields)按 tag 匹配键名, 不加 tag 时 yaml.v3 会把它映射成 allowedhosts(同 RepoRoot 的处理)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL