envfile

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: 11 Imported by: 0

Documentation

Overview

Package envfile 解析 handoff 的 env 文件,并把它换算成可注入子进程的环境变量。

职责:

  • Parse:把 dotenv 形态的文本解析为有序 KV,值支持单层 $VAR/${VAR} 展开
  • Resolver(resolver.go):按 agent 名定位 <DataDir>/env/<文件名>,读盘并 返回 KEY=VALUE 切片

边界:

  • 不是 shell:不做命令替换、不支持多行值、不支持行内注释(理由见 Parse 注释)
  • 不管密钥:不加密、不接 secret 后端;值一律不进日志(本包只在 Resolver 里 打 key 名)
  • 不启动进程:注入由各 adapter 自行完成(经 executor.StartReq.Env)

files.go —— env 文件的列举与读写(控制台配置面用,B158)。

职责:

  • List/Read/Write:<DataDir>/env 下**纯文件名**的查与改
  • resolvePath:包级的纯文件名校验,与 Resolver 共用,判据只有一处

边界:

  • **本层不打日志**:纯文件操作,错误一律 %w 带上下文,日志由 agentd 的 handler 层统一打(与 internal/discipline/files.go 同一条纪律)
  • **不解析内容**:语法校验是 Parse 的事,调用方在写盘前自行调用;本层 连「这是不是一个 env 文件」都不判断
  • **错误文本里绝不出现文件内容**:env 的值常是凭据,错误会进日志与响应体
  • 不碰配置映射(那是 Resolver 与 config 的事)
  • 不做删除与改名:改名会让配置里的映射静默指空(见 spec §1.1)

resolver.go —— env 文件的定位、读盘与日志。

职责:

  • Dir:收口 <DataDir>/env 的目录布局知识,避免各调用方自己拼路径后漂移
  • Resolver.For:按 agent 名解析出可注入的 KEY=VALUE 切片
  • Resolver.Preflight:agentd 启动时把坏文件暴露在启动日志里

边界:

  • 不解析语法(交 Parse)、不注入进程(交各 adapter)、不缓存(见 For 注释)

Index

Constants

View Source
const MaxFileSize = maxEnvFileSize

MaxFileSize 是单个 env 文件的大小上限(64 KiB),与 Parse 的判据同源。

Variables

View Source
var (
	// ErrBadName 表示文件名不是「纯文件名」,调用方应答 400。
	ErrBadName = errors.New("env 文件名非法")
	// ErrTooLarge 表示正文超过 MaxFileSize,调用方应答 400。
	ErrTooLarge = errors.New("env 文件超过大小上限")
	// ErrExists 表示新建时同名文件已存在,调用方应答 409。
	ErrExists = errors.New("同名 env 文件已存在")
	// ErrBaseMismatch 表示前置哈希与磁盘现状不符,调用方应答 409 并回带现状。
	ErrBaseMismatch = errors.New("env 文件已被改动")
)

Functions

func Dir

func Dir(dataDir string) string

Dir 返回 env 文件目录(<dataDir>/env)。

目录布局知识只此一处:manager 与 agentd 各自构造 Resolver,若各拼各的路径, 日后改布局必然漏改一处。

func Read added in v0.3.0

func Read(dir, name string) (content, sha string, size int64, err error)

Read 读一个 env 文件的正文。

返回:

  • 正文、sha256、字节数;文件不存在时错误可用 errors.Is(err, fs.ErrNotExist) 判定

注意:返回的正文**含值**。调用方只应在用户显式要求「编辑正文」时把它交出去; 默认视图走 Parse + 丢值的路径(见 agentd 的 keys 端点)。

func Static added in v0.3.0

func Static(m map[string]string) func() map[string]string

Static 把一份固定映射包成取值函数,供测试与不需要热更新的调用方使用。

func Write added in v0.3.0

func Write(dir, name, content, baseSHA string) (sha string, size int64, err error)

Write 写一个 env 文件,带前置哈希保护。

参数:

  • baseSHA: 空串 = 新建(目标必须不存在);非空 = 覆盖(须与磁盘现状一致)

返回:

  • 新内容的 sha256 与字节数;调用方可直接拿 sha 当下一次写入的 base
  • 冲突时返回**磁盘现状的哈希** + ErrBaseMismatch,供 409 响应体带上现状

注意:

  • **本函数不做语法校验**。调用方须在此之前跑 Parse——先校验再落盘, 写坏的文件不该进磁盘(写进去了才发现,症状会拖到下一次派发)
  • 目录不存在时以 0700 创建;文件 0600——env 里带凭据是常态,权限基线 不能松于 DataDir 下其余内容

Types

type FileInfo added in v0.3.0

type FileInfo struct {
	Name   string
	Size   int64
	SHA256 string
}

FileInfo 是 env 目录下的一个文件(不含正文)。

func List added in v0.3.0

func List(dir string) ([]FileInfo, error)

List 列举 env 目录下的全部普通文件,按名字升序。

参数:

  • dir: env 目录,通常取 Dir(cfg.DataDir)

返回:

  • 文件列表(含大小与哈希);目录不存在时返回空切片与 nil

注意:

  • **目录不存在不是错误**:<DataDir>/env 没有任何东西自动创建,首次打开 设置页时它本来就不存在,报错会把「还没建」画成「读不了」
  • 子目录与非普通文件跳过:env 文件只有一层,不递归

type KV

type KV struct {
	Key   string
	Value string
}

KV 是一条解析结果,按文件内首次出现的顺序排列。

func Parse

func Parse(r io.Reader, lookup func(string) (string, bool)) (kvs []KV, dups []string, err error)

Parse 解析 env 文件内容。

参数:

  • r: 文件内容
  • lookup: 展开时的外部变量查找(生产传 os.LookupEnv);nil 表示外部无变量

返回:

  • kvs: 按首次出现顺序排列的键值对(重复键后者覆盖前者的值,位置保持在首次出现处)
  • dups: 出现过重复定义的键名,供调用方打 WARN(本函数是纯函数,不打日志)
  • err: 语法错误(带行号与原行)或超出大小上限

语法(完整规则见 spec §3):

  • 行尾 \r 先剥离(兼容 CRLF);trim 后的空行与 # 开头行跳过
  • 可选 `export ` 前缀;第一个 = 分割;key 须匹配 keyRe
  • 值 trim 后:'...' 字面量不展开,"..." 与无引号都展开

为什么不支持行内注释:`HTTPS_PROXY=http://host/a#b` 里 # 是合法字符,支持行内 注释会把这类值静默吃掉半截——症状是「代理配了但连不上」,离根因隔了十万八千里。

为什么展开时文件内的键优先于外部环境:让文件自洽,读文件的人不必脑补外部环境 是什么。查不到的变量展开为空串(os.Expand 的默认行为)。

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver 按 agent 名把配置里的文件名换算成可注入的环境变量。

无状态:每次 For 都重新取映射并重新读盘,因此配置改动与文件改动有同一种 时效——都在下一个任务生效,都不需要重启 agentd。

func NewResolver

func NewResolver(dir string, mapping func() map[string]string, log *slog.Logger) *Resolver

NewResolver 构造 Resolver。

参数:

  • dir: env 文件目录,通常取 Dir(cfg.DataDir)
  • mapping: 取当前映射的函数(生产上指向 agentd 的活配置);nil 视为空映射, 此时所有 agent 都不注入
  • log: 日志入口;nil 时退回 slog.Default()

注意:mapping 会在每次 For 时被调用,实现方必须是廉价且并发安全的 (Server.EnvMapping 读的是 atomic 快照,满足这两条)。

func (*Resolver) For

func (r *Resolver) For(agent string) ([]string, error)

For 返回该 agent 启动时应注入的环境变量(KEY=VALUE 形式)。

参数:

  • agent: executor 名(如 opencode)

返回:

  • 该 agent 未配置 env 文件时返回 (nil, nil)——不是错误,是「没配」
  • 文件名非法 / 打不开 / 解析失败时返回错误,错误文本带完整路径与行号

注意:

  • 每次调用都重新读盘,不缓存。改了代理下一个任务就生效,不必重启 agentd (重启会打断正在跑的任务的事件订阅,代价不小);读一个几百字节的文件 相对于拉起一个 agent 的开销可以忽略
  • 日志只打 key 名,绝不打值:环境类变量里 HTTPS_PROXY=http://user:pass@host 是正常写法,值里带凭据的概率不低

func (*Resolver) Preflight

func (r *Resolver) Preflight()

Preflight 读一遍所有被引用的 env 文件,把问题以 WARN 暴露在启动日志里。

为什么只 WARN 不阻断启动:env 文件是数据文件不是配置键,可能在 agentd 启动后 才创建,为它拒绝启动太硬;但完全不检查会把问题拖到第一次派发才暴露——WARN 让它 在启动日志里就可见,真正的拒发发生在 Dispatch(见 spec §6)。

Jump to

Keyboard shortcuts

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