Skip to content

Branded Types & the Wire-Boundary Caveat

Branded types (AbsPath, InnerPath, Pid, FdNumber, ExitCode) are declared in packages/core/src/kernel/types.ts. Each carries a readonly __brand: '…' phantom tag so that plain strings or numbers cannot satisfy the type — callers must pass values minted by the designated constructor helpers (absPath(), innerPath(), etc.).

InnerPath is an in-process brand

This note spells out one property that the rest of this codebase treats as implicit:

  • (a) InnerPath is an in-process brand. It has no wire representation, no 9P encoding, no on-disk form. It exists exclusively to witness — at the TypeScript level — that a path has been resolved through Namespace.resolve() and is ready to be handed to a Fileserver method.
  • (b) The AbsPath → InnerPath mint happens exclusively in Namespace.resolve() today. No other code mints an InnerPath, and no test or production site should reach for innerPath(someString) as a shortcut. Tests that need a fileserver-relative path go through the namespace (or, when that is genuinely not an option, use innerPath() with an inline justifying comment — this is the rare escape hatch).
  • (c) A future 9P-over-wire mount MUST mint InnerPath on Twalk success at the wire adapter — not by casting strings at the Fileserver boundary. Concretely: the adapter receives the peer’s Twalk response, walks the qid/name list into a local string, and at that boundary — where the wire has already proven the path is valid — mints the InnerPath. Casting at the Fileserver boundary in the adapter would silently launder unverified strings into branded ones, defeating the whole point of the brand.

Why this note exists

The brand is the type-system’s witness of namespace resolution. Every production caller is protected by it. Tests and spike code have repeatedly wanted to skip resolution (it’s verbose, and branded-path APIs feel ceremonial in a test). The correct response has always been to make minting cheap — not to relax the brand.

When we eventually add a 9P mount adapter, the temptation will be to accept strings from the wire and as InnerPath them at the Fileserver method call. That would be wrong. The point of the brand is to remember where validation happened; a wire adapter that received a valid walk result is a valid minting site. A blanket as InnerPath at the method call is not.

Enforcement in test code

In test code, only absPath() / innerPath() are permitted to introduce brand witnesses — enforced by lint on test/** and ports/*/test/** (T3). The companion rule to the wire-boundary note above: tests may not invent branded paths by casting through any, and the mint helpers are the single sanctioned entry point for the rare case where a test genuinely needs a literal.

See Also

  • packages/core/src/kernel/types.ts — brand declarations and constructors
  • docs/architecture/fileservers/namespaces.md — how Namespace.resolve() produces InnerPath values today
  • docs/product/tickets/debt-as-any-audit.md — the umbrella that motivated this note (track T8 “prevention”)