docs(audit): fix stale docs against the code (TLS, net.connect, RNG, lang-41); jarvis deps

Code is the source of truth; these claims no longer matched runtime/src:

- "net.connect does not exist" — landed 2026-09-07 (id 110); net.connect_tls /
  read_tls / write_tls (115-117) + net.accept_tls (118), WO_B_MAX 118. Fixed
  in jarvis 00-story (problem statement + architecture + out-of-scope), porch
  00-story (proxy middleware row), rv2 7 (push-collector fork), 00-code-review
- "TLS: none / proxy-mandated forever" — retired by rv2 9 (in-process TLS both
  directions). Fixed in porch + web-app + site example READMEs (proxy is now a
  deployment choice; HSTS row), 00-code-review
- "no RNG anywhere in the runtime" — imprecise: the runtime has a getrandom(2)
  source since rv2 9 (TLS ephemerals), but nothing exposes it to .wo yet.
  Fixed in CODE-LOGIC (digests), lang 34, porch 2, status lang-39 row
- "porch 9 blocked on language 41" — lang 41 fixed 63065ff. Fixed in porch 1,
  jarvis 00-story, status NEXT PLAN, dependency graph (L41 done, P9 ready)
- dependency graph §7 rewritten: the runtime side is done; jarvis 1 waits only
  on porch (developer's porch-first order). Adds jarvis 1's dependency table +
  the build order that satisfies it
- 00-code-review: a dated 2026-09-09 re-verification appended (record kept)
- site README lives in the writeonce-site submodule: committed there, pointer
  bumped here

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit f1049dd9b7c7770da28bcabfc1cb1324621e7ee6)
This commit is contained in:
shoney.arickathil 2026-09-09 10:47:20 +02:00
parent 00214bd68e
commit ad1ad8361c
13 changed files with 148 additions and 73 deletions

View file

@ -131,3 +131,28 @@ Two capability gaps this re-run named that the original critique did not, now
`fs` has six builtins (ids 40–45) and can create, grow and read a file but never `fs` has six builtins (ids 40–45) and can create, grow and read a file but never
replace, truncate, delete or rename one; and there is no `net.connect` anywhere replace, truncate, delete or rename one; and there is no `net.connect` anywhere
in `runtime/src/`, so no program can open an outbound connection. in `runtime/src/`, so no program can open an outbound connection.
## Re-verification 2026-09-09
Code is the source of truth; the standing critique's production-plumbing row and
the 2026-08-26 re-verification have been overtaken by shipped work. Kept as
written above; corrected here:
- **"No TLS anywhere (proxy-mandated forever)"** — false since 2026-09-09.
Runtime-v2 9 landed hand-rolled TLS 1.3 in-process, both directions:
`net.connect_tls`/`net.read_tls`/`net.write_tls` (ids 115–117) and
`net.accept_tls` (118), live-gated (`just tls`, `just tls-server`). The
proxy-termination doctrine is retired.
- **"no crypto primitives"** — false. `crypto.c` holds SHA-1/SHA-256/HMAC (iteration
34), ChaCha20-Poly1305, AES-GCM, HKDF, X25519, RSA-PSS/PKCS1 + ECDSA-P256 verify
*and* constant-time sign (RFC 6979), and an X.509 layer — all RFC/NIST-vector
gated.
- **"there is no `net.connect` anywhere in `runtime/src/`"** — false since
2026-09-07 (`WO_B_NET_CONNECT` = 110; the id ceiling is now `WO_B_MAX` 118).
- **"send is one-way — no reply/request-response"** — overtaken by iteration 24's
`call`; **"no supervision, links, or actor death"** — overtaken by iteration 24's
monitors/death notices; the cross-shard message double free that shadowed the
actor path (language 41) is fixed (`63065ff`, marshal on the crossing).
- Still true: no HTTP/2, no debugger/LSP, git-rev-only deps, and — precisely —
**no RNG exposed to `.wo`** (the runtime has a `getrandom` source since rv2 9,
unsurfaced until porch 2's `random_bytes`).

View file

@ -302,8 +302,8 @@ flowchart TD
P6["porch 6 streaming core"]:::ready P6["porch 6 streaming core"]:::ready
P7["porch 7 SSE + compression"]:::ready P7["porch 7 SSE + compression"]:::ready
P8["porch 8 static files + lifecycle"]:::ready P8["porch 8 static files + lifecycle"]:::ready
P9["porch 9 idempotent replay (⏸ hold; readiness: ready)"]:::held P9["porch 9 idempotent replay (ready — unblocked 2026-09-09)"]:::ready
L41["language 41 actor-arena hang (in-progress)"]:::held L41["language 41 actor-arena double free ✅ fixed 63065ff (cross-shard marshal)"]:::done
RB --> P2 RB --> P2
P2 --> P3 P2 --> P3
@ -315,7 +315,7 @@ flowchart TD
P5 --> P8 P5 --> P8
DFL --> P7 DFL --> P7
TU --> P8 TU --> P8
L41 -.blocks.-> P9 L41 -.fixed 2026-09-09 — no longer blocks.-> P9
P1 -.re-scope 79e6da4: replay-on-retry split out of 1.-> P9 P1 -.re-scope 79e6da4: replay-on-retry split out of 1.-> P9
``` ```
@ -426,58 +426,78 @@ transport.
## 7. jarvis — the AI-assistant track and everything it waits on ## 7. jarvis — the AI-assistant track and everything it waits on
The sixth track ([jarvis](stories/jarvis/00-story.md)): an AI assistant built in The sixth track ([jarvis](stories/jarvis/00-story.md)): an AI assistant built in
writeonce. Only **jarvis 1** (the chat loop) is close to startable; it sits on writeonce. **The runtime side is done** — the whole outbound HTTPS path landed
two chains — the **outbound HTTPS path** (the real blocker, mostly runtime-v2 9's 2026-09-09 (runtime-v2 9, both directions, live-gated) and language 41 is fixed.
TLS ladder) and the **framework path** (porch, all `ready`). Phase-level, because What jarvis 1 waits on now is purely the **framework**: the developer set the
the TLS ladder is where the waiting actually happens. order *porch first, then jarvis*. So this graph is the porch→jarvis chain.
```mermaid ```mermaid
flowchart TD flowchart TD
classDef done fill:#1a7f37,color:#fff,stroke:none classDef done fill:#1a7f37,color:#fff,stroke:none
classDef ready fill:#0969da,color:#fff,stroke:none classDef ready fill:#0969da,color:#fff,stroke:none
classDef refine fill:#eac54f,color:#000,stroke:none classDef refine fill:#eac54f,color:#000,stroke:none
classDef held fill:#6e7781,color:#fff,stroke:none
NC["net.connect (id 110) ✅"]:::done NC["net.connect (id 110) ✅"]:::done
A["rv2 9 A — AEAD ✅ (= rv2 8 A–C: ChaCha20-Poly1305 + AES-GCM)"]:::done TLS["rv2 9 in-process TLS 1.3 ✅ — net.connect_tls / read_tls / write_tls (115–117), net.accept_tls (118)"]:::done
B["rv2 9 B — HKDF ✅"]:::done L41["language 41 cross-shard marshal ✅"]:::done
C["rv2 9 C — X25519 ✅"]:::done
D["rv2 9 D — signatures: RSA-PSS/PKCS1 + ECDSA-P256 ✅"]:::done
E["rv2 9 E — ASN.1/DER + X.509 chain + trust store"]:::refine
F["rv2 9 F — record layer + handshake FSM (client), net.connect_tls"]:::refine
G["rv2 9 G — inbound server (porch TLS termination)"]:::refine
P2["porch 2 randomness+cookies (ready)"]:::ready
P3["porch 3 sessions (ready)"]:::ready
P6["porch 6 streaming (ready)"]:::ready
P7["porch 7 SSE (ready)"]:::ready
WOHTML["wo-html / writeonce-view ✅"]:::done WOHTML["wo-html / writeonce-view ✅"]:::done
J1["jarvis 1 — the chat loop (unwritten)"]:::refine RB["random_bytes builtin (porch 2 phase A / lang 39) — surfaces the runtime's getrandom"]:::ready
J2["jarvis 2 — tool use / agent loop"]:::refine P2["porch 2 randomness + cookies (ready)"]:::ready
J3["jarvis 3 — retrieval (RAG) + embeddings/vector sub-gap"]:::refine P3["porch 3 sessions (ready)"]:::ready
P4["porch 4 CSRF (ready)"]:::ready
P5["porch 5 routing + response ergonomics (ready, zero deps)"]:::ready
P6["porch 6 streaming core (ready)"]:::ready
P7["porch 7 SSE + compression (ready)"]:::ready
P8["porch 8 static + lifecycle (ready)"]:::ready
P9["porch 9 idempotent replay (ready — unblocked by L41)"]:::ready
NC --> F J1["jarvis 1 — the chat loop (ready; forks auto-approved, review_pending)"]:::ready
A --> F J2["jarvis 2 — tool use / agent loop (refine)"]:::refine
B --> F J3["jarvis 3 — retrieval (RAG) (refine)"]:::refine
C --> D
D --> E NC --> TLS
E --> F RB --> P2
F --> J1
P2 --> P3 P2 --> P3
P2 --> P4
P3 --> P4
P5 --> P7
P5 --> P8
P6 --> P7 P6 --> P7
P6 --> P8
L41 --> P9
TLS --> J1
P2 --> J1 P2 --> J1
P3 --> J1 P3 --> J1
P4 -. CSRF-protects the POST once built .-> J1
P6 --> J1
P7 --> J1 P7 --> J1
WOHTML --> J1 WOHTML --> J1
J1 --> J2 J1 --> J2
J1 --> J3 J1 --> J3
``` ```
The outbound path is the critical one: **A/B/C/D landed, E→F remain** (G is **jarvis 1's dependency list, from the code and the stories (2026-09-09):**
inbound, not needed for jarvis dialling out). The framework path (porch 2/3/6/7)
is entirely `ready` and unblocked — buildable in parallel with the TLS ladder. | jarvis 1 needs | for | state |
jarvis 1 itself is not yet written; jarvis 2/3 follow it. | --- | --- | --- |
| `net.connect_tls` / `net.read_tls` / `net.write_tls` (runtime-v2 9) | dialing the LLM API over HTTPS, streaming its SSE reply | ✅ landed, live-gated |
| `net.connect` (id 110) | the TCP under it | ✅ landed |
| language 41 fix | actors carrying messages across shards without the double free | ✅ landed |
| `@table` | durable `Conversation` / `Message` history | ✅ exists |
| wo-html / writeonce-view | the chat page | ✅ exists |
| **porch 2** randomness + cookies (needs the `random_bytes` builtin first) | session id + signed cookie | ready, **unbuilt** |
| **porch 3** sessions | the session principal history is keyed to | ready, unbuilt (after 2) |
| **porch 6** streaming core | incremental response writes | ready, unbuilt |
| **porch 7** SSE + compression | token streaming to the browser | ready, unbuilt (after 5 + 6) |
| porch 4 CSRF | protecting `POST /message` (bearer-gated until then) | ready, unbuilt (after 2 + 3) |
| porch 5 routing + response ergonomics | the route surface | ready, unbuilt, zero deps |
**Build order that satisfies it** (the porch critical path to jarvis): the
`random_bytes` builtin → porch 2 → porch 3 → porch 5 → porch 6 → porch 7 (→ porch
4, 8, 9 to complete porch) → **jarvis 1**. Nothing on the runtime side is
outstanding; every remaining edge into jarvis 1 is a porch iteration.
## Maintenance rule ## Maintenance rule

View file

@ -72,9 +72,13 @@ porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" }
cannot live in the library). Parallel requests, stalled-client cannot live in the library). Parallel requests, stalled-client
eviction and parked idle keep-alive are gate-proven. The plain eviction and parked idle keep-alive are gate-proven. The plain
`serve()` stays single-threaded for simple apps. `serve()` stays single-threaded for simple apps.
- **TLS: none, anywhere.** Deploy behind nginx/caddy; the proxy terminates - **TLS: available in the runtime, not used by this sample yet.** Since
TLS+ALPN and gives browsers HTTP/2 while this backend speaks HTTP/1.1 runtime-v2 9 (2026-09-09) the runtime terminates TLS 1.3 itself —
keep-alive. See the web-app sample's README for the nginx sketch. `net.accept_tls(listener, cert, key)` (see `docs/examples/tls-server`) — so a
front proxy is no longer mandatory. This sample still runs plaintext
HTTP/1.1 keep-alive behind nginx/caddy (which also supplies ALPN/HTTP/2);
see the web-app sample's README for the nginx sketch. HTTP/2 itself is a
separate future slice.
- `Content-Length` bodies only (no chunked encoding); **WebSockets ARE - `Content-Length` bodies only (no chunked encoding); **WebSockets ARE
supported since 2026-08-27** — `ws_accept` (`http/ws.wo`) performs the RFC supported since 2026-08-27** — `ws_accept` (`http/ws.wo`) performs the RFC
6455 handshake and hands back the hijacked `net.Conn`, and `http/wsframe.wo` 6455 handshake and hands back the hijacked `net.Conn`, and `http/wsframe.wo`
@ -211,7 +215,7 @@ needs to know, found in the course of building them ([porch 1](../../stories/por
| --- | --- | | --- | --- |
| Constant-time comparison · Authorization parsing · Basic auth · principal | ✅ `http/auth.wo`, `req.principal` | | Constant-time comparison · Authorization parsing · Basic auth · principal | ✅ `http/auth.wo`, `req.principal` |
| CORS | ✅ `Cors { allow_origin }` — preflight 204 (before) + origin stamp on every response (after) — slice 2 | | CORS | ✅ `Cors { allow_origin }` — preflight 204 (before) + origin stamp on every response (after) — slice 2 |
| Security headers | ✅ `SecurityHeaders` after-middleware (nosniff, DENY, referrer-policy); HSTS stays at the TLS proxy by design — slice 2 | | Security headers | ✅ `SecurityHeaders` after-middleware (nosniff, DENY, referrer-policy); HSTS is set wherever TLS terminates — the front proxy today, or the runtime itself once a sample adopts `net.accept_tls` (runtime-v2 9) — slice 2 |
| Host validation | ✅ `HostAllow { host }` answers 421 before any route — slice 2 | | Host validation | ✅ `HostAllow { host }` answers 421 before any route — slice 2 |
| Strict parsing | ✅ same item as Transport's row: duplicate Content-Length is a 400 | | Strict parsing | ✅ same item as Transport's row: duplicate Content-Length is a 400 |

@ -1 +1 @@
Subproject commit 81ef9b9ca00c7941dd37d3d3e3e4de6af849a5b4 Subproject commit 003073f2df04edd53ac68aae18a06180b41c40fb

View file

@ -26,9 +26,12 @@ the token comes from the `WA_TOKEN` env var.
## TLS / HTTP2 ## TLS / HTTP2
None here, deliberately: deploy behind nginx/caddy — the proxy terminates This sample runs plaintext behind nginx/caddy — the proxy terminates TLS+ALPN
TLS+ALPN and speaks h2 to browsers while this backend serves HTTP/1.1 and speaks h2 to browsers while this backend serves HTTP/1.1 keep-alive. The
keep-alive. Sketch: runtime itself can now terminate TLS 1.3 (`net.accept_tls`, runtime-v2 9,
2026-09-09; see `docs/examples/tls-server`), so the proxy is a deployment
choice here, not a requirement — HTTP/2 is the remaining reason to keep it.
Sketch:
server { server {
listen 443 ssl; listen 443 ssl;

View file

@ -102,7 +102,8 @@ RSA-PSS + ECDSA-P256 with an RFC 6979 nonce, plus `wo_pkey_parse`
Deferred (named follow-ups, none blocking): park-based handshake, `TlsConn` Deferred (named follow-ups, none blocking): park-based handshake, `TlsConn`
language object, connection pooling, close_notify on shutdown, complete-formula language object, connection pooling, close_notify on shutdown, complete-formula
EC ladder. **Next: porch** (then jarvis — the developer set jarvis to follow EC ladder. **Next: porch** (then jarvis — the developer set jarvis to follow
porch completion). Separately open: language 41's marshal fix (unblocks porch 9). porch completion). Language 41's marshal fix has since landed (above), so porch 9
is unblocked too.
<details><summary>outbound client detail (F-phases)</summary> <details><summary>outbound client detail (F-phases)</summary>
@ -139,8 +140,9 @@ auto-approved 2026-09-08/09, `review_pending` for a developer second review.
The developer set the order: **jarvis is implemented once porch is complete and The developer set the order: **jarvis is implemented once porch is complete and
ready for all future jarvis iterations**. So with rv2 9 (the outbound seam) done, ready for all future jarvis iterations**. So with rv2 9 (the outbound seam) done,
the path is **porch first** — the framework the jarvis chat loop is written on — the path is **porch first** — the framework the jarvis chat loop is written on —
then jarvis 1–3. porch's own blocker is language 41's marshal fix (unblocks then jarvis 1–3. porch's former blocker — language 41's marshal fix — landed
[porch 9](porch/09-idempotent-replay.md)); the porch track (2–8) is brainstormed 2026-09-09, so [porch 9](porch/09-idempotent-replay.md) is buildable; the porch
track (2–8) is brainstormed
to `ready`, its language bill three small builtins (`random_bytes`, to `ready`, its language bill three small builtins (`random_bytes`,
`deflate`/`crc32`, `time.utc`). `deflate`/`crc32`, `time.utc`).
@ -1266,7 +1268,7 @@ that sequences its tasks. Read one, approve, then the next starts.
| 33 | [Single-file store](databasev2/07-single-file-db.md) | ⬜ off-chain, small — `WO_DATA=<path>.db` file form; driver-only (story written 2026-08-22) | | 33 | [Single-file store](databasev2/07-single-file-db.md) | ⬜ off-chain, small — `WO_DATA=<path>.db` file form; driver-only (story written 2026-08-22) |
| 34 | [Crypto builtins](language-runtime-database/34-crypto-builtins.md) | 🔄 **code landed** as 24's T1 (`d14fa9f`): `sha1`/`sha256`/`hmac_sha256`, ids 85–87 in `wob.h`, `runtime/src/crypto.c`, RFC/FIPS vectors 18/0, corpus pin. The 24 gate that once needed it is cleared. Frontmatter keeps `status: refine` only until 24's T10 closeout sets it to `done` | | 34 | [Crypto builtins](language-runtime-database/34-crypto-builtins.md) | 🔄 **code landed** as 24's T1 (`d14fa9f`): `sha1`/`sha256`/`hmac_sha256`, ids 85–87 in `wob.h`, `runtime/src/crypto.c`, RFC/FIPS vectors 18/0, corpus pin. The 24 gate that once needed it is cleared. Frontmatter keeps `status: refine` only until 24's T10 closeout sets it to `done` |
| 38 | [Content platform capabilities](language-runtime-database/38-content-platform-capabilities.md) | ⬜ off-chain, needs a spec — the two capability families no iteration owns, confirmed against `runtime/src/wob.h`: `fs` mutation verbs (six fs builtins, ids 40–45; `append` creates-if-absent, so nothing is ever replaced, truncated, deleted or renamed) and `net.connect` (ids 51–55 + 91–95, no connect, and no `connect()` anywhere in `runtime/src/` — so no OIDC/SMTP/object-store/webhook/federation). Driven by a `docs/examples/vault` content-collaboration workload, in 28's mould. New builtins from 96 (89/90 reserved for 31); no `.wob` bump (`WOB_VERSION 6u`, last moved by 36). Story written 2026-08-26 from the "can it build a Nextcloud?" ask | | 38 | [Content platform capabilities](language-runtime-database/38-content-platform-capabilities.md) | ⬜ off-chain, needs a spec — the two capability families no iteration owns, confirmed against `runtime/src/wob.h`: `fs` mutation verbs (six fs builtins, ids 40–45; `append` creates-if-absent, so nothing is ever replaced, truncated, deleted or renamed) and `net.connect` (ids 51–55 + 91–95, no connect, and no `connect()` anywhere in `runtime/src/` — so no OIDC/SMTP/object-store/webhook/federation). Driven by a `docs/examples/vault` content-collaboration workload, in 28's mould. New builtins from 96 (89/90 reserved for 31); no `.wob` bump (`WOB_VERSION 6u`, last moved by 36). Story written 2026-08-26 from the "can it build a Nextcloud?" ask |
| 39 | [Web framework parity](language-runtime-database/39-web-framework-parity.md) | ⬜ off-chain, needs a spec — from [the Fiber v3.5.0 study](../plan/exploration/fiber/00-fiber-parity.md) (all 32 of its middleware read against `porch`; **nine already have a counterpart**). Leads with a **random-bytes builtin**: the framework ledger claimed CSRF/sessions were unblocked by iteration 34's HMAC, but HMAC authenticates a token and cannot mint one — there is no RNG anywhere in the runtime. Then cookies (absent both ways; `Resp.headers` being a map cannot carry two `Set-Cookie` lines), then limiter/idempotency (cheapest wins — `@table` + `time.ticks`, nothing new), sessions, CSRF, and the routing/response sugar. Streaming/SSE/compression, `@derive` binding, TTL cache, `proxy` and metrics all excluded with owners named | | 39 | [Web framework parity](language-runtime-database/39-web-framework-parity.md) | ⬜ off-chain, needs a spec — from [the Fiber v3.5.0 study](../plan/exploration/fiber/00-fiber-parity.md) (all 32 of its middleware read against `porch`; **nine already have a counterpart**). Leads with a **random-bytes builtin**: the framework ledger claimed CSRF/sessions were unblocked by iteration 34's HMAC, but HMAC authenticates a token and cannot mint one — and while the runtime now has a `getrandom(2)` source internally (runtime-v2 9's TLS ephemeral keys), nothing exposes it to `.wo` yet. Then cookies (absent both ways; `Resp.headers` being a map cannot carry two `Set-Cookie` lines), then limiter/idempotency (cheapest wins — `@table` + `time.ticks`, nothing new), sessions, CSRF, and the routing/response sugar. Streaming/SSE/compression, `@derive` binding, TTL cache, `proxy` and metrics all excluded with owners named |
| 40 | [Shutdown drain guarantee](language-runtime-database/40-shutdown-drain-guarantee.md) | ✅ **LANDED 2026-08-27 — chain 3, with 31; split out of 24.** One rule: **a message sent before the stop flag is observed must be delivered and run before the engine stops.** Found by measurement, not review: making the chat gate's drain leg start its OWN (cold) server exposed that **5 of 16** fresh-server SIGTERM drains left a WebSocket client at EOF with no close frame and no diagnostic. Traced to `shard_main` — `NEXT_RUNNABLE()` already stated the contract ("a WORKER on stop keeps DRAINING … close frames!") but the IDLE branch reaped and broke, abandoning its inbox for teardown to free. An actor between messages is exactly that idle case, which is why a WARM soak server hid it for so long. Fix is one branch honouring the primary's drain window, yielding on an empty poll. **20 of 20 clean after**; `just chat` 11 checks 0 failures at the full 1000-client soak (which also settled the fd question: 1000 connections left the count at 44); runtime battery 36 suites 0 fail, compiler 556 checks 0 fail. Ruled out: a bigger spin (a 1 s wall-clock deadline still failed 2 of 12) and spawn-during-shutdown. Outstanding: a pin below the gate — nothing in `runtime/test/` drives the engine start/stop and no corpus fixture can trigger a stop | | 40 | [Shutdown drain guarantee](language-runtime-database/40-shutdown-drain-guarantee.md) | ✅ **LANDED 2026-08-27 — chain 3, with 31; split out of 24.** One rule: **a message sent before the stop flag is observed must be delivered and run before the engine stops.** Found by measurement, not review: making the chat gate's drain leg start its OWN (cold) server exposed that **5 of 16** fresh-server SIGTERM drains left a WebSocket client at EOF with no close frame and no diagnostic. Traced to `shard_main` — `NEXT_RUNNABLE()` already stated the contract ("a WORKER on stop keeps DRAINING … close frames!") but the IDLE branch reaped and broke, abandoning its inbox for teardown to free. An actor between messages is exactly that idle case, which is why a WARM soak server hid it for so long. Fix is one branch honouring the primary's drain window, yielding on an empty poll. **20 of 20 clean after**; `just chat` 11 checks 0 failures at the full 1000-client soak (which also settled the fd question: 1000 connections left the count at 44); runtime battery 36 suites 0 fail, compiler 556 checks 0 fail. Ruled out: a bigger spin (a 1 s wall-clock deadline still failed 2 of 12) and spawn-during-shutdown. Outstanding: a pin below the gate — nothing in `runtime/test/` drives the engine start/stop and no corpus fixture can trigger a stop |
| 37 | [wo-html components](language-runtime-database/37-wo-html-components.md) | ✅ off-chain — LANDED 2026-08-25. Raw text literal (backtick, margin stripped at lex time, `{{ }}` auto-escapes) + the component layer: `Component`/`render_all`/`Layout` in wo-html, `ok_html` moved into the framework, site and shop both migrated | | 37 | [wo-html components](language-runtime-database/37-wo-html-components.md) | ✅ off-chain — LANDED 2026-08-25. Raw text literal (backtick, margin stripped at lex time, `{{ }}` auto-escapes) + the component layer: `Component`/`render_all`/`Layout` in wo-html, `ok_html` moved into the framework, site and shop both migrated |
| 35 | [net runtime seams](language-runtime-database/35-net-runtime-seams.md) | ⬜ off-chain — fd deadlines on the park plane, Unix sockets, peer address; owns the ledger's three 🔧 rows (story written 2026-08-22) | | 35 | [net runtime seams](language-runtime-database/35-net-runtime-seams.md) | ⬜ off-chain — fd deadlines on the park plane, Unix sockets, peer address; owns the ledger's three 🔧 rows (story written 2026-08-22) |

View file

@ -14,13 +14,14 @@ frontmatter is the only place state lives, no directory encodes it.
## The problem, stated once ## The problem, stated once
An assistant's whole job is to reach a model, and the runtime cannot reach An assistant's whole job is to reach a model. When this track was written
anything outbound. It learned to *listen* — sockets in 8/11/35, unix sockets and (2026-09-07) the runtime could not reach anything outbound — it had learned to
peer address in 35 and runtime-v2 — but it has never learned to *dial*: there is *listen* (sockets in 8/11/35, unix sockets and peer address in 35 and runtime-v2)
no `net.connect` (outbound TCP), confirmed against `runtime/src/wob.h` (the net but never to *dial*: no `net.connect`, no outbound TLS. **Both gaps are now
builtins stop at listen/accept/read/write plus the unix-socket client), and no closed** (confirmed against `runtime/src/wob.h`): `net.connect` (id 110, TCP) and
outbound TLS client anywhere. An LLM API is HTTPS on a remote host. jarvis the full TLS 1.3 client `net.connect_tls`/`net.read_tls`/`net.write_tls` (ids
therefore does not begin until two things exist. 115–117, runtime-v2 9, live-gated) — plus inbound `net.accept_tls` (118) for porch.
An LLM API is HTTPS on a remote host, and the runtime can now dial it directly.
The design chosen is **direct outbound HTTPS** — jarvis dials the LLM API The design chosen is **direct outbound HTTPS** — jarvis dials the LLM API
itself, keeping the pure single-binary story. That gates the whole track on itself, keeping the pure single-binary story. That gates the whole track on
@ -49,7 +50,7 @@ owns its own connection rather than shipping a second executable.
## Architecture ## Architecture
browser ⇄ jarvis (a porch app) ⇄ net.connect ✅ + TLS (rv2 9, in progress) ⇄ LLM API browser ⇄ jarvis (a porch app) ⇄ net.connect_tls ✅ (rv2 9, done) ⇄ LLM API
Requests arrive at a porch web app; the answer streams the other way, token by Requests arrive at a porch web app; the answer streams the other way, token by
token, LLM → jarvis → browser, over porch's SSE. Conversation state is durable token, LLM → jarvis → browser, over porch's SSE. Conversation state is durable
@ -108,7 +109,7 @@ Blockers, which must land before iteration 1 starts:
| local, in-process model inference | needs an ML runtime and heavy FFI — against the no-external-dependency doctrine | | local, in-process model inference | needs an ML runtime and heavy FFI — against the no-external-dependency doctrine |
| the local-gateway companion process | considered and rejected (above) in favour of the single-binary story | | the local-gateway companion process | considered and rejected (above) in favour of the single-binary story |
| voice / audio in or out | a separate surface with its own capture and codec story; no rung asks for it | | voice / audio in or out | a separate surface with its own capture and codec story; no rung asks for it |
| starting before the blockers land | like [porch 9](../porch/09-idempotent-replay.md) waiting on language 41, jarvis waits on the outbound seam — documented, not worked around | | starting before porch is complete | the runtime blockers (`net.connect`, TLS, language 41) have all landed; what jarvis now waits on is the **framework** — the developer set jarvis to follow porch completion (see Sequencing) — documented, not worked around |
## Review protocol ## Review protocol

View file

@ -27,7 +27,9 @@ readiness: ready
> from `.wo`. This unblocked chain position 4: the WebSocket handshake needs > from `.wo`. This unblocked chain position 4: the WebSocket handshake needs
> SHA-1, and `just chat` verifies the accept-key independently. > SHA-1, and `just chat` verifies the accept-key independently.
> >
> **The gap it did NOT close:** there is still no RNG in the runtime. HMAC > **The gap it did NOT close:** no RNG reaches `.wo` (the runtime gained an
> internal `getrandom(2)` source with runtime-v2 9's TLS, but no builtin exposes
> it — porch 2 / iteration 39's `random_bytes` does that). HMAC
> authenticates a token and cannot mint one, so CSRF and sessions stay blocked > authenticates a token and cannot mint one, so CSRF and sessions stay blocked
> — which is why [39](39-web-framework-parity.md) leads with a random-bytes > — which is why [39](39-web-framework-parity.md) leads with a random-bytes
> builtin rather than treating them as unblocked. > builtin rather than treating them as unblocked.

View file

@ -72,7 +72,7 @@ risky work starts.
| --- | --- | | --- | --- |
| typed binding of query/params/form into a class | language: [`@derive`](../language-runtime-database/29-compile-time-metaprogramming.md) — reflection is forbidden by principle 13 | | typed binding of query/params/form into a class | language: [`@derive`](../language-runtime-database/29-compile-time-metaprogramming.md) — reflection is forbidden by principle 13 |
| TTL cache, `transaction { }`, durable job queue | language: [iteration 18](../language-runtime-database/18-memory-db-features.md) | | TTL cache, `transaction { }`, durable job queue | language: [iteration 18](../language-runtime-database/18-memory-db-features.md) |
| a `proxy` middleware | language: [iteration 38](../language-runtime-database/38-content-platform-capabilities.md) — needs `net.connect`, which does not exist | | a `proxy` middleware | language: [iteration 38](../language-runtime-database/38-content-platform-capabilities.md) — needs `net.connect`, ✅ landed 2026-09-07 (id 110; plus `net.connect_tls` for an HTTPS upstream, runtime-v2 9). Buildable now |
| metrics, profiling, per-change CI, fuzzing | [runtime-v2 7](../runtime-v2/07-observability.md) — observability (was language iteration 30; metrics/profiling/trace-on-trap; CI + fuzz are tooling, split out) | | metrics, profiling, per-change CI, fuzzing | [runtime-v2 7](../runtime-v2/07-observability.md) — observability (was language iteration 30; metrics/profiling/trace-on-trap; CI + fuzz are tooling, split out) |
| TLS | ✅ [runtime-v2 9](../runtime-v2/09-in-process-tls.md) — in-process TLS 1.3 both directions (2026-09-09); porch can terminate inbound TLS with `net.accept_tls`, no front proxy required. The proxy-termination doctrine is retired | | TLS | ✅ [runtime-v2 9](../runtime-v2/09-in-process-tls.md) — in-process TLS 1.3 both directions (2026-09-09); porch can terminate inbound TLS with `net.accept_tls`, no front proxy required. The proxy-termination doctrine is retired |
| HTTP/2 | nobody yet — a separate protocol slice; TLS is its prerequisite, now met | | HTTP/2 | nobody yet — a separate protocol slice; TLS is its prerequisite, now met |

View file

@ -5,12 +5,22 @@ status: done
readiness: ready readiness: ready
--- ---
# porch 1 — store-backed middleware: rate limiting and idempotency # porch 1 — store-backed middleware: rate limiting
> Part of [Story — `porch`, the writeonce web framework](00-story.md). > Part of [Story — `porch`, the writeonce web framework](00-story.md).
> Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §2. > Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §2.
> Spec: [`2026-08-29-porch-store-backed-middleware-design.md`](../../superpowers/specs/2026-08-29-porch-store-backed-middleware-design.md). > Spec: [`2026-08-29-porch-store-backed-middleware-design.md`](../../superpowers/specs/2026-08-29-porch-store-backed-middleware-design.md).
> >
> **Re-scoped 2026-08-30 — this iteration is now RATE LIMITING ONLY.**
> Idempotency was built, reviewed and reverted; it moved to
> [porch 9](09-idempotent-replay.md); the C-runtime crash that blocked it
> ([language 41](../language-runtime-database/41-actor-arena-crash.md)) was
> fixed 2026-09-09, so porch 9 is now buildable.
> The split was made because the limiter is provably stable (five consecutive
> gate runs, 56 checks, 0 failures) while idempotency's gate flaked on a
> runtime defect — and a feature whose test passes some of the time is not
> shipped. Detail in History.
>
> **Rewritten 2026-08-29** after the brainstorm settled every fork. The > **Rewritten 2026-08-29** after the brainstorm settled every fork. The
> iteration's premise changed: it was scoped as the cheapest slice because it > iteration's premise changed: it was scoped as the cheapest slice because it
> needed "only a `@table` and `time.ticks`", and it now serializes through an > needed "only a `@table` and `time.ticks`", and it now serializes through an
@ -63,14 +73,15 @@ globally.
| Phase | State | | Phase | State |
| --- | --- | | --- | --- |
| A — store convention | ✅ `519d411` two purpose-shaped tables; `3a9bddc` added the digest column the refusal criterion needs | | A — store convention | ✅ `519d411`, `3a9bddc` — two purpose-shaped tables plus the digest column. `IdempotencyKey` is retained but unused; the file says why |
| B — rate limiter | ✅ `676e651` the shared key-pool actor (also C's foundation), `153fd29` its `reset_at` unit fix; `a653dd0` the limiter delegates all counting to the pool, `831e9d8` `trust_proxy`'s absent-XFF fallback fix | | B — rate limiter | ✅ `676e651`, `153fd29` (the pool), `a653dd0`, `831e9d8` (the limiter). Exact counting under 30 parallel clients, restart-durable, `trust_proxy` off by default |
| C — idempotency | ✅ `eae1b06` rebuilt on the same pool actor (the actor runs the route's `Handler` itself); three fix rounds: `e61015f` never replay a transient 5xx, `464147a` close the ephemeral-row race, `9ad5947` delete the ephemeral row after one read | | C — idempotency | ⏸ **moved to [porch 9](09-idempotent-replay.md).** Built and reviewed (`eae1b06`…`9ad5947`, plus the final wave), then reverted — see History |
| D — gate and ledger | ✅ + this commit — the pool-saturation gate leg (§19, `scripts/web-app-accept.sh`), README ledger rows to ✅, pool-size capacity docs, `make_pool`'s mod-by-zero guard | | D — gate and ledger | ✅ `21934b1`, `2ac1b8b`, and the re-scope commit. 56 checks, 0 failures, stable across five consecutive runs |
Superseded pre-rewrite commits (`5b1e82a`, `aee7926`, `5c3544d`) are kept in **What the split bought.** Before it the suite reported anywhere from 0 to 6
History below rather than deleted — the reasoning for the rebuild survives failures run to run; after it, five consecutive runs at 56/0. The limiter was
there. finished either way — it was being held hostage by a defect in code it does not
call.
## Phases ## Phases

View file

@ -30,8 +30,11 @@ randomness and cookies maps onto primitives writeonce already has.
- **Randomness is the one gap.** fiber's `SecureToken` is - **Randomness is the one gap.** fiber's `SecureToken` is
`base64.RawURLEncoding` over 32 `crypto/rand` bytes; `UUIDv4` is the same `base64.RawURLEncoding` over 32 `crypto/rand` bytes; `UUIDv4` is the same
entropy behind a format; `encryptcookie.GenerateKey` is a raw `rand.Read`. entropy behind a format; `encryptcookie.GenerateKey` is a raw `rand.Read`.
All three **panic** if the source fails. writeonce has no RNG at all. This is All three **panic** if the source fails. writeonce exposes no RNG to `.wo`
phase A, and it is the whole of the language work. yet — the runtime does have a `getrandom(2)` source internally (runtime-v2 9's
TLS uses it for ephemeral keys), so phase A is surfacing that as a
`random_bytes` builtin, not inventing entropy. It is the whole of the language
work.
- **Repeated `Set-Cookie` is not language work.** fiber gets multiple lines from - **Repeated `Set-Cookie` is not language work.** fiber gets multiple lines from
fasthttp appending them; porch expresses the same with a `multi SetCookie` fasthttp appending them; porch expresses the same with a `multi SetCookie`
field, and `multi <Class>` is an existing language feature. field, and `multi <Class>` is an existing language feature.

View file

@ -63,9 +63,10 @@ GC pauses) live in the C runtime, not in `.wo`.
an `expvar`-style JSON blob, or both. The format decides who can consume it an `expvar`-style JSON blob, or both. The format decides who can consume it
without a translator. without a translator.
3. **Pull endpoint or push.** A mounted `/metrics` endpoint (pull) fits the 3. **Pull endpoint or push.** A mounted `/metrics` endpoint (pull) fits the
proxy-fronted, single-binary model; a push to a collector needs single-binary model; a push to a collector needs `net.connect` — which now
`net.connect`, which does not exist (iteration 38) — so pull is almost exists (id 110, landed 2026-09-07; `net.connect_tls` for an HTTPS collector,
certainly the answer, but say so. runtime-v2 9) — so push is possible, but pull is still the simpler default;
say which.
4. **Is stack-trace-on-trap in this iteration at all?** It is separable, it is 4. **Is stack-trace-on-trap in this iteration at all?** It is separable, it is
the highest debugging value per line, and it touches the trap path rather than the highest debugging value per line, and it touches the trap path rather than
the metrics path — a candidate to land first and alone. the metrics path — a candidate to land first and alone.

View file

@ -418,9 +418,12 @@ need SHA-256, and that is the whole of the demand so far.
Correctness is pinned to the published vectors rather than to itself: Correctness is pinned to the published vectors rather than to itself:
RFC 3174 for SHA-1, the FIPS/RFC 6234 vectors for SHA-256, RFC 4231 for HMAC, RFC 3174 for SHA-1, the FIPS/RFC 6234 vectors for SHA-256, RFC 4231 for HMAC,
in `runtime/test/test_crypto.c` (18 checks). **There is still no RNG anywhere in `runtime/test/test_crypto.c` (18 checks). ~~There is still no RNG anywhere in
in the runtime** — HMAC authenticates a token but cannot mint one, which is why the runtime~~ — **corrected 2026-09-09:** the runtime now has a `getrandom(2)`
iteration 39 leads with a random-bytes builtin. source (`tls_rand` in `sysio.c`, used for TLS ephemeral keys, ClientHello
randomness and RSA-PSS salts), but nothing exposes it to `.wo` yet — HMAC still
cannot mint a token from the language, which is why iteration 39 / porch 2 lead
with a `random_bytes` builtin that surfaces this source.
## Hand-rolled TLS 1.3 client (runtime-v2 9, ids 115–117) ## Hand-rolled TLS 1.3 client (runtime-v2 9, ids 115–117)