package tools // workdir.go — per-invocation working-directory override for cwd-aware tools. // // Problem: BashTool / BashToolBackground / FileEditTool / GitignoreTool bake // the working directory at construction (NewBashTool(cwd, ...) etc). When a // SubAgent is forked with a different cwd (worktree isolation mode sets // cfg.Cwd = wtInfo.Path), the sub-agent inherits the parent engine's tool // registry as-is, so those tools still execute in the parent cwd -- the // "worktree" is a label on the SubAgent with no runtime effect. // // Fix: sub-agent injects its cwd into ctx before each tool.Execute; cwd-aware // tools read WorkdirFromContext(ctx) at Execute start and fall back to the // construction-time cwd when the value is empty. // // Security: the context key is a private struct type, so external code // (plugin tools, user callbacks) cannot construct the same key and forge a // cwd value. Only code in this package (and transitively, code receiving the // derived ctx) can write the override. The FileEditTool symlink guard relies // on this — a forged cwd would let a tool write outside the intended // directory, which is precisely what the guard exists to prevent. // // workdir.go — cwd 感知工具的单次调用工作目录覆盖. // // 问题: BashTool / BashToolBackground / FileEditTool / GitignoreTool 在构造 // 期固化 cwd (NewBashTool(cwd, ...) 等). SubAgent fork 时若传了不同的 cwd // (worktree 隔离模式下 cfg.Cwd = wtInfo.Path), 子 agent 继承父 engine 的工具 // 注册表, 工具仍在父 cwd 执行 — "worktree" 只是挂在 SubAgent 上的标签, 运行 // 时毫无作用. // // 修复: 子 agent 每次 tool.Execute 前把自己的 cwd 注入 ctx; cwd 感知工具在 // Execute 开头读 WorkdirFromContext(ctx), 为空时回退构造期 cwd. // // 安全: context key 是私有 struct 类型, 外部代码 (plugin 工具 / 用户 callback) // 无法构造同一 key 伪造 cwd 值. 只有本包代码 (以及接收派生 ctx 的下游) 能 // 写入. FileEditTool 的 symlink guard 依赖这一点 — 伪造 cwd 会让工具写到 // 预期目录外, 而 guard 的存在正是为了阻止这种情况. import "context" // workdirKey 是 WithWorkdir / WorkdirFromContext 使用的私有 context key. // 类型是未导出 struct, 保证外部无法用 context.WithValue(ctx, X, Y) 冒充同 // 一 key. workdirKey is the private context key used by WithWorkdir / // WorkdirFromContext. The unexported struct type guarantees no external // package can spoof the same key via context.WithValue(ctx, X, Y). type workdirKey struct{} // WithWorkdir 派生一个携带 dir 的 context, 供 cwd 感知工具在 Execute 开头 // 读取. dir 空串视为 "无覆盖" — 直接返回原 ctx 不污染调用链. // // WithWorkdir derives a context carrying dir, to be read by cwd-aware tools // at Execute start. An empty dir is treated as "no override" — the original // ctx is returned unchanged to keep the call chain clean. func WithWorkdir(ctx context.Context, dir string) context.Context { if dir == "" { return ctx } return context.WithValue(ctx, workdirKey{}, dir) } // WorkdirFromContext 返回 WithWorkdir 设置的 cwd, 未设置返回 "". 工具实现 // 模式: cwd := WorkdirFromContext(ctx); if cwd == "" { cwd = t.cwd }. // // WorkdirFromContext returns the cwd set by WithWorkdir, or "" if absent. // Tool implementation pattern: cwd := WorkdirFromContext(ctx); if cwd == "" // { cwd = t.cwd }. func WorkdirFromContext(ctx context.Context) string { v, _ := ctx.Value(workdirKey{}).(string) return v }