The edges: how Cruft meets other code

A map of every edge where a value or a call crosses from one part of Cruft into another: typed to untyped code, engine to host, realm to realm, JavaScript to WebAssembly or native code, a database row into a program. One model describes each crossing (what crosses, the default policy, the failure mode), and the edges fail closed by default.

Most of what a runtime does happens inside one world: the engine runs JavaScript, values are objects, calls are calls. The interesting behavior is at the edges, the places where code or a value crosses from one world into another: typed code calling untyped code, the engine reaching the operating system, one isolate handing a value to another, JavaScript calling WebAssembly or a native addon, a database row entering a program. Every bug that "shouldn't be possible" tends to live at one of these crossings.

Cruft is unusually well placed to make the edges legible, because, unlike a Node stack where V8, libuv, and npm meet across vendor lines, both sides of almost every edge are Cruft's own. The crossings are designed, not accidents of where two libraries happened to meet. This page is the map: the single model that describes every crossing, a matrix of the edges Cruft has, and links to the page that documents each one in depth.

The worlds

Cruft contains six worlds. An edge is any point where a value or a call passes between two of them.

The crossing model

Every edge is described by the same three characteristics. Learn them once and any edge in Cruft becomes readable.

  • What crosses. A crossing carries a value (data), an authority (the power to do something, a capability or a callable), or both. The two are governed separately.
  • The default policy. What happens at the crossing with no annotation: validate the value against a contract, erase the type and pass through, convert the value to the other world's representation, or refuse it.
  • The failure mode. What happens when the crossing cannot be honored: HALT (throw and unwind), coerce (substitute a safe value), throw a host error, or drop (reject at send time). A crossing whose failure mode is "silently produce a wrong value" is the thing Cruft's design exists to remove.

Two principles run through all of them:

  • Directionality: the inside is protected. Every edge has a protected side and an untrusted side. Responsibility is asymmetric: the untrusted side bears no obligation to preserve the invariant, and all of the enforcement lives on the protected side. The sound side validates what enters it; the host governs the authority it hands out; a realm never exposes its mutable heap to another. The crossing exists to protect the inside, so the inside does the checking.
  • Authority crosses only by explicit grant. A value can be copied, converted, or refused, but the power to act, a live function, a capability, a host handle, never crosses a trust or thread boundary by being smuggled inside a payload. It crosses only when it is deliberately handed over.

The edge matrix

Every crossing Cruft has, with its protected side, default policy, and failure mode. Each links to the page that documents it in full.

EdgeProtected sideDefault policyOn failureDepth
.fts ↔ JS / TSthe sound .fts codevalidate every value against its typeHALTCruftScript · Value marshalling
TS → JS (erasure)neither (types are a promise, not a check)erase types, or refuse to emitthrow at emitTypeScript
engine ↔ host (node:*, web, IO)the host's authoritydispatch a call; IO is capability-gatedcapability denial throwsNode layer · Web globals · Capability gateway
compartment ↔ compartment / workereach realm's mutable heapclone the value; keep identity localdrop at sendImogen workers · Compartments
JS ↔ WebAssemblythe engine (memory-safe host)convert numeric value types; share linear memory as bytesthrow a WebAssembly errorWebAssembly
JS ↔ native (N-API)the engine heaphand out opaque handles, never raw pointersthrow / refuseNative addons
sound ↔ data (DB rows)the typed compartmentvalidate every row against a contractHALT / SANITIZE / propagateORM · SQL stack
install ↔ load (packages)the running programone resolved graph; capabilities enforced at loadrefuse to loadModule system · Package manager

Reading the edges by world

The language edge (sound ↔ unsound)

The defining edge of CruftScript. A .fts export called from JavaScript, or an import of JavaScript into a .fts compartment, crosses a boundary wrapper installed at module-link time. The default policy is secure: every value is validated against its declared type at the call, and a violation HALTs rather than letting an unsound value reach typed code. This is also where the runtime does fail-closed value conversion, refusing the value kinds that cannot be made sound (callables, promises, host objects, and more). The conversion rules, in both directions, are their own page: value marshalling. The policy choices at this edge, HALT, propagate-as-unknown, sanitize, skip-return, are the CruftScript boundary system.

TypeScript is a different edge entirely: its types are erased, not enforced. There is no runtime check when an erased value flows; the type was a compile-time promise. Cruft's TS support either erases cleanly or refuses to emit, never half-erases. The boundary is what re-imposes a check when an erased value is later called through a typed .fts export.

The host edge (engine ↔ platform)

The engine owns language semantics and knows nothing about files or sockets. The host owns everything a program can observe about its environment. When code calls fs.readFileSync, the engine dispatches an ordinary function and the part that touches the disk is host territory. What makes this an edge rather than a seam is that all host authority flows through one composition point, so IO is a capability that can be granted, withheld, or recorded. Under --sealed the default flips to deny. See the Node compatibility layer, web platform globals, and the capability gateway.

The isolation edge (realm ↔ realm)

Between two compartments, or between the main thread and an Imogen worker, the invariant is that mutable object identity is local. A value crossing is cloned and rebuilt in the receiver's heap (identity does not survive), or shared as raw bytes through a SharedArrayBuffer, or shared as a deeply frozen value through an immutable-arena handle. A function or live host object cannot cross and is dropped at send time, because authority crosses only by explicit grant on the far side. The worker page documents the three lanes exhaustively.

The foreign-code edge (JS ↔ WASM / native)

WebAssembly exchanges numeric value types directly and shares linear memory as bytes; the engine remains the memory-safe host, and today the WASM engine interprets rather than compiles. Native N-API addons never expose a raw pointer to the engine heap: a JavaScript value handed to native code, or back, travels as an opaque handle (a dense slot index), so a stale or forged handle resolves as empty rather than corrupting memory.

The data edge (program ↔ database)

A database row is untyped data entering a typed program, so the ORM treats it as a boundary crossing: every row is validated against a declared contract as it comes back, and an uncontracted query is refused rather than letting raw rows in. This is the one place the non-HALT continue-modes, SANITIZE and propagate, already ship at full strength, because a database is exactly the kind of untrusted-but-recoverable source they were designed for.

Default behavior at a glance

If you take nothing else from this page: Cruft's edges fail closed by default.

  • A value entering sound code is validated; on mismatch it HALTs.
  • A value crossing to another realm is copied, not aliased; authority does not cross unless granted.
  • IO is denied under --sealed unless declared.
  • A database row without a contract is refused.
  • An erased TypeScript value is not re-checked, which is exactly why the sound boundary exists to re-impose one.

Every one of these is a deliberate default, chosen so that the failure mode of a crossing is a loud, located error rather than a silently wrong value. Where you want a different trade, the escape hatches (weaken, skip return validation, debug, --audit) are explicit and visible at the point of use.