Capability Seam:Definition / Provider / Consumer 三角
34 个可替换能力;换两个 provider 把执行世界搬到远程
本章基准:dsh
0.1.1-rc.2,commitb150a551b8d465e31e418e1b2eaf5e79bbb7d28e主要源码:
packages/shell/、packages/subprocess/、packages/fs/、packages/sandbox/、packages/e2b/,以及docs/capability-seams.md(生成物,34 个 seam)
前七章讲的都是"骨架":Cordis、装配、循环、日志、提示词、工具。这一章讲 dsh 怎么让骨架上的能力可替换。
先看一个具体问题。模型要跑一条 bash 命令。这条命令可能:
- 在本机直接跑;
- 在本机跑但被 Landlock / Seatbelt / bwrap 限制;
- 在一台 E2B 远程 Linux 沙箱里跑;
- 在 Windows 上根本不是 bash,而是 pwsh。
而 tool-bash 这个模型可见的工具,在这四种情况下是同一个包、同一段代码、同一份 schema。
这一章讲实现这件事的模式,以及它在 dsh 里被用了 34 次的原因。
全章图示:assets/ch08-capability-seam.svg
1. 三个角色,一个完整的能力
docs/glossary.md:9 给的定义很精确:
a seam — a swappable capability with three roles: a Service Definition (the Cordis
Servicethat owns itsctx.<key>and vocabulary types — an abstract class such asShellExecutor, or a concrete registry such asWebRuntime, never a TypeScriptinterface), one or more Service Providers, and one or more Consumers that inject the service.The seam is the complete capability, never one role.
packages/shell 是标准样板:
| 角色 | 包 | 干什么 |
|---|---|---|
| Service Definition | dsh-shell |
拥有 ctx.shell,定义 ShellExecutor 抽象类和 request/spec/result 词汇 |
| Provider | dsh-bash-local |
走本机 ctx.subprocess |
| Provider | dsh-bash-sandbox |
先套 ctx.sandbox 再本机执行 |
| Provider | dsh-pwsh-local |
Windows 上的 PowerShell |
| Consumer | dsh-tool-bash |
给模型的 bash 工具 |
| Consumer | dsh-hooks-claude-code |
Claude Code hook 桥 |
两个词值得单独强调。
"never a TypeScript interface"。 Service Definition 必须是 Cordis Service(抽象类或具体注册表),不能是纯 interface。因为它承担的不只是类型:它拥有 ctx.shell 这个键(重复注册会抛错,这是 Cordis 的语义)、它拥有生命周期(第 2 章的 Fiber)、它拥有共享常量。一个 interface 只能给类型检查用,运行时什么都不占。
"the seam is the complete capability, never one role"。 这条是词汇纪律。dsh-shell 不叫 seam,它是 seam 的 Service Definition;dsh-bash-local 不叫 seam,它是一个 Provider。这不是咬文嚼字——把某一个角色叫成 seam,下一个人就会以为"换 provider 要改 Definition"。
三个角色通常分包,但一个概念可以占多个角色(dsh-llm 同时是 Definition 和 Consumer)。分包的标准是"是否独立演化"。
2. Service Definition 拥有什么
ShellExecutor(packages/shell/shell/src/index.ts:65):
export abstract class ShellExecutor extends Service {
constructor(ctx: Context) { super(ctx, 'shell') }
get sandboxMode(): SandboxMode | undefined { return undefined }
abstract resolve(request: ShellExecRequest): ShellExecSpec
abstract run(spec: ShellExecSpec): Promise<ShellRunResult>
abstract start(spec: ShellExecSpec): ShellProcess
}
看起来很小,但它拥有四样东西。
2.1 语义契约,写在 JSDoc 里
类的 JSDoc 列了实现必须遵守的语义:
runrejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with aShellRunResult.startreturns immediately; no timeout applies to background processes.donesettles at process close and never rejects; spawn failures settle askilledwith the error on stderr.readOutputis incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.- A still-running background process is stopped and awaited when its owning composition tears down.
第一条是最值钱的:非零退出不是异常。模型跑 grep foo file 没找到,退出码 1——这是正常结果,不是失败。如果一个 provider 在这里 reject,tool-bash 就要给每个 provider 写一套异常处理。把它写进 Definition 的契约,所有 Consumer 只需要处理一种形状。
第四条点出了一个隐蔽的所有权问题:后台进程的清理边界在哪个 Fiber。注释说得很具体:
With the subprocess seam that boundary is
ctx.subprocessdisposal, so a background process survives an executor-only reload.
热更新 bash-local(改个配置)不会杀掉正在跑的后台进程,因为进程属于 ctx.subprocess。这就是第 2 章那套 Fiber 生命周期在能力分层上的直接后果。
2.2 request / spec 分离
这是 AGENTS.md:112 点名的模板:
Explicit > implicit at package boundaries: defaulting is an explicit
resolve(request): Specstep in the owning implementation, never a hidden?? defaultinsiderun()(thedsh-shellrequest/spec split is the template).
两个类型(shell/src/types.ts:38 和 :86)差别就在可选性:
export interface ShellExecRequest {
command: string
workdir?: string | undefined
timeoutMs?: number | undefined
stdoutMaxBytes?: number | undefined
…
}
export interface ShellExecSpec {
command: string
workdir: string // 必填
timeoutMs: number // 必填
stdoutMaxBytes: number // 必填
…
sandboxPolicy: SandboxExecutionPolicy | undefined
}
run() 和 start() 的 @param 都写着 a resolved spec from resolve(), never a raw request。
为什么值得多一个类型和一次调用?
默认值属于实现,不属于契约。 bash-local 的默认超时和 E2B 的默认超时可以不同,而 Consumer 不该知道这件事。
默认值变成可观察的。 resolve() 的结果可以被日志、被测试、被断言。藏在 run() 里的 ?? 120_000 只有读源码才能发现。
Consumer 可以先 resolve 再决定。 拿到 spec 之后可以看清最终 workdir 和 policy,然后再决定要不要真的执行。
注意 sandboxPolicy 在 spec 上是必写但可为 undefined,注释:ignored by executors that do not confine。词汇属于 seam,字段永远在;给不给值由 Consumer 决定,用不用由 provider 决定。
2.3 一个能力问答方法
get sandboxMode(): SandboxMode | undefined { return undefined }
默认返回 undefined(不沙箱),bash-sandbox 覆盖它。tool-bash 靠它决定要不要给模型的 schema 加上升级参数——bash-sandbox 的 README 写明:
no alternate tool plugin is needed;
dsh-tool-bashdetects the executor'ssandboxModecapability and adds the escalation fields.
同一个 Consumer,两种模型可见 schema。 这是 seam 设计里最实用的一手:不用给沙箱版再写一个 tool-bash-sandbox,也不用让 tool-bash 去 import 沙箱包。一个能力问答方法就够了,而且默认值让不沙箱的 provider 什么都不用写。
2.4 一个 settings namespace
export const SHELL_SETTINGS_NAMESPACE = settingsNamespace('shell')
注释解释了为什么这个常量归 Definition 而不是归任一 provider:
it names the capability, not an implementation: a host composes exactly one provider of
ctx.shell(the win32 layer swaps the POSIX rows for the pwsh ones, and mounting both fails loud on a duplicate service registration), so the providers share one namespace without ever registering it twice, and a settings document carried between platforms keeps resolving on both.
最后半句是关键:一份 settings 文档在 Linux 和 Windows 上都能解析。如果 namespace 叫 bash 和 pwsh,同一份用户配置跨平台就失效了。namespace 命名的是能力,所以它归 Definition。
3. Provider:三种替换方式
ctx.shell 有三个 provider,替换方式各不相同,正好覆盖了三种典型形态。
3.1 继承:fs-sandbox extends fs-local
SandboxedFileSystem 直接继承 LocalFileSystem,README 说得很干脆:
It inherits every text-storage mechanic verbatim (resolve, stat, read/stream, list, the atomic write, the read-match-write edit critical section) and adds only a per-call MODE fence on
writeText/editText. Reads always pass through — every mode permits reading.Loading it INSTEAD OF
dsh-fs-local, together with actx.sandboxPolicy, is the whole swap; the model-facing tools (dsh-tool-fs) are untouched.
"整个替换就是换一行 cordis.yml + 加一个 policy 插件"。配置类型也保持不变(Its plugin config is the local backend config unchanged),所以已有配置不用改。
这种形态适合"加一层检查"。代价是继承耦合:fs-local 的内部结构变了,fs-sandbox 要跟着改。
3.2 组合:bash-sandbox 消费 ctx.sandbox
bash-sandbox 继承了 bash-local 的进程机制,但真正的沙箱能力来自另一个 seam:
Every command is confined by handing the provider the exact
['bash', '-c', command]argv this executor is about to spawn and spawning the returned argv directly.
ctx.sandbox 的 Service Definition 注释(sandbox/src/index.ts:1)划了范围:
Service Definition for the same-world process-confinement capability seam: wrap exact subprocess argv under a host-path file policy. Containers, microVMs, and remote execution replace the surrounding capability seam instead; this service shares the host kernel and filesystem.
"same-world"。这个 seam 只管"同一个内核里的文件效果限制"。要换成容器或远程执行,不是给 ctx.sandbox 加一个 provider,而是替换外层 seam(换 ctx.subprocess 和 ctx.fs)。
一个 seam 明确声明自己不覆盖什么,和声明覆盖什么一样重要。否则 ctx.sandbox 迟早会长出 mode: 'docker',然后 SandboxMode 这个只有三个值的词汇就崩了。
SandboxPolicy 上还有一条设计说明(sandbox/src/index.ts:69):
What one confined execution is allowed to touch — carried PER CALL, not fixed on the provider: two consumers may confine under different policies at the same instant (bash under
read-onlywhile a confined child agent needs its state directory writable), and an approved escalated retry is a new call with a wider policy.
策略按调用携带,不固定在 provider 上。 同一时刻两个消费者可以用不同策略。而"批准的升级重试"就是一次带更宽策略的新调用——不是修改现有状态。这让升级审批天然地不可累积:批准一次就是一次。
3.3 换掉整个执行世界:E2B
packages/e2b 是三种形态里最激进的:
The existing
dsh-bash-local,dsh-terminal-bash, anddsh-lsp-stdioneed no E2B-specific forks. They delegate every execution-world operation toctx.fsandctx.subprocess, so mounting the two E2B adapters places their mutable work in the same sandbox.
装两个 provider(fs-e2b + subprocess-e2b),bash 执行器、PTY 后端、LSP 宿主全都跑到远程去了,三个包一行没改。
这是 seam 分层的复利:bash-local 不认识 E2B,它只认识 ctx.subprocess。谁提供 ctx.subprocess 是组合时的事。
而边界写得同样清楚:
This boundary does not move the harness process, Cordis objects, model calls, agent/session state, session persistence, skills, higher-level protocol state, or E2B SDK buffers.
移动的只是"执行世界"——文件和进程。会话状态、模型调用、插件树都还在本地。E2B 三个包里 ctx.e2b 的存在也有讲究:它是"共享生命周期拥有者",两个 adapter 注入它、等同一个 SDK 句柄,所以它们必然在同一个远程工作树里。如果两个 adapter 各自建 sandbox,fs 写的文件 subprocess 就看不见了。
4. Consumer:为什么 Definition 不能被单个 Consumer 定形
packages/AGENTS.md:10 有一条针对 Definition 设计的规则:
Design Service Definitions for all current Consumers. Keep tool-schema, Loader, UI, transport, and provider-specific behavior in the Consumer or provider; do not let one Consumer dictate the service contract. Inverse smell: a public service method with one internal caller — pass a private capability closure instead.
两条相反方向的约束,值得一起看。
不要让一个 Consumer 定义契约。 ctx.shell 有四个 Consumer(bash 工具、pwsh 工具、两个 hook 桥)。如果 Definition 按 tool-bash 的需要设计,那些"模型可见的 schema 字段""升级提示文本"就会漏进 Definition,hook 桥就得吞下一堆和它无关的概念。
bash-sandbox 那句 Deny-only at the seam: a denial is a reported fact, and this executor never negotiates permissions itself — the approval question lives in the tool layer 就是这条规则的实践:seam 只报告事实,不做交互。审批是 UI 概念,属于 Consumer。
反向的坏味道:一个只有一个内部调用者的公共服务方法。 这种情况该传一个私有能力闭包,而不是在服务上开个公共方法。它的破坏性同样大:一个公共方法就是一份长期契约,而它实际上只服务一个调用点。
ctx.fs 那三个 waterfall 事件(fs/src/index.ts:58 起)是"用扩展点代替公共方法"的例子:
'fs/write-intent'(target, actor, next): Promise<FsWriteIntent | undefined>
'fs/edit-intent'(target, actor, next): Promise<{ version: FsVersion } | undefined>
'fs/observed'(target, observation, actor): void
注意这两个 waterfall 的 JSDoc 都说 Single-slot decision … the first listener that returns an intent owns the decision rather than composing with peers——单槽决策,第一个返回的赢。这和第 7 章工具流水线里那些"可组合"的 waterfall 不同:这里是"要不要要求版本号一致",两个答案不能合并。
fs-observation-policy 就是靠这三个事件挂进去的(它在 seam 表里被标为 Companion plugins,既不是 provider 也不是普通 Consumer)。ctx.fs 因此不需要一个 setObservationPolicy() 公共方法。
5. seam 之间怎么共享事实
一个跑起来的 dsh 里有 34 个 seam。它们之间必然有共享的事实,dsh 的处理方式有两条清晰的模式。
5.1 共享一个策略拥有者
ctx.sandboxPolicy 在生成表里被标为 core 而不是 seam,注释:
The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service … Both enforcing families read it so bash and fs cannot confine to different roots.
bash 和 fs 是两个独立的 seam,但它们必须限制在同一个根下。 做法是让它们读同一个策略服务,而不是各自配一个 workspaceRoot。
fs-sandbox 的 README 把这条落到了实现层:
the workspace root plus the platform temp areas (
/tmp,os.tmpdir()), the SAME set the Seatbelt profile grants, derived from the onewritableRootsfunction so the fs fence and the bash runner cannot drift.
不只共享配置,还共享计算可写根的那个函数。因为"同一个策略"如果由两段代码各自解释,迟早会解释出两个答案——而这个分歧就是一个沙箱逃逸。
5.2 明确区分"策略围栏"和"内核边界"
fs-sandbox 的 README 有一节标题就叫 Threat model: a policy fence, not a kernel boundary:
The fence is a check in TRUSTED code over a MODEL-CONTROLLED path — the operations are the seam's own (open, rename), only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface … Kernel-grade isolation of untrusted CODE stays
ctx.shell's job.
两个 seam 的威胁模型不同,而且写清楚了为什么:
ctx.fs的操作是自己的代码,只有路径来自模型 → 规范化后做包含检查就够了;ctx.shell跑的是模型写的任意代码 → 需要内核级隔离(Landlock / Seatbelt / bwrap)。
残留风险也写明了:
The residual TOCTOU (an ancestor symlink swapped between the containment re-check and the syscall) is narrowed by re-canonicalizing immediately before the write and is accepted for this threat model; a kernel-tight boundary needs
openat2-class primitives not worth their portability cost here.
已知的、被接受的、写下理由的残留风险。 这比一句"我们做了沙箱"诚实得多,也让下一个人知道边界在哪。
5.3 结构化失败 vs 文本推断
同一个"拒绝",两个 seam 表达方式不同:
fs-sandbox:
A denial is a structured
FsError(FS_SANDBOX_DENIED, carrying the effective mode) — no stderr text inference (unlike bash's kernel denials), because an in-process fence knows exactly what it refused.
bash-sandbox:
Denials are result facts. A failed run whose stderr carries the selected backend's own denial dialect — the signatures the provider stamps on every wrap (EROFS text under bwrap, EACCES under Landlock, EPERM under Seatbelt) — is reported as
ShellRunResult.sandbox.denied: true(conservative classification, read from the collected stderr tail).
进程内的围栏知道自己拒了什么,所以给结构化错误。内核拒绝只留下 stderr 文本,所以只能按 provider 自己的方言做保守分类。
而且分类规则被提炼成了 Definition 上的一个类型(sandbox/src/index.ts:81):
export interface RunnerFailureRule {
allowedExitCodes?: readonly number[]
fatalSignatures: readonly string[]
informationalLines?: readonly string[]
}
JSDoc 给了确定的判定顺序:A consumer first applies allowedExitCodes when present, removes informationalLines by case-insensitive exact line equality, then matches fatalSignatures case-insensitively within each remaining stderr line. Exit status alone never proves runner failure.
最后一句是重点:光看退出码永远不能证明是 runner 挂了。命令自己也会返回非零。所以判定必须要有 stderr 证据。
两个模型可见的结果最终又被统一了:fs-sandbox 的 README 说它的拒绝被渲染成 [sandbox: file access denied under <mode> mode] 标记加同轮升级提示,exactly as bash's do。内部表示按各自最准确的方式,模型看到的表面统一。
6. 34 个 seam 里的几种形态
docs/capability-seams.md 是生成的(scripts/gen-doc-graphs.ts,带完整性守卫)。表里每个服务被标成 seam / core / bundle 三类。挑几个不同形态的:
ctx 键 |
形态 |
|---|---|
ctx.llm |
多 provider 注册进一个适配器注册表(deepseek / pi-ai / replay),Definition 自己也是 Consumer |
ctx.sessionPersistence |
同一份 SessionEvent 词汇,JSONL 和 SQLite 两个后端(第 5 章) |
ctx.approval |
缺席即 fail-closed:没有 provider 时降级为 unavailable(第 7 章) |
ctx.codeRuntime |
按"基质 + 语言"区分后端,Consumer 是工具注册表(第 6 章的 Code Mode) |
ctx.userQuestions |
provider 是 UI 前端;tool-ask-user 在一个 provider-neutral 的 ask() promise 上挂起工具调用 |
ctx.web |
search 和 fetch 各自多 provider,注册进同一个 seam;tool-web 拥有稳定的模型可见名字 |
ctx.lsp |
刻意不留协议逃逸口:the seam offers no protocol escape hatch, so a backend translates into the normalized request and result |
ctx.spillStore |
Definition + provider 存溢出文本,policy 包作为 tools/post-execute Consumer 决定何时溢出 |
ctx.workflowEngine |
One engine per context, as in bash, with no named-provider registry |
最后两行体现了一个反复出现的判断:什么时候需要"命名 provider 注册表",什么时候"一个上下文一个实现"就够。
ctx.llm 和 ctx.web 需要注册表——一个部署会同时用多个 LLM provider、多个搜索后端,运行时要按名字选。
ctx.shell 和 ctx.workflowEngine 不需要——一个部署只有一个 shell 执行器,装两个是配置错误,而 Cordis 的重复服务注册会自然地 fail loud。
选注册表的成本是:每个调用点都要处理"这个名字没注册"。不需要多实例还上注册表,就是白付这个成本。
ctx.lsp 那句"没有协议逃逸口"是另一类判断。留一个 raw(request) 方法会很方便,但那等于让每个 Consumer 都可能绑死在某个后端的协议细节上——seam 就名存实亡了。四个规范化操作,一个都不多。
7. 设计代价
包数量。 34 个 seam,加上 provider 和 Consumer,是 227 个 workspace package 的主要来源。改一个能力的语义要同时动 Definition、若干 provider、若干 Consumer 和它们的 README。
一次跳转变成两次。 读代码时看到 ctx.shell.run(spec),要先查组合配置才知道跑的是哪个实现。docs/capability-seams.md 这类生成的导航图是对这个代价的补偿,但它是补偿不是消除。
词汇必须一次设计对。 SandboxMode 只有三个值,ctx.lsp 只有四个操作。这种"闭合词汇"是 seam 有价值的前提,但它意味着加一个真正的新语义要动所有 provider。dsh 靠 pre-release 阶段的姿态对冲(AGENTS.md:5 那节 foundation over blast radius:没有外部消费者,宁可重命名重打包也要把地基做对)。
"same-world" 这类边界要靠散文维持。 编译器不会阻止有人给 ctx.sandbox 加一个 mode: 'docker'。守住它的只有 Definition 的模块注释和 review。
跨 seam 的共享事实需要显式的第三方拥有者。 ctx.sandboxPolicy 的存在纯粹是因为两个 seam 必须一致。这类"协调服务"会随着 seam 增加而增加,而它们既不是 Definition 也不是 provider,分类上永远尴尬(表里被标成 core)。
能力问答方法(sandboxMode)是弱类型的协商。 Consumer 靠一个可选返回值判断 provider 有没有某个能力。比"给每种组合写一个 Consumer"好得多,但它是运行时约定,类型系统不会提醒你漏了一种情况。
8. 可迁移的经验
1. 一个可替换能力有三个角色,缺一个就不完整。 Definition(拥有键和词汇)、Provider(实现)、Consumer(使用)。只有 interface 没有 Definition 服务,就没有生命周期和唯一性保证;只有一个 provider 也要按 seam 写,因为第二个 provider 出现时才补太晚了。
2. Service Definition 用抽象类,不用 interface。 它要拥有运行时的键、生命周期和共享常量,这些 interface 都给不了。
3. 把语义契约写进 Definition,而不是让每个 Consumer 各自处理。 "非零退出不是异常"这一句,省掉了每个 Consumer 的一套异常处理,也防止了 provider 之间的行为漂移。
4. request / spec 分离:默认值是实现的显式一步,不是藏在执行里的 ?? default。 好处是默认值可观察、可测试、可因 provider 而异,而 Consumer 不需要知道。
5. 用一个能力问答方法代替"给每种组合写一个 Consumer"。 sandboxMode 返回 undefined 就是"我不沙箱",Consumer 据此调整模型可见 schema。默认实现让不关心的 provider 什么都不用写。
6. 声明 seam 不覆盖什么。 "same-world 进程限制;容器和远程执行请替换外层 seam"——这句话保住了一个只有三个值的闭合词汇。没有这句,词汇会被逐步扩张到失去意义。
7. 策略按调用携带,不固定在 provider 上。 两个消费者可以同时用不同策略,而"批准的升级"就是一次带更宽策略的新调用,天然不可累积。
8. 两个必须一致的 seam,共享一个策略拥有者和那段计算函数。 只共享配置不够——同一份配置被两段代码各自解释,迟早解释出两个答案,而那个分歧就是漏洞。
9. 写下威胁模型的差异和被接受的残留风险。 "受信代码 + 不受信路径"和"不受信代码"需要的防护级别不同。把这个区别和它的取舍写下来,比一句"我们做了沙箱"有用得多。
10. 内部表示按各自最准确的方式,用户可见表面统一。 进程内围栏给结构化错误,内核拒绝只能做保守的 stderr 分类——但模型看到的是同一个标记和同一种升级提示。
11. 判断"要不要命名 provider 注册表"。 一个部署真的会同时用多个实现(LLM provider、搜索后端)才需要;否则"一个上下文一个实现"更好,重复挂载会自然报错,而且每个调用点少一条"没注册"分支。
12. 别留协议逃逸口。 一个 raw(request) 方法很方便,但它会让 Consumer 绑死在某个后端上,seam 就名存实亡了。
9. 本章源码位置
| 位置 | 内容 |
|---|---|
docs/glossary.md:9 |
capability-seam 的权威定义:三个角色、"never a TypeScript interface" |
docs/capability-seams.md:426 |
34 个服务的 seam / core / bundle 分类表(生成物) |
packages/shell/README.md |
一个 seam 家族的完整角色表 |
packages/shell/shell/src/index.ts:22 |
SHELL_SETTINGS_NAMESPACE:namespace 命名能力,所以跨平台配置仍可解析 |
packages/shell/shell/src/index.ts:47 |
类 JSDoc:run/start/readOutput/teardown 的四条语义契约 |
packages/shell/shell/src/index.ts:65 |
ShellExecutor:抽象 Service,三个抽象方法 |
packages/shell/shell/src/index.ts:75 |
sandboxMode:能力问答,默认 undefined |
packages/shell/shell/src/types.ts:38 |
ShellExecRequest:字段可选 |
packages/shell/shell/src/types.ts:86 |
ShellExecSpec:字段必填;sandboxPolicy 必写可为 undefined |
packages/shell/bash-sandbox/README.md |
组合式 provider:交出确切 argv、拒绝是结果事实、deny-only |
packages/fs/fs-sandbox/README.md |
继承式 provider;策略围栏 vs 内核边界;共享 writableRoots |
packages/fs/fs/src/index.ts:1 |
ctx.fs Definition:backend 拥有什么,policy 和读窗口留给别人 |
packages/fs/fs/src/index.ts:58 |
fs/write-intent / fs/edit-intent:单槽决策,第一个返回的赢 |
packages/fs/fs/src/index.ts:76 |
fs/observed:监听器必须同步,抛错会让工具调用失败 |
packages/sandbox/sandbox/src/index.ts:1 |
"same-world":明确声明容器/microVM/远程请替换外层 seam |
packages/sandbox/sandbox/src/index.ts:29 |
SandboxMode:三个值的闭合词汇,网络和进程可见性不在其中 |
packages/sandbox/sandbox/src/index.ts:69 |
SandboxPolicy:按调用携带;升级重试是新调用 |
packages/sandbox/sandbox/src/index.ts:81 |
RunnerFailureRule:确定的判定顺序;退出码单独不构成证据 |
packages/subprocess/subprocess/README.md |
契约清单:spawn/stdio/tree-kill/PTY/env scrub,以及"teardown ladder 归 Consumer" |
packages/e2b/README.md |
换掉整个执行世界;三个 Consumer 零改动;边界不含会话状态 |
packages/e2b/e2b/README.md |
ctx.e2b 作为共享生命周期拥有者,保证两个 adapter 在同一远程工作树 |
packages/AGENTS.md:10 |
"Design Service Definitions for all current Consumers" 与反向坏味道 |