- frames a writeonce port of ~/projects/skillhost (C++ MCP host that
links libllama in-process, discovers filesystem skills, runs their
scripts confined) as the sample that drives host capabilities into
the open — the way log-watcher drove the systems stdlib
- names each gap as a candidate iteration (surveyed 2026-08-16 vs the
running compiler + skillhost source):
- Blocker A: in-process native-lib FFI (no FFI today) — fork:
FFI-as-language vs out-of-process model driver over proc/net+json
(llama-server, needs nothing new); leaning out-of-process
- Blocker B: stdio transport — no stdin/stdout builtins; port uses
a TCP socket meanwhile; io.stdin_read/stdout_write a candidate
- Blocker C: bounded/killable subprocess — proc.run has no timeout/
signal/process-group kill; smallest + most broadly useful, do 1st
- partials: recursive fs walk, exec-bit check, symlink-resolving
confinement (realpath) — one small fs-metadata iteration
- records what is already expressible (catalog via @table/9g skill-
catalog, discovery, frontmatter text-parse, config, single-thread
serve, context-gate arithmetic — no VRAM query needed)
- out of scope: in-process libllama/CUDA, VRAM introspection, exact
sampler chain / per-turn memory clear
- roadmap + board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
199 lines
10 KiB
Markdown
199 lines
10 KiB
Markdown
# Iteration 14 — skillhost: a host-shaped workload, and the capability gaps it exposes
|
|
|
|
> Format: `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.
|