💥 Contain document filesystem access behind API.Files (#227) - #403
Conversation
PR #403: 💥 Contain document filesystem access behind API.Files (#227)35 files, +5546 / -611 Scope🔴 PR has 6157 lines changed. Split into focused PRs. 🟡 6157 lines changed. PRs under 400 receive more thorough review. 🟡 35 files changed. Are all changes related? 🟡 Changes span 9 directories. 🟡 PR mixes config and source changes. Structural🟡 Type declarations with no consumers: FatalFailure.
Oxlint structural signals:
Slop
Static AnalysisOxlint: 8 diagnostics across 5 files (5 rules) no-shadow (2): packages/core/src/components/Glob.ts, packages/core/src/components/File.ts CorrectnessNo extraneous code patterns detected. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // separately loaded copy of `printErrors` bundles, and the runtime's root | ||
| // re-exports the host Apis — process, fetch, filesystem — one of which carries a | ||
| // native addon no bundler can inline. Recognizing a Files failure needs none of | ||
| // that: it is a structural tag and a parser. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // that: it is a structural tag and a parser. |
| mkdirSync(outside); | ||
|
|
||
| // Absence is checked before a provider exists, so the terminal handler is the | ||
| // one answering. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one answering. |
| check("a temporary directory is removed with its scope", !exists(temporary)); | ||
|
|
||
| // Diagnostics rather than a result, so they go to stderr: this probe's whole | ||
| // output is the account of what it checked. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // output is the account of what it checked. |
182dd56 to
6be3844
Compare
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| * is that a workflow run cannot silently touch the caller's filesystem. | ||
| */ | ||
| export const Files: Api<FilesHandler> = createApi<FilesHandler>("executablemd.runtime.files", { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *writeTextFile(_input: FileWriteInput): Operation<Result<FileWriteSuccess>> { | ||
| throw new FilesProviderUnavailableError(); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| *globFiles(_input: GlobInput): Operation<Result<string[]>> { | ||
| throw new FilesProviderUnavailableError(); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| let step: FileWritePhase = "temporary"; | ||
| // Whether the write reached its own end. Cleanup runs on every exit, and | ||
| // the two exits need different answers: one has a Result to compose with | ||
| // and the other does not. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and the other does not. |
| mkdirSync(outside); | ||
|
|
||
| // Absence is checked before a provider exists, so the terminal handler is the | ||
| // one answering. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one answering. |
| check("a temporary directory is removed with its scope", !exists(temporary)); | ||
|
|
||
| // Diagnostics rather than a result, so they go to stderr: this probe's whole | ||
| // output is the account of what it checked. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // output is the account of what it checked. |
e669b00 to
e0c8409
Compare
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| if (carried) { | ||
| // Present, so it has to be readable *and* be the absent payload. Asking | ||
| // only whether the value is `undefined` would let a member that refused to | ||
| // be read pass as one that read as nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // be read pass as one that read as nothing. |
| expect(result.ok && result.value).not.toContain("first"); | ||
|
|
||
| // Rejection is per event: the offending one is absent, everything after it | ||
| // still persisted. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // still persisted. |
| expect(persisted.map(label)).toEqual(["yield(import_component)", "yield(exec)", "close(root)"]); | ||
|
|
||
| // The bytes on disk are exactly the accepted events — the rejected one | ||
| // left no partial record behind. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // left no partial record behind. |
1cb3b76 to
6dca5e2
Compare
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // Document filesystem access resolves in the caller's own filesystem here. | ||
| // It is installed explicitly, and at the same depth, because `API.Files` has | ||
| // no host default: a run with no provider must fail rather than reach the | ||
| // host by accident. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // host by accident. |
| // separately loaded copy of `printErrors` bundles, and the runtime's root | ||
| // re-exports the host Apis — process, fetch, filesystem — one of which carries a | ||
| // native addon no bundler can inline. Recognizing a Files failure needs none of | ||
| // that: it is a structural tag and a parser. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // that: it is a structural tag and a parser. |
| let step: FileWritePhase = "temporary"; | ||
| // Whether the write reached its own end. Cleanup runs on every exit, and | ||
| // the two exits need different answers: one has a Result to compose with | ||
| // and the other does not. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and the other does not. |
| // beside — and manufacturing one would turn a halt into a write | ||
| // result. It leaves the scope as an infrastructure failure instead, | ||
| // carrying neither the platform's error nor the generated temporary's | ||
| // name, and the engine's fatal discovery finds it there. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // name, and the engine's fatal discovery finds it there. |
| // The Api compiles patterns as it starts, so an unusable one — an | ||
| // unterminated character class — arrives as a `SyntaxError` from `RegExp` | ||
| // rather than as an errno. It is the one failure here a document can fix | ||
| // by editing what it wrote. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // by editing what it wrote. |
| mkdirSync(outside); | ||
|
|
||
| // Absence is checked before a provider exists, so the terminal handler is the | ||
| // one answering. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one answering. |
| check("a temporary directory is removed with its scope", !exists(temporary)); | ||
|
|
||
| // Diagnostics rather than a result, so they go to stderr: this probe's whole | ||
| // output is the account of what it checked. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // output is the account of what it checked. |
`<File>`, `<Glob>` and `<TempDir>` reached the host filesystem directly, so `xmd run` and a workflow run could not mean the same thing for one document. Document filesystem access now goes through `API.Files`, a contextual Api of whole semantic operations with no host default. The four CLI entrypoints install the host provider explicitly; a run with none installed fails rather than reaching the host. The three components make no filesystem call of their own and import no host path or fs module. What they keep is order: a write's lexical check runs before its children, and the semantic write that follows repeats admission and owns every later phase, so the earlier check authorizes nothing. Ordinary failures cross the boundary as frozen structural data — a reason from a fixed vocabulary and the phase it came from — and every printed message is byte-identical to before. A provider that is absent, refuses an operation, or breaks its own contract throws instead, and core's fatal traversal ranks it between a durability failure and a documentation failure, by identity and by structural tag.
`errors.ts` is in the graph a separately loaded copy of `printErrors` bundles, and importing the runtime's package root pulled the host Apis in with it — including a native addon no bundler can inline. The Files recognizer needs none of that, so it comes from the leaf module through a new `./files` subpath. The five-target job prepares with `deno install` rather than `deno task deps`: the task caches graphs this job does not need and reaches them by spawning a child, which does not survive the Windows runner's path handling. Its compile now carries the repository's isolation flags, and the rule that enforces them reads every compile in a workflow rather than letting the first invocation's flags answer for the rest.
`toSorted` needs es2023, which `tsconfig.node.json` does not select. The array is built from a Set on the line above, so nothing shared is being mutated.
…nup fatal Structural parsing runs from `fatalCause`, which every generic catch in expansion consults. A provider is free to hand back a Proxy that refuses to be inspected, and one of these parsers throwing would replace the failure being classified with a failure about classifying it. Every read is now total, and so is the cause traversal: a hostile wrapper narrows what discovery finds instead. Recognizing a Files fatal is a decision to let that exact object travel onward by identity, so it now requires the whole public contract — frozen data with no extra fields, the fixed diagnostic for its kind, and no cause. A candidate carrying a raw message or an errno chain is replaced by a fresh invariant rather than preserved. Durability recognition is unchanged: it stays #394's class-based mechanism, and only the Files boundary is crossed by a second loaded copy. A host cleanup that fails while cancellation is unwinding has no outcome to report beside it. It leaves the scope as a fixed teardown invariant instead of manufacturing a write result, carrying neither the platform's error nor the generated temporary's name. The loaded-copy claim is now proved by a real second copy: the Files module is bundled, imported as its own module, and its failures are recognized in both directions.
Recognizing a Files fatal hands that exact object onward, so the contract now covers the whole Error: the fixed name and diagnostic for its kind, frozen data with exactly the kind's fields, no cause, and no other enumerable member — string or symbol. A path riding on `name`, on an extra property, or under a symbol key fails the contract, and `invokeFiles` replaces the candidate rather than preserving it. A `Result` is only conventionally a Result. The TypeScript signature is a claim about the provider, not a guarantee, so a component that read `ok`, `value`, or `error` first would be the thing that ran a hostile accessor — outside anything that sanitizes. The core wrappers now inspect the container totally and rebuild every outcome from validated parts: no provider-originated container, error object, or payload reaches `<File>`, `<Glob>` or `<TempDir>`, and a search result is copied rather than passed along. A container that will not say how it settled, and a success it cannot describe, are provider-contract failures. A malformed non-write failure is not: the vocabulary already has a sentence for it, so a fresh generic failure is substituted and the document carries on. FA24 now runs all six orderings of the three fatal kinds, through both wrappers.
`undefined` was standing for three different answers: a member read fine and
held undefined, a member was absent, and reading a member threw. Collapsing
them let two containers through that never described their outcome —
`checkFilePath` accepted a success whose `value` refused to be read, and a
non-write failure whose `error` refused was downgraded to the printable generic.
Presence and readability are now asked separately, and every operation says
which it requires. A search array is copied by index through the same reader,
so length and element traps are covered too and the iterator is never consulted.
The one place the contract bends is `checkFilePath`: Effection spells a
payload-free success as its shared `Unit`, `{ ok: true }` with no `value` member
at all, so absence there is the ordinary success rather than a failure. What is
refused is a `value` that cannot be read, and one that is present but is
something other than undefined.
The seam now passes live Proxies around real `Ok`/`Err` values through
`API.Files`, so the boundary is the first thing to run a hostile trap — the
previous JSON round-trip invoked the getter inside the provider handler and
proved only the already-covered handler-throw path.
`Array.isArray` is itself an operation on provider-controlled data: it throws on a revoked Proxy. Running it outside the total readers let a raw TypeError leave the boundary untagged, so a search whose result was revoked before it was returned surfaced the platform's message and let the document carry on. The brand check now answers instead of throwing, and a value whose array identity cannot be inspected is malformed success data like any other. The guard sits inside the payload contract rather than around the call site, so it stays the thing under test: restoring the unguarded call reds the regression. The specification catalog gains the rows for behavior already implemented — HF12b and FF11 through FF15 — and the provider-failure prose now separates an outcome that will not say what it is, which is a contract violation, from one that reads fine and reports a failure this version does not recognize, which is the generic sentence.
The provider-failure passages named only a read and a search, which left path admission and TempDir unstated even though they take the same path: readable data that does not validate selects the generic sentence there too. The FF14 row described a stricter rule than the boundary implements. It now matches FF14c and FF15: an unreadable settlement is a contract failure, so is an unreadable selected member, and an absent success value only where the operation carries one — path admission succeeds without one. A readable but unrecognized non-write failure stays printable. FF11 is renamed to what it asserts. Declining to recognize a hostile shape is not recognizing it as a valid structural failure.
6dca5e2 to
ab552cc
Compare
| // separately loaded copy of `printErrors` bundles, and the runtime's root | ||
| // re-exports the host Apis — process, fetch, filesystem — one of which carries a | ||
| // native addon no bundler can inline. Recognizing a Files failure needs none of | ||
| // that: it is a structural tag and a parser. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // that: it is a structural tag and a parser. |
| mkdirSync(outside); | ||
|
|
||
| // Absence is checked before a provider exists, so the terminal handler is the | ||
| // one answering. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // one answering. |
| check("a temporary directory is removed with its scope", !exists(temporary)); | ||
|
|
||
| // Diagnostics rather than a result, so they go to stderr: this probe's whole | ||
| // output is the account of what it checked. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // output is the account of what it checked. |
Why
Issue #227.
<File>,<Glob>and<TempDir>reached the host filesystemdirectly, through
API.Fsandnode:path/node:fs. That makes one documentmean two different things depending on where it runs, and it means the
containment those components promise is stated in the components rather than
owned by whoever supplies the filesystem.
This is PR A of a two-PR stack: the neutral Api and the host provider. PR B
adds the transaction-bound DOFS provider.
What changes
Before: a document's file operations resolved against the process's filesystem
in every environment, and the components themselves did the path arithmetic,
the
realpathwalk, the temporary-plus-rename write, and the traversal.After: every document file operation goes through
API.Files, a contextual Apiof whole semantic operations. The three components hold no host path and import
no host path or filesystem module.
xmd runinstalls a host provider from allfour entrypoints; a run with no provider installed fails rather than reaching
the host.
Every printed message a document can see is unchanged. The whole FL, GB and TD
tiers pass byte-for-byte through the new boundary without an expectation being
edited.
How it works
API.Fileshas five operations. Four are whole semantic acts — read this path,replace this path, list what these patterns select, give me a temporary
directory. The fifth,
checkFilePath, is deliberately weak: pure patharithmetic, no filesystem access, and it returns nothing usable.
That weakness is the design.
<File>'s write form callscheckFilePathtodecide whether its children may expand — FL18b and FL18c — and the later
writeTextFilerepeats the same admission from the same authored pathbefore owning resolution, target classification, parent creation and the
commit. Nothing is handed between them, so a check that was skipped, replaced
by middleware, or answered by another provider cannot authorize the write. FL18
still holds: a child that swaps the parent for an escaping symlink is caught,
because resolution happens inside the write.
An ordinary filesystem condition comes back as
Errcarrying a frozen plainobject: a reason from a fixed 20-value vocabulary, plus the phase it came from.
The reason selects a phrase; no message, errno code, resolved path, temporary
name, or symlink target crosses. A write's phase decides what may be said about
the target, and the constructor refuses any other combination.
A provider that is absent, that refuses an operation, or that broke its own
contract throws.
packages/core/src/files.tsis the sole wrapper: itrethrows an existing durability failure by identity, then an existing Files
failure by identity, and only replaces something that is neither — with a fixed
protocolinvariant carrying no cause and no host value.errors.tsextends#394's traversal so precedence is durability → Files → documentation, decided
by kind rather than by aggregate position, with the selected object returned by
identity.
Revision (review round 2)
Rebased onto
mainthrough #400, #401, #402 and #404. #400's Workspacecoordination seams are untouched;
packages/workflow/tests/workspace-effect.test.tspasses on this head.
Four blockers resolved:
a value a provider handed back, and they run from
fatalCause— which everygeneric catch in expansion consults. One of them throwing would replace the
failure being classified with a failure about classifying it. Every read
now goes through a helper that answers
undefinedinstead, each exportedparser is wrapped, and
causesOftreats an unreadablecause,errorsorcausesas aggregating nothing. A hostile wrapper narrows what discoveryfinds; it never replaces what discovery was finding.
identity." A Files fatal must now carry frozen data with no extra fields,
the fixed diagnostic for its kind, and no cause. A candidate with the right
tag but a raw platform message or an errno chain fails the contract and
invokeFilesreplaces it with a freshprotocolinvariant carrying none ofit. Durability failures keep 🐛 Restore the durable eval journal boundary #394's exact class-based recognition and
precedence.
outcome to compose with, so the adapter leaves the scope as a fixed
teardowninvariant rather than manufacturing a write result — carryingneither the platform error nor the generated temporary's name. A completed
write whose cleanup failed still composes as an
Errexactly as before.packages/core/tests/loaded-copy-files.test.tsbundles
packages/runtime/files.ts, imports it as its own module, and provesrecognition in both directions across two copies that share no class. The
hand-built object in FA27 remains, as parser coverage.
architecture.mdnow attributes structural-tag recognition specifically toFiles failures and says plainly that durability recognition is unchanged.
Revision (review round 5)
Retargeted from
e0c8409on35060af. TheResult<void>decision is unchanged.The array brand was the last escape.
Array.isArrayis itself an operationon provider-controlled data — it throws on a revoked Proxy — and it ran outside
the total readers, so a raw
TypeErrorleft the boundary untagged and thedocument carried on. Recognizing the brand now answers instead of throwing, and
a value whose array identity cannot be inspected is malformed success data like
any other.
The guard sits inside the payload contract rather than as a catch-all around
the call site. A wrapper there would have made every guard inside each contract
untestable — and in fact my first attempt added one, which is why the
mutation-check passed when it should not have. Payload contracts are total by
construction instead, and restoring the unguarded
Array.isArraynow redsFF14b. The regression was written first and reproduced the escape before the
fix.
FF14b gains a live
Proxy.revocablestring array: built valid, wrapped inOkwithout inspection, revoked before the provider returns. It asserts the
sanitized
protocolinvariant, that no later sibling runs, that the selectedfailure is not the raw
TypeErrorand carries no cause, and that neitherIsArraynorrevokedappears anywhere. The iterator-only success regressionand the copied-array independence checks are retained.
Specification catalog synced with what is implemented:
HF12b, andFF11through
FF15. The provider-failure prose now separates an outcome that willnot say what it is — no readable settlement, or an unreadable selected member,
which is a contract violation — from one that reads fine but reports a failure
this version does not recognize, which selects the generic sentence. It
describes observable behavior only; no internal helper appears as syntax.
Clean-worktree verification
Fresh clone at this head, on a resolved path:
deno task lintdeno task check(unscoped)deno task check:jsrpnpm exec tsc -p tsconfig.node.json --noEmitgit diff --check origin/main...HEADdeno task test(full Deno suite)Revision (review round 4)
Retargeted from
e669b00, then rebased ontomainthrough #405 and #406.undefinedwas standing for three different answers inpackages/core/src/files.ts: a member read fine and heldundefined, a memberwas absent, and reading a member threw. Presence and readability are now asked
separately, and each operation states which it requires. The two containers the
review named no longer get through:
checkFilePathrefuses a success whosevaluecannot be read, and a non-write failure whoseerrorcannot be read isa
protocolinvariant rather than the printable generic. Only a readableerrorwhose data fails validation gets the generic (FF15). Write keeps itsstricter commit-state validation.
Glob copying is total.
lengthand every element go through the samereader, and the walk is by index — so the iterator is never consulted and a
for…ofregression is caught (FF14b asserts both the hostile traps and that aniterator-only trap copies cleanly). The array that comes back is the document's
own: mutating the provider's afterwards does not reach the bound value.
The FF14 seam is replaced. Every hostile shape is now a live Proxy around a
real
Ok/Errpassed throughAPI.Files— the declared type survives withouta cast, and the traps have never run when the provider hands the value back, so
the normalization boundary is the first thing to touch them. The previous
JSON round-trip invoked the getters inside the test and proved only the
handler-throw path FF7 already covers. Shapes are paired only with the
settlement they actually bite, because a
valuetrap is never consulted by afailure and pairing them would have demanded a fatal outcome from a legitimate
ordinary path.
All three new guards were mutation-checked: restoring the
checkFilePathconflation, downgrading an unreadable
error, and switching the Glob walk backto
for…ofeach red FF14/FF14b.One deviation from the stated contract, deliberately
The directive asks that an absent
valueoncheckFilePathbe fatal.Effection spells a payload-free success as its shared
Unit—{ ok: true },with no
valuemember at all — which is exactly what the host adapterreturns from
checkFilePath. Making absence fatal there would reject the realprovider on every admitted path.
So absence is the ordinary success for that one operation, and what is refused
instead is a
valuethat is present but cannot be read, and one that is presentand is something other than
undefined. Both are covered (FF14 "a throwingvalue", FF14c's payload-carrying Proxy). Every other operation — read, write,
Glob, TempDir — treats an absent
valueas fatal, sinceOk(x)always carriesone. Absent
erroris fatal everywhere, sinceErralways sets it.Clean-worktree verification
Fresh clone at this head, on a resolved path:
deno task lintdeno task check(unscoped)deno task check:jsrpnpm exec tsc -p tsconfig.node.json --noEmitgit diff --check origin/main...HEADdeno task test(full Deno suite)Revision (review round 3)
Retargeted from
6be3844;mainhad not moved.Files fatal hands that exact object onward, so it must match what a
constructor here produces: the fixed name and diagnostic for its kind,
frozen data with exactly that kind's fields, no cause, and no other
enumerable member — string or symbol. FF12 now includes a path-bearing
name, an extra Error-levelpath, an enumerable symbol payload, andProxies with hostile
ownKeys/getOwnPropertyDescriptor. Each is rejectedby
filesFatalFailure, replaced byinvokeFileswith a freshprotocolinvariant, and proved not to survive stringification, spreading, or symbol
enumeration — while the three real constructors stay recognizable.
Resultis validated and rebuilt inside the boundary. AResultis only conventionally a Result: the TypeScript signature is a claimabout the provider, not a guarantee, and a component reading
ok,value,or
errorfirst would be the thing that ran a hostile accessor — outside thesanitizer. The core wrappers now inspect the container totally and rebuild
every outcome from validated parts, so no provider-originated container,
error object, or payload reaches a component;
globFilesreturns a copiedarray. A container that will not say how it settled, and a success it cannot
describe, are
protocolinvariants that stop later siblings (FF14, acrossread, write,
checkFilePath, Glob and TempDir). A malformed non-writefailure is not fatal — a fresh generic failure is substituted, the existing
"filesystem operation failed" sentence renders, and the document carries on
(FF15). Cancellation is still never converted into a Result.
through both
AggregateErrorandInvocationTeardownError; every oneselects the durability failure by identity. The earlier six-permutation edit
had silently not applied — the loop had been reflowed by
oxfmt— so this isthe first revision where it is actually true.
Both new contracts were mutation-checked: weakening
isFilesFatalreds FF12,and reading
okwithout the settlement check reds FF14.Clean-worktree verification
Run in a fresh clone at this head, which is also where the previously-reported
deno task checkcaveat goes away:deno task lintdeno task check(unscoped)deno task check:jsrpnpm exec tsc -p tsconfig.node.json --noEmitdeno task test(full Deno suite)git diff --check origin/main...HEADReview guide
Start with:
packages/runtime/files.ts— the contract, and why the data isstructural rather than class-based.
Then review:
packages/core/src/components/File.ts— the ordering and the message tablepackages/runtime/host-files.ts— the host provider and where its guaranteestops
packages/core/src/files.tsandpackages/core/src/errors.ts— the throwordering and the extended fatal traversal
.github/workflows/ci.yml+scripts/files-contract-probe.ts— thefive-target evidence
Look carefully at:
writeTextFileinhost-files.ts: the cleanupensureis registered beforethe temporary is written, and
stepis what decides the reported phase.refusal()andreport()inFile.ts: these reproduce the exact currentsentences from structural data instead of from caught errors.
What must stay true
enforced by
checkFilePathreturningResult<void>, checked by FL18b, FL18cand FF4.
writeTextFiledoingits own admission, checked by FL18 and HF10.
reason vocabulary, checked by FL23, FL26, GB13, GB14 and HF4.
checked by FF1, FF2 and HF14 (which also asserts zero
API.Fscalls).by
fatalCause, checked by FA20–FA28 and FF8/FF9.structural tag, checked by LC1/LC2 against a really-bundled second copy, and
by FA27 for the parser itself.
packages/runtime/files.tsandcausesOf, checked by FF11 against throwingaccessors and Proxies on
data, its fields, key enumeration,cause,AggregateError.errorsand teardowncauses.isFilesFatal's full-contract check, checked by FF12.How to verify it
packages/runtime/tests/host-files.test.ts(HF1–HF16) proves the providercontract directly and fails if a phase, a reason, or the absence behavior
moves. HF10/HF10b assert the documented weakness — a parent or target
replaced synchronously between observation and use is written or read through
— so a future change that silently claimed race containment would fail here.
packages/core/tests/files-fatal.test.ts(FF1–FF13) fails if a missingprovider ever becomes a printed error, if a malformed write outcome invents a
commit state, if a nested durability failure loses its identity, if a hostile
shape makes classification throw, or if a cleanup failure during cancellation
stops being fatal.
HF12bsuspends the write after the temporary is created, halts it, andforces removal to fail — deterministically, with no timing race — and fails if
a halt is ever turned into a write outcome.
packages/core/tests/fatal-cause.test.tsFA20–FA28 fail if precedencebecomes position-dependent or if recognition regresses to
instanceof.printed sentence moved.
deno test --allow-all --frozen \ packages/runtime/tests/host-files.test.ts \ packages/core/tests/files-fatal.test.ts \ packages/core/tests/fatal-cause.test.ts \ packages/core/tests/file-component.test.ts \ packages/core/tests/glob-component.test.ts \ packages/core/tests/temp-dir.test.ts \ scripts/tests/filesystem-contract-workflow.test.tsResults
deno task lintdeno check --frozen packages scripts+.reviews/components/*.tsdeno task check:jsrSuccess Dry run completedeno task test packages/core/tests packages/runtime/tests packages/cli/testsdeno task test --changed=origin/main packages scriptsxmd test packages/core/src --rawdeno compile+ runscripts/files-contract-probe.tstsx --testandbun teston the new suitesScope
Included
packages/runtime/files.ts— the Api, structural failure data, constructors,parsers and recognizers
packages/runtime/host-files.ts— the host provider and its test-only phaseobserver
packages/core/src/files.ts— the soleinvokeFileswrapperpackages/core/src/errors.ts— Files discovery added to 🐛 Restore the durable eval journal boundary #394's traversalfilesystem-contractCI matrix over the five release targets, held toRELEASE_TARGETSand added togreen.needsarchitecture.mdand executable-MDX §§1.2, 6.11, 6.13, 6.14 and theFL/TD/FA/HF/FF test-plan tiers
Intentionally unchanged
and no workflow command reaches
API.Filesyet.API.Fsstays the low-level host surface and the host provider's owndependency. Middleware wrapping it still sees a document's access.
the eval compiler, the diagnostic journal, the test target — keeps reading
host paths through
API.Fs.remove,rename,realpathorstatoperation: those areprivate phases of the write, and are tested only as
write.commitandwrite.cleanup.New abstractions
API.Filesexists because the same three components must resolve paths intwo environments that share no filesystem primitive; consumers are
<File>,<Glob>and<TempDir>today and the DOFS provider in PR B.The structural failure data exists because
instanceofanswers false acrosstwo loaded copies of the runtime package, which is exactly the case where a
provider failure must not become an unrecognized throw.
hostFilesHandler's observer exists so the stable-namespace limitation isfalsifiable rather than merely stated; production entrypoints omit it.
Each new abstraction has multiple concrete uses or a clear justification.
No speculative functionality is included.
Risks and limitations
execute()directly must now install a provider (
useHostFiles()), or<File>,<Glob>and<TempDir>fail the execution. That is the point — a missingprovider must not silently reach the host — but it is a real API break, and
the affected in-repo suites were updated to install it the way an entrypoint
does.
filesystem-contractruns
host-files.test.tsonwindows-2025, and that suite creates filesymlinks. Directory links use junctions, which an unprivileged process can
create; file symlinks depend on the runner being elevated. If that row goes
red on privileges, the fix is to reshape those cases — the finding is exactly
what the matrix exists to surface, and I could not verify it from macOS.
says containment holds while the host pathname namespace is stable, and HF10
demonstrates a replacement being written through. Nothing regressed; the
earlier text implied more than the implementation could deliver.
whole; no persisted state or schema is involved.