# ADR 0088: Managed files bind one application-owned root

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0088-managed-files-bind-one-root.md

---

# 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.
