fix(fs): harden guarded-create publication
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-09-filesystem-absence-observation.md
|
||||
2026-08-09-filesystem-absence-observation.md: e0c5c8b1845550ac6380ea4b2782cde13cba3ce9
|
||||
2026-08-09-filesystem-absence-observation.zh.md: 808698a7cb7a3575ef500dced7a646691d65c424
|
||||
2026-08-09-filesystem-absence-observation.md: e01719651f54cafc86a3972690507b1ee9b95cc9
|
||||
2026-08-09-filesystem-absence-observation.zh.md: 126f85e95b1debbccddf82fda432d31a69250388
|
||||
|
||||
@@ -12,13 +12,13 @@ Treating a failed read as permission to create also exposes a second boundary. B
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a `read` or `str_replace_editor view` metadata miss emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence.
|
||||
`dsh-fs` owns an explicit observation union: `{ kind: 'present', version: FsVersion } | { kind: 'absent' }`. The `fs/observed` event carries that union. Successful reads and mutations emit present; a metadata miss from `read` or the `str_replace_editor` `view`, `str_replace`, or `insert` command emits absent synchronously before returning `FS_NOT_FOUND`. Other read failures do not manufacture absence.
|
||||
|
||||
`dsh-fs-policy` stores three logical states per owner and target without injecting or calling `ctx.fs`: missing map entry is unseen, `absent` is confirmed absence, and `present(version)` is a replacement/edit basis. Write maps unseen and absent to the existing `createIfAbsent` intent and present to `replaceIfVersion`. Edit maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. A successful create or mutation replaces absence with its produced present version.
|
||||
|
||||
Every provider must enforce `createIfAbsent` at the publication point, not only at its initial probe. `dsh-fs-local` stages and fsyncs in a private sibling directory, then hard-links the staged file to the destination; an existing destination makes the no-replace link fail and preserves the competitor. `dsh-fs-e2b` uses remote `ln` with an explicit created/existing result and derives the committed target version from metadata obtained before the non-cancellable commit. Replacements and bare unconditional writes retain their existing publication paths.
|
||||
Every provider must enforce `createIfAbsent` at the publication point, not only at its initial probe. `dsh-fs-local` stages and fsyncs in a private sibling directory, then hard-links the staged file to the destination; after a failed link it inspects the destination entry so a regular-file collision returns `FS_NOT_OBSERVED`, a non-regular entry returns `FS_NOT_REGULAR_FILE`, and a failure against a still-missing target returns `FS_IO_ERROR`. `dsh-fs-e2b` uses remote `ln -T` with an explicit created/existing result and derives the committed target version from metadata obtained before the non-cancellable commit. Replacements and bare unconditional writes retain their existing publication paths.
|
||||
|
||||
This decision does not claim cross-process linearizability for `replaceIfVersion`: the provider version check and replacement remain protected only against writers represented by the provider's own lock and detectable metadata. The narrower guarantee is exact and sufficient for absence recovery: guarded creation never clobbers a target that appears before publication.
|
||||
This decision does not claim cross-process linearizability for `replaceIfVersion`: the provider version check and replacement remain protected only against writers represented by the provider's own lock and detectable metadata. The narrower guarantee is exact and sufficient for absence recovery: guarded creation never clobbers a target that appears before publication. Local guarded creation requires hard-link support; once any local publication succeeds, staging cleanup is best effort because private residue cannot make the committed write false.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -29,7 +29,7 @@ This decision does not claim cross-process linearizability for `replaceIfVersion
|
||||
|
||||
## Consequences
|
||||
|
||||
The first mutation after an unobserved external deletion still fails `FS_STALE_VERSION`; the user or model must follow the existing reread remedy. That missing reread returns `FS_NOT_FOUND` while changing policy state, after which edit remains forbidden and write may recreate the path. If another writer wins the create race, the retry returns `FS_NOT_OBSERVED` and leaves the winner intact.
|
||||
The first mutation after an unobserved external deletion still fails `FS_STALE_VERSION`; the user or model must follow the existing reread remedy. That missing reread returns `FS_NOT_FOUND` while changing policy state, after which edit remains forbidden and write may recreate the path. If another writer wins the create race, the retry returns `FS_NOT_OBSERVED` and leaves the winner intact; a competing directory, special entry, or dangling symbolic link instead returns `FS_NOT_REGULAR_FILE` without prescribing another read.
|
||||
|
||||
The observation payload is a package-owned event contract change, so every producer, listener, invariant, generated Cordis catalog, subsystem document, and both filesystem tool families move together. The policy keeps its one-stat read and zero-stat write/edit budget, owner isolation, disposal behavior, and optional deployment boundary from the [event-gate decision](../architecture/2026-06-26-file-context-as-event-gate.md).
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 或 `str_replace_editor view` 的元数据未命中会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。
|
||||
`dsh-fs` 拥有一个显式观测联合类型:`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测;`read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中时,都会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。
|
||||
|
||||
`dsh-fs-policy` 按所有者与目标存储三种逻辑状态,既不注入也不调用 `ctx.fs`:映射中无条目即未见,`absent` 表示确认缺失,`present(version)` 是替换/编辑基准。写入把未见和缺失映射到现有 `createIfAbsent` 意图,把存在映射到 `replaceIfVersion`。编辑把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。成功创建或变更后,系统会用其产生的存在版本取代缺失状态。
|
||||
|
||||
每个提供方都必须在发布点执行 `createIfAbsent`,不能只在初始探测时执行。`dsh-fs-local` 在私有同级目录中暂存并执行 fsync,再通过硬链接把暂存文件发布到目标位置;目标已存在时,不替换链接会失败,并保留竞争创建者写入的文件。`dsh-fs-e2b` 使用远程 `ln` 返回明确的已创建/已存在结果,并根据不可取消提交前取得的元数据推导已提交目标的版本。替换操作和裸无条件写入仍沿用现有发布路径。
|
||||
每个提供方都必须在发布点执行 `createIfAbsent`,不能只在初始探测时执行。`dsh-fs-local` 在私有同级目录中暂存并执行 fsync,再通过硬链接把暂存文件发布到目标位置;链接失败后,它会检查目标条目:与普通文件冲突时返回 `FS_NOT_OBSERVED`,条目非普通时返回 `FS_NOT_REGULAR_FILE`,目标仍然缺失时返回 `FS_IO_ERROR`。`dsh-fs-e2b` 使用远程 `ln -T` 返回明确的已创建/已存在结果,并根据不可取消提交前取得的元数据推导已提交目标的版本。替换操作和裸无条件写入仍沿用现有发布路径。
|
||||
|
||||
本决策不宣称 `replaceIfVersion` 具有跨进程线性一致性:提供方的版本检查与替换仍只能防范被其自身锁纳入协调的写入方,以及能通过元数据检测到的写入方。更窄的保证边界准确且足以支持缺失恢复:带防护的创建绝不会覆盖在发布前出现的目标。
|
||||
本决策不宣称 `replaceIfVersion` 具有跨进程线性一致性:提供方的版本检查与替换仍只能防范被其自身锁纳入协调的写入方,以及能通过元数据检测到的写入方。更窄的保证边界准确且足以支持缺失恢复:带防护的创建绝不会覆盖在发布前出现的目标。本地带防护的创建要求支持硬链接;任何本地发布一旦成功,暂存清理便采用尽力而为语义,因为私有残留无法否定已提交写入的成功。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -29,7 +29,7 @@ Status: implemented
|
||||
|
||||
## 影响
|
||||
|
||||
尚未观测到外部删除时,第一次变更仍以 `FS_STALE_VERSION` 失败;用户或模型必须遵循现有的重新读取恢复指令。该次针对缺失目标的重新读取会返回 `FS_NOT_FOUND` 并同时改变策略状态,此后 edit 仍被禁止,而 write 可以重新创建该路径。如果另一个写入方赢得创建竞态,本次重试会返回 `FS_NOT_OBSERVED`,并保留获胜方写入的文件。
|
||||
尚未观测到外部删除时,第一次变更仍以 `FS_STALE_VERSION` 失败;用户或模型必须遵循现有的重新读取恢复指令。该次针对缺失目标的重新读取会返回 `FS_NOT_FOUND` 并同时改变策略状态,此后 edit 仍被禁止,而 write 可以重新创建该路径。如果另一个写入方赢得创建竞态,本次重试会返回 `FS_NOT_OBSERVED`,并保留获胜方写入的文件;若竞态目标是目录、特殊条目或悬空符号链接,则改为返回 `FS_NOT_REGULAR_FILE`,且不会要求再次读取。
|
||||
|
||||
观测载荷是由包拥有的事件约定变更,因此所有生产方、监听器、不变式、生成的 Cordis 目录、子系统文档以及两套文件系统工具都必须同步更新。策略保留[事件门禁决策](../architecture/2026-06-26-file-context-as-event-gate.md)确立的 read 一次 `stat`、write/edit 零次 `stat` 预算、所有者隔离、dispose 行为和可选部署边界。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user