TTTPOB
dsh-workspace-envrc
Native direnv environments for DSH workspace Bash and persistent terminals
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 15, 2026
- Updated
- Aug 15, 2026
Introduction
dsh-workspace-envrc
English version: docs/README.en.md
DSH 树外独立 bundle:把本机原生 direnv 环境应用到显式归属于 Agent/workspace 的 Bash 执行(foreground 与 background)与持久终端创建。插件把「发现、.envrc 求值、授权 hash、allow/deny、stdlib、环境变更」全部委托给已安装的 direnv 可执行文件——它绝不解析或 source .envrc、绝不维护授权数据库、绝不调用 direnv allow/permit/grant/edit、绝不使用 direnv export、不 watch 也不缓存任何 .envrc、绝不修改 Harness 进程的 process.env,并且不向模型暴露任何 allow/deny 工具。授权始终由用户在 DSH 之外的终端里用 direnv allow <exact .envrc> 完成。
目标 DSH:0.1.0-rc.6。运行时 peer 包括 @deepseek-ai/cordis 4.0.1、@deepseek-ai/dsh-agent / @deepseek-ai/dsh-scope / @deepseek-ai/dsh-shell / @deepseek-ai/dsh-sandbox / @deepseek-ai/dsh-subprocess / @deepseek-ai/dsh-terminal / @deepseek-ai/dsh-timeout 0.1.0-rc.6 与 dsh-workspace-overlay ^0.1.0;@deepseek-ai/schemastery 按实际身份策略作为普通依赖(与 DSH 各包及兄弟仓库 dsh-workspace-overlay 的声明方式一致)。版本均与安装版一致。
当前状态
全部能力已实现并有测试覆盖:workspaceEnvrc provider core、可逆 Bash adapter、持久终端 adapter、集成行,以及两条真实组合测试路径(真实 direnv 的 allow/deny/内容变更状态机、真实 Cordis Loader 组合内置 dist 的 provider/integration 行)。实现计划 docs/implementation-plan.md 状态为 implemented and published,全部完成标准已达成。本 README 描述当前实现事实,不再按历史 Block 分期叙述。
依赖与安装
- 本 bundle 是独立仓库,依赖
dsh-workspace-overlaybundle:workspaceCordis(canonical workspace 身份与 scope 映射)与公开的dsh-workspace-overlay/method-wrapper(可逆方法装饰)。 - 安装顺序:先装 overlay bundle,再装本 bundle。 本 bundle 的 patch(
cordis.patch.yml)只插入自己的两行(provider 行 + 集成行),绝不自动插入 overlay 行——overlay 行由 overlay 自己的 bundle patch 提供,dsh plugin add不会跨 bundle 改写 profile。 - 系统必须已安装 direnv:激活 preflight 会运行
direnv version。本 bundle 不安装 direnv,也不调用direnv allow;.envrc授权由用户在 DSH 外人工完成(见「原生 direnv 语义」)。
# 1) 先装 workspace overlay bundle(提供 workspaceCordis 与 method-wrapper)
dsh plugin --profile web add /path/to/dsh-workspace-overlay
# 2) 再装本 bundle
dsh plugin --profile web add /path/to/dsh-workspace-envrc
# 3) 检查最终组合:本 bundle 的两行与完整 config 均可见
dsh --profile web --dump-config
卸载:dsh plugin --profile web remove dsh-workspace-envrc(overlay 保持在原位;没有本 bundle 的包装时,Bash 与终端回到原生未包装行为)。
原生 direnv 语义
- 固定 canonical workspace root 查找:workspace 只来自发起 Agent 的 scope 映射——以
scopeOf(agent.ctx)为起点沿scopeParentOf上行,逐 key 询问workspaceCordis.workspaceForScope,首个命中即 canonical root(覆盖 agent→preset→workspace 链);V1 绝不从 per-commandworkdir选择嵌套.envrc,也不读session.header.cwd。 direnv exec DIR不 chdir:环境按 canonical root 加载,子进程 cwd 保持调用方(Bash request 的workdir/ 终端 backend 解析的 cwd)不变。- 无
.envrc透传:没有适用的.envrc/.env时,原生 direnv 以继承环境正常执行,无任何插件级 fallback 分支。 - blocked / deny / 内容变更:
.envrc未 allow、被 deny、或 allow 后内容变更使原生 hash 失效时,原生 direnv 拒绝执行,其原始 stderr/exit status 原样到达 Bash/terminal 调用方,原程序不会运行。 - 授权在 DSH 外:用户在本机终端(与运行 DSH 相同的 OS 账号)人工执行
direnv allow <exact .envrc>;重新 allow 前,后续执行持续失败。DSH 侧没有任何 allow/deny/编辑入口。 - 插件绝不调用:
direnv allow/permit/grant/edit、direnv export、.envrc的 parse/source/hash/watch/cache,以及process.env的任何读写。不向模型暴露 allow/deny 工具。
执行语义
Bash(foreground 与 background)
- 每次执行都是新进程(foreground 或
run_in_background),环境快照在进程启动时冻结;没有常驻 shell 复用。 - 装饰
ctx.shell.resolve:bashEnabled为 false、无ctx.agents.currentInitiator()(agentless / 直接 Shell 调用)、或workspaceEnvrc.workspaceForAgent(agent)无映射时,一律原样透传——即使workdir落在某个 workspace 内也不猜测。 - 有映射时只替换
request.command为exec <direnv> exec <canonical-root> <managed-env-shim> <original>;workdir/timeout/stdoutMaxBytes/signal/stdin/env/dshEnv/sandboxPolicy 等其余字段引用与值原样保留,调用方 request 对象不被修改。 - background 路径的
jobs.startrun starter(同步)在继承的 initiator 上下文里调用ctx.shell.resolve,仍然拿到发起调用的精确 Agent 及其 canonical workspace,而不是碰巧的 workdir。 - sandbox 开启时,整个 wrapped chain(含
.envrc求值)位于 executor 的 confine 之内。
Terminal
- 所有权显式:只在
ctx.terminals.spawn(owner, request, signal)创建链内生效;terminalEnabled为 false、owner 缺失、或workspaceForAgent(owner)无映射时,原样委托(不建立 context)。 - 每个 spawn 链用一个 operation-local
AsyncLocalStorage上下文{owner, canonical, wrapped}跨整个未发布创建链(含 returned Promise),并发 owner 互不串扰。 - deferred wrapper 在 confine 之前:
ctx.sandbox.confine(argv, policy)(terminal-bash 的 argv commit seam)收到的是 deferred envrc wrapper——sandbox 包住整个 direnv 链,.envrc求值留在 confinement 内;wrapper/confine 抛错原样传播。 - danger final fallback:
danger-full-access或不调用 confine 的 backend 走ctx.subprocess.spawnTerminal,只替换spec.argv为 deferred wrapper;已 wrapped 绝不 double wrap;spawn 链之外的直接调用一律原样。 - DSH 最终环境捕获:backend 在 confine 之后才构造最终
SubprocessTerminalSpawnSpec.env(DSH_SESSION_ID/DSH_PTY_SESSION_ID此时尚不可见),因此外层 capture shim 在 direnv 之前从自身进程环境枚举 Bash 3.2+ 兼容的${!DSH_*}(即 subprocess provider 合并后的最终环境),再exec <direnv> exec <canonical>+ post-direnv 恢复 shim(删除 direnv 后所有DSH_*、恢复 captured exact snapshot、exec 原 argv)。 - 新 terminal 快照:环境在 spawn 时冻结;已运行 terminal 不变——adapter dispose 不杀进程、不重启,in-flight 创建不被 kill;只有新 terminal 重新 direnv。
- 交互式
cdhook 不仿真:终端内目录变化由用户 shell 自己的 direnv hook 处理。
环境安全
- 普通环境变量(包括被允许的
.envrc显式导出的 credential-shaped 变量)遵循原生 direnv 语义:一旦用户 allow,这些变量进入该进程环境,模型可读取(这是用户原生direnv allow的刻意后果)。 DSH_*归属:direnv 求值后 shim 删除环境里全部DSH_*,只恢复本次请求的精确 managed 快照(terminal 路径由 deferred capture 在 direnv 之前从 spawn 进程环境捕获精确快照)。managed name 严格DSH_[A-Z0-9_]+且 value 为 string;value 全部走 argv,不拼进脚本。BASH_ENV/ENV是明确例外:post-direnv shim 与原始程序都在env -u BASH_ENV -u ENV后运行,因此看不到 direnv 设置的这两个控制变量。Bash工具的既有外层executor shell以及direnv自身的求值shell仍可能读取启动前ambient的BASH_ENV;插件不把这个Host输入继续传给post-direnv段。Terminal路径的deferred argv则从最外层开始移除它们。普通direnv shell不会做这种移除,因此这是文档化差异。- preflight只验证version与shim语义,不读
.envrc:激活先运行direnv version,再在配置的shell下实际运行一次DSH_*清除/恢复probe;两个child都bounded,不用shell: true,不执行、不读取任何workspace.envrc,不改process.env。probe同时验证Bash 3.2+兼容的${!DSH_*}行为;失败消息只含stage与executable/path,不含子进程stdout/stderr/env/secret。 - sandbox必须能读取原生allow数据库:direnv在confine内部校验授权。若部署把
XDG_DATA_HOME放在sandbox会遮蔽的位置(例如local bwrap用tmpfs覆盖的/tmp),sandbox内会把外部已allow的.envrc视为blocked;请把direnv授权状态放在sandbox可读的持久目录。 - 诊断与错误不打印 stdout/stderr/env/secret;公开 API 不接受、不返回环境快照之外的敏感内容。
失败与生命周期边界
- 激活失败:direnv 缺失/不可用、shim shell 非法、或 preflight 超时 → provider 激活即失败,任何 adapter 都不安装。
- blocked/denied/内容变更:原程序不运行,原生错误原样到达调用方;Agent、workspace lease、插件全部存活。
- HMR/dispose:还原确切的先前 method descriptor(幂等;后装 wrapper 不会被先装者的 dispose 移除,完全还原按逆安装序 dispose);已启动进程保留其环境与进程属主,不因 decorator 卸载被杀。
- overlay 先于本插件卸载:后续 Agent 查找无映射 → 原样透传,或按普通 workspace 生命周期失败;没有缓存的 workspace 路径比映射活得更久。
- 并发:不同 workspace 的 Agent / 终端创建各自独立 direnv 求值,互不串扰。
- 组合要求:唯一integration row同时注入
agents、shell、sandbox、subprocess、terminals与workspaceEnvrc,保证terminal的direnv链不会落到late sandbox外;缺少任何一个service时整行保持pending,Bash adapter也不会单独安装,即使enableTerminal: false。 - 不覆盖:Workspace MCP、global MCP、LSP、subagent providers 与 generic
ctx.subprocess.spawn()明确不在范围(完整非目标清单见 docs/implementation-plan.md §8)。Windows 不支持(激活即失败)。 - no watcher / no auto restart:本 bundle 不 watch 任何文件(包括
.envrc——内容变更由原生 direnv 在下次执行时按 hash 拒绝);.envrc变化后不自动重启后台 job 或已运行 terminal。
Config
workspaceEnvrc provider 行的 schema(schema + 语义校验,实现在 dsh-workspace-envrc/core):
| 字段 | 默认 | 约束 |
|---|---|---|
executable | direnv | 非空、无 NUL;PATH 命令或绝对路径 |
shimShell | /bin/bash | 绝对路径、无 NUL |
enableBash | true | boolean;false 时 Bash adapter 可安装但永远透明 |
enableTerminal | true | boolean;false 时 terminal adapter 可安装但永远透明 |
versionCheckTimeoutMs | 5000 | 正整数且 ≤ MAX_TIMER_DELAY_MS |
只读 getter bashEnabled/terminalEnabled 供 adapter 读取,不暴露可变 config。第三个构造函数参数是可注入的 preflight spawn seam(确定性测试用,不进 Config schema)。
API 与 exports
- 根导出
dsh-workspace-envrc(dist/provider.js,默认导出WorkspaceEnvrc extends Service):workspaceForAgent(agent)、wrapArgv(canonicalWorkspace, originalArgv, dshEnv?)、wrapCommand(canonicalWorkspace, originalCommand, dshEnv?)、wrapDeferredArgv(canonicalWorkspace, originalArgv)(终端的 deferred capture 链),只读bashEnabled/terminalEnabled。 dsh-workspace-envrc/core:defaultConfig、assertWorkspaceEnvrcConfig、managedEnvPairs、buildManagedEnvShimArgv、buildExecArgv、buildDeferredManagedExecArgv、DEFERRED_ENV_CAPTURE_SCRIPT、DEFERRED_ENV_SHIM_LABEL、shq、wrapCommand、resolveAgentWorkspace、runPreflight、assertPosixPlatform、PreflightError与相关类型;均为无框架纯函数。dsh-workspace-envrc/bash-adapter:installWorkspaceEnvrcBashAdapter(ctx)与WorkspaceEnvrcBashAdapterHandle。dsh-workspace-envrc/terminal-adapter:installWorkspaceEnvrcTerminalAdapter(ctx)与WorkspaceEnvrcTerminalAdapterHandle(operation-local ALS 上下文为内部实现)。dsh-workspace-envrc/integration-plugin:name/inject/Config/apply(函数插件,无 default)。dsh-workspace-envrc/cordis.patch.yml:bundle patch(两行,见「依赖与安装」)。
开发
pnpm install # repo-local store(.npmrc)
pnpm test # vitest 全量(先 build dist)
pnpm typecheck # src + tests 严格类型检查
pnpm build # tsc -> dist
测试从不读取、不写入真实用户 direnv 授权状态:tests/direnv-native.spec.ts 用真实 direnv 驱动完整 allow/deny/内容变更状态机与 deferred terminal wrapper,授权状态全部落在仓库内隔离的 XDG_DATA_HOME/XDG_CONFIG_HOME/XDG_CACHE_HOME/HOME(.artifacts/ 下,gitignored);shim 脚本与 wrapped command 用真实子进程在隔离显式环境中验证(不改 process.env);后台路径用真实 AgentRegistry + ToolRuntime + tool-bash + jobs provider 验证;终端路径用真实 TerminalSessionService + terminal-bash + SandboxPolicyService 验证;tests/loader-composition.spec.ts 用真实 Cordis Loader 读取 test cordis.yml,组合内置 dist 的 provider/integration 行与真实 DSH services/overlay 依赖。
安全与信任边界
- 授权始终由用户在 DSH 外、本机 direnv 侧完成(
direnv allow只属于用户);.envrc内容变更后原生 hash 失效会阻塞后续执行,直到用户重新 allow。DSH 侧没有任何 allow/deny/编辑入口,也不给模型提供 allow/deny 工具。 - 每个启用执行的包装形状为
direnv exec <canonical-root> <managed-env-shim> <original>:求值与命令在同一进程树内(sandbox 开启时整体位于 executor/terminal 的 confine 之内);DSH_*归属在求值后恢复(terminal 路径由 spawn 前的 deferred capture 提供精确快照);BASH_ENV/ENV是控制变量例外(post-direnv段与原始程序不可见;Bash外层executor的ambient启动行为见上文);其余普通环境变量(含.envrc显式导出的 credential-shaped 变量)遵循原生 direnv 语义。 - 诊断与错误只含 stage、可执行文件路径与退出事实,不打印 stdout/stderr/env/secret;公开 API 不接受、不返回环境快照之外的敏感内容。
许可证
MIT,见 LICENSE。