Namespaces

Namespaces

This document describes how objects in an image find and reach each other: Layer 0 of the protocol. Almost everything is done by objects, by convention, in a Plan 9–style namespace; the runtime only decides where each path leads.

The model

Every object has it's own view into the namespace, this view combines paths that the object itself serves and paths served by other objects bound into it's view.

Mounting instance pages at /www in http's view means that when http sends a request to /www/index.html, pages receives it as a request for /index.html in its own tree.

http's view                         pages' view
  /net/tcp  → the listener            /files → files (read-only)
  /www      → pages
  /dev/cons → the console

An image has a view of its own as well. Its startup requests resolve in it, its instances can be given parts of it with bind, and what it exports comes from it (see Building images).

Views are private. The same path can mean different things to different instances. Conventional names (/dev/cons, /net/tcp, /files) keep that manageable.

Names are the capabilities

A path is resolved in the sender's view. A path that isn't in the view is not_found, exactly as if it didn't exist. Paths are plain strings, not unforgeable tokens. Putting a path in a payload doesn't grant anything: the receiver resolves it in its own view. Access is controlled by what each view can see, and the images decide that.

Paths

Paths are absolute and /-separated. The runtime normalizes // and . segments and keeps a trailing /, because /docs and /docs/ can mean different things to a server (HTTP certainly treats them differently). A path containing a .. segment, a NUL byte, or one that doesn't start with / is rejected with invalid before it is resolved. Without that rule, root + clientPath could step from /www into any other part of the sender's view.

A trailing / means nothing in a mount point: /www/ and /www name the same one.

Resolution in a view picks the longest mount point that is a prefix of the path, on segment boundaries. /www matches /www, /www/ and /www/a/b, but not /wwwx. Then it checks the target's exposures (see below) for the longest prefix of the remaining path, and repeats until it reaches a server. A read-only mount or exposure anywhere along the way makes the result read-only.

Building views

An image builds each view from three lines (see Building images):

Rearranging a view

An instance can change its own view, and splice parts of it into the tree it serves, while it runs:

Operation Semantics
bind(src, at, flags) at now means whatever src means in our view at this moment.
unmount(at) at no longer means anything in our view.
expose(at, src, flags) Requests under at in our served tree go straight to src in our view; they never reach our serve.

The only flag so far is READONLY; BEFORE and AFTER, for union mounts, are planned.

Every view is built by an image and none is inherited: there is no equivalent of Plan 9's rfork, since objects don't make other objects.

import { Server, Request, Verb, Flag, ns, run } from "othismo"

class Site extends Server {
  start(): void {
    ns.expose("/static", "/files", Flag.ReadOnly)    // /static/** never reaches serve()
  }

  serve(req: Request): void {
    req.forward("/files" + (req.path.endsWith("/") ? req.path + "index.html" : req.path))
  }
}
run(new Site())

Environment objects

Environment objects are servers implemented by the runtime, and they are the only way to reach the outside world. No image declares one. An image says what it needs with needs, and only the root image, the one being run, has its needs filled by the runtime with the real thing:

needs /net/tcp  Listener      # a TCP listener
needs /dev/cons Console       # stdin and stdout
needs /srv/www  Files         # a directory on the host

The trait a root need names picks the object. Each root need is an object of its own: two needs ... Listener lines are two listeners. A root need that names any other trait, or a shape with no trait's name, is an error when the image is built. The root image can then configure each one, from its own view, and give it to the instances that should have it with bind.

So everything a program can do to the host is listed in one place, the root image's needs, and --check prints it.

An environment object is configured by an operator write of its root, which only an image can send. No object can open another port or move a files root. Changing the configuration once it has started isn't supported yet: they answer later writes of their root with not_supported.

Trait Configured with Serves
Console nothing watch /: a stream of text/plain, one line of stdin each, without its line ending, until EOF. write /: prints the payload on a line of its own: text/* as it is, a stat record as its fields, anything else as its type and length
Listener text/plain: the address, e.g. 127.0.0.1:8080 read /address: the bound address. watch /accept: a stream of text/plain connection names. watch /{conn}/data: the bytes received, as application/octet-stream, until the peer closes. write /{conn}/data: sends the payload's bytes. read /{conn}/remote: the peer's address. remove /{conn}: closes our side, once everything is sent
Files text/plain: the directory, relative to the image read: the file, with a content type from its extension. stat and list: stat records, the listing sorted by name. Read-only: unsafe verbs get read_only

Console reads stdin only if something is watching it by the time the image has started. When there are several Consoles, each line goes to the next one in turn, and every watcher of that Console gets it.

A connection's data can have one watcher; a second gets exists. Data that arrives before anyone watches is kept for the first watcher. Files answers anything outside its root, including through symlinks, with not_found.

In memory

The runtime also provides objects that serve the same traits and touch nothing outside the image. They are ordinary objects, imported from env, and any image can declare them and pass them to what it instantiates:

Object Serves
MemoryConsole Console, and Terminal: write /typed types a line, watch /printed streams whatever is written to it
MemoryListener Listener, and Dialer: create /dial with an application/octet-stream payload connects, the server receives the bytes, and the request's answers are everything the server writes, until it closes the connection
PackedFiles FileTree, from files copied into the image when it was built (see Building images)

This is how an image is tested: the test image is a root with no needs, and gives the image under test in-memory objects where its deployment would give it real ones.

Startup order

  1. Every instance is created, and every view is built from the images' mounts and binds. Module top-level code runs. It may already make requests, which are queued.
  2. Every image's startup requests are delivered, each image's in the order they are written, and an instantiated image's before those of the image that instantiates it, so it is set up by the time its parent configures it through what it exports.
  3. start() runs on every Wasm instance, in start order.
  4. Environment objects start listening (stdin, sockets).

Start order follows the mounts and binds: an instance starts after everything it mounts, so whatever it finds in its view has been configured and started first. Mounting an instance of an image counts as mounting what it exports. Instances that don't depend on each other keep image order. Where instances mount each other, the one that comes first in the image starts last.

The run ends when the queue is empty and no outside event can arrive any more.

What the checker can promise

An instance can only bind what it can see and only expose what it can see. So the set of instances it can ever reach is bounded by the transitive closure of the images' mount and bind edges, which can be computed without running anything. Read-only flags carry through the closure, and an instance of an image reaches only what it exports. othismo --check prints it:

slot listen : text/plain = "127.0.0.1:8080" (default)

from the host:
  /net/tcp    listens on 127.0.0.1:8080
  /dev/cons   reads stdin, writes stdout

web.http    sees /net/tcp → /net/tcp, /dev/cons → /dev/cons, /www → web.pages
            can reach at most: /net/tcp rw, /dev/cons rw, web.pages rw, web.files ro (via web.pages)
web.pages   sees /files → web.files ro
            can reach at most: web.files ro
web.files   serves 12 built-in files, 152.5 KB

The checker also holds the images to their objects' interfaces (see Objects): every mount must serve what its object needs there, and every startup request must be one its object serves.

The shell

othismo shell <image> runs an image and lets whoever runs it look around the running image and make requests of it. Only they can: the shell runs the image in its own process and reaches it over an in-process channel, never a socket, so nothing outside the process can make a request through it.

Every view has an owner, so the shell always looks from one. It starts at the image, which lists the roots: the image's own view, and every instance with a view that nothing mounts. Only a mount makes a parent; a bind or an export passes on what is already held. From a root, the shell steps into the views of what that view holds. The bar at the top shows the route: the shortest chain of mounts from the root entered to where it looks from, however it got there.

 ◆ static.test.oth ›  web  ─/http→  web.http  ─/www→  web.pages
NAME                 OBJECT     KIND    LINK   ACCESS  TYPE         SIZE      VER  INFO
/                    web.pages
└── ▾ files          web.files  memory  mount  ro      PackedFiles                 40 req
    ├── ▸ docs/      web.files  dir            ro
    └──   style.css  web.files  file           ro      text/css  10.0 KB  2f1c09e4
web.pages $ read /files/style.css

A request is written as an image's startup request, verb path [payload], and sent from the view looked from. From an image (the root, or an instance of one) it carries the operator flag, as the image's own requests do. From an object it doesn't, and the runtime holds it to the object's needs, as it would the object's own. The shell completes requests as they are typed: the verbs, the paths in the view, what servers' headers say they serve there, and a message's fields from its format. It says why the runtime would refuse a request before it is sent. Opening a path in the table lists what is served there, which the operator asks of the server directly, whoever the shell looks from. Looking changes nothing, so only safe verbs can be sent that way.

--plain, or a shell without a terminal, reads commands a line at a time. :help lists the shell's own commands.

Not yet