Repository navigation
Conversation
ops_write_file listed expectedRevision as required in both the host schema and the native argument struct, so only the literal "absent" could ask for create-only and a strict adapter that drops empty optional values had no way to express it. The field leaves the required list and the worker defaults an omitted revision to the "absent" sentinel. write_single still refuses an existing target, so the fallback stays create-only instead of becoming an overwrite wildcard, and edit_file, copy, move and remove keep requiring an observed revision. Explicit blank and null values keep their previous refusals: T-Auto#422 leaves "blank means create-only" open, so this change does not take that decision. Contract hashes are re-pinned with the reason recorded.
ExSchwi
force-pushed
the
work/write-file-absent-revision
branch
from
October 30, 2026 15:48
cb5b6e6 to
72d6d06
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #419
What was wrong
`ops_write_file` could only ask for create-only by passing the literal string `"absent"`. The field was required twice over: `expectedRevision` sat in the schema's required list (`src/capabilities/file.ts`) and `WriteArgs.expected_revision` was a bare `String` in the worker, so serde rejected a missing value before publication.
Reproduced on Windows x64, Node v22.19.0, upstream main through the worker call the suite uses:
```
{"status":"error","error":{"code":"INVALID_ARGUMENT","phase":"preflight","retryable":false,"effectState":"none",
"message":"write_file: missing field `expectedRevision`"}}
```
A strict adapter that drops empty optional strings therefore had no way to request create-only without fabricating the sentinel.
The change
Scope note for #422
#422 (opened by the maintainer 33 minutes after this report) records the write-side decision boundary: the literal-`"absent"` discoverability problem is tracked in #419, and that issue "does not duplicate it, and does not pre-accept turning an empty string or null into create-only".
This PR therefore relaxes only the omitted field and leaves every explicit value on its previous path: an explicit blank still returns `INVALID_ARGUMENT` from the worker and `null` is still a type error. One consequence is worth stating up front rather than hiding in a test: because the field is no longer required, the generic host rule in `src/file/contract.ts:22-25` — which drops blank and `null` values of optional string fields — now also drops them here, so a call that goes through the bridge with `""` or `null` reaches the worker as an omitted field. That rule predates this PR and applies to every optional string field; `test/file-args.test.mjs` pins the behaviour so a reviewer can see it. Keeping blanks as errors end to end would need either the field back in `required` or a write-specific exception in that normalizer, which is exactly the contract decision #422 reserves. If you prefer that shape, say so and I will follow it.
Validation
Behaviour-level red and green, on the rebased tree (Rust worker rebuilt between runs):
```
$ git checkout origin/main -- native/crates/ops-files/src/file_single.rs && npm run build:ptc && node test/run.mjs file-operations
FAIL an omitted expectedRevision is create-only while an explicit blank or null stays a refusal
AssertionError [ERR_ASSERTION]: {"error":{"code":"INVALID_ARGUMENT","effectState":"none",
"message":"write_file: missing field `expectedRevision`","phase":"preflight","retryable":false},"status":"error"}
dsh-ops file-operations: 1 of 15 check(s) failed
$ cp <the PR's file_single.rs> native/crates/ops-files/src/file_single.rs && npm run build:ptc && node test/run.mjs file-operations
dsh-ops file-operations: all 15 checks passed
$ node test/run.mjs file-args
dsh-ops file-args: all 9 checks passed
$ cd native && cargo test -p ops-files
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ node test/run.mjs capabilities
dsh-ops capabilities: all 6 checks passed
```
Full gate, the script CI runs:
```
$ node scripts/verify-gate.mjs --skip-install
=== versions / cargo version: node v22.19.0, cargo 1.98.1
=== build the plugin: 11 browser modules (67749 bytes); PTC declarations for 18 capabilities (113581 bytes); 50 assets
=== native worker unit tests: ok
=== build and stage the native worker: win32-x64 3884544 bytes, sha256 b2fca132952fc76e3a47941c58c9f82c927116ee104d5b0768ba9d14ebdb8489
=== Windows IPC payloads: ok
=== manifest contract: all 83 checks passed
=== full suite verification: 2 of 161 suite(s) failed, 4 skipped in 677s
FAIL 60 levels of nesting and dot segments resolve to the same file
AssertionError: a trailing separator is refused, not silently trimmed
+ undefined - 'INVALID_ARGUMENT' (test/linux-paths.test.mjs:143)
--- bash-ipc.test.mjs exceeded the 120s suite deadline; stopping its owned process tree
$ node test/run.mjs bash-ipc
dsh-ops bash-ipc: all 8 checks passed
dsh-ops test: 3 suite(s) passed in 80s
```
Neither red belongs to this change. `test/linux-paths.test.mjs:143` is the pre-existing Windows difference described under Environment (upstream `CI required` is green on win32-x64). `bash-ipc.test.mjs` hit the 120 s per-suite deadline while quarantining a tampered runtime under load and passes standalone in 80 s, with nothing in this PR touching the Bash lane. The four skips are the host-scoped ones the gate allows (linux-attributes, linux-shell, TUI host, native TUI).
CI required run (pull_request event):
```
$ gh run view 41084069191 --json url,event,conclusion,headSha
{"conclusion":"success","event":"pull_request","headSha":"72d6d0652662a98e47c336eb2fe1ef39a93d3a73",
"url":"https://github.com/T-Auto/dsh-ops/actions/runs/41084069191","workflowName":"Cross-platform validation"}
$ gh pr checks 500
CI required pass 4s
node-floor pass 3m42s
source-size pass 7s
tools-trial pass 30s
validate-macos-intel pass 8m54s
validate-posix (macos-latest, darwin-arm64) pass 2m55s
validate-posix (ubuntu-24.04, linux-x64) pass 3m24s
validate-windows (windows-latest, win32-x64) pass 5m43s
workflow-lint pass 8s
bench-smoke skipping
```
Environment
Windows 11 x64 (win32-x64), Node v22.19.0, npm 10.9.3, Rust 1.98.1 / cargo 1.98.1; base commit 3882e69 (upstream main); dependencies installed with `npm ci --ignore-scripts` from the checked-in lockfile. The one local red that is not part of this change is `test/linux-paths.test.mjs:143` ("a trailing separator is refused"): on Windows the worker accepts a trailing separator because Rust's `Path::components` drops it. Upstream CI was green on win32-x64 at a6e215a (run 40458041379), and this PR does not touch `file_path.rs`, so it is an environment difference rather than a regression.