ADR 0088 — Managed files bind one application-owned root
Context
View, download and upload had different containment, size and write semantics. Passing absolute host paths through tools also leaked deployment layout and made security fixes application-specific.
Decision
createManagedFileBoundary asynchronously resolves one existing directory and
returns the only read/write capability. Public paths are canonical POSIX-style
relative paths validated by ManagedFilePathSchema; results are neutral
ManagedFileRefSchema values. The physical root never enters transport output.
Read resolves and contains pre-existing symlinks, opens with O_NOFOLLOW, checks
the opened handle is a regular file and enforces its cap while reading. Write
uses an exclusive same-directory .stitchkit-<uuid>.tmp file with mode 0600,
measures a byte/abort budget while streaming, optionally inspects bounded leading
bytes, then commits. Default replace:false uses hard-link creation so an
existing target is never replaced; replace:true uses same-filesystem rename.
durable:true syncs file contents before the atomic visibility cutover.
O_NOFOLLOW is applied where the host exposes it. Windows is not currently a
supported security target for this boundary: its filesystem constants and
reserved/trailing-name rules differ, and ManagedFilePathSchema deliberately
does not claim to reject names such as CON, NUL or a trailing dot/space.
Containment still uses canonical realpath plus root membership, but Windows
support requires a separate threat-model decision and conformance lane.
Portable Node/Bun APIs cannot make every path component descriptor-relative.
The supported threat model therefore covers untrusted input and pre-existing
symlinks under a root exclusively controlled by the application or trusted OS
actor. A hostile actor concurrently replacing directories is outside this
portable guarantee and would require a platform-specific openat-class mode.
Caught failures attempt temp cleanup; recognizable debris may remain after a
process crash or cleanup failure and is observable through onCleanupError.
Managed view/download/upload use this boundary. serveFile, multipart storage,
retention and provider/domain artifact state retain their existing owners.
Consequences
- Download writes stream to disk instead of buffering its full limit and returns a relative ref. Upload callbacks receive bounded bytes/ref, never an arbitrary reopenable path.
- Inspector output cannot replace measured size or canonical path.
- Atomic visibility and crash durability are distinct, explicit guarantees.
- The filesystem capability lives in peer-free
stitchkit/files; its schemas also live in the contract-safe entrypoint.