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
replace, truncate, delete or rename one; and there is no `net.connect` anywhere
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
P7["porch 7 SSE + compression"]:::ready
P8["porch 8 static files + lifecycle"]:::ready
P9["porch 9 idempotent replay (⏸ hold; readiness: ready)"]:::held
L41["language 41 actor-arena hang (in-progress)"]:::held
P9["porch 9 idempotent replay (ready — unblocked 2026-09-09)"]:::ready
L41["language 41 actor-arena double free ✅ fixed 63065ff (cross-shard marshal)"]:::done
RB --> P2
P2 --> P3
@ -315,7 +315,7 @@ flowchart TD
P5 --> P8
DFL --> P7
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
```
@ -426,58 +426,78 @@ transport.
## 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
writeonce. Only **jarvis 1** (the chat loop) is close to startable; it sits on
two chains — the **outbound HTTPS path** (the real blocker, mostly runtime-v2 9's
TLS ladder) and the **framework path** (porch, all `ready`). Phase-level, because
the TLS ladder is where the waiting actually happens.
writeonce. **The runtime side is done** — the whole outbound HTTPS path landed
2026-09-09 (runtime-v2 9, both directions, live-gated) and language 41 is fixed.
What jarvis 1 waits on now is purely the **framework**: the developer set the
order *porch first, then jarvis*. So this graph is the porch→jarvis chain.
```mermaid
flowchart TD
classDef done fill:#1a7f37,color:#fff,stroke:none
classDef ready fill:#0969da,color:#fff,stroke:none
classDef refine fill:#eac54f,color:#000,stroke:none
classDef held fill:#6e7781,color:#fff,stroke:none
NC["net.connect (id 110) ✅"]:::done
A["rv2 9 A — AEAD ✅ (= rv2 8 A–C: ChaCha20-Poly1305 + AES-GCM)"]:::done
B["rv2 9 B — HKDF ✅"]:::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
TLS["rv2 9 in-process TLS 1.3 ✅ — net.connect_tls / read_tls / write_tls (115–117), net.accept_tls (118)"]:::done
L41["language 41 cross-shard marshal ✅"]:::done
WOHTML["wo-html / writeonce-view ✅"]:::done
J1["jarvis 1 — the chat loop (unwritten)"]:::refine
J2["jarvis 2 — tool use / agent loop"]:::refine
J3["jarvis 3 — retrieval (RAG) + embeddings/vector sub-gap"]:::refine
RB["random_bytes builtin (porch 2 phase A / lang 39) — surfaces the runtime's getrandom"]:::ready
P2["porch 2 randomness + cookies (ready)"]:::ready
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
A --> F
B --> F
C --> D
D --> E
E --> F
F --> J1
J1["jarvis 1 — the chat loop (ready; forks auto-approved, review_pending)"]:::ready
J2["jarvis 2 — tool use / agent loop (refine)"]:::refine
J3["jarvis 3 — retrieval (RAG) (refine)"]:::refine
NC --> TLS
RB --> P2
P2 --> P3
P2 --> P4
P3 --> P4
P5 --> P7
P5 --> P8
P6 --> P7
P6 --> P8
L41 --> P9
TLS --> J1
P2 --> J1
P3 --> J1
P4 -. CSRF-protects the POST once built .-> J1
P6 --> J1
P7 --> J1
WOHTML --> J1
J1 --> J2
J1 --> J3
```
The outbound path is the critical one: **A/B/C/D landed, E→F remain** (G is
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 itself is not yet written; jarvis 2/3 follow it.
**jarvis 1's dependency list, from the code and the stories (2026-09-09):**
| jarvis 1 needs | for | state |
| --- | --- | --- |
| `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

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
eviction and parked idle keep-alive are gate-proven. The plain
`serve()` stays single-threaded for simple apps.
- **TLS: none, anywhere.** Deploy behind nginx/caddy; the proxy terminates
TLS+ALPN and gives browsers HTTP/2 while this backend speaks HTTP/1.1
keep-alive. See the web-app sample's README for the nginx sketch.
- **TLS: available in the runtime, not used by this sample yet.** Since
runtime-v2 9 (2026-09-09) the runtime terminates TLS 1.3 itself —
`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
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`
@ -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` |
| 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 |
| 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
None here, deliberately: deploy behind nginx/caddy — the proxy terminates
TLS+ALPN and speaks h2 to browsers while this backend serves HTTP/1.1
keep-alive. Sketch:
This sample runs plaintext behind nginx/caddy — the proxy terminates TLS+ALPN
and speaks h2 to browsers while this backend serves HTTP/1.1 keep-alive. The
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 {
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`
language object, connection pooling, close_notify on shutdown, complete-formula
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>
@ -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
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 —
then jarvis 1–3. porch's own blocker is language 41's marshal fix (unblocks
[porch 9](porch/09-idempotent-replay.md)); the porch track (2–8) is brainstormed
then jarvis 1–3. porch's former blocker — language 41's marshal fix — landed
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`,
`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) |
| 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 |
| 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 |
| 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) |

View file

@ -14,13 +14,14 @@ frontmatter is the only place state lives, no directory encodes it.
## The problem, stated once
An assistant's whole job is to reach a model, and the runtime cannot reach
anything outbound. It learned to *listen* — sockets in 8/11/35, unix sockets and
peer address in 35 and runtime-v2 — but it has never learned to *dial*: there is
no `net.connect` (outbound TCP), confirmed against `runtime/src/wob.h` (the net
builtins stop at listen/accept/read/write plus the unix-socket client), and no
outbound TLS client anywhere. An LLM API is HTTPS on a remote host. jarvis
therefore does not begin until two things exist.
An assistant's whole job is to reach a model. When this track was written
(2026-09-07) the runtime could not reach anything outbound — it had learned to
*listen* (sockets in 8/11/35, unix sockets and peer address in 35 and runtime-v2)
but never to *dial*: no `net.connect`, no outbound TLS. **Both gaps are now
closed** (confirmed against `runtime/src/wob.h`): `net.connect` (id 110, TCP) and
the full TLS 1.3 client `net.connect_tls`/`net.read_tls`/`net.write_tls` (ids
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
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
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
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 |
| 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 |
| 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

View file

@ -27,7 +27,9 @@ readiness: ready
> from `.wo`. This unblocked chain position 4: the WebSocket handshake needs
> 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
> — which is why [39](39-web-framework-parity.md) leads with a random-bytes
> 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 |
| 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) |
| 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 |

View file

@ -5,12 +5,22 @@ status: done
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).
> 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).
>
> **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
> 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
@ -63,14 +73,15 @@ globally.
| Phase | State |
| --- | --- |
| A — store convention | ✅ `519d411` two purpose-shaped tables; `3a9bddc` added the digest column the refusal criterion needs |
| 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 |
| 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 |
| 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 |
| 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`, `153fd29` (the pool), `a653dd0`, `831e9d8` (the limiter). Exact counting under 30 parallel clients, restart-durable, `trust_proxy` off by default |
| 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 | ✅ `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
History below rather than deleted — the reasoning for the rebuild survives
there.
**What the split bought.** Before it the suite reported anywhere from 0 to 6
failures run to run; after it, five consecutive runs at 56/0. The limiter was
finished either way — it was being held hostage by a defect in code it does not
call.
## 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
`base64.RawURLEncoding` over 32 `crypto/rand` bytes; `UUIDv4` is the same
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
phase A, and it is the whole of the language work.
All three **panic** if the source fails. writeonce exposes no RNG to `.wo`
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
fasthttp appending them; porch expresses the same with a `multi SetCookie`
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
without a translator.
3. **Pull endpoint or push.** A mounted `/metrics` endpoint (pull) fits the
proxy-fronted, single-binary model; a push to a collector needs
`net.connect`, which does not exist (iteration 38) — so pull is almost
certainly the answer, but say so.
single-binary model; a push to a collector needs `net.connect` — which now
exists (id 110, landed 2026-09-07; `net.connect_tls` for an HTTPS collector,
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
the highest debugging value per line, and it touches the trap path rather than
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:
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 the runtime** — HMAC authenticates a token but cannot mint one, which is why
iteration 39 leads with a random-bytes builtin.
in `runtime/test/test_crypto.c` (18 checks). ~~There is still no RNG anywhere in
the runtime~~ — **corrected 2026-09-09:** the runtime now has a `getrandom(2)`
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)