feat(users): per-user sealed folders users/<id>/{home,apps,data,permissions}; storage SET_USER/LOCK/PURGE; init sign-in/sign-out loop (purge fresh/removed/guest, seed from factory, session start/stop); shell logout; pm user+system app shelves; terminal SIGNED_OUT; per-program stack (gatekeeper 12p); 128K load scratch

This commit is contained in:
2026-10-28 01:08:50 +01:00
parent 29a25bd8c2
commit 91f7d28834
38 changed files with 1156 additions and 378 deletions

View File

@@ -1,12 +1,12 @@
# Apps, manifests and the permission manager
How a program gets its authority. The kernel only knows capabilities; this
is the userspace layer that decides who holds which, on the owner's behalf
is the userspace layer that decides who holds which, on the user's behalf
(see the vision's Capability Management and App Manifest notes).
## Apps
Every program the owner runs is an **app**: a manifest plus a binary.
Every program a user runs is an **app**: a manifest plus a binary.
An app is identified by its **id** (`kos.snake`), never by a file name or a
process id. The id is what decisions, the app's data folder and (later) its
encryption keys hang off.
@@ -18,12 +18,12 @@ starts them from its own table, with exactly the capabilities listed there.
An app's capabilities have exactly two sources:
1. **Its manifest and the owner's consent.** Services the app declared
1. **Its manifest and the user's consent.** Services the app declared
(`display`, `storage`, …), granted only by the permission manager, only
as far as the owner approved.
as far as the user approved.
2. **Its launcher.** Whoever starts an app may pass along what it already
holds: the terminal as its output, a read-only grant to the current
folder, a file the owner picked. A launcher can only give what it has.
folder, a file the user picked. A launcher can only give what it has.
`cat` declares nothing and gets only what the shell passes it. Snake
declares `display`. Nothing gets anything "just in case".
@@ -31,22 +31,27 @@ declares `display`. Nothing gets anything "just in case".
## Disk layout
```
/home/ the owner's files; the shell's `/` (encrypted)
/apps/<id>/ an installed app: manifest.toml and its binary
/data/<id>/ the app's private folder (its `storage`) (encrypted, its own key)
/system/permissions/ the owner's decisions, one file per app id (encrypted)
/bin/ system servers and the shell (init starts these)
/factory/ plaintext starter content the encrypted areas are
seeded from with a new master key
/apps/<id>/ a system app: manifest.toml and its binary, shared by every user
/users/<user>/ one user's folder (encrypted, under their own key)
home/ their files; the shell's `/`
apps/<id>/ an app they installed
data/<id>/ an app's private folder (its `storage`, its own key)
permissions/<id> their decisions, one file per app id
/bin/ system servers and the shell (init starts these)
/factory/ plaintext starter content a user's folder is
seeded from (home/, permissions/, data/)
```
Only the permission manager reaches `/apps`, `/data` and `/system`. The
shell's storage grant is `/home`, so neither it nor anything it runs can
read another app's data or rewrite a decision.
Users each decide for themselves, even about the system's apps, and what
one installs the others do not see. An app id a system app has cannot be
installed. Only the signed-in user's folder opens. Only the permission
manager reaches their `apps/`, `data/` and `permissions/` (and `/apps`,
read-only). The shell's storage grant is their `home/`, so neither it nor
anything it runs can read an app's data or rewrite a decision.
## Manifest
`/apps/<id>/manifest.toml`, a strict subset of TOML:
`apps/<id>/manifest.toml`, a strict subset of TOML:
```toml
[app]
@@ -60,7 +65,7 @@ icon = "icon.png" # optional: a file in the app's folder
[capabilities]
required = ["display"] # cannot work without; denied = not installed
optional = [] # works without; the owner decides each
optional = [] # works without; the user decides each
[appearance]
accent = "#e91e63" # optional: checked for format here, contrast by K-UI
@@ -79,29 +84,29 @@ optional one is simply not granted.
| Capability | What the app gets |
|---|---|
| `display` | the display server: the screen, and the keyboard while in focus |
| `storage` | `/data/<id>/`, read-write (its own folder only) |
| `storage` | the user's `data/<id>/`, read-write (its own folder only) |
## Decisions
`/system/permissions/<id>`, the same TOML subset:
The user's `permissions/<id>`, the same TOML subset:
```toml
[decision]
granted = ["display"]
```
Written by the permission manager when the owner decides; apps on the
Written by the permission manager when the user decides; apps on the
system image ship with their decisions already recorded under
`factory/system/` (pre-approved by whoever built the image), and init
seeds them into the encrypted `system/` on a fresh setup. Changing one later is the owner's call, through
the permission manager.
`factory/permissions/` (pre-approved by whoever built the image), and init
seeds them into each new user's `permissions/`. Changing one later is that
user's call, through the permission manager.
## Permission manager
A system server with no hardware. Its logic (checking manifests, deciding
what an app gets, keeping decisions) is pure and generic over two ports:
- `Consent`: asks the owner. Today: a yes/no prompt on the terminal.
- `Consent`: asks the user. Today: a yes/no prompt on the terminal.
- `Store`: reads manifests and decisions, writes decisions, installs files.
Today: the storage server.
@@ -113,9 +118,9 @@ From a folder the launcher passes as a read-only grant:
1. Read and check its `manifest.toml`; refuse if the id is installed.
2. Required capabilities this device lacks: refuse.
3. Ask the owner about the required ones together (no = not installed),
3. Ask the user about the required ones together (no = not installed),
then each optional one.
4. Copy the binary and manifest to `/apps/<id>/`, create `/data/<id>/` if
4. Copy the binary and manifest to the user's `apps/<id>/`, create `data/<id>/` if
`storage` was granted, record the decision.
### Launch
@@ -136,7 +141,7 @@ permission manager:
### Revoke
The owner withdraws a capability: the decision changes and the next launch
The user withdraws a capability: the decision changes and the next launch
gets less. A running app that held it is **stopped**: all its access dies
at once. (Endpoint capabilities like `display` are copies the permission
manager cannot take back from one app alone yet; once servers issue

View File

@@ -91,22 +91,37 @@ and budget listed there. Storage, started before the log server, receives
its log endpoint from init later (`SET_LOG`, accepted only from the
unbadged holder of its endpoint).
Nothing that touches user data starts before the owner is authenticated.
Nothing that touches a user's files starts before someone has signed in.
Init starts the log, input, display and terminal servers, then the
gatekeeper (`types::gatekeeper`), handing it only the terminal, the keystore
(a small disk of its own, told from the data disk by its size) and the
virtio-rng device. Init calls `UNLOCK`, which returns once the owner is in,
then `KEY`, whose reply is the master key; init passes it to storage
(`SET_KEY`, from the unbadged holder only, each half once), and a fresh
32-byte seed (`SEED` → `SET_SEED`) for storage's randomness this boot.
Storage's encrypted areas (`home/`, `system/`, each app's `data/<id>/`;
crate `filecrypt`, layer `storage_server::encrypted`) open once both are
in. With a new key (a fresh device, or one just wiped: `UNLOCK`'s word 1)
init empties those areas and seeds them from the plaintext `factory/`;
an area that is missing is seeded too. Only then does init start the
permission manager and the desktop. If the gatekeeper cannot
unlock, init stops there. The gatekeeper exits, and its capabilities go
with it.
virtio-rng device. Init calls `UNLOCK`, which returns once someone is in:
`[OK, flags, user id, role]`, a guest (`GUEST`) or a new keystore
(`FRESH`) among the flags. Then `KEY` (their master key), `SEED` (32 fresh
random bytes for storage this sign-in), `NAME`, and `REMOVED` until `DONE`
(users the owner removed just now). The gatekeeper exits.
Each user's files are in `users/<id>/` (`types::user`: the id in hex, or
`guest`): `home/`, `permissions/`, and per app `apps/<id>/` and
`data/<id>/`, each sealed under its own key derived from that user's
master key (crate `filecrypt`, layer `storage_server::encrypted`); the rest
of the disk is plaintext. While storage is still locked, init removes what
must go (`PURGE`, init only, only in `users/`, only while locked): every
user's folder on a new keystore, a removed user's, the last guest's. Then
it tells storage who is in (`SET_USER`) and hands over the key and seed
(`SET_KEY`, `SET_SEED`, a half per call, each once a sign-in; init only).
Only the signed-in user's folder opens; anyone else's is refused. Init
seeds whatever area of theirs is missing from the plaintext `factory/`,
then starts the session: the permission manager (the user's folder, and
`apps/` read-only) and the desktop. If the gatekeeper cannot unlock, init
stops there.
Signing out (`types::spawn::SIGN_OUT`, sent one way through the copy of
init's endpoint the shell holds) stops everything the session ran (the
shell, the permission manager, every app), tells the terminal
(`SIGNED_OUT`, init only), locks storage (`LOCK`, init only: every grant
taken back, every file closed, the key forgotten), removes a guest's
files, and runs the gatekeeper again.
A program's `uses` endpoints arrive as badged copies with `Execute` and
`Delegate`: the badge (unique per copy) tells the server which client is
@@ -135,12 +150,12 @@ log endpoint is the terminal prints there with `println!`: standard output
is whatever text endpoint a program was given. `READ` (`types::terminal`)
blocks until a line is typed and returns it in chunks, the last ending in
`\n`. Ctrl+C there drops the line. Keys typed while no line is read are
kept for the next one, except Ctrl+C, which signals the notification its
first `ON_INTERRUPT` caller gave. `REDRAW` repaints the screen and takes
kept for the next one, except Ctrl+C, which signals the notification the
session's first `ON_INTERRUPT` caller (its shell) gave. `REDRAW` repaints the screen and takes
input focus back.
The desktop is the shell, given the terminal, the permission manager and
`/home` (its `/`). Everything the owner runs is an app (docs/apps.md): a
The desktop is the shell, given the terminal, the permission manager,
the user's `home/` (its `/`) and signing out. Everything the owner runs is an app (docs/apps.md): a
command that is not a built-in asks the permission manager (`types::apps`,
client `libkos::apps`) to run it, passing the terminal as its output and a
read-only grant to the current folder (`cd`, `pwd`). The permission manager