Workspace lineage 超小单文档审查
基线状态:Historical fixed-target dossier。 本文按
b6458b41...审查 workspace owner;本科生 guide 当前 target 为94fab78...,当前导航见implementation/source-map.md。
范围、基线与状态
- 状态:Current
- 审查基线:
agent-python-runtime精确 commitb6458b41f0fa32085fdd4cad0a528d98602569d2、tree5e8154fc26527181c66e612584d79dd4e620649d。 - 范围:
runtime/workspace;以及直接使用其Manager、Ref、Root、Branch、Attempt、Lease的 streaming、subagent、wazero runner、cmd/apyrunbinding 与对应 tests。 - 排除:
runtime/capability/workspace.go的内存 typed-tool workspace;semantic、COW、benchmark、native 全链路;本单不把它们的旁证升级为 lineage 结论。 - 证据规则:以下为源码与测试断言的静态观察;本次没有运行测试或 campaign。
核心对象与身份边界
- 状态:Observed
runtime/workspace/manager.go:34-35:Ref是 Host-owned opaque identity,格式为ws-加随机 16-byte hex,既不含路径也不等于内容 digest。Manager(manager.go:80-101)只管理其私有 0700 base 下的 root;不扫描、收养或复用任意既有子目录。Create(manager.go:104-170)从InitialFilevalue-copy 建立可变 root;CreateFromDirectory(ingress.go:15-76)只在 provisioning 时复制 trusted Host directory。CapsuleInfo.WorkspaceSHA256是普通文件树的可移植内容/清单身份;本地Ref被有意排除于 capsule 与 identity。Root(branch_root.go:21-34)是不可变 lineage record;Root.IdentityDocument(142-148)只含 schema、workspace digest、parent identity、depth、delta 计数,不含ref。Root.IdentitySHA256绑定上述 document;Root.Ref()仍只是当前 Manager 的本地 materialization handle。PortableIdentity(branch_root.go:38-59)对 mutable base 产生 depth 0 的 parent identity;对 sealed root 返回其 lineage identity 与 depth。
状态机:基本 workspace、Ref 与 Lease
- 状态:Framing
Create/CreateFromDirectory/ImportCapsule:Absent -> Mutable(Ref, owner="");失败由 deferredRemoveAll回滚,不登记 entry。Acquire(ref, owner)(manager.go:194-226):Mutable -> Leased;要求 Ref、owner 合法、树重新扫描通过、未 immutable、无其他 owner。Lease.FS()只给 rooted WASI adapter;Snapshot返回 bounded metadata,不返回 body;BindMountSource只对 exact active lease 暴露 runtime-owned root。Lease.NewTemporary(manager.go:382-418):Leased -> Leased + Temporary;Temporary 没有 Ref、没有 continuation identity,目录位于 Manager base。Temporary.Close(438-457):关闭 rooted adapter、删除 scratch tree、从 lease 的 temporary 集合移除;重复 close 幂等。Lease.Release(460-484):只有无 live Temporary 才能执行;关闭 FS、核对(ref, owner),再将 entry owner 清空,Lease进入 released 状态。Manager.Destroy(250-271):仅能删除 inactive entry;Manager.Close(274-301)在任一 lease active 时返回ErrWorkspaceBusy,否则删除所有 roots 并关闭 Manager。- Host cleanup 的可重试性由
cmd/apyrun/workspace_binding.go:196-213保持:busy close 不清空 binding;成功才移除 Manager base。
状态机:Branch / Root / Seal / Discard
- 状态:Current
ForkBranch(base, expectedBaseSHA256)(branch_root.go:205-263):验证 base inactive 且当前 WorkspaceSHA256 等于 expected,然后完整复制 ordinary tree,建立私有 mutable childRef与Branch。Branch保存 base Ref、expected base digest、parent lineage/depth、base snapshot;它不携带 Guest、capability 或 Temporary 状态。Branch的 child Ref 可经Acquire/Release写入;兄弟 branch 之间不共享可变目录,base 也不被原地修改。Branch.Seal(expectedBaseSHA256)(280-330):要求 Branch 未 terminal、base 与 child 均 inactive,并再次检查 base digest;然后比较 snapshots,生成Root(depth 加一、changed entries/bytes)。Seal成功:child entry 标记immutable、保存rootRecord,Branch 进入 terminal;base 保持原 identity,未选 branch 不会自动合并。Branch.Discard(180-202):仅对未 sealed、inactive child 生效,删除 child root 并 terminalize;sealed Branch 返回ErrAttemptTerminal。Root的本地 materialization 可由Manager.Destroy删除;Root value 与其 digest 仍可作为 portable record 留在调用者手中。BindImportedRoot(branch_root.go:61-100)先验证 Root identity document digest,再核对 imported tree digest;成功后把新本地 Ref 标为 immutable。SelectRoot(103-139)只接受同一 expected parent 的已绑定候选,按 selected identity 选择一个,不 merge、不 mutation。
状态机:Attempt / Publish / Discard
- 状态:Observed
ForkAttempt(base)(attempt.go:22-70)也先要求 base inactive 并完整复制;返回独立 mutable child Ref 与 Attempt。- Attempt 写入通过同一个 exclusive Lease;base 的内容不随 attempt 写入改变。
Attempt.Publish()(attempt.go:86-104)只在 child inactive 且重扫通过后将 Attempt handle 标为 terminal,并返回该 child Ref;它不写Root、不设置 entry immutable、不修改 base。- 因而
Publish != Seal:Publish 是一次 caller-level terminal disposition,返回的 Ref 不是自动冻结的 portable Root;后续 Ref 生命周期仍由 Host/caller 管理。 Attempt.Discard()(106-128)要求未 terminal、inactive,删除 child root;发布或丢弃后再次操作返回ErrAttemptTerminal。runtime/streaming/run.go:54-76把 run error、close error、malformed/non-okresponse 都导向 Attempt.Discard;仅 Guest responsestatus=ok且 runner 已关闭后调用 Publish。runtime/subagent/orchestrator.go:289-344则对每个 child Branch.Seal,再保留 selected Root、对未选 sealed roots 调Manager.Destroy;错误路径 cancel 后 discard。
End-to-end 调用与数据流
- 状态:Observed
cmd/apyrun/prepareMountedWorkspace(workspace_binding.go:34-88):校验 config,创建私有 Manager,从 Host directory、capsule 或 empty tree 得到 Ref,再保存 initialCapsuleInfo。main.go:269-279把 Manager/Ref/runIdentity绑定到 wazero Factory;Factory 要求三者 all-or-none。runtime/engine/wazero/engine.go:295-309只在 request admission 通过后Acquire,同一 Engine 只保留一个 workspace Lease;workspaceRun串行化 mounted-workspace run。moduleConfig(606-625)将 leased workspace 挂到 Guestworkspace,将每次NewTemporary挂到 Guesttmp;Temporary 在 run/module 结束时关闭。- Runner close 先释放 Lease(
engine.go:570-588);随后main.go:328-347Inspect final workspace、按 disposition 决定 discard 或 stage export。 stageExport(workspace_binding.go:135-168)写 0600 temporary capsule、hash、sync、close;publish(171-185)才 rename 到目标并尝试 sync parent directory。main.go:396-410在响应和 receipt 再验证成功后才 publish staged capsule;任一前置失败由discard删除临时文件,避免半成品目标。- imported capsule 首先得到新 mutable Ref;只有显式
BindImportedRoot才获得 immutable Root binding,避免把 capsule receipt 当作 lineage authority。
Copy、identity 与 Host/Guest 边界
- 状态:Framing
- Host-owned:Manager、物理 root、Ref、owner token、limits、Root record、capsule 文件路径、stage/publish/discard 决策。
- Guest-owned:通过 rooted FS 可见的普通文件/目录与 Guest 内部对其的读写结果;Guest 不接触 Host path、device、inode、symlink 或 hard-link identity。
- Shared-but-bounded:
Snapshot/CapsuleInfo/WorkspaceReceipt/Root.IdentityDocument只跨边界传递 digest、路径名、类型、大小、权限与 lineage counters。 - Private:
Lease、rootedFS、Temporary、runner、interpreter/capability state;capsule 明确不含 Lease、Temporary、interpreter 或 capability。 cleanGuestPath、scanOrdinaryTree、copySourceDirectory和rootedFS共同拒绝 traversal、absolute path、反斜杠、.git、symlink、special entry、cross-device tree、hard-linked file。- rooted FS mask stat identity(
fs.go:211-216),以 synthetic inode 代替 Host inode;quota 在 open/write/truncate/rename/unlink/rmdir 边界重新计账。 CaptureFile(capture.go:11-60)只读已 sealed immutable root 的 bounded regular file,并返回 defensive byte copy;它不是任意 Host 文件读取接口。
测试与机器可读证据
- 状态:Observed
- workspace 单测覆盖:
manager_test.go的 lease continuation、Attempt publish/discard、path/link/quota、Temporary cleanup、directory ingress 与 snapshot;branch_root_test.go覆盖 seal recursion、parent conflict、portable identity、root selection。 - capsule 单测覆盖:
capsule_test.go的 deterministic bytes、round-trip、tamper/trailing/manifest/quota rejection;capture_test.go覆盖 immutable bounded capture;ingress_unix_test.go覆盖 FIFO atomic rejection。 - 直接 caller 单测覆盖:
streaming/run_test.go只发布成功 attempt;subagent/orchestrator_test.go覆盖 parallel children、selected root、sibling discard、abort cascade;cmd/apyrun/workspace_binding_test.go覆盖 stage-before-publish、policy、invalid capsule、busy close retry。 - 机器可读字段锚点:
Root.IdentityDocument;CapsuleInfo{WorkspaceSHA256,TreeSHA256,EntryCount,TotalBytes};WorkspaceReceipt{InitialWorkspaceSHA256,FinalWorkspaceSHA256,CapsuleSHA256,Disposition};JoinResult{SelectedRoot,DiscardedRefs}。 - 状态:Deferred;上述 tests 是可审计的源码证据,不在本只读单中宣称已执行、通过或覆盖任意 Python/Host 环境。
Trade-offs 与不主张
- 状态:Framing
- Trade-off 1:完整物理 copy 换取 branch/attempt 的简单隔离、可重扫与 deterministic capsule;代价是 provisioning 与 materialization 成本随树大小增长,未采用 COW 作为本单依据。
- Trade-off 2:opaque local Ref 与 digest-only Root 分离本地路径和可移植 lineage;代价是跨 Manager 必须 Import 后 Bind,并由 SelectRoot 显式选择,不能靠 Ref 相等判断同一 lineage。
- Trade-off 3:exclusive Lease 加 per-instance Temporary 换取 owner/cleanup 清晰与 scratch 不回流;代价是同一 mounted workspace 的写入被串行化,Temporary 未关闭会阻断 Release/Manager.Close。
- Trade-off 4:stage-then-rename 换取目标文件不被半成品覆盖;代价是需要 caller 处理 temporary cleanup、directory sync 与 publish error。
- 状态:Unsupported;
Attempt.Publish不建立 immutable Root、不证明后续 Ref 不再写入,也不把 child 内容 merge 回 base。 - 状态:Unsupported;Snapshot、CapsuleInfo、receipt、digest 是 bounded evidence/identity,不是外部世界真相、权限授权或完整 interpreter state。
- 状态:Unsupported;本单不推导 COW、semantic pre-dispatch、native backend、benchmark latency 或任意 Guest Python 支持范围。
答辩一句话
- 状态:Current
- Pysolate 以 Host-owned opaque Ref 管理可变物理副本,以 exclusive Lease 限定唯一写者,以 Seal/Root 或显式 Attempt Publish 区分可移植 lineage 与 caller disposition,并在失败、未达成或未选路径上 fail-closed、discard 或 orphan,而不把 Host 路径和后续 authority 带入 Guest。