Skip to content

Protos Filesystem v0.1

Canonical source: spec/io/FILESYSTEM.md at locked revision e99b712ecf658a527b1fe82c8beba2c1286d8dc9. The source document’s own status and authority declarations apply; this website rendering does not upgrade, downgrade, or redefine them.

Language version: 0.1 Status: Draft Last updated: 2026-10-05

This document is the primary normative owner of File opening, filesystem authority, and Path semantics.

The modular I/O specification consists of IO_CORE.md, BYTE_IO.md, TEXT_IO.md, FILESYSTEM.md, and PROCESS_IO.md. Legacy section numbers from IO_CORE.md and the applicable sibling I/O module are intentionally retained so historical citations remain understandable. Normative ownership now belongs to these modules; the former monolithic file is removed by revision 326.

File means an already-open file resource. Path represents a filesystem path value. Filesystem represents authority over a filesystem namespace.

The fundamental open operation is conceptually:

filesystem.open(path, options) -> Future<File>

Core v0.1 does not use opaque mode strings such as "r", "w", "a", or "r+" as its normative semantic model.

Open configuration has separate dimensions:

access:
read
write
read + write
creation:
existing
create
createNew
initial content:
preserve
truncate

Core v0.1 defines no append open option, append mode, or other write-placement dimension. Every writable standard File uses the positioned write semantics of section 18.2.1.

The portable call forms are filesystem.open(path) and filesystem.open(path, options). The one-argument form means read access, existing target, and preserved content.

options is an ordinary Protos object. Its standard option slots are exactly read, write, create, createNew, and truncate. Only local slots owned by that object participate; delegated slots are ignored. Each present standard slot must contain exactly canonical true or false. Missing slots default to false, except read, which defaults to true. Any other local slot makes the options invalid. The options object need not have a special prototype and need not be frozen or closed. Explicit null and non-object-domain values are invalid options.

The tuple is: access from read/write; creation is createNew when true, otherwise create when true, otherwise existing; initial content is truncate when true, otherwise preserve. create plus createNew is invalid, in addition to the combination rules below.

After ordinary argument evaluation, open reads these local slots and snapshots their Boolean/default values exactly once during invocation. No getter/callback protocol is invoked. Later mutation cannot affect the captured configuration. An Error while evaluating the expression supplying options propagates before open is invoked.

After successful dispatch, an invalid Path-domain argument, invalid options-domain value, invalid/unknown option slot, or invalid captured combination fails the returned Future with a fresh InvalidIOArgument under IO_CORE.md, before namespace/backend effects.

filesystem.open(path, options) captures the complete semantic open configuration at invocation time, before the operation may remain pending, wait for another operation, or begin host/backend I/O. The captured configuration consists of the access, creation, and initial-content choices defined above.

Later mutation of an ordinary object, collection, builder, or other library value that was used to express options cannot change the configuration of an already-invoked open. Each open invocation captures its own configuration independently.

This is a semantic value capture, not a requirement to eagerly copy a particular public options object. An implementation or standard library may use immutable option values, builders, compact flags, copy-on-write state, or another representation. It must not impose a hidden caller-visible borrow/freeze/“do not mutate until the Future completes” rule merely to keep an open’s configuration stable.

Capturing the configuration does not itself commit any filesystem effect and does not by itself prevent cancellation. The ordinary open commitment rules below still determine when creation, truncation, or File-result custody makes cancellation impossible.

Validation of access/creation/initial-content combinations is performed against that captured configuration. A later mutation of the value originally used to express the options cannot turn an invalid captured open into a valid one, change a valid captured open into a destructive one, alter its access rights, or otherwise change its eventual File capability shape.

At least read or write access is required.

Truncate requires write access.

create and createNew are mutually exclusive choices.

Write access alone does not imply create or truncate.

Invalid combinations fail open.

Validation of the captured standard open-configuration tuple is a preflight semantic step. After ordinary argument evaluation has supplied the Filesystem, Path, and captured open configuration required by the invocation, the access/creation/initial-content combination is validated before that open performs filesystem namespace resolution, resource selection/acquisition, creation, truncation, or other host/backend I/O for the target.

If that captured combination is invalid under the standard rules above, the filesystem.open Future fails for invalid open configuration without exercising filesystem authority against the supplied Path and without producing any namespace, content, cursor, resource-acquisition, or other target effect. In particular, an implementation must not discover missing read/write access, read-only truncate, or another standard-invalid combination only after it has looked up, created, opened, or modified the target.

Because this validity decision depends only on the already-captured semantic configuration, implementations may and normally will return an already-failed Future. The standard asynchronous API is preserved: this rule does not introduce a separate synchronous exception path for an otherwise well-formed filesystem.open invocation merely because its captured option combination is invalid.

This preflight rule establishes deterministic precedence over target-dependent filesystem outcomes for a valid Path value. For example, an invalid captured configuration fails as invalid configuration rather than sometimes reporting target absence, target existence, permission/confinement failure, or another backend/path outcome according to whether an implementation happened to resolve the Path before validating its options.

The rule does not require every valid configuration to be supportable by every backend. A configuration may be semantically valid yet later fail because the selected resource/backend cannot provide a required standard capability or invariant. Such support/resource failures occur under the ordinary open, confinement, acquisition, cancellation, and commitment rules; they are distinct from the configuration tuple being invalid in the first place.

Likewise, this rule does not redefine ordinary language-level receiver/argument validation needed to establish that the invocation has usable Filesystem, Path, and option-expression values at all. It only fixes the first filesystem-domain step once the semantic open configuration has been captured: validate the tuple before touching the target namespace/resource.

Standard-invalid open configurations therefore fail through the open Future before target namespace/resource work begins; invalid options can never create, truncate, acquire, or otherwise affect the supplied filesystem target.

Multiple filesystem.open invocations do not form one ordered namespace-operation stream merely because they use the same Filesystem capability, equal Path values, or one Actor invokes them sequentially without awaiting an earlier Future. Filesystem carries namespace authority; it is not a mutable sequence cursor whose independent acquisitions are implicitly serialized in caller invocation order.

Accordingly, after two valid open operations have both been invoked and remain simultaneously pending, Protos does not define which operation reaches its own namespace selection/commitment point first unless another normative dependency already establishes that order. This remains true when the opens use equal Paths and their effects can interact, such as createNew racing existing, two createNew operations, or an open with truncate racing another acquisition.

Each open still individually obeys its race-free selection, creation, confinement, stable-resource-binding, cancellation, and commitment rules. Once one open commits a namespace/content effect, another open that reaches its selection point later observes the namespace/resource state then applicable under the Filesystem/backend semantics. Protos does not permit an implementation to rewrite an already-selected File merely to manufacture caller-order serialization afterward.

A program that requires one open’s terminal result or committed effects to precede a later acquisition establishes that dependency through ordinary Protos sequencing: it waits for the first operation’s Future to reach the required terminal outcome before invoking the dependent open, or uses a future explicit higher-level protocol whose normative contract supplies the needed ordering. Merely retaining both Futures or invoking both calls from one Actor without such a dependency does not establish it.

An implementation may internally serialize some or all opens for a backend when doing so cannot change outcomes allowed by this rule, but that queueing policy is not portable Protos ordering. Conversely, implementations may perform independent opens concurrently, batch them, or use backend-native asynchronous acquisition. No global Filesystem lock, per-Path queue, or same-Actor namespace FIFO is required solely by the standard open protocol.

This rule concerns ordering among distinct open operations. It does not weaken the stable ordering domains of a File after acquisition or any stronger future Filesystem operation that explicitly defines its own transaction/order domain.

Standard Filesystem opens are independent asynchronous acquisitions: same Filesystem, same Path, or same-Actor invocation does not by itself order their namespace selection/commitment points; dependencies must be established explicitly.

existing is one race-free select-and-open operation at the Filesystem/backend semantic boundary. At that selection point the target must exist; if it is absent, open fails. If it exists, the operation selects that resource and acquires the File binding as one semantic operation.

The standard operation must not expose an implementation gap between a separate existence/metadata probe and acquisition in which a competing namespace change can produce behavior belonging to two different namespace states. In particular, a successful preliminary probe does not reserve the target, and an implementation must not claim that the probed resource was selected if the actual acquisition selected a replacement instead.

This guarantee does not impose a predetermined winner among genuinely concurrent namespace operations. Another authorized operation may create, remove, rename, replace, or redirect the target before this open’s selection point; the namespace state at the selection point determines whether existing fails or which authorized resource it selects. Once selection succeeds, the stable File-resource binding rules apply immediately, so later namespace changes do not retarget the returned File or any truncate-on-open effect belonging to this open.

An implementation may realize existing with one backend primitive or with a race-safe/emulated resolution-and-acquisition mechanism. Any helper probes may be used only when their intermediate results are not promoted into Protos-visible selection state. If the backend cannot provide or emulate one confined select-and-open result under concurrent namespace change, it must fail rather than expose an uncertain standard File binding.

Standard existing selects and opens one existing authorized resource as a single race-free semantic acquisition; helper existence probes do not reserve identity or split resource selection across namespace states.

create is one race-free open-or-create selection at the Filesystem/backend semantic boundary. At that selection point, if the target exists, the operation selects and opens that existing resource; if the target is absent, the operation creates and selects the new resource. The standard operation must not expose an implementation gap between an existence probe and acquisition/creation in which a competing namespace change can turn an otherwise valid create into a spurious already-exists/not-found outcome or cause it to acquire a resource different from the one selected by its own open-or-create decision.

This guarantee does not impose a predetermined winner among genuinely concurrent namespace operations. Another authorized operation may create, remove, rename, or replace the target before the create operation’s selection point, and that namespace state may determine which resource this open selects. Once this open has selected its resource, the stable File-resource binding rules apply: later namespace changes do not retarget the resulting File or any truncate-on-open effect belonging to this open.

An implementation may realize create with one backend primitive or by emulation, but any emulation must preserve the same race-free semantic result. In particular, a probe-then-exclusive-create sequence must recover from a concurrent creator by selecting the then-existing target when that is the result a single standard open-or-create operation requires, rather than leaking the helper primitive’s race as a Protos-visible failure. If a backend cannot provide or emulate this invariant while also preserving Filesystem confinement, it must not expose standard create semantics for that resource.

createNew creates a new target and fails if the target already exists. The existence check and creation are one race-free/atomic creation operation at the backend’s semantic boundary.

Whenever standard create selects the absent-target branch, and whenever standard createNew succeeds, the newly created file resource begins with logical byte size zero and contains no octets at the creation commitment point. This empty-content state is part of the standard file-creation semantic result, not a host-selected default.

The rule is independent of initial content: preserve versus truncate. preserve has no pre-creation content to preserve on a newly created resource, so the created resource is empty. truncate likewise finds the newly created resource already at logical size zero and introduces no additional destructive content effect or commitment merely to re-establish zero size.

Creation emptiness is a logical content guarantee rather than a physical allocation rule. A backend may create a sparse object, lazy object, copy-on-write object, virtual file, remote resource, or other representation without eagerly materializing an empty byte buffer, provided that the resource’s Protos-visible initial byte sequence is exactly empty.

The empty state is established at this open’s creation commitment point. It is not a promise that the resource remains empty until the open Future resolves or until some later observer examines it: independently authorized writes or other backend changes may occur afterward according to the existing cross-capability, namespace, and resource-visibility semantics. Such later changes do not retroactively alter the content state that this creation operation itself established.

If creation commits and the open later fails before returning a File, the ordinary no-compensating-rollback rule still applies: the operation does not delete the created target merely because later acquisition work failed. The created resource was established empty by this operation, although independent authorized activity may subsequently change it.

A backend whose primitive for creating the standard file resource necessarily exposes non-empty implementation-selected initial content, inherited template bytes, residual storage contents, or another non-empty default must clear/emulate that state before creation can commit as a standard Protos creation. If it cannot do so while preserving the required race-free creation, confinement, and commitment semantics, it must not expose that operation as standard create/createNew.

A standard newly created File resource therefore starts as an empty logical byte sequence of size zero; host/backend creation defaults cannot supply portable initial content.

Standard create is an atomic/race-free open-or-create selection, not a portable existence-check-then-create recipe; concurrent namespace changes may determine the selected state before the selection point, but implementation helper races must not become observable results.

Writable open with truncate establishes initial file size zero as part of open.

Read-only plus truncate is invalid.

The truncate-on-open content effect is indivisible at the Protos semantic boundary. For an already-existing selected resource whose size/content would actually change, that open either has not yet committed any truncate-attributable content change, or it has established the complete requested truncation to logical size zero. A standard open must not expose a failed or cancelled truncate-on-open whose own aftermath is an intermediate nonzero truncation merely because a backend performed resizing in several steps.

Before the complete zero-size effect has committed, cancellation or an open failure attributable to the truncate attempt leaves the selected resource’s content and size unchanged by that truncate-on-open operation. An implementation may use tentative backend work only when it can restore, virtualize, defer publication, or otherwise prevent a partial resize from becoming the failed/cancelled open’s observable effect.

Once the selected existing resource has been completely truncated to size zero, that content effect is an irreversible open commitment under section 18.5. Cancellation can no longer win. The open may still fail later for another reason, but the open performs no compensating restoration of the discarded content.

If the selected resource was already size zero, the truncate request is a successful content no-op and creates no additional irreversible content commitment merely by checking or confirming that state. If create or createNew creates a new empty resource, creation remains the relevant namespace commitment; requesting truncate does not invent a second destructive effect when the newly created resource already has size zero.

This failure-atomicity concerns only content change attributable to the truncate-on-open operation. It does not freeze the selected resource against independently authorized writes, truncations, or other backend changes. Such independent activity may make a later observer see nonzero size even after this open’s complete zero-size effect occurred, according to the existing cross-capability/backend ordering and visibility rules.

Support for this open-time invariant does not require the returned File to expose Truncatable. A backend may have a race-safe/atomic open-with-truncate primitive or may emulate the invariant specifically during acquisition. If it cannot provide or emulate the all-or-zero truncate-on-open effect while preserving the other standard open guarantees, it must reject that standard open configuration rather than expose an implementation-dependent partial destructive failure.

Standard truncate-on-open is therefore failure-atomic with respect to its own content effect: before commitment it contributes no truncation, at commitment it establishes complete logical size zero, and a later open failure does not roll that committed effect back.

For a writable standard File, each ByteWritable.write(bytes) starts at that File’s logical sequence position applicable when the operation reaches its ordered evaluation point. Let that starting position be p and let the captured write sequence have length N.

The write replaces existing file octets beginning at p; it does not insert bytes and shift later file content. If the operation contributes a prefix of length k under the ordinary ByteWritable success/failure rules, its own byte contribution occupies exactly offsets p through p + k - 1, in source order. For k > 0, the File’s logical position after that operation is p + k, whether the Future ultimately succeeds or fails. For k = 0, the logical position is unchanged.

If contributed bytes extend beyond the file size applicable to the write, the file grows as necessary to include the last contributed byte. If p is greater than that file size when the first byte of this operation is contributed, every newly created logical octet between the former EOF and p reads as zero. Those zero-valued gap octets are part of the file-growth effect required to place this operation’s first contributed byte; they are not bytes from the captured write sequence and do not increase k.

A positioned write that contributes zero bytes does not grow the file or create a gap merely because its logical position is beyond EOF. In particular, cancellation or failure with k = 0 preserves both the logical position and the file size/content effects attributable to that write. An implementation that tentatively extends, allocates, seeks, or materializes backend storage before the first contributed byte must reconcile that work so a zero-contribution outcome does not expose such tentative growth.

The zero-gap rule is logical file content, not a requirement to physically allocate or write every intervening octet. Sparse allocation, holes, extents, virtual zero ranges, explicit zero filling, or another backend representation are all permitted when reads and size observations produce the specified result. A backend that cannot provide or emulate deterministic zero-valued gap content must not expose that resource as a standard positioned File whose writes may extend beyond EOF.

Independent operations through separately opened File capabilities, external actors/processes, or backend agents remain governed by the existing cross-capability/backend semantics. This rule does not create a new cross-File ordering or atomicity domain. In particular, such independently authorized changes may alter file size or content before or after this write’s own placement/effects according to the backend contract; they do not change which offsets are attributable to this write’s contiguous contributed prefix once its starting logical position has been established.

For operations ordered on the same logical File, the ordinary sequence-state domain applies: a later position-sensitive operation starts from the post-write logical position and sequence aftermath defined above, including the permitted prefix and any required zero-valued gap created by that prefix.

A standard positioned File write overwrites from the File’s current logical position, advances that position by exactly its contributed prefix, grows the file when necessary, and exposes deterministic zero-valued logical gap octets when placement begins beyond EOF.

18.3 No append mode; initial logical position

Section titled “18.3 No append mode; initial logical position”

Standard filesystem.open has no append option or append mode. A writable standard File never selects its write placement from the current file end independently of its logical position; every write follows section 18.2.1.

Where a File exposes ByteSeekable, a program may invoke seekToEnd() and then write(bytes). That is an ordinary composition of two operations on one logical receiver: seekToEnd() sets the File’s logical position to the end applicable when it is evaluated, and the later write is an ordinary positioned write starting at the File’s logical position applicable at its own ordered evaluation point. The composition does not re-select the file end at write time, is not atomic, and establishes no placement coordination, non-overlap, or non-interleaving guarantee with writes through other File capabilities, aliases, external writers, or backend agents selecting the same resource. Those interactions remain governed by section 18.4.

Every successful standard File open whose read/write behavior uses a logical sequence position establishes that position as zero before the File result commits. This is independent of existing/create/createNew and preserve/truncate choices.

The zero position is a Protos logical-position guarantee, not a requirement that the backend’s native cursor already has that value. An implementation may use positional I/O, a virtual cursor, a native handle whose cursor is initialized differently, or another representation, but the first position-sensitive Protos operation must observe the same state as if the File’s logical position had begun at zero.

Likewise, truncate-on-open establishing file size zero does not create a distinct initial-position rule: the returned File’s logical position is zero because every standard positioned File begins there, not because truncation happened to make EOF zero.

A backend or host adapter that cannot provide or emulate this initial logical-position state must not expose a standard File whose operations depend on that position while leaking an implementation-selected starting cursor.

A newly opened standard File begins at logical byte position zero whenever it has position-sensitive read/write behavior.

Writes invoked on the same receiver still preserve their required invocation ordering.

18.4 File resource identity and capabilities by mode

Section titled “18.4 File resource identity and capabilities by mode”

A successful filesystem.open(path, options) binds the returned File to the concrete filesystem resource selected by that open operation. The Path used to open it is not a continuing lookup instruction and is not the identity of the returned File.

After successful open, subsequent namespace changes do not retarget the File. Renaming, removing, replacing, relinking, or otherwise changing the directory entry used to reach an already-open resource does not make that File begin referring to the newly named or newly created resource. Operations on the File continue to apply to the resource selected by the successful open, subject to that resource’s own backend semantics and to independently authorized changes to that resource’s contents.

Conversely, opening the same Path again is a new resource-acquisition operation. The resulting File is a distinct capability with its own lifecycle and, when applicable, its own logical sequence position. Two File values obtained by separate successful opens are not required to share a cursor, buffering state, lifecycle, shutdown state, or I/O ordering domain merely because their opens used equal Path values.

Two separately opened File capabilities may nevertheless refer to the same underlying resource according to the Filesystem’s namespace semantics, including through aliases such as hard links or other backend-equivalent names. Such aliasing does not merge the Protos File objects or create a new global ordering domain.

When separately opened File capabilities refer to the same underlying resource, Protos preserves each capability’s own logical ordering and lifecycle independently, but does not define a relative ordering, atomicity, visibility latency, or byte-level non-interleaving guarantee between those distinct logical receivers unless a stronger concrete protocol explicitly provides one. An implementation may therefore use independent native operations, shared backend state, locking, or another mechanism, provided that each File’s own observable contract remains satisfied.

In particular, Protos does not make two successful opens of one resource behave like two handles to one Protos logical receiver merely because the host reports a common inode, file identifier, object, or equivalent identity. A write through one File may become observable through another according to the underlying Filesystem’s resource semantics, but the timing and ordering of such cross-capability observation are not elevated into a portable Protos guarantee by File identity alone.

Observable interactions between independently opened capabilities are therefore governed by the resource/backend semantics and the Protos ordering rules for each logical receiver, not by Path equality.

For a readable standard File, EOF is evaluated against the represented file resource at each ordered read evaluation point; observing null does not permanently latch that File at EOF while the resource can still change. If the File’s current logical position is at or beyond the then-current file size, that read returns null and does not advance the logical position. If a later independently authorized change grows or otherwise changes the same represented resource so that readable octets then exist at that unchanged logical position, a later read on this File may return those octets without requiring an intervening seek.

The same rule applies whether the resource growth came through another standard File capability, another authorized Process/host agent, or another backend mechanism whose effects are visible under the Filesystem’s resource semantics. Protos does not add a new ordering or visibility guarantee for when such an independent change becomes observable; it only defines the result once that changed resource state is the state applicable to the later File read.

Conversely, merely observing EOF does not make a pending File read wait for hypothetical future growth. A read evaluated at a state where its logical position is at or beyond current EOF completes with null; following growth is considered only by a later read operation. This keeps regular-file EOF distinct from stream readiness and avoids turning ordinary File reads into implicit tail/follow operations.

This File rule does not alter a TextReader wrapper’s stronger text-stream lifecycle. A TextReader that has itself committed its standardized permanent text EOF remains at text EOF according to section 14/16 even if its separately accessible underlying File later grows; Core v0.1 defines no TextReader reset/reopen operation. Programs that need to observe later file growth use the raw File/byte capability or construct a new text-reading layer under the ordinary authority/lifecycle rules.

A readable File therefore treats EOF as a point-in-sequence-state observation, not as permanent closure of a mutable file resource: later visible growth past the File’s unchanged logical position can make a subsequent read return data.

If the backend cannot maintain a stable binding from the successful open to the selected resource for the lifetime of the returned File, or cannot otherwise emulate the required stable-resource semantics, it must not expose that resource as the standard File capability merely because the host API returned a handle-like value.

This rule does not require exposing a portable numeric inode, file ID, handle, descriptor, or other host identity token. Resource identity is a semantic property of the open File capability and remains distinct from portable Path equality.

Open mode determines the access capabilities that a successful standard File must expose; backend semantics determine which orthogonal optional file capabilities can additionally be exposed honestly.

A successful File open binds the File to the selected resource rather than to a continuing Path lookup; later namespace changes do not retarget it, while separate opens remain separate capabilities even when they select the same underlying resource. Separate File capabilities that alias one underlying resource retain independent Protos ordering/lifecycle domains; cross-capability ordering, visibility latency, and atomicity are not portable guarantees unless a stronger protocol provides them.

A readable File exposes ByteReadable and Closable.

A writable File exposes ByteWritable and Closable.

A read/write File exposes both ByteReadable and ByteWritable, plus Closable.

ByteSeekable is additionally exposed only when the backend can provide or emulate the standard logical-position and failure-atomic seek contract.

ByteSized is additionally exposed only when the backend can provide the standard current-size query semantics for that file resource.

Truncatable is additionally exposed only for a writable File whose backend can provide or emulate the standard failure-atomic truncate contract. Support for truncate-on-open does not by itself imply that the returned File can support later Truncatable.truncate operations.

Syncable is additionally exposed when the backend provides the required durability semantics.

Thus open access mode is a lower bound on the File’s access authority, not a promise that every backend implements every orthogonal random-access/size/truncate/durability protocol. The capability set of the returned File must remain stable for that File’s lifetime; an implementation does not add or remove these Traits merely because a particular invocation would succeed or fail at the current moment.

This rule is the concrete File specialization of the general capability principle in section 13: a concrete object exposes only protocols it can correctly implement. It also means that a backend must not satisfy section 18.4 by exposing a protocol whose normative guarantees it cannot meet and then failing every operation merely because a host API has weaker semantics.

A raw File is not required to expose Flushable.

18.5 Open commitment, cancellation, and resource custody

Section titled “18.5 Open commitment, cancellation, and resource custody”

filesystem.open(path, options) follows the general I/O commitment rule, but its commitment boundary depends on which requested open effects are observable.

An open may resolve to cancelled only while both of the following remain true:

  • no portable filesystem namespace/content effect required by that open has become observable; and
  • no successful File result has committed.

For an open that would only acquire an existing file with preserved content, acquiring an implementation/native handle is not by itself a portable Protos filesystem effect. Cancellation may therefore still win before the File result commits, provided the implementation can dispose of every internally acquired resource without exposing a File.

If create or createNew actually creates a previously absent target, creation is an irreversible open commitment. If truncate changes an existing target to size zero, that content change is an irreversible open commitment. Once either effect can be observed through the Filesystem capability, cancellation cannot make the open Future cancelled as though the target had remained unchanged.

A committed open may subsequently fail before producing a File. Such failure does not perform compensating rollback of portable effects already committed by that open. In particular, the open operation itself does not delete a target that it already created and does not restore content that it already truncated.

This rule constrains only compensating action performed by the failed open. It does not guarantee that a later observer will still find the created target present or the file still empty: independent Actors, external processes, or backend activity may subsequently rename, remove, replace, truncate, or write the target according to their own authority and ordering.

If an open reaches failed or cancelled without returning a File, no undisclosed live File resource is transferred to the program. Any native or backend resource acquired internally remains under implementation custody and must be released before that terminal result. If cancellation cannot satisfy that cleanup obligation, cancellation does not win; the operation continues to a non-cancelled terminal outcome.

When open resolves successfully, custody of the resulting resource is represented by the returned File, whose subsequent lifecycle is governed by Closable.

Host effects not otherwise exposed by the Filesystem capability, such as platform-specific metadata side effects outside the portable model, do not create additional Protos commitment boundaries merely because a particular native open primitive happens to have them.


Path, Filesystem, and File are distinct concepts.

Path is a non-authoritative value describing a path according to filesystem-path semantics.

Possessing a Path grants no filesystem access.

Filesystem is a capability representing authority over a filesystem namespace.

Possessing a File grants access only through the capabilities exposed by that already-open resource.

Knowing a Path does not grant filesystem access. Filesystem authority is carried by a Filesystem capability.

Filesystem is not a required Core-prelude binding and has no standard public constructor: authority cannot be manufactured by ordinary code. A Process host may provision zero or more filesystem capabilities according to policy. The Root bootstrap path is owned by PROCESS_IO.md; imported modules obtain Filesystem authority only when ordinary code explicitly passes a capability. Multiple capabilities may expose different bases, subtrees, virtual namespaces, or restrictions.

Path is a standardized path value rather than merely an alias for String. Core v0.1 defines no special path literal syntax. Path is a standard frozen-prelude factory/prototype binding because it carries no filesystem authority.

The portable construction protocol is Path.relative() for the empty Path and path.child(name) to append one normal component. child(name) requires a String other than "", ".", or ".."; it appends exactly that String as one component. Slash, backslash, colon, drive/UNC syntax, normalization, and host separators inside name acquire no structural meaning. No implicit String-to-Path coercion exists. All these operations are synchronous, immutable value construction and access no Filesystem.

Core defines no rooted Path, no parent (upward) component, and no parentComponent() selector. path.parent() remains the ordinary structural reflection operation defined by semantics/OBJECT_MODEL.md and reports the receiver’s immutable delegation parent; a Path does not specialize or overload it to mean filesystem traversal.

Every Filesystem defines the namespace/root/base used to interpret paths supplied to it.

Protos does not require mutable Process-global current-working-directory state. Path interpretation belongs to the Filesystem capability: every Path is interpreted relative to the interpreting Filesystem’s explicitly provisioned authority/base. Derived Filesystem capabilities may use different bases without mutating other Actors’ path interpretation.

A Filesystem operation must resolve every supplied Path entirely within the authority represented by that Filesystem capability.

Path syntax or backend name-resolution behavior cannot enlarge that authority. In particular, symbolic links, reparse points, aliases, mount/redirection mechanisms, or other backend indirections must not cause an operation to access a resource outside the capability’s authorized namespace.

A relative path is resolved from the Filesystem’s configured base, but successful resolution may move only within that same authorized namespace. If resolving a path would cross the Filesystem authority boundary, the operation fails. Protos does not silently reinterpret an escaping path as ambient host access, does not fall back to a Process-global current directory, and does not obtain broader authority merely because the host API used internally would permit it.

Confinement applies to the complete resolution operation, not only to lexical preprocessing of . or ... An implementation must remain confined in the presence of backend indirections and concurrent namespace changes. If it cannot establish that the requested resolution stays within authority because of a race or backend limitation, the operation fails rather than proceeding with uncertain authority.

This rule does not require host ancestry to define the namespace. A virtual, mounted, remote, or otherwise mediated Filesystem may deliberately include resources that are not descendants of one host directory. The invariant is capability authority: every resource reached by resolution must belong to the namespace that the Filesystem is authorized to expose.

Path normalization, component/equality rules, and host-native path conversion remain separate questions; none of them may weaken this authority boundary.

The portable semantic content of a Path is filesystem-independent. A Path consists of:

  • one ordered sequence of normal path components.

A normal path component contains one valid Protos String name. The empty String, ".", and ".." are not normal component names. There is no rootedness dimension and no parent component: every Path is downward from the interpreting Filesystem’s base. A current-directory component is semantically redundant and is not retained in the portable value.

The empty Path (Path.relative()) denotes the interpreting Filesystem’s configured base.

Path value equality and hash are structural and filesystem-independent: two Paths are equal exactly when they have the same ordered sequence of normal-component String values, and equal Paths have equal hashes. (D169 narrows the structure that participates in equality under D037 to this sequence.) Equality does not access a Filesystem and does not use host case folding, Unicode normalization, drive-letter rules, inode/file identity, symlink resolution, or backend aliases.

Therefore unequal Path values may resolve to the same resource in a particular Filesystem, and an equal Path value may resolve differently at different times if the authorized namespace itself changes. Resource identity and Path value equality are distinct concepts.

A Path is immutable and carries no authority. It may cross Actor boundaries according to the ordinary rules for immutable values without transferring a Filesystem capability.

Beyond Path.relative() and child(name), display syntax, convenience parsers, and native-path conversion APIs remain outside Core v0.1. Any standardized constructor/parser that produces a portable Path must produce the semantic value described above rather than embedding the host platform’s separator, drive, UNC, device-prefix, case-folding, or current-directory rules into Path identity.

When a Filesystem maps a normal component to a concrete backend, that component is one logical child name. A backend that cannot represent that name may reject the operation, but it must not reinterpret one component as multiple components, a root/prefix change, a drive/device selector, or another authority-changing native syntax. Host-native path values that require such semantics belong behind an explicitly host-specific/native boundary.

Whether a concrete Filesystem treats two distinct normal names as referring to the same backend entry is a property of that Filesystem’s namespace semantics. That lookup behavior does not change portable Path equality.

20.3 Atomic namespace-entry replacement and removal

Section titled “20.3 Atomic namespace-entry replacement and removal”

Core v0.1 additionally closes the minimum namespace-mutation surface needed to publish a completely prepared file without exposing partial target content:

filesystem.replace(sourcePath, targetPath) -> Future<Filesystem>
filesystem.remove(path) -> Future<Filesystem>

Both operations are standardized asynchronous Filesystem operations. After successful dispatch, each invocation produces its own fresh standard Future identity under the general Future result-identity rule. On successful completion the Future resolves to the exact Filesystem receiver.

Each supplied path must be a semantic Path. Invalid Path-domain arguments fail the returned Future with a fresh InvalidIOArgument before namespace/backend effects. Target absence, source absence, confinement rejection, unsupported namespace-entry kind, lack of backend support, and other operational failures are ordinary IOError outcomes unless another existing I/O rule requires a narrower category.

The one Filesystem receiver supplies all authority for both paths. replace does not combine authority from two Filesystem capabilities and does not fall back to ambient host paths. Both paths are resolved under section 20.1 confinement. If a backend cannot prove confinement and the required atomic transition for the selected entries, it must fail without committing the operation.

The mutation surface selects final namespace entries directly rather than first classifying a mutable name as an ordinary file. A source selected by replace must be an existing namespace entry; its target may be absent or an existing namespace entry. remove likewise requires an existing namespace entry. The final component of every supplied Path is operated on as that entry itself: symbolic-link, reparse, mount/redirection, directory, regular-file, and other backend entry kinds are not followed or reinterpreted merely to make replace/remove succeed. Intermediate resolution remains governed by the ordinary Filesystem confinement rules.

Portable success is therefore not conditioned on a separate pre-mutation file-kind query. A backend may fail with IOError when it cannot perform the required atomic transition for the selected entry kind or source/target-kind combination, including host restrictions on directory replacement or removal. It must not implement such a restriction as a check-then-act sequence whose checked final entry can be substituted before the namespace mutation. A successful standard operation acts on the entries selected by its atomic namespace transition; otherwise it fails without commitment.

replace(sourcePath, targetPath) performs one indivisible namespace transition. At its commitment point the entry selected by sourcePath ceases to exist under that source name and targetPath names that exact selected source resource. If a target entry existed immediately before commitment, that target entry is replaced in the same transition. An observer resolving the target across the transition therefore sees either the previously selected target resource or the source resource; it never observes an operation-created missing-target window or partially copied target content. If the target was absent, source removal and target appearance are still one transition rather than a remove-then-create sequence.

If source and target resolve to the same existing namespace entry/resource at the operation’s selection point, replace succeeds as a namespace no-op: it does not remove that entry merely because the two supplied Paths are distinct values or backend aliases.

replace is a namespace operation, not a byte-copy protocol. It does not modify, snapshot, flush, close, or synchronize the selected source resource’s contents. Callers that require a prepared immutable-at-publication byte state establish that state through ordinary File sequencing before invoking replace. Independently authorized writes to the selected resource remain governed by their existing File and backend ordering semantics.

remove(path) performs one indivisible namespace transition in which the selected namespace entry ceases to exist. It never recursively removes descendants merely to make an entry removable. It does not close, cancel, retarget, or otherwise revoke already-open File capabilities bound to a resource represented by that entry. The stable selected-resource rule remains in force: a File opened before a successful replace/remove continues to denote the resource it selected even if the namespace entry later names another resource or no longer exists.

For both operations, all validation and tentative backend work before commitment must remain non-observable as a namespace mutation attributable to the operation. If cancellation wins or the operation fails before commitment, the source and target namespace entries remain unchanged by that operation. The atomic namespace transition itself is the irreversible semantic commitment boundary. Once it has committed, cancellation can no longer win and the standard operation resolves successfully; it does not report a failed/cancelled result whose own namespace effect is implementation-selectably old, new, or uncertain. A backend that cannot provide a determinate committed-success versus uncommitted-failure outcome must not expose that attempt as a conforming standard replace/remove operation.

These operations introduce no implicit Filesystem FIFO. Separate opens, replacements, and removals remain independently progressing asynchronous operations unless ordinary Protos sequencing or another normative dependency orders them. When operations are genuinely concurrent, the backend may choose which atomic selection/commitment point occurs first; once chosen, each operation must preserve its own stable resource selection and atomic transition semantics.

Atomic namespace visibility is distinct from crash durability. Successful replace or remove does not imply a namespace-durability barrier, and File.sync() remains scoped to the selected File resource under BYTE_IO.md; it does not make creation, removal, replacement, or parent-directory metadata portable-crash-durable. Core v0.1 therefore does not promise which otherwise valid namespace binding survives a later host/backend crash solely because a replace/remove Future previously resolved. A future namespace-sync capability may add such a guarantee without weakening the live atomic-visibility contract here.

The initial Filesystem namespace-mutation surface remains open, namespace-entry replace, and namespace-entry remove. D046 additionally adds the read-only entries observation operation in section 20.4; D171 removed the former public captureTree operation from the standard protocol. Existence queries, general metadata/stat, mkdir, general rename/move beyond the replacement contract, symlink creation/target inspection, recursive tree removal, and richer namespace mutation remain outside Core v0.1.

20.4 Capability-confined directory observation and host-provisioned captured Filesystems (D046, D171)

Section titled “20.4 Capability-confined directory observation and host-provisioned captured Filesystems (D046, D171)”

Core v0.1 exposes exactly one additional general read-only Filesystem operation for code that must inspect namespace structure without acquiring ambient host filesystem authority:

filesystem.entries(path) -> Future<Array>

This operation does not create a standard Directory or DirectoryEntry prototype. It extends an explicitly provisioned Filesystem capability and therefore cannot be used without that authority. A backend may use an open directory handle, secure relative descriptor, directory stream, or equivalent resource internally, but such machinery does not become a distinct Protos-visible Directory identity, authority or lifetime merely because the backend uses it.

The standard Filesystem protocol has no captureTree operation. D171 removed the public selector that D046 originally defined. Core v0.1 defines no guest operation, under any selector, that captures, snapshots, clones or freezes an arbitrary directory subtree of a Filesystem into a fresh Filesystem capability. Stable captured trees exist in Core v0.1 only as Filesystem capabilities that a host/runtime provisions under section 20.4.3.

20.4.1 Invocation, authority, failure, and cancellation rules

Section titled “20.4.1 Invocation, authority, failure, and cancellation rules”

entries accepts exactly one semantic Path argument.

After ordinary receiver/argument evaluation and successful dispatch, a non-Path argument or otherwise invalid standard invocation fails the returned Future with a fresh InvalidIOArgument under IO_CORE.md. The standard operation does not signal a synchronous host exception for a well-formed Filesystem invocation.

Every path resolution remains completely inside the receiver Filesystem authority under section 20.1. A backend must fail rather than perform uncertain traversal or allow a path, symbolic/reparse indirection, mount, alias, native prefix, or other mechanism to enlarge authority.

The final supplied path selects the directory entry itself; the operation does not follow a final symbolic-link/reparse/link-style indirection in order to turn it into a directory. Intermediate traversal remains subject to the receiver Filesystem’s ordinary confined path-resolution semantics.

If the selected final entry is absent, is not a directory, cannot be observed under the authority, or the backend cannot provide the required standard operation, the Future fails through the ordinary IOError family. The portable surface does not add a distinct standard NotFound/NotDirectory/Unsupported error taxonomy in this revision.

entries does not mutate the source namespace or source file contents and introduces no source-side portable commitment point. Cancellation may therefore win until the operation has reached terminal completion. Tentative host resources from a cancelled/failed operation are not returned and carry no Protos authority after that outcome. Actor/task termination composes with the existing Future/I/O cancellation rules.

Separate invocations are not implicitly ordered merely because they use the same Filesystem or Path.

filesystem.entries(path) observes the direct children of the selected directory without following any child entry in order to classify it.

On success it resolves with one fresh standard Array. Each element is one fresh frozen ordinary object whose immediate delegation parent is the standard root Object and whose local slots are exactly:

name
kind

name contains the exact semantic String spelling of one stored direct-child name. The implementation performs no case folding, normalization, path cleanup, separator translation, percent/URI decoding, or locale transformation before returning it.

A returned name represents exactly one Path component. It is never empty and is never the synthetic names . or ... A backend entry whose name cannot be represented as one semantic Path component must make the operation fail rather than normalize, split, rename, omit, or substitute that entry.

kind contains exactly one of these semantic String values:

"regular"
"directory"
"link"
"other"

The categories mean:

  • "regular" — a non-directory, non-link ordinary file-like resource whose content may be acquired through standard File semantics when that backend and authority support the requested open;
  • "directory" — a namespace container that may itself be selected for entries;
  • "link" — a namespace indirection entry, including a symbolic-link, junction/reparse-style traversal indirection, or equivalent backend entry whose traversal may select a different namespace target;
  • "other" — a namespace entry that is neither a regular resource, directory, nor link indirection, including device/socket/FIFO-like or backend-specific special entries.

Classification is a final-entry no-follow observation. A link whose target is a regular file is "link", not "regular"; a link whose target is a directory is "link", not "directory".

The descriptor is inert data, not authority. Possessing it does not grant access to the named child, reserve that namespace entry, or make a later path.child(entry.name) operation select the same resource. A caller that acts later through the original mutable Filesystem is subject to ordinary concurrent namespace races.

The Array order is deliberately unspecified. Portable programs that need an order establish it themselves from descriptor data. A successful result contains at most one descriptor for each exact direct-child name.

The successful Array is the complete finite direct-child observation produced by that invocation. Core v0.1 deliberately does not expose a standard incremental directory iterator, stream, cursor, callback walker, pagination token, or chunked entries variant. An implementation may enumerate incrementally internally, but entries resolves only with the complete Array result. A future independently designed incremental facility may coexist with this operation without changing its semantics.

On a mutable source Filesystem, entries is an observation operation rather than a directory transaction. An entry added or removed while the operation is in flight may or may not appear. However each returned descriptor’s name and kind belong to one no-follow observation of that named child; the implementation must not combine the name of one selected child with the kind of another. A backend that cannot provide a finite, non-duplicated, authority-confined result under its namespace model fails instead.

Mutation of the returned Array by user code does not alter the Filesystem. Entry descriptors are frozen, so their name and kind bindings cannot be rewritten after success.

20.4.3 Host-provisioned captured Filesystems

Section titled “20.4.3 Host-provisioned captured Filesystems”

A host/runtime may provision a captured Filesystem: a Filesystem capability whose namespace is one finite, immutable logical tree image captured from a selected source directory by host/runtime machinery outside the guest-visible protocol. Package-content custody is one such provisioning path. Provisioning is a host/runtime act, like provisioning any other Filesystem capability under section 20; it is not reachable through a standard guest selector.

A provisioned captured Filesystem is an ordinary standard Filesystem capability. Guest code consumes it only through the retained standard operations. It carries no additional guest-visible selector, and in particular it cannot mint another Filesystem from itself or from any of its subtrees.

The captured tree presents source children as follows:

  • a "regular" child presents its exact child name and one finite captured byte sequence obtained from a stable selection of that regular resource;
  • a "directory" child presents its exact child name and recursively captured descendants;
  • a "link" child presents only an opaque entry with that exact name and kind; capture does not follow its target;
  • an "other" child presents only an opaque entry with that exact name and kind.

Host capture never escapes the selected source directory by following a child link. For each traversed child, selection/classification and any regular-file or directory binding used by the capture must be race-safe enough that a concurrent replacement cannot silently turn a selected regular resource or directory into a followed link/other authority. A host that cannot bind a selected child without such uncertainty, or cannot finish a finite read of a selected regular resource, does not provision the capability. Capture is not a source transaction or global filesystem snapshot; Protos does not promise that all captured source bytes and names coexisted at one source instant.

Once provisioned, the captured Filesystem’s namespace, exact child spellings, entry kinds, directory structure, and regular-file bytes never change for the lifetime of that capability. Subsequent mutation or removal of the source cannot retarget or modify the captured tree, and the capability does not retain authority to reach uncaptured source entries.

A captured Filesystem does not expose a source host path, source directory handle, cache/store locator, URL, backing location, or equivalent capability-bearing implementation detail. Physical backing representation, sharing, deduplication, spill strategy, content-addressing, copy-on-write/versioning and reclamation are implementation choices; they are not Protos identity and confer no ambient authority. The capability is not a standard Closable receiver and transfers no programmer-managed release obligation to guest code; backing lifetime remains host/runtime custody. Files subsequently opened from it keep the ordinary File capability/lifecycle rules, including Closable where the File contract requires it.

A captured Filesystem is permanently read-only:

  • standard read/existing opens of captured regular resources may succeed;
  • a semantically valid open requiring write, create, createNew or truncate authority fails through the ordinary I/O support/resource failure boundary;
  • replace and remove fail and cannot mutate the captured namespace;
  • entries observes the captured immutable namespace.

Opaque captured "link" and "other" entries remain observable through entries. This revision does not require a captured Filesystem to expose a link-target reader or to make open/entries traverse such entries; attempts to use an unsupported opaque entry fail through ordinary I/O failure.

Relative-path interpretation of a captured Filesystem is based at the captured directory root. The zero-component Path.relative() therefore selects that root for entries; its direct children are addressed by appending their exact returned name components through the standard Path protocol. The authority rules of section 20 continue to apply.

Because the captured Filesystem is immutable, repeated successful entries observations and regular-file reads against it observe the same namespace, entry-kind and byte state. Result Array/descriptor object identities from separate calls need not be reused. A host/runtime may provision several Filesystem capabilities, including in different Actor domains, over the same captured tree; each is a distinct capability observing the same immutable content.

entries intentionally does not turn a mutable namespace into a transaction and does not reserve its returned names.

Code that needs a stable tree for hashing, indexing, compilation, packaging, security validation, reproducibility, or another verify-then-use workflow performs all relevant enumeration and reads against a captured Filesystem that its host/runtime provisioned, and then uses that same captured capability after validation.

A program must not compute a validation result by enumerating and reading one mutable Filesystem and later treat the original Paths as though that validation had authorized unchanged resources. A host/runtime that provisions a captured Filesystem for verification likewise uses that same captured content after successful verification rather than reopening the original mutable source.

D046 and D171 define a general Filesystem mechanism. They do not define package hashes, package stores, VCS behavior, registry acquisition, archive formats, or implicit network access. Whether Protos should later offer a guest-visible capture facility is outside Core v0.1 and requires a new design decision.


URL is not a subtype of Path.

A URL contains URL semantics such as scheme, authority, URL path, query, and fragment. A URL path component is not a filesystem Path merely because both use slash-like notation on some platforms.

Core defines no conversion between URLs and Paths. file: URL to Path conversion, including any Filesystem-provided pathFromURL operation, is not part of Core Path semantics and is deferred.

Neither a Path nor a URL grants resource-access authority.

Resource identifiers are values. Resource access is performed through capabilities.

Network-resource access therefore belongs to separately provisioned network/HTTP/etc. capabilities rather than to ambient url.fetch() authority.

Relative URL references follow URL resolution semantics, not Filesystem path semantics.

Core v0.1 requires no URI/URL/URN class hierarchy or special URL literal syntax.