会话环境

运行时进程启动时拿到什么:环境变量、PATH、临时目录与登录凭据;以及它怎么结束:进程组、信号与超时。目标是调用方能完整控制会话环境,同时调用方环境里残留的变量不会悄悄改变登录方式。

代码位置

文件主要类型与函数本页对应的章节
src/session.rsSession(assemble、runtime_dir)、home_placeholder、CODEX_AUTH_VARIABLE、PI_AUTH_VARIABLE、SessionInputs(read、assemble)、find_executable、is_executable环境变量、--env、env 文件格式、PATH
src/run/mod.rscreate_tempdir会话临时目录
src/run/execute.rsexecute、temp_environment会话临时目录、进程、信号与超时
src/runtime/pi.rs、src/runtime/codex.rs、src/sandbox/mod.rspi.rs 的 user_state_dir、STATE_HOME_SUBDIR、LOGIN_FILE;sandbox/mod.rs 的 PiState;codex.rs 的 user_home、HOME_SUBDIR、LOGIN_FILE、login_file、check_login、create_homepi 的状态目录与 codex、dsh 的私有配置目录、多个运行同时进行
src/runtime/dsh.rsREMOVED_PREFIX、KEY_VARIABLE环境变量、dsh 的私有配置目录、凭据
src/credential.rswrite_session_credential凭据
src/process_tree/mod.rs 的 ProcessTable、Recorded、wait_for_exit、kill_group_members、kill_tree、wrapped_child;linux.rs 与 macos.rs 的 claim_orphans、ForkWatcher、snapshot进程、信号与超时
src/run/signal.rsSignals(install、prepare、checking、doctoring、running、finishing、exited、kill_group、stop_for_limit、hold)进程、信号与超时

Signals 在进程启动时安装一次,之后按运行所处的阶段决定收到信号时的动作:启动前只记录,检查沙箱时终止检查进程,运行中转发给进程组。理由:信号可能在任何时刻到达,每个阶段的清理动作不同,由同一个对象记录阶段,处理函数里只读它。

环境变量

会话的环境变量按以下顺序组装,后一步覆盖前一步:

  1. 从调用方自己的环境开始。
  2. 按运行时删除会改变登录方式的变量,见下表。
  3. 按给出的顺序加载 --env-file。
  4. 按给出的顺序应用 --env。
  5. 读取 AGENTRUN_ 开头的变量(AGENTRUN_SANDBOX 与两个凭据变量),然后从会话环境删除所有 AGENTRUN_ 变量。
  6. 设置 PATH、TMPDIR、TMP、TEMP;codex 的 CODEX_HOME 指向私有配置目录;网络经过滤代理时设置代理变量;claude-code 设置 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1,理由见运行;claude-code 的沙箱运行时设置 SOCAT_DEFAULT_LISTEN_IP=4,理由见网络;dsh 设置 DSH_HOME、DSH_AGENTS_HOME、DSH_PERMISSION_MODE 与 DSH_TELEMETRY_DISABLED,见dsh 的私有配置目录。
运行时删除的变量效果
claude-code以 ANTHROPIC_ 或 CLAUDE_CODE_USE_ 开头的变量使用订阅登录,不受调用方环境里的 API key 或第三方端点影响
codexCODEX_API_KEY、OPENAI_API_KEY、OPENAI_BASE_URL使用 auth.json 里的登录
pi不删除pi 从环境变量或 auth.json 读取服务商 key
dsh以 DSH_ 开头的变量dsh 只用 agentrun 给出的私有目录与设置,调用方环境里的 DSH_TOOLS_MODE、DSH_TELEMETRY_OTLP_URL 这类变量不改变工具与上报。DEEPSEEK_API_KEY 与 DEEPSEEK_BASE_URL 不删除

表里没有的变量原样进入会话。claude-code 读取的其他 CLAUDE_ 变量(例如 Cloud Managed 容器里设置的 CLAUDE_CODE_INCLUDE_PARTIAL_MESSAGES,它让 claude-code 多输出 stream_event,这些事件只写入 --raw)不删除,因为 CLAUDE_CODE_OAUTH_TOKEN 这样的登录变量也在这个前缀之下。

删除只作用于从调用方环境继承来的变量,--env 与 --env-file 显式给出的变量不受影响。理由:删除规则防的是调用方没有意识到的残留变量;显式给出的变量是调用方的意图,例如用 --env ANTHROPIC_BASE_URL=... 接入兼容 Anthropic 接口的其他服务。这时登录方式由调用方负责,和 -- 透传一样。

AGENTRUN_ 变量从会话环境删除的理由:凭据变量里是登录文件的完整内容,留在会话环境里,agent 运行 env 就能读到。AGENTRUN_ 变量可以来自 --env 与 --env-file,所以在第 5 步才读取。

--env

env 文件格式

# 注释行与空行忽略
AGENTRUN_SANDBOX=off
export CLAUDE_CODE_OAUTH_TOKEN="..."
GREETING='hello world'

每行 KEY=VALUE,行尾的 \r 去掉;允许 export 前缀;等号两侧不允许空格;值两端成对的单引号或双引号去掉,引号内不处理转义;不做变量展开,不支持多行值;未加引号的值里的 # 是值的一部分。文件不存在、不是 UTF-8 或某一行格式错误时,启动前报错(退出码 2),错误信息写出文件名与行号,不写出这一行的内容。

工具不自动加载工作目录里的 .env。理由:工作目录 agent 可写,agent 可以写一个 .env 改变下一次运行的登录方式或 PATH;项目里的 .env 也常有调用方不打算交给 agent 的密钥。

只显示变量名

start 事件的 env 字段是 --env-file 与 --env 设置过的变量名,按首次出现顺序去重,不含 AGENTRUN_ 变量。--debug 与 --dry-run 另外写出被删除的变量名、被设置的变量名、读到的 AGENTRUN_ 变量名与会话 PATH。任何输出都不写变量的值,PATH 除外。

PATH

会话的 PATH 是 --path 给出的目录(转为绝对路径,按给出的顺序)加上组装后的 PATH。运行时(claude、codex、pi、dsh)、bwrap 与 socat 都在这个 PATH 里查找,用找到的绝对路径启动,不解析符号链接。

这样运行时在调用方的终端里能启动,在会话里也能启动。运行时可能是一个 sh 脚本,里面再调用 node 或其他程序,工具无法事先知道它会用到哪些命令。

PATH 不限制 agent 能执行哪些程序,agent 可以用绝对路径执行任何程序。限制 agent 的是沙箱的写范围与网络限制。

会话临时目录

工具在 $TMPDIR(未设或为空时为 /tmp)下创建 agentrun-<随机后缀>,权限 0700。会话的 TMPDIR、TMP、TEMP 指向它,沙箱把它加入可写路径。正常结束、失败、超时与被中断时都删除它;给了 --debug 时保留,路径写到标准错误。claude-code 的沙箱把命令看到的 TMPDIR 改成名为 claude-<uid> 的目录:Linux 上它在会话临时目录之下(云端与本机 Linux 实测);macOS 上它是 /tmp/claude-<uid>,由同一用户的所有 claude 会话共用(macOS 实测)。macOS 上的这个差异被接受,agentrun 不为此限制会话临时目录的位置。

理由:工作目录里不出现临时文件;只开放这一个目录,不把整个 /tmp 设为可写,因为 /tmp 由整台机器共用,里面有其他程序的文件与 socket。

agentrun 自己的文件也放在这里:pi 与 dsh 包裹用的 Seatbelt 配置 seatbelt.sb、过滤代理的 Unix socket、--debug 下的 raw.jsonl、给出 --session-dir 时 pi 的会话记录 pi-session/(运行结束后移到 --session-dir,见保存与继续会话),以及 dsh 的补丁文件 dsh-patch.yml 与私有目录 dsh-home/、dsh-agents/。

pi 的状态目录与 codex、dsh 的私有配置目录

pi 使用用户的状态目录

pi 直接使用用户的状态目录 $PI_CODING_AGENT_DIR,未设时为 ~/.pi/agent。agentrun 不改这个变量,也不另建目录;pi 读到用户完整的 settings.json、models.json、bin/ 与 keybindings.json。agentrun 按同一规则算出这个目录(user_state_dir),用于沙箱放行、读默认模型设置与写入 AGENTRUN_PI_AUTH。目录不存在时在启动前递归创建,新建的每一级权限 0700;创建失败按启动前检查报错,detail 为 cannot create the pi state directory <路径>: <系统错误>。--dry-run 不创建目录。PI_CODING_AGENT_DIR 与 HOME 都没有时算不出目录,不创建也不放行。

沙箱只放行目录里的五个条目可写:auth.json、auth.json.lock、models-store.json、models-store.json.lock、settings.json.lock,即 src/runtime/pi.rs 的 WRITABLE_STATE_ENTRIES。state 读一次目录,生成 PiState:dir 是目录的真实路径;writable 是 dir 下这五个条目的路径,顺序同上,供 macOS 的 Seatbelt 配置逐个放行;login_target 是 auth.json 的真实路径,只在它是指向别处的符号链接时有值,这时链接目标也放行;readonly 是其余条目里跟随符号链接后是目录或普通文件的那些,按文件名排序,供 Linux 的沙箱只读覆盖。各平台怎样放行见沙箱与网络。

理由:pi 刷新 OAuth 令牌时在 <状态目录>/auth.json.lock 上加锁,加锁不跟随符号链接,所以锁必须建在用户的状态目录里,几次运行之间、运行与用户自己的 pi 之间才互斥。pi 启动时就在状态目录里创建 auth.json(内容为 {})与 models-store.json,读 models-store.json 时建 models-store.json.lock,读 settings.json 时建 settings.json.lock(pi 1.0.4)。锁目录建不出来时 pi 不报错停止:读设置文件的锁建不出来时,它只在标准错误警告 Invalid settings file,把设置文件当作不存在,默认模型随之改变。所以这五个条目都要放行;用户没有 auth.json 时,运行后目录里会多出 pi 创建的这两个文件。

codex 的私有配置目录

codex 不直接使用用户的 CODEX_HOME。每次运行由 codex 适配器的 launch 在会话临时目录与过滤代理之后新建一个私有目录(0700),通过 Launch.private_dirs 交给运行框架;运行框架在同一个 Hold 内登记它。--dry-run 不创建。它与会话临时目录在同样的时机删除(正常结束、失败、超时、被中断,以及启动前收到信号),给了 --debug 时同样保留,并在标准错误写一行 [debug] codex home: <路径>。用户的登录文件以符号链接放进去,其他内容不带进来。

位置内容
与会话临时目录同级的 agentrun-<同一后缀>-codex/,CODEX_HOME 指向它auth.json:指向用户登录文件($CODEX_HOME/auth.json,未设时为 ~/.codex/auth.json)的符号链接。继续会话时另有 sessions/ 下复制进来的会话记录,见保存与继续会话。codex 运行中在这里写它自己的数据库、缓存与会话记录

用户登录文件的位置按应用 --env-file 与 --env 之后的会话环境计算。调用方给出的 CODEX_HOME 只决定从哪里带入登录文件,组装环境的最后一步把这个变量改为指向私有目录。

几个容易被改坏的决定:

几个运行(包括用户自己在沙箱外运行的 pi 或 codex)同时刷新同一个登录文件里的令牌时会发生什么,见多个运行同时进行。

dsh 的私有配置目录

dsh 不使用用户的 ~/.dsh。每次运行由 dsh 适配器的 launch 在会话临时目录里创建 dsh-home/ 与空目录 dsh-agents/(0700),DSH_HOME 与 DSH_AGENTS_HOME 指向它们;--dry-run 不创建。dsh 运行时在 dsh-home/ 里写入 profile 模板、匿名的用户 id 与会话记录(实测约 72 KB),这些内容随会话临时目录删除,给了 --debug 时一起保留。继续会话时 dsh-home/sessions/ 下另有复制进来的会话记录,见保存与继续会话。另外两个变量 DSH_PERMISSION_MODE=danger-full-access 与 DSH_TELEMETRY_DISABLED=1 的理由见运行页的 dsh 一节。这四个变量在 --env 之后设置。

用户 ~/.dsh 里的 .env、AGENTS.md、profile 目录里的插件与补丁、~/.agents 里的指令与技能都读不到。私有目录与补丁文件一起使用,理由:只用补丁时,用户自己的 profile 补丁与 home 补丁能新增插件条目,按 id 关闭的补丁关不掉 agentrun 不知道 id 的条目,profile 目录里也可能装了插件包(dsh 0.2.1-rc.2 源码与实测)。私有目录不增加启动时间,首次使用与重复使用同一目录的耗时相同(实测)。

凭据

本机沿用用户已有的登录,工具不改动用户自己的登录文件。容器里由启动方把登录文件的内容放进环境变量,工具在运行与 doctor 开始时写到运行时读取的位置。这两个变量可以来自调用方环境,也可以来自 --env 与 --env-file。

环境变量写入位置
AGENTRUN_CODEX_AUTH$CODEX_HOME/auth.json,未设 CODEX_HOME 时为 ~/.codex/auth.json
AGENTRUN_PI_AUTH$PI_CODING_AGENT_DIR/auth.json,未设时为 ~/.pi/agent/auth.json

一次运行只写当前运行时的变量。变量未设或为空时不写。路径变量与 HOME 从组装后的会话环境读取,相对路径按 --cwd 解析。

写入位置由 codex::user_home 与 pi::user_state_dir 算出,与运行时实际读取的目录是同一个函数。理由:两处分开写时,改了一处的目录约定,凭据就会写到运行时读不到的地方。

每次写入时,工具在同一目录记一个旁注文件 auth.json.agentrun-sha256,内容是这次变量值的 SHA-256(小写十六进制加换行)。下次运行时:登录文件不存在就写入;变量值的 SHA-256 与旁注相同就不动;不同或没有旁注就覆盖登录文件并更新旁注。理由:运行时会在使用中刷新令牌并改写登录文件,变量没变时保留刷新后的令牌;启动方轮换了凭据时,即使登录文件在持久卷上,会话也会改用新账号。

写入前先创建登录文件所在的目录(新建的每一级权限 0700,已有目录不改权限),再对这个目录加排他的 flock,比较旁注、写登录文件、写旁注都在锁内完成。理由:两次运行同时写入不同的值时,没有锁会出现登录文件来自一次、旁注来自另一次,之后的运行按旁注判断会保留错误的账号。登录文件与旁注都用同目录的临时文件写入,以排他方式创建、权限 0600、fsync 后重命名,同时启动的其他运行不会读到写了一半的文件。写入失败时启动前报错(退出码 2),detail 写变量名与路径,不写值。

登录文件是符号链接且链接目标存在时,工具写到链接目标,链接本身保留;目录锁、旁注与临时文件都放在链接目标所在的目录,旁注名是目标文件名加 .agentrun-sha256。链接目标不存在时按普通文件处理,链接被替换为登录文件。理由:几个状态目录链接到同一个登录文件时,它们的写入因此排队,旁注也只有一份。--debug 的 credentials: 一行与错误信息里的路径仍是链接本身的路径。

claude-code 在容器里用它自己的令牌变量 CLAUDE_CODE_OAUTH_TOKEN 登录,没有登录文件。工具不做转换,只保证它不被删除(它不以 ANTHROPIC_ 或 CLAUDE_CODE_USE_ 开头)。

dsh 用 DeepSeek 的 API key 登录,没有登录文件,也没有对应的凭据变量:key 由调用方放进会话环境的 DEEPSEEK_API_KEY,来自调用方环境、--env 或 --env-file 都可以。会话环境里没有这个变量或值为空时启动前报错(退出码 2),见运行页的 dsh 一节。

codex 要求登录信息以文件形式保存,登录文件不存在时启动前报错,提示先运行 codex login 或在容器里用 AGENTRUN_CODEX_AUTH 传入。

凭据内容不出现在事件流、标准错误、--raw 与错误信息里,只显示变量名与文件路径。

进程、信号与超时

无论运行以哪种方式结束,都满足三条:事件流以 end 结尾;会话临时目录被删除(--debug 除外);运行时及其子进程不再运行。

收到 SIGINT 或 SIGTERM--timeout 到时把同一个信号转发给进程组向进程组发 SIGTERM最多等 5 秒,输出照常解析等待中再收到信号时不再等待运行时主进程自行退出主进程退出,或等待结束进程组 SIGKILL,再按进程树清理最多再等 1 秒,读完剩余输出输出 end,删除临时目录,退出
任何结束方式最后都向整个进程组发 SIGKILL,再按进程树清理离开了进程组的进程,运行时启动的后台命令不会留下。

是否被中断只看 agentrun 自己收到的信号与自己的计时,不看运行时的退出码与事件。理由:实测 claude-code 收到 SIGINT 后以退出码 0 结束,SIGTERM 后以 143 结束且没有 result;pi 收到 SIGINT 后以 130 结束,最后没有 message_end;codex 收到 SIGTERM 后在 macOS 上以 143 结束,在 Linux 上以 0 结束;dsh 收到 SIGINT 后以 130 结束,SIGTERM 后以 0 结束,两种情况都没有 turn_end。

已知限制:macOS 上一个进程在 fork 之后、agentrun 读到进程表之前(约 1 毫秒)就退出时,它刚启动的子进程记录不到;子进程还在同一个进程组里时仍会被清理。macOS 上进程组的 SIGKILL 与组里进程的 fork 同时发生、并且 agentrun 读进程表时新的子进程还没有出现在表里,这个子进程会留下;doctor 的检查进程不做进程树监视,这种情况下没有其他兜底。

多个运行同时进行

同一个用户可以同时启动多个 agentrun,运行时可以相同也可以不同,--cwd 也可以相同。agentrun 不在运行之间加锁,也不限制同时运行的数量。

每次运行各自独立的部分:

共用的部分: