capabilities.md raw


moxie capability scheme (revised)

the pipe primitive

pipe is chan byte with constraints. a single declaration carries direction, protocol, and scope:

var r <-pipe[Msg1 | Msg2]{:4096:"/path/to/prefix"}
var w ->pipe[HttpReq | HttpResp]{::"tcp://*:443"}

three layers compose orthogonally:

layerpositionenforced
direction<-pipe / ->pipe / pipecompile-time
protocol[Type1 | Type2]compile-time via codec
scope{:buf:"prefix"}compile-time for literals, runtime for variables

braces are positional: {:bufferCapacity:"prefix"}. the first colon marks the channel metadata block. missing values: {::"prefix"}, {:4096:}, or {} for neither. the two colons always present even when values are absent -- same delimiter pattern as spawn.

builtins

len(p)     // buffered elements waiting
cap(p)     // total buffer capacity
prefix(p)  // scope string ("" if un-prefixed)

stdio

stdin (<-pipe), stdout (->pipe), stderr (->pipe) are per-actor keywords bound at spawn, immutable for the actor's lifetime. no import. fmt.Println writes to the actor's stdout automatically.

spawn

spawn(worker)                              // inherit all stdio from parent
spawn[:logPipe:](worker)                   // override only stdout
spawn[::errPipe](worker)                   // override only stderr
spawn[inPipe:outPipe:errPipe](worker)      // all three overridden

brackets appear only when overriding. absent means inherit. positional: [stdin:stdout:stderr]. colons always present. parentheses hold the spawned function and any additional typed channels passed to the child.

attenuation

extracting a directional end narrows permission. no wrapper types:

var readOnly <-pipe = cap.readEnd
var httpOnly ->pipe = net.Cap.Ports(80, 443)

a function receiving only the read end cannot write. no annotation, no struct.

the net.Cap type above is a convenience method for generating a path specification, in the example above that would be:

://:[(80)|(43)]

filesystem & network

the filesystem actor creates a ->pipe[FsMsg]{::"/home/app"} and hands it to the caller. open() sends a message through the pipe; the actor resolves the realpath and rejects escapes (.., symlinks) outside the prefix. network works identically: prefix tcp://*:443 constrains host and port, DNS resolved before containment check.

receive-side type matching

the variable's type selects which message to dequeue. mismatch skips:

select {
case req := <-pipe[HttpRequest]:
    // fires only if buffer head is HttpRequest
case resp := <-pipe[HttpResponse]:
    // fires only if buffer head is HttpResponse
}

the codec inspects the buffer head. no enum switch, no type tag on the wire.

Errors

the Error type is implicitly part of every pipe's receive union. no declaration needed -- pipe[Msg1 | Msg2] silently includes Error. the codec produces it when the bytes don't decode: wrong size prefix, truncated message, corrupt type tag, bytes that deserialize into no valid union member.

at the receive side:

select {
case req := <-pipe[HttpRequest]:
    // handle request
case resp := <-pipe[HttpResponse]:
    // handle response
case err := <-pipe[Error]:
    // corrupt bytes at offset err.Offset, reason err.Msg
}

the Error value carries:

resync behavior

the pipe codec is responsible for recovery. for size-prefixed protocols, the codec skips sizePrefix bytes and resumes at the next boundary. for self-synchronizing codecs (protobuf-style varint delimited), it scans forward to the next valid frame. for raw chan byte with no protocol constraint, errors propagate as raw byte dumps with an error marker -- the receiver gets whatever was in the buffer.

the codec emits one Error per corrupt frame, then continues. the pipe stays open. the caller decides whether to close.

send side

no error type on send. the codec only encodes valid union members -- the compiler already rejected invalid types. send-side failure is only about the channel buffer being full or the remote end closed, which the nonblocking send already surfaces:

select {
case pipe <- msg:
    // sent
default:
    // buffer full, remote not consuming
}

revision to the scheme text -- add after "receive-side type matching":

the Error type is implicit in every pipe's receive union. no declaration needed. the codec produces it when bytes don't decode. an Error carries offset and message. the codec skips the corrupt frame and resumes. the pipe stays open. the caller's select chooses whether to log, retry, or close.

backward compatibility

all Go-style code using os.Stdout, fmt.Fprintln, io.Reader, io.Writer compiles unchanged. the interfaces become thin wrappers around native pipe syntax. rewire the internals; the surface stays the same.