diff --git a/docs/00-status.md b/docs/00-status.md index 5ce312f..715f627 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -126,6 +126,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 11 | [Fibers](stories/language-runtime-database/11-fibers.md) | ⬜ | Hold | | 12 | [Blue-green deploy](stories/language-runtime-database/12-blue-green-deploy.md) | ⬜ | Hold | | 13 | [Compile-time metaprogramming](stories/language-runtime-database/13-compile-time-metaprogramming.md) | ⬜ needs a spec first | +| 14 | [skillhost host workload](stories/language-runtime-database/14-skillhost-host-workload.md) | ⬜ gaps recorded (branch query-grammar found skillhost needs no new query grammar); each gap a candidate iteration | --- @@ -334,6 +335,7 @@ Ecommerce sample (verified 2026-06-13): `api.rest` 17/17 expected statuses pass. | 9e | Durability + throughput + scale — restart-persistence, read/write benchmark, ~1M rows; the gate every later optimization re-runs | **no spec yet** — four forks recorded; the measurement backbone | | 9f | io_uring group-commit write path — batched durability overlapped on shard threads, fsync fallback | **no spec yet** — brainstorm after iterations 8 + 9e | | 9g | Query grammar from real embedded-DB corpora — whole-query count + correlated exists, driven by the skillhost SQL catalogue; add only what a corpus uses | **no spec yet** — three forks; may collapse to "confirm len(query) + add exists" | +| 14 | skillhost host workload — port skillhost (MCP host + confined script runner) to writeonce; drives the missing host capabilities into the open (bounded subprocess, stdin/stdout transport, fs metadata, FFI-vs-out-of-process) | **no spec yet** — gaps recorded in the iteration; each gap brainstormed on demand, bounded-subprocess first | | 10 | HTTP service layer | [plan 6](superpowers/plans/2026-08-01-http-service-layer.md) | | 11 | Fibers | vision §3, [blue-green exploration](plan/exploration/blue-green-vm/00-vision.md) | | 12 | Blue-green deploy | [spec](superpowers/specs/2026-08-03-blue-green-vm-design.md) — plan authored after iterations 9–10 | diff --git a/docs/stories/language-runtime-database/00-story.md b/docs/stories/language-runtime-database/00-story.md index 4f896b3..7382c24 100644 --- a/docs/stories/language-runtime-database/00-story.md +++ b/docs/stories/language-runtime-database/00-story.md @@ -62,6 +62,7 @@ iterations); no commits by agents — drafts go to `.dev/commit.md`. | 11 | [Fibers](11-fibers.md) | green threads on the shard scheduler: reduction-budget preemption, park on I/O | | 12 | [Blue-green deploy](12-blue-green-deploy.md) | two VM slots, in-runtime compile, atomic switch, resident rollback | | 13 | [Compile-time metaprogramming](13-compile-time-metaprogramming.md) | `@derive(Json/Csv/Eq/Hash/Show)` — the compiler generates per-type code from the class-table metadata; generic capabilities within principle 13, no reflection | +| 14 | [skillhost host workload](14-skillhost-host-workload.md) | a host-shaped driving workload (writeonce port of skillhost) that names the runtime gaps it exposes: bounded/killable subprocess, stdin/stdout transport, fs metadata, and FFI-vs-out-of-process model driver — each a candidate iteration | Review protocol: the developer reads one iteration, approves or amends; the next starts only after approval. Each iteration is an unsplittable diff --git a/docs/stories/language-runtime-database/14-skillhost-host-workload.md b/docs/stories/language-runtime-database/14-skillhost-host-workload.md new file mode 100644 index 0000000..f290d36 --- /dev/null +++ b/docs/stories/language-runtime-database/14-skillhost-host-workload.md @@ -0,0 +1,199 @@ +# Iteration 14 — skillhost: a host-shaped workload, and the capability gaps it exposes + +> Format: fiberloom `product/story-iteration-template`. Part of +> [Story — one language, one runtime, one database, one binary](00-story.md). +> +> **Inserted 2026-08-16.** A driving-workload iteration, the way iteration 7's +> log-watcher drove the systems stdlib. The workload is a writeonce port of +> **`~/projects/skillhost`** (a C++ MCP host that links `libllama` in-process, +> discovers filesystem skills, and runs their scripts under confinement). The +> point is not the port for its own sake — it is that a *host-shaped* program +> (an MCP tool server that orchestrates subprocesses) exercises runtime +> capabilities no prior sample needed, and this iteration names each gap so it +> becomes schedulable. +> +> **No spec exists yet.** This iteration frames the workload and records the +> gaps (surveyed 2026-08-16 against the running compiler + the skillhost +> source); each gap below is a candidate iteration of its own. + +## Goals + +- **A writeonce program that is skillhost's shape**: one MCP tool + (`delegate_task`) exposed to a hosted caller, an on-disk skill catalog + discovered at startup, and a confined subprocess runner that executes a + skill's scripts and returns exit code + captured output. Target sample: + `docs/examples/skillhost`. +- **Drive the runtime's missing host capabilities into the open.** Each thing + the port cannot express today is a named gap with a decision attached — the + same discipline log-watcher used to grow `fs`/`proc`/`net`/`time`. +- **Ship the expressible part now, block honestly on the rest.** Much of + skillhost is already writable (below); the port lands in the pragmatic + variant that avoids the hard blockers, and the blockers become their own + iterations. + +## What is already expressible (verified 2026-08-16) + +- **The skill catalog** — `@table` with the query surface already exceeds + skillhost's in-memory SQLite `skills` table; iteration 9g's + `docs/examples/skill-catalog` is literally this table, running. (Or a plain + `map`/`multi` would do — the catalog is a lookup cache, not persistence.) +- **Discovery** — `fs.exists`/`fs.list` (one level) + `fs.read_all` walk + `.agents/*/SKILL.md` exactly as skillhost does one level deep. +- **Frontmatter** — skillhost's own parser is a hand-rolled YAML subset + (`--- … key: value …`); that is plain Text parsing in writeonce, no YAML + library needed. +- **Config** — `env.get` (the `SKILLHOST_*` overrides), `fs.read_all` + + `json.decode` (the config file), `fn main(args)` (flags). The "context + gate" is pure Int arithmetic (`tokens + max_tokens >= n_ctx`); skillhost + does **no** runtime VRAM query, so none is needed. +- **Single-threaded serving** — skillhost handles one request at a time (its + mutexes are defensive only); writeonce's blocking single-thread model maps + cleanly, as log-watcher's MCP mode already proved. + +## The gaps (each a candidate iteration) + +### Blocker A — in-process native library binding (FFI) + +skillhost links `libllama`'s C API in-process (`llama_model_load_from_file`, +`llama_decode`, the sampler chain, GBNF grammar, tokenizer, chat template). +writeonce has **no FFI**, so none of it can be linked or called. The +architecture doc is explicit that in-process linkage is *what forces C++* — +so this gap is the whole reason skillhost is not already portable. + +Two directions, and they are a real fork, not a detail: + +- **FFI as a language capability** — a way to declare and call C functions + from writeonce. This is a large, doctrine-level addition (the runtime is + libc-only by principle; the one sanctioned exception so far is the vendored + Ed25519, 9d). FFI would reopen the dependency-sprawl question the whole + project is built to avoid. Likely its own spec, likely contested. +- **Out-of-process model, no FFI** — drive a llama.cpp binary via `proc` + (`llama-cli`) or `llama-server` over `net` + `json` (it accepts a GBNF + `grammar` param, so grammar-constrained tool calls survive). This needs + **nothing new** and is how a host in any language would do it — the doc + notes `llama-server` "a host in any language could drive." It loses + skillhost's per-turn `llama_memory_clear` + custom sampler control, which + become server request options. **This is the variant the port should use.** + +Leaning: build the port on the out-of-process model; record FFI as a separate +iteration to be opened only if a workload genuinely needs in-process linkage, +and expect it to be weighed hard against the libc-only doctrine. + +### Blocker B — stdio transport (process stdin/stdout) + +skillhost speaks MCP as **newline-delimited JSON-RPC over stdin/stdout** +(`server.serve(std::cin, std::cout)`), which is what a stdio-launching MCP +client expects. writeonce has **no stdin builtin and no stdout-write +builtin** — only `net` sockets and `print`. So a faithful stdio transport is +impossible today. + +- The pragmatic port re-hosts MCP on a **TCP socket** (`net.listen/accept/ + read/write` + newline framing + `json`), exactly as log-watcher's MCP mode + does — a legitimate but *different* transport than skillhost's stdio. +- The real capability gap: **raw stdin/stdout byte I/O** (read fd 0, write fd + 1, flush), so a writeonce program can be a stdio-transport MCP server — the + default an MCP client launches. Small stdlib addition (`io.stdin_read` / + `io.stdout_write`, or an `fd` surface). A candidate iteration. + +### Blocker C — bounded, killable subprocesses + +skillhost's `run_script` spawns a script, drains stdout+stderr, and — on a +120s deadline — `SIGKILL`s the whole **process group** so backgrounded +grandchildren die too (`poll`-with-deadline + `kill(-pid, SIGKILL)`). +writeonce's `proc.run` captures stdout/stderr/exit but has **no timeout, no +signal control, no process-group kill**, and blocking-only I/O with no +threads means it cannot wait on a child with a wall-clock deadline. A runaway +skill script cannot be bounded — unacceptable for a host that runs untrusted +scripts. + +The capability gap: **`proc.run` with a timeout that kills the child (group) +on expiry** — a deadline-bounded subprocess. This composes with the +log-watcher Task-4 stop-flag work (interruptible blocking calls) and is a +clear candidate iteration. + +### Partial cluster — filesystem metadata + +Smaller gaps, all in `fs`: + +- **Recursive directory walk** — skillhost lists a skill's executables/ + references recursively; `fs.list` is one level, so this is built by hand + (fine) or `fs` grows a recursive walk. +- **Executable-bit detection** (`access(X_OK)`) — skillhost only offers + runnable scripts to the model; unclear whether `fs.stat` exposes mode bits. + Candidate `fs.stat` extension. +- **Symlink-resolving path confinement** (`weakly_canonical`/`realpath`) — + skillhost resolves symlinks before its prefix check so a link pointing out + of the skill dir is rejected. writeonce has no `realpath`; a string-prefix + check alone is **weaker** (a symlink escape). Candidate `fs` addition — + and a security-relevant one, since confinement is the host's trust boundary. + +## Acceptance Criteria + +- What to achieve? + - **Given** the expressible subset (catalog + discovery + frontmatter + + config + a TCP-hosted `delegate_task` tool + a subprocess runner), + - **when** `docs/examples/skillhost` is written and compiled, + - **then** it runs as an MCP server over a socket, discovers `.agents/*` + skills, answers `initialize`/`tools/list`/`tools/call`, and runs a + skill's script returning its exit code and captured output — the + skillhost shape, minus the named blockers. +- What to achieve? + - **Given** a skill script that runs forever, + - **when** the runner has no bounded-subprocess capability (Blocker C), + - **then** the port documents that it cannot yet bound it — the gap is + demonstrated, not hidden, and the acceptance records it as the reason + Blocker C's iteration exists. +- What to achieve? + - **Given** each gap iteration lands (bounded subprocess, stdio + transport, fs metadata, and — if ever — FFI), + - **when** the port adopts it, + - **then** the port moves one step closer to skillhost's exact behavior, + and the divergence list in its README shrinks by exactly that gap. + +## Out Of Scope + +- **In-process `libllama` / CUDA offload** — requires Blocker A (FFI); the + port uses an out-of-process model instead and says so. +- **Runtime VRAM/NVML introspection** — skillhost does none (the context + gate is arithmetic); nothing to build. +- **A resident MCP-over-stdio server** until Blocker B lands — the port uses + a socket transport meanwhile. +- **Reproducing skillhost's exact sampler chain / per-turn memory clear** — + those are in-process libllama specifics; the out-of-process variant + approximates them with server request options. + +## Info + +The three blockers are independent and each merits its own iteration; the +filesystem partials could ride together as one small `fs`-metadata iteration. +Priority order by leverage: + +1. **Bounded subprocess (Blocker C)** — the smallest, most broadly useful, and + a hard requirement for any host that runs untrusted scripts. Do first. +2. **stdio transport (Blocker B)** — small, unblocks writeonce being a + *standard* MCP server (stdio is the default an MCP client launches), useful + far beyond skillhost. +3. **fs metadata (the partials)** — small, security-relevant (symlink + confinement). +4. **FFI (Blocker A)** — largest and most contentious; open only on genuine + demand, and expect the out-of-process variant to make it unnecessary for + this workload. + +The port itself is buildable today in the out-of-process / TCP-transport +variant *once Blocker C exists* (a host that cannot bound a runaway script is +not honest to ship) — so the natural first step is Blocker C, then the port, +then B and the partials narrow the gap to skillhost's real behavior. + +## Proposed Solution + +- **Brainstorm each gap iteration on demand**, starting with the bounded + subprocess (Blocker C) since the port cannot responsibly run scripts + without it. +- **Write `docs/examples/skillhost`** in the out-of-process / socket-transport + variant, with a README whose "divergence from the C++ skillhost" list is + exactly the open gaps (A: out-of-process model, B: socket not stdio, C: + bounded once its iteration lands, plus the fs partials) — the list is the + iteration's own scoreboard. +- Reuse iteration 9g's `skill-catalog` as the catalog layer, log-watcher's + MCP mode as the transport skeleton, and the systems stdlib for discovery + and execution.