# Authority: who may name what (design, 2026-10-14)

A design, not yet built. It settles how sysk decides what a task, a shell or a job may reach:
the kernel's tree `/sys`, the network's `/net`, the disk's files. It came out of the networking
discussion (`docs/directions.md`, "Decided since"), when the shell was about to be reachable
from the network, and after two passes over the options and their prior art.

## The question underneath

A command typed at the keyboard is the person's. A command that arrives over the network, or that
a program writes into the shell (the shell reads s-expressions precisely so programs can drive
it), is not. Today the shell holds `(sys)` from boot, so anything that can send it a command can
read the kernel. Every design below is judged by one test: **does it still hold when the person
is not the one typing?**

## The design

```
                       manifest: profiles, needs, never-rules  (checked by tools/wire.rkt)
                                         |
        console --- terminal --- console shell  [profile: observer]  -- its tree:  /  /sys (read)
       (trusted                         |
         path)                          | names a path in a command
                                        v
                                  job  [gets exactly what was named, within its declared needs;
                                        the kernel's job check refuses anything more]

        network --- net --- network shell   [profile: guest]       -- its tree:  /guest  (nothing else)

        operator (control of tasks): assumed at the console, through the trusted path, for a session
```

Five parts, each the smallest that does its job:

1. **Profiles: named bundles of lends, in the manifest.** A profile says what a shell starts
   holding: `observer` is `/sys` to read and the disk's root; `guest` is one directory of the
   disk; `operator` adds control of `/sys`. *From Solaris' rights profiles.* Not roles with
   identities: a profile is a list of handles, nothing more.

   **Built** (sysk 062), for the console shell. A `(profile NAME ...)` clause holds everything a
   shell holds but its own plumbing (its heap, its commands, its answers): grants written as a
   task's arguments, `(jobs out) (catalogue) (clock)`, and its trees, each mounted by its server,
   `("/" fsd)`, `("/sys" kernel)`, as `/sys/task/shell/ns` shows them. A server says it serves a
   tree, `(tree (bytes 576))`, and `tools/wire.rkt` makes the two streams between each holder and
   each server it mounts, so a profile names servers, not channels, and means the same whoever
   holds it. The shell's line is `(task shell (heap 8192) (rx commands) (tx answers) (profile
   observer))`. A profile is a subject of the rules: it reaches what its holders reach, and what
   the jobs they start reach, and a job reaches a service only when its launcher can send to the
   service's server, since it is lent only what its launcher holds. A server takes the requests of
   every holder of its tree, up to four, in turn (sysk 063), so a second shell can mount fsd beside
   the console's; `guest` comes with its holder, step 4.

2. **Authority follows the input channel.** There is a shell per input. The console's shell
   starts with `observer`; the network's with `guest`. What a command can reach depends on where it
   was typed, structurally: the network shell does not hold `/sys`, so no command it is sent can
   name it, and there is no tag on a command to forge. *From Unix's `/etc/securetty` (root only on
   the physical console) and Solaris' profile shells.*

3. **Naming a path lends it.** `wc /notes/shopping` lends that one file to `wc`, read-only, until
   the job ends. This is already how jobs get files (sysk 031); `/sys` joins it unchanged, so
   `cat /sys/task` at the console just works. *From the powerbox pattern (CapDesk, Polaris, the
   macOS sandbox's file picker): designation is authorization.* Sound because of part 2: only the
   console's shell can designate `/sys`.

4. **Programs declare what they need.** A program's contract in the catalogue gains a clause:
   what it reads or controls, by argument (`cat` reads its path argument) or by fixed path (`top`
   reads `/sys/task`). The shell lends only what is declared, and the kernel's job check refuses
   a lend beyond it, as it refuses a parameter out of range today. Even a confused shell cannot
   over-lend. *From macOS entitlements, OpenBSD's `pledge` and `unveil`, Fuchsia's component
   manifests.*

   **Built** (sysk 059), in three holders, each enforcing what it can see. A program's need is in
   the catalogue, `(wc (needs (read file)))`, `(ping (times 3) (needs (talk icmp host)))`, and each
   service says the words of its rights, `(rights read write icmp)`. The **kernel** enforces the
   kind: a job may bind a program only to a service whose rights have its need's words, so a program
   that needs nothing binds none. It stamps the lease with the need in those words, a byte from a
   generated table it never reads, as seL4 mints a badge. The **server** enforces the rights: it caps
   what was lent by the stamp, so a lend of every right to `wc` is a lend to read, and `netd` lets a
   job write only a conversation's `data`, and only with `write` and `icmp` in its stamp. The
   **shell** only designates, which file or which peer: the worst a confused shell does is lend the
   wrong one, within the need. The kernel check alone, of the binding, would leave the rights the
   shell's word; a service per right would multiply services as servers times rights.

5. **`never` rules on authority, checked before anything runs.** A few lines in the manifest:

   ```
   (never (reaches guest (sys)))          ; nothing the network shell starts ever holds /sys
   ```

   Handles move only by lending, down a tree, with rights that only narrow, so who can ever hold
   what is decidable (the take-grant model, 1977). `tools/wire.rkt` decides it over the manifest,
   and the job check over each job, and refuses with the path that breaks the rule. *From
   SELinux's `neverallow`, checked here against the real wiring rather than a description of it,
   and from seL4's capDL: the manifest is already a complete, checked statement of every handle.*

   **Built** (sysk 061), for tasks and jobs, ahead of profiles. A `(rules ...)` clause in the
   manifest holds `(never (reaches S O))`, `(must (reaches S O))` and `(only (S ...) O)`; a subject
   is a task, a program or `job`, an object written as the manifest writes what a task is handed,
   `(tx disk-ask)`, `(device)`, or a service's right, `(files mutate)`. `wire.rkt` decides them
   over the values it writes the kernel's tables from (a job's stamps are `need-stamp`'s), so a
   rule and its table cannot disagree; since nothing moves a handle at run time but a lend within
   a need, no check at run time is needed. A broken rule is refused with its chain, each step with
   its manifest line; a rule in error refuses the manifest; rules only deny, and the wiring is the
   same without them. `must` keeps a manifest from passing by wiring nothing (Tailscale's policy
   tests). The first draft wrote these rules with paths, `(never (reaches guest /sys))`, to be resolved
   through the profile's namespace (sysk 062 dropped that): a path names different trees in
   different namespaces, and in a guest profile that mounts only the disk, `/sys` is a file on the
   disk. Objects stay what a holder is handed, `(sys)`; a path object comes only for what a handle
   cannot name, a subtree of one server such as guest's, which a mount names since sysk 064,
   `("/" fsd "/pub")`, and a rule will with step 4. Control of `/sys`
   will be named the same way, as a right, when there is a `ctl` file. The prior art
   behind these choices is in [prior-art.md](prior-art.md).

And one later part:

6. **`operator`, the one elevation.** Control (a `ctl` write that stops a task or restarts a service,
   [services.md](services.md)) is never held by
   default. At the console, `!sh /sys ctl` asks the terminal, which shows the request on a line
   only it can write and takes a key no program can type into it; granted, a nested shell holds
   control until `exit`, with a prompt that says so. Built when there is a `ctl` file to control.
   *From Solaris (root as a role you assume, never log in as) and Windows' secure attention
   sequence.*

## The six rules for every tree (from directions.md), and where each is enforced

| Rule | Enforced by |
| --- | --- |
| No ambient root | profiles (1): a shell holds only its profile |
| Walks only go down | every server (holds today) |
| Rights only narrow | lending; declared needs (4, sysk 059): the kernel binds by the need, the server caps by its stamp |
| Every server proven total | sysp, per server (holds for `/sys`) |
| Reading and controlling are separate | separate rights; control only through `operator` (6) |
| Narrow views by default | a job gets what it named; `/sys` shows no addresses |

## The second pass: what changed, what was dropped

**Changed: `never` rules are about authority, not information.** The first draft said
`(never (flow /sys -> /net))`. Over the wiring that cannot be checked soundly: data travels through
servers. The console's shell may `cat /sys/task > /notes/t`, and if the network shell could read
`/notes`, kernel state would reach the network with no channel between them. Checking information
flow properly needs labels on every file that follow the data (multi-level security), which is a
large mechanism for a small system. Instead:

- `never` rules say who can ever **hold** what. That is exact and decidable.
- Information is separated by **partitioning storage**: the network shell's profile holds
  `/guest` and nothing else on the disk, so what the console writes elsewhere is out of its reach.
- A crossing is a person's deliberate act at the console (copying a file into `/guest`), which is
  the person's to make.

Timing and other covert channels are out of scope, as they are for every system here but seL4's
proofs of noninterference.

**Checked: the trusted path cannot be imitated.** A job's output reaches the screen through the
shell and the terminal. The terminal marks its own prompts in a way job output cannot produce (it
escapes the marker in anything it did not write), and a key press only answers a request the
person made with `!`. A program can draw a fake prompt; it cannot make a real one, or answer one.

**Kept apart: the network shell needs authentication.** With part 2, a stranger reaching the
network shell gets only `/guest` and the pool's jobs. That is contained, not acceptable: anyone
who can send a packet can use it. QEMU's user networking reaches it only from the host, which is
enough for the first demo; keys come before it is reachable from anywhere else.

**Dropped:**

| Option | Why not |
| --- | --- |
| The shell holds `/sys` always (today) | ambient: anything that can drive the shell reads the kernel |
| A prompt for every use (`sudo` as the rule) | prompt fatigue; UAC's lesson. Kept only for control (6) |
| `su`, a mode holding everything | replaced by narrow, visible, session-long `operator` |
| Several roots (`A:`, `sys:`) | syntax over the same table of handles; one tree keeps tools uniform and stand-ins possible (`directions.md`) |
| Visible but locked (`permission denied`) | advertises, and invites identity thinking; absent is the honest answer |
| Labels on every object (SELinux, MLS) | policy grows without bound, drifts on copy; partitioning gets the separation that matters here |
| Learning mode, an off switch | `audit2allow` and `setenforce 0`: policy written by accepting denials. A refusal names its rule instead |
| Users and passwords | one person at one console; the question is presence, which the trusted path answers |

## `/net` (2026-10-14, after sysk 048)

The network's tree is served by the net task, over sysn's stack (`directions.md`). What a holder
of it may do comes in four levels, each harder to get than the last:

| Level | What | Who gets it |
| --- | --- | --- |
| **Read** | `/net/eth0/status`, `addr`, `arp`, `stats`; a conversation's `status` | the console shell's `observer` profile; never `guest` |
| **Talk** | one conversation, to one peer | a job, only the conversation its command named |
| **Listen** | announcing a port, taking connections | services the manifest declares (`svcd`), never a job; and the console's shell, at a person's command, answering one call for a job, which holds only the call's data (sysn 013, `tell *!7777`) |
| **Control** | `/net/eth0/ctl` (address, up or down), routes, flushing ARP | `operator`, through the trusted path |

**Talking is the dangerous one.** A job that can open conversations can send what it reads
anywhere. So no job holds `/net/udp/clone`. The shell opens and connects for it, as Plan 9's
`dial` does, and lends it **only the conversation's `data`**: `(ping 10.0.2.2)` gets a pipe to
10.0.2.2 and nothing else, and cannot redirect it, since connecting is a write to `ctl`, which it
does not hold. Naming the peer is granting it, as naming a path is (part 3); the net server needs
per-fid rights and lending, as `fsd` has (sysk 031).

**Declared needs** (part 4) gain `(needs (talk PROTO ARG))`: the program may hold a conversation of
PROTO to the peer its argument names (sysk 059). The job check binds it only to a service that can
stamp the need, and the server caps the lend by the stamp, so a program that declares nothing has
no network, whatever the shell does.

**`never` rules** (part 5) close what must stay closed, before anything runs:

```
(never (reaches guest (talk)))      ; the network shell cannot use the network to go further
(never (reaches job (talk listen))) ; no job opens a port, when listen is a word of talk's rights
(never (reaches job (talk raw)))    ; frames and raw IP, the same way
```

**Quotas.** A client that clones until the conversation table is full denies everyone else. The net
server counts conversations per holder (the console shell, each job's lease) against limits the
manifest sets, and answers `busy` past them.

**Raw access is capture, not introspection.** Reading frames is sniffing; writing them is
spoofing. If it is ever wanted, it is an `operator` stream with a loss marker, recorded in
`/sys/audit`. A conversation's `last` (sysn 015), the start of the last frame that came for it, is
not capture: it holds only what the conversation already receives, so it needs no more than
holding the conversation, and a new conversation starts without the slot's last one's.

**Order.** The first milestone, `ping` from the console, needs Read and Talk. Steps 1 to 3 above
(profiles, declared needs, `never` rules) still come before the network shell, when commands
first arrive from outside.

## Prior art, in one place

What is taken so far; the wider survey it came from, and what is not taken yet, is
[prior-art.md](prior-art.md).

| From | Taken |
| --- | --- |
| Plan 9 | per-task namespaces; `/net`; no superuser |
| Solaris | rights profiles; root as an assumed role; `/proc/<pid>/ctl`; zones' narrowed `/proc`; the trusted stripe |
| Unix `/etc/securetty` | authority by where you are typing |
| CapDesk, Polaris, macOS sandbox | the powerbox: naming is granting |
| macOS, OpenBSD, Fuchsia | programs declare their needs |
| Singularity | a need is a channel requirement, in the program's manifest, checked with its channels (sysk 059) |
| Hydra | rights whose meaning each type manager defines; the kernel carries them unread (sysk 059) |
| seL4 badges, Genode labels | the server learns, from the kernel or the parent, what its client may do: the stamp (sysk 059) |
| Deno, and Android's `INTERNET` | a need names its argument, never "the network" |
| SELinux | negative rules checked ahead of time; servers enforce for their own objects |
| seL4 capDL, take-grant | the whole system's handles in one checked statement; reachability decidable |
| KeyKOS, EROS | confinement: a job is a closed world (sysk 017) |
| Windows | what to avoid: links in a privileged namespace, prompt fatigue; and the secure attention sequence |
| Linux `/proc` | what to avoid: introspection grown without limit, leaking addresses |

## Order of work

| Step | What | Needs |
| --- | --- | --- |
| 1 | Profiles in the manifest; the shell starts with `observer` instead of a bare `(sys)` (done, sysk 062) | wire.rkt, the manifest |
| 2 | Declared needs in program contracts; the job check and the servers enforce them (done, sysk 059) | the catalogue, `job-check` |
| 3 | `never` rules over the manifest (done, sysk 061; over profiles, sysk 062) | wire.rkt |
| 4 | The network shell, profile `guest`, `/guest` on the disk: servers that take several clients (done, sysk 063), a mount of a subtree (done, sysk 064), the shell on a connection | sysn, the net task |
| 5 | `operator` through the trusted path | a `ctl` file worth controlling |
| 6 | `/sys/audit`: grants, elevations, refusals | step 5 |

Steps 1 to 3 change nothing a person sees at the console today and close the hole before step 4
opens the network.
