Capabilities and frames
The seam between Faber and every way an operating system can implement I/O.
call is Faber's low-level capability call primitive — the boundary
between Faber code and the outside world. It opens a typed conversation
(channel) with a host resource identified by a route string, then
exchanges structured frames (frame) over directional half-streams.
The host kernel dispatches each route to a pluggable provider crate, which
implements the actual I/O — filesystem, networking, console, time, randomness,
or anything else the OS can do.
The call primitive#
call is a keyword, not a function. It opens an opaque conversation
with a route named by an ascii literal and optional opener data:
# Simple materialized call: open, send opener, drain response
const string content ← call 'solum:lege' ("config.toml") ↦ string
# Typed conversation handle for streaming interaction
const channel s ← call 'processus:curre' ("ls", ["-la"])The route string follows a prefix:verb pattern. The host kernel
matches on the prefix only — the provider owns all verbs under that
prefix:
solum:lege ─┐
solum:modum ─┼─► prefix "solum" ──► only provider crate
solum:vincula─┘call is not a foreign function interface. It does not call C
functions, load dynamic libraries, or embed inline assembly. It is a
structured message passing boundary: Faber sends typed frames and receives
typed frames, without knowing whether the provider is implemented in Rust,
runs in-process, delegates to a system call, or forwards to a remote host.
Frame types#
Five compiler-owned types form the frame system:
| Type | Role | Key surface |
|---|---|---|
channel | Conversation handle — an in-flight bidirectional exchange | Created by call; drained via ↦ T or split into views |
frame<T> | Frame envelope — one structured message in a conversation | Fields: id, call, status, data, created_ms, from, trace |
status | Lifecycle marker enum | request, item, byte, bulk, done, error, cancel |
send<T> | Outbound half-stream — send frames to the provider | da(T), fini() → status |
recv<T> | Inbound half-stream — receive frames from the provider | accipe(), cursor(), exhauri(), fini() |
Using directional views#
# Open a conversation, get directional views
const channel s ← call 'solum:scribe' ("output.txt")
const send<string> out ← s.meus<string>()
const recv<string> input ← s.tuus<string>()
# Send content frames
out.da("line one")
out.da("line two")
out.fini()
# Read response frames
for from input.cursor() const frame {
print frame.data
}
const status inbound ← input.fini()Simple materialization#
For the common case — open, send opener, drain all response frames into one
value — channel ↦ T collapses the conversation:
# Read a file: open + drain into string
const string body ← call 'solum:lege' ("config.toml") ↦ string
# Parse JSON from an HTTP response
const json data ← call 'http:peti' ("https://api.example.com/data") ↦ jsonMaterialization uses a type-directed collector: ↦ string
concatenates all inbound frames, ↦ json parses the concatenated
payload, ↦ list<T> collects frames into a list.
Host providers#
Effect families are implemented as separate provider crates under
faberlang/hosts/crates. Each provider owns all verbs under
its prefix:
| Provider | Prefix | I/O domain |
|---|---|---|
only | only:* | Filesystem: read, write, metadata, directory operations |
processus | processus:* | Process execution: spawn, pipe, exit codes |
consolum | consolum:* | Console I/O: stdin, stdout, stderr |
tempus | tempus:* | Time: now, sleep, timers |
aleator | aleator:* | Randomness: entropy, distributions |
http | http:* | HTTP client (Tier D, when landed) |
Providers are separate crates with their own dependencies — only
does not pull in HTTP, http does not pull in filesystem code.
Each provider exports a register() function that the generated
host manifest calls at startup.
Layer stack#
Faber source: call 'solum:lege' (path) ↦ string
Compiler: channel open + generic attach (no provider crate names)
Runtime: HostDispatch + conversation protocol (faber-runtime)
Kernel: route(frame) → provider for prefix
Provider: only provider reads file, returns contentThe compiler emits generic dispatch — it never embeds provider crate names
into generated code. The runtime provides HostDispatch and the
conversation protocol. The kernel (from hosts/crates/host-kernel) routes
frames to the correct provider based on prefix. The provider (from
hosts/crates/* providers) performs the actual I/O.
This means generated Faber code is provider-neutral. The same compiled binary can be linked against different provider implementations — a real filesystem provider for production, a mock provider for testing — by changing the compile manifest.
Compile manifest#
Which providers to link is controlled by the generated compile manifest and
the faber.toml [dispatch] table:
[target.rust]
host = "native"
[dispatch]
providers = ["solum", "processus", "consolum", "tempus", "aleator"]
[dispatch.providers.http]
enabled = trueDuring authoring, missing providers produce a runtime E_NO_ROUTE
error. In strict mode (future), every call prefix in the program
must appear in the compile manifest, and the compiler validates that the
provider's capability manifest covers the routes used.
Architecture#
The host platform is split across three repositories in the
faberlang organisation:
| Repository | Role |
|---|---|
hosts/crates/host-kernel | Thin router — owns Frame, Conversation, terminal lifecycle, prefix dispatch, structured errors (E_NO_ROUTE), capability manifest aggregation |
hosts/crates/host-native | Native attach — workers, register_providers startup hook, generated host_register.rs integration |
hosts/crates/* providers | Provider implementations — Cargo workspace with per-family crates (only, processus, etc.) |
Each provider crate owns its own native dependencies. The http
provider pulls in hyper and tokio only when HTTP
is enabled. The only provider uses standard file APIs with no
additional network dependencies.
Same route, any host. Because
calldispatches on route strings and providers are pluggable, the same Faber source can target a native binary (host-native-rs), a WASM runtime (host-kernel as a Frame/Wasm adapter), or a TypeScript Node.js process (host-providers-ts) without changing a single line of Faber code.
Norma wrappers#
Most Faber code does not call call directly. The Norma standard
library wraps common call routes in typed functions:
# Norma wraps call in typed, reviewed functions
fn read(string via) → string {
return call 'solum:lege' (via) ↦ string
}
fn write(string via, string content) → void {
const void _ ← call 'solum:scribe' (via, content) ↦ void
}
fn curre(string command, list<string> args) → string {
return call 'processus:curre' (command, args) ↦ string
}These wrapper functions provide type safety, documentation, and error
handling without hiding the fact that I/O crosses the call
boundary. The Norma wrappers are open source and live under
norma/src/.
References#
radix/docs/design/frame-stream-types.md— full spec for channel, frame, status, send, recvradix/docs/design/host-provider-gateway.md— thin router architecture, provider contracts, compile manifestfaberlang/hosts/crates/host-kernel/— kernel router implementationfaberlang/hosts/crates/host-native/— native attach and registrationfaberlang/hosts/crates/— provider crates (only, processus, consolum, tempus, aleator, http)radix/corpus/ad/— channel exempla files