diff --git a/docs/examples/employee-list/README.md b/docs/examples/employee-list/README.md new file mode 100644 index 0000000..a755455 --- /dev/null +++ b/docs/examples/employee-list/README.md @@ -0,0 +1,42 @@ +# employee-list — program B: attach, authenticate, read + +> **Status: target workload — does not compile on today's toolchain.** +> Written ahead of iterations +> [9c (cross-program tables)](../../stories/language-runtime-database/09c-cross-program-tables.md) +> and [9d (keypair attach auth)](../../stories/language-runtime-database/09d-keypair-attach-auth.md), +> the way every acceptance sample here precedes its features. It also leans +> on 9/9b (the [employee sample](../employee/) it attaches to must run +> first). + +Two programs, one database, one writer: + +``` +employee (A) employee-list (B) + owns WO_DATA + WAL no database of its own + [share] listen = unix:...sock [connect.employee] ipc = unix:...sock + [[share.clients]] public_key = + public_key = project = ../employee (shapes) + rights = "read" + ▲ │ + └── every statement executes here ◄───┘ (typed, over the wire) +``` + +The manifests are the design: **A grants, B pins.** A's `[share]` names B's +public-key fingerprint with rights (`read` here); B's `[connect.employee]` +names A's IPC string AND A's fingerprint, so neither side talks to an +impostor. Fingerprints are printed by each program's `--identity` after +first boot (keys are generated into `WO_DATA`, never written into a toml) +and pasted — the `PASTE-…-HERE` placeholders mark exactly where. The +connect-section name is the code's namespace: `[connect.employee]` is why +the source says `employee.Employee`. + +| Mode | What it proves | +| --- | --- | +| `employee-list list` | typed reads over the wire, `e.dept.name` ref navigation executing inside A | +| `employee-list report` | **byte-identical output to A's own `report`** — attach + GroupBy compose, the wire changes nothing | +| `employee-list staff ` | unique-name index probe + `staff` backlink scan, both in A | +| `employee-list probe-write` | the rights matrix: registered read-only, so the insert traps with access-denied (caught, `DENIED …`, exit 4) and A's row count is unchanged | + +The 9d acceptance drives the rest from the outside: wrong key, no key, +same-uid-wrong-key, impostor socket, handshake replay, key rotation — see +the iteration's criteria; this sample is the workload they run against. diff --git a/docs/examples/employee-list/main.wo b/docs/examples/employee-list/main.wo new file mode 100644 index 0000000..b08993c --- /dev/null +++ b/docs/examples/employee-list/main.wo @@ -0,0 +1,73 @@ +-- employee-list — program B of the cross-program story (iterations 9c/9d). +-- Attaches to the RUNNING employee program (A) named by [connect.employee] +-- in wo.toml: keypair handshake first (9d), then typed statements over the +-- wire (9c). A registered this program read-only, so every mode here reads — +-- except probe-write, which exists to prove the rights matrix refuses. +-- +-- The `employee.` prefix is the manifest's connect-section name: these are +-- A's tables, checked against A's shapes at compile time and re-verified by +-- the schema handshake at attach. A stays the single writer; every statement +-- below executes inside A. + +fn main(args: multi Text) -> Int { + if len(args) >= 1 and args[0] == "list" { return list(); } + if len(args) >= 1 and args[0] == "report" { return report(); } + if len(args) >= 2 and args[0] == "staff" { return staff(args[1]); } + if len(args) >= 1 and args[0] == "probe-write" { return probe_write(); } + print_err("usage:"); + print_err(" employee-list list every employee, with department"); + print_err(" employee-list report aggregates by department (A's own report, over the wire)"); + print_err(" employee-list staff one department's staff"); + print_err(" employee-list probe-write prove read-only: the insert must be DENIED"); + return 1; +} + +-- Every employee with forward ref navigation — each `e.dept.name` is a +-- point read executing inside A. +fn list() -> Int { + for e in from x in employee.Employee order by x.name select x { + print("EMP ${e.name} ${e.salary} ${e.dept.name}"); + } + return 0; +} + +-- Byte-identical output to A's own `employee report` — the 9c acceptance +-- line: attach + query compose, and the wire changes nothing. +fn report() -> Int { + let rows = from e in employee.Employee + group e by e.dept into g + order by avg(g.salary) desc + select { dept: g.key.name, headcount: count(g), + avg_salary: avg(g.salary), min_salary: min(g.salary), + max_salary: max(g.salary) }; + for r in rows { + print("DEPT ${r.dept} headcount=${r.headcount} avg=${r.avg_salary} min=${r.min_salary} max=${r.max_salary}"); + } + let payroll = sum(from e in employee.Employee select e.salary); + print("PAYROLL ${payroll}"); + return 0; +} + +-- Unique-name index probe + backlink scan, both executing in A. +fn staff(name: Text) -> Int { + let ds = from d in employee.Department where d.name == name take 1 select d; + if len(ds) == 0 { print_err("no such department: ${name}"); return 1; } + for e in from s in ds[0].staff order by s.salary desc select s { + print("STAFF ${e.name} ${e.salary}"); + } + return 0; +} + +-- The rights matrix, exercised: this program is registered READ-ONLY, so +-- the insert must trap with the access-denied code — caught here, printed, +-- and nothing was applied or WAL-logged in A (the acceptance asserts A's +-- row count is unchanged). +fn probe_write() -> Int { + let id = try insert employee.Department { name: "Intruders" } catch (e) nil; + if id == nil { + print("DENIED write to employee.Department (registered read-only)"); + return 4; + } + print_err("UNEXPECTED: write succeeded with id ${id} — rights not enforced"); + return 1; +} diff --git a/docs/examples/employee-list/wo.toml b/docs/examples/employee-list/wo.toml new file mode 100644 index 0000000..646ed57 --- /dev/null +++ b/docs/examples/employee-list/wo.toml @@ -0,0 +1,29 @@ +name = "employee-list" +version = "0.1.0" +description = "Program B of the cross-program story: attaches read-only to the running employee program (A) over its IPC channel after keypair authentication, and lists/aggregates A's tables over the wire" + +[runtime] +wo = ">= 0.1" + +[build] +runtime = "../../../runtime/wovm" + +# Iteration 9c/9d surface (target — compiles once those land). +# The section NAME is the namespace this program's code uses: [connect.employee] +# makes A's tables reachable as `employee.Department` / `employee.Employee`. +[connect.employee] +# A's IPC string (9c fork 1: unix socket, listening beside A's WO_DATA; +# A declares the same path in its [share] section). +ipc = "unix:../employee/target/data/employee.sock" + +# A's PINNED public-key fingerprint (9d): B refuses to speak to anything on +# that socket path that cannot sign A's challenge with this key — the +# impostor-socket acceptance line. Printed by `employee --identity` after +# A's first boot; paste it here. Never a private key, never a secret. +public_key = "ed25519:PASTE-EMPLOYEE-FINGERPRINT-HERE" + +# Where B's compiler reads A's table shapes at compile time (9c fork 2's +# milestone lean: project-directory reference). The runtime handshake +# re-verifies the shapes against A's live class table at attach — a stale +# checkout refuses the attachment with both shapes named. +project = "../employee" diff --git a/docs/examples/employee/wo.toml b/docs/examples/employee/wo.toml index 2b8e567..6c2a151 100644 --- a/docs/examples/employee/wo.toml +++ b/docs/examples/employee/wo.toml @@ -10,3 +10,19 @@ wo = ">= 0.1" # toward, sample-first like log-watcher was. [build] runtime = "../../../runtime/wovm" + +# Iteration 9c/9d surface (target): this program OWNS its database and +# shares it. The runtime listens on `listen` beside WO_DATA; every client +# below is a grant — no registration, no attach, same uid included. +[share] +listen = "unix:target/data/employee.sock" + +# Program B (docs/examples/employee-list), granted read-only. Identity is +# B's public-key fingerprint (9d): printed by `employee-list --identity` +# after B's first boot; paste it here. Rights: "read" or "rw" — B's +# probe-write mode exists to prove "read" refuses. Rotation and revocation +# are edits to this table plus a restart, never an API. +[[share.clients]] +name = "employee-list" +public_key = "ed25519:PASTE-EMPLOYEE-LIST-FINGERPRINT-HERE" +rights = "read" diff --git a/docs/stories/language-runtime-database/09c-cross-program-tables.md b/docs/stories/language-runtime-database/09c-cross-program-tables.md index b18fea3..b840dc5 100644 --- a/docs/stories/language-runtime-database/09c-cross-program-tables.md +++ b/docs/stories/language-runtime-database/09c-cross-program-tables.md @@ -161,10 +161,10 @@ SIGTERM'd B parked on a channel read exits cleanly). doc's wire protocol, profiled for unix sockets, values in the WAL's encoding. - **The acceptance workload extends the employee sample**: A = the employee - program with `[share]`; B = a new thin `docs/examples/employee-report` - client attaching read-only for the GroupBy report, plus a read+write - audit-log writer path exercising the rights matrix and the refusal - traps. The sample stays the test. + program with `[share]`; B = `docs/examples/employee-list` (pre-authored + 2026-08-15, sample-first — both manifests designed as a pair), attaching + read-only for the list/report/staff modes and proving the rights matrix + with its `probe-write` mode. The sample stays the test. - Depends on iterations 9 (engine, WAL — done through Task 3 as of 2026-08-15) and 9b (typed statements and queries worth sharing); wants iteration 8's event loop for A's serving side but can prototype on a diff --git a/docs/stories/language-runtime-database/09d-keypair-attach-auth.md b/docs/stories/language-runtime-database/09d-keypair-attach-auth.md index 32dc470..139b55f 100644 --- a/docs/stories/language-runtime-database/09d-keypair-attach-auth.md +++ b/docs/stories/language-runtime-database/09d-keypair-attach-auth.md @@ -132,10 +132,13 @@ authorization input. 9c without 9d ships a placeholder identity and 9d without 9c has nothing to authenticate. The 9c milestone may still land first with the uid bootstrap, flagged loudly as pre-9d. -- **Acceptance extends the 9c workload**: the employee-A / report-B pair - gains the key exchange in both manifests; the acceptance script adds the - wrong-key, no-key, same-uid-wrong-key, replay, impostor-socket, and - rotation checks above, each asserting the exact trap/refusal. +- **Acceptance extends the 9c workload**: the employee-A / + employee-list-B pair (`docs/examples/employee-list`, pre-authored + 2026-08-15) carries the key exchange in both manifests — A's + `[[share.clients]]` names B's fingerprint, B's `[connect.employee]` pins + A's; the acceptance script adds the wrong-key, no-key, + same-uid-wrong-key, replay, impostor-socket, and rotation checks above, + each asserting the exact trap/refusal. - Expected shape: handshake module beside the channel code (both ends), `[share]`/`[connect]` manifest keys for fingerprints, first-boot keygen in the runtime's data-directory setup, vendored signature primitive with