- defensive ASN.1/DER reader: every length/bound checked; malformation
is rejection, never over-read (truncated input KAT-gated)
- x509_parse: tbsCertificate span, sig-alg OID, signature,
SubjectPublicKeyInfo (RSA n/e or EC P-256 x/y), validity dates
- wo_x509_verify_one: one chain link's signature, dispatching to
phase-D RSA-PKCS1/PSS + ECDSA-P256 by the issuer key type
- wo_x509_parse_spki + wo_x509_check_validity (caller supplies time)
- KAT against real python-generated chains (test/gen_x509.py):
RSA CA+leaf (SHA256withRSA), EC P-256 CA+leaf (ecdsa-with-SHA256);
leaf-vs-CA, self-signed CA, wrong-issuer/tampered/truncated reject,
validity window, SPKI extraction. test_crypto 84 pass, ASan/UBSan clean
- deferred to phase F: SAN/hostname match + multi-cert chain walk to a
system CA bundle (both need the target host / trust store, known at
handshake time)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 4ec1c75f889df3612e31b9a1a77c4c927fb546c1)
- jarvis 1 (chat loop) brainstormed to ready with forks AUTO-APPROVED for
autonomous execution and flagged in `review_pending` frontmatter for the
developer's second review: Anthropic Messages API backend, env-var API key,
actor-per-conversation SSE relay, durable @table history, session-gated routes
- jarvis 2 (tool use) + 3 (retrieval/RAG) created at refine with forks named
- 00-story iterations table linked to the new files
- blocked until rv2 9 TLS reaches phase F; pure .wo on porch 2/3/6/7 + the seam
(cherry picked from commit 8e160c3fcf9c50a05af073c036c693b192c9e595)
- wo_ecdsa_p256_sha256_verify: NIST P-256 signature verify for EC-cert chains
and TLS 1.3 CertificateVerify
- Jacobian point arithmetic (double a=-3, general add with the H==0 special
cases), double-and-add scalar mult; field/scalar arithmetic reuses the
bignum Montgomery multiply and modexp (Fermat inverses mod p and mod n)
- validates r,s in [1,n-1] and that Q is on the curve (invalid-curve guard)
- verify-only public data -> not constant-time by design
- renamed the P-256 field mul to fpmul to avoid the clash with X25519's fmul
- VERIFIED against a python ECDSA-P256 vector; tamper + wrong-hash rejected;
test_crypto 69/0; ASan/UBSan clean; battery green
- phase D COMPLETE (RSA PKCS1+PSS + ECDSA-P256). Next E: ASN.1/X.509
(cherry picked from commit 92c996ba96d785d098c173d4ac6ace9052ef6a2f)
- wo_rsa_pkcs1_sha256_verify + wo_rsa_pss_sha256_verify (SHA-256), for the
server cert chain and TLS 1.3 CertificateVerify
- bignum: Montgomery multiply (CIOS, 64-bit limbs, __int128), modexp with the
public exponent (R^2 via 128k modular doublings, no division); MGF1-SHA256
- verification is public data only -> NOT constant-time by design (correct and
much simpler than a private-key op)
- assumes a full-length modulus for PSS emBits (standard RSA-2048/3072/4096)
- VERIFIED against python cryptography RSA-2048 vectors (PKCS#1 v1.5 + PSS,
salt 32); tamper + wrong-hash rejected; test_crypto 66/0; ASan/UBSan clean;
battery green
- internal C, no builtin/compiler change. Remaining in D: ECDSA-P256 (D2)
(cherry picked from commit 9118177fbfd03eff9757defea6931afdd68830c4)
- wo_x25519: constant-time Montgomery ladder + mask-based conditional swap,
radix-2^51 field arithmetic with __int128 products (curve25519-donna-c64,
public domain); scalar clamped, u-coord high bit masked per RFC 7748
- internal C (consumer is the TLS ECDHE handshake); no builtin/compiler change
- KAT-gated in test_crypto: RFC 7748 §5.2 both direct vectors AND the
1000-iteration base-point test; test_crypto 61/0; ASan/UBSan clean; battery green
- fixed one transcription bug found via the KAT: crecip needs 5 final squarings
(p-2 = 2^255-21 = (2^250-1)*2^5 + 11), not 3
- rv2 9 ladder: A (AEAD) + B (HKDF) + C (X25519) done; next D signatures/RSA
(cherry picked from commit f41b1c5f56caa841d1904382830baff0f75525d9)
- wo_hkdf_sha256_extract/expand (RFC 5869) + expand_label (RFC 8446 §7.1),
internal C over the existing hmac_sha256; SHA-256 (mandatory-suite hash;
SHA-384 a later add for the AES-256 suite)
- no builtin, no compiler change -- no .wo consumer yet (the TLS handshake
is the consumer); exposed for the C unit test
- KAT-gated in test_crypto: RFC 5869 Test Case 1 (PRK + 42-byte OKM) and
three HKDF-Expand-Label vectors (key/iv/derived-secret shape); 57/0,
ASan/UBSan clean; runtime battery green
- rv2 9 ladder: A (AEAD, = rv2 8) and B (HKDF) now done; next C X25519
(cherry picked from commit c8d27b6c89a80cd97a996ff7d8b64ff4895e4b26)
- portable constant-time AES-GCM software path; AES-GCM now on any CPU
(hw-or-sw dispatch), NIST-KAT-gated both paths (48/0). Remaining D/E;
ARMv8 hw path deferred. Board synced
(cherry picked from commit 138de17988e6bd274abe5e1e2d444ff2689747cd)
- no-intrinsics AES: S-box = GF(2^8) inverse via a fixed-exponent power ladder
(constant-time in the input, no tables), constant-time gf8_mul, byte-oriented
ShiftRows/MixColumns/key-expansion (AES-128 and AES-256)
- constant-time GHASH: bit-by-bit GF(2^128) multiply (mask-driven, no tables)
- aes_gcm_seal/open now dispatch: AES-NI path when present (and not forced
software), else this portable fallback -> AES-GCM works on ANY CPU, so the
phase-B no-AES-NI trap is retired
- wo_aes_force_software test hook; both hw and sw paths verified against NIST
SP 800-38D cases 4 (AES-128) and 16 (AES-256) byte-for-byte; test_crypto 48/0;
ASan/UBSan clean; full runtime battery green
- ARMv8 crypto-extension hardware path deferred (untestable on x86-64 host)
(cherry picked from commit dccf650899798401a9adac8489f34c85ed9304af)
- phases A + B done; B = AES-128/256-GCM via AES-NI/PCLMULQDQ, NIST-KAT-gated,
ASan clean, portable binary (CPUID-gated). Phase C now owns the software
fallback AND the ARMv8 hardware path (deferred, untestable on x86-64 host)
(cherry picked from commit 249b1dbd72122ed6c05f4f0aadb7e1a5e87f844c)
- a second consumer (rv2 9 TLS phase A) reshaped the forks since the draft
- locked: BOTH AES-GCM (128/256, TLS-mandatory per RFC 8446) AND
ChaCha20-Poly1305 (RFC 8439, easy constant-time, cookie default)
- AES constant-time via AES-NI/ARMv8 hardware + bitsliced software fallback
(compiler intrinsics, zero external dep)
- caller-supplied nonce (TLS builds its own per-record nonce); random-nonce
is a cookie WRAPPER (phase D) not the primitive. shape:
seal(key,nonce,aad,pt)->Bytes / open->?Bytes; AES variant by key length
- raw key + length check; hand-rolled (matches rv2 9); ids from 111
- phases A ChaCha -> B hardware AES-GCM -> C software AES -> D cookie wrapper
-> E gate (RFC 8439 + NIST GCM vectors, ASan, reference cross-check)
- risk/test: constant-time mandatory, KAT-gated, reused-nonce documented
- retires the stale "TLS proxy-terminated" OOS line (rv2 9 overturned it)
(cherry picked from commit c8a5a31c39a5d14952056cd0af1b8c1e42d4ed2d)
- decision: HAND-ROLL TLS 1.3 (no vendored lib) per developer call; keeps the
zero-external-dep single binary, and raises risk rather than lowering it —
recorded, owned, with mandatory mitigations
- 1.3-only; RSA-PSS/PKCS1 + ECDSA-P256 + full ASN.1/X.509 chain validation +
trust store + hostname (the scope needed to reach real LLM APIs)
- decomposed into a bottom-up phase ladder: A AEAD (=rv2 8, forces AES-GCM
there) -> B HKDF -> C X25519 -> D signatures/RSA -> E X.509 -> F record+FSM
client -> G inbound server; C/D/E may each split into own iterations
- risk + test strategy section: constant-time, reference-tested (openssl +
RFC 8448 vectors), negative tests first-class, no partial-trust states
- deps: rv2 8 (AEAD), lang 34 (SHA/HMAC), net.connect (110, landed). Board synced
(cherry picked from commit f1881cca8cd0cdd58b1ac844e3e3ea9c234bb99c)
- jarvis (00-story): 6th track, 2nd software built with writeonce — an AI
assistant; direct-HTTPS design; blockers named (net.connect + TLS)
- runtime-v2 7 observability + 8 symmetric cipher: moved from the language
track (were 30/43); 9 in-process TLS: created from the gap jarvis surfaces,
RETIRES the "TLS is the proxy's job" doctrine (both directions)
- language 41 (arena hang): fix design to ready — marshal cross-shard
messages (root), align the shard_id % nshards route/compare + assert bound;
poison-on-free + minimal fixture as follow-ups
- fiber scope-gap analysis (plan/exploration/fiber/01): porch vs fiber, what
porch lacks, would developers prefer porch
- board + dependency-graph synced (porch 2-8 ready; rv2 table; §5/§5a graphs)
(cherry picked from commit 203470ceb2a151fe3584931cd4237af3f96a9f29)
- new builtin net.connect(host, port) -> Int: the outbound-socket gap
language 38 named and jarvis surfaced; the client half of the net verbs
- getaddrinfo for DNS (v4/v6, numeric or hostname), blocking connect with
the same EINTR/stop handling as net.connect_unix, then O_NONBLOCK for the
park plane; returns the same fd-scalar accept yields
- wob.h enum + WO_B_MAX 110; types.ml registration; loader.c arity;
builtin.c sysio dispatch range extended to WO_B_NET_CONNECT; sysio.c impl
- verified: numeric IP + hostname (DNS) connect to a local listener, closed
port traps cleanly; ASan-clean; runtime battery 0 fail
- deferred (next slice): net.connect_dl deadline/park variant (no shard
stall during handshake), on the accept_dl pattern
(cherry picked from commit 13c6f124428243b4956fbb4eceb1e7d0206d45f2)
- whole porch track (2-8) now brainstormed and locked (all ready)
- four decisions: three hooks (on-listen/on-shutdown/on-route-registered);
healthcheck ships BOTH /livez + /readyz; directory listing off-by-default,
documented; Last-Modified via a new small time.utc(ms)->TimeParts builtin
- language enhancement: YES, one small builtin -- time.utc, a gmtime sibling
of time.local (time.local is local-tz, time.iso is UTC-but-ISO); IMS by
string-equality, no date parser. The track's third + smallest language touch
- byte ranges/large files via fs.read_at + iteration 6 writer; not lang-41-exposed
- track language bill now explicit: random_bytes (2), deflate+crc32 (7),
time.utc (8) -- each a builtin with a named consumer, none decoration
- validated against .dev/reference/fiber. Board: whole track marked ready
(cherry picked from commit 9801fceade799e25718606177f09e4306a579e98)
- five decisions: refuse incoherent heartbeat/idle_ms pair at construction;
codec = two C builtins deflate+crc32 (perf over pure-.wo; hand-rolled, no
zlib dep; gzip framing in .wo); ETag over uncompressed bytes + Vary;
Last-Event-ID explicitly unsupported (not silently ignored); Vary via
comma-join
- language enhancement: YES, two builtins -- the track's SECOND language
dependency after iteration 2's random_bytes. CRC32 finally gets its
consumer; inflate deliberately not built (request-body decompression OOS)
- corrected stale dependency: Vary uses iteration 5's comma-join, so story 7
depends on 6 + 5, NOT 2; codec is pure compute, not lang-41-exposed
- confirmed CRC32 absent + iteration 36 bit operators landed (pure-.wo was
viable, traded for hot-path speed)
- validated against .dev/reference/fiber. Board synced
(cherry picked from commit 07f53574dd90f502235b79d4920eab3d684c8b77)
- re-scoped to OUTBOUND streaming only
- three decisions: separate StreamHandler/BodyProducer parallel path (Resp
path untouched -> existing responses byte-identical); streaming routes opt
out of the after-chain, framework refuses at registration to combine with
header-mutating middleware (loud, never silent), security_headers() helper
lets handlers stamp them; chunked REQUEST bodies split into their own future
iteration (parse.wo refusal stays, smuggling cases enumerated for later)
- no language enhancement (net.write framing, fs.read_at/actor source,
interfaces for producer); rides the fiber loop not the actor pool, so not
lang-41-exposed
- fixed title inconsistency: "three iterations wait on" -> "two" (7 and 8)
- validated against .dev/reference/fiber + the app.wo/serve.wo pipeline. Board synced
(cherry picked from commit 15205408e03c3c02a38e00e5d2017a8a11f62f28)
- five decisions: head auto-registers with opt-out (+ patch/options/all);
request ids mirror limiter trust model with a NON-crypto source; per-route
body_limit is a SECOND check after routing (global BODY_MAX stays the
pre-routing ceiling, over-limit = 413); Route fields are corpus-free;
Vary accumulates by comma-join
- key finding: story 5 has NO upstream dependency, not even iteration 2 --
request ids are not secrets, so a non-crypto source (time.ticks+counter)
keeps it startable today; the one porch slice buildable right now
- three story assumptions corrected: per-route limit cannot replace the
global (body read before routing); the container-owned-move corpus fixture
has its OWN Route (adding fields is free); Vary needs no iteration 2
- validated against .dev/reference/fiber; zero language enhancement. Board synced
(cherry picked from commit 0589a13db1f3b7c220d9d9fdc76142af6a63c390)
sessions (3):
- six decisions: pure-auth-primitive row (no payload bag); wall-clock
time.now not monotonic time.ticks (restart durability); login always
mints a fresh id (fixation, no anon-session model); throttled last_seen
touch at idle/20 (not a WAL write per request); Session writes
req.principal; config refuses absolute < idle
- finding: no per-key actor pool, so NOT blocked on lang-41 (plain @table
CRUD, same path storefront uses); the no-bag rule closes the one place
fiber's Set(key,any)+msgp+RegisterType would have hit principle 13
csrf (4):
- five decisions: fiber's hybrid transport (session-stored CsrfToken
@table + double-submit cookie, both must pass; no CSRF for sessionless
apps); opt-in single-use (checkout example); double-click -> distinct
SPENT refusal, NOT coupled to lang-41-blocked idempotency; trusted
origin/referer/Sec-Fetch-Site second layer; refusal classes distinct in
logs, opaque in body
- no actor pool, not blocked on lang-41
both validated against .dev/reference/fiber (v3, 3ca9a9d); exactly ZERO
language enhancement needed beyond iteration 2's random_bytes. Board synced.
(cherry picked from commit 3a4fb4215b23d2516362e2dd0acc5bec6c9aebc0)
- five forks locked: cookies: multi SetCookie beside unchanged headers
map; bare-name random_bytes(n)->Bytes; structural-400 in parse_request
+ on-demand cookie() helper; base64(value).base64(mac) signing;
app-supplied key, no middleware (that is iteration 3)
- validated against .dev/reference/fiber (v3, 3ca9a9d): exactly ONE
language enhancement needed (the CSPRNG); repeated Set-Cookie, cookie
attributes, parsing and signing all map to existing primitives
- corrects phase A registry: random_bytes joins the crypto-family
bare-name table (emit.ml b_* + types.ml), NOT wob.h's module enum;
next free id 84/90, not 110
- board: story 2 marked ready, porch-2 row rewritten off the stale
wob.h/110 claim
(cherry picked from commit 4d31d5359436496aed40cb25611abc7ccd4d7875)
- wo_schema_diff poisons a class (fmap=NULL, new_cid=NONE) when a
referenced/nested type changed or a field type is incompatible — the
field-level map does not apply and wo_wal_migrate transcodes it instead.
- main.c's pre-migration "migrating `X`: +/-fields" print loop dereferenced
fmap unconditionally, so a poisoned-but-field-added class (e.g. a wmux
Window whose nested Vte gained fields) was a NULL read → SIGSEGV at boot,
before the migrate call could refuse or transcode.
- guard the field detail on fmap != NULL; for a poisoned class print
"(a referenced type changed — cannot migrate in place)" and let
wo_wal_migrate proceed. It then transcodes cleanly when no record blocks
it — so an additive nested change (Vte +oscbuf +title) now migrates and
the session replays, instead of crashing serve.
- test_wal 5966/0; verified against the real WAL that crashed (recovers
session `main`); wmux gate 52/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 35efa214d20dd7b055910ae66e4bfcc6201821c9)
- five stories status: done; 00-story records the one-run landing
- spec History: three implementation amendments (Signal record not
scalar, caller-owned stdio fds, handler-latch instead of signalfd)
- board NEXT PLAN entry with measured findings (zero transport code
added; the tty-across-the-socket handover proven; the double-raw
refusal restoring the terminal — the "bug" that was the design
working); section rows flipped; graph nodes green
- CODE-LOGIC.md: the runtime-v2 section
- full belt quoted on the board: suites 0 fail both flavors (test_proc
193/0, test_term 60/0), woc 557/0, subprocess 12/0, site 23/0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit bc1b4f070693eb755ad6a9fd0c853fb3e2bda347)
- sendmsg/recvmsg with one SCM_RIGHTS fd and a sentinel byte; EAGAIN
parks in the net mould; the received fd arrives nonblocking as a plain
Int every fd verb accepts
- SO_DOMAIN gate: send_fd on anything but a unix socket refuses by name;
plain bytes deliver nil from recv_fd
- net.connect_unix carried here (iteration 38 still pending)
- legs (single-fiber: unix connect completes while the listener holds
the handshake): a pipe's read end crosses and still reads "ping"; a
tty crosses, term.raw works on the RECEIVED copy and destroy restores
it; refusal and nil legs verbatim. test_term 60/0
- full belt: all suites 0 fail both flavors, woc 557/0,
subprocess-accept 12/0, site-accept 23/0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 1d689027920d6814f87b97c216b0cb42f7eba3e9)
- two verbs on any tty fd; saved termios in a per-shard 8-entry table;
double-raw and restore-without-save refuse by name
- restore is a RUNTIME obligation: vm_unwind at depth 0 (uncaught trap,
fiber reap) restores the dying fiber's entries newest-first, and
wo_vm_destroy sweeps the rest — proven twice in the legs: a DIV0
while raw restores, and even the double-raw REFUSAL (itself a trap)
restores the first raw
- test_term 39/0 against a real PTY pair made by the test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit b439387dbf4f653e034c721bc3f08b83616c5e24)
- mechanics amendment to the spec (recorded at close-out): no signalfd —
the stop-latch pattern generalized. An async-signal-safe handler
latches the number, bumps a sequence and pokes shard 0's wake eventfd;
wo_io_wait's loop head drains latches into fresh Signal{sig} records
delivered via runtime_notify (exported as wo_actor_notify)
- payloads must be heap objects (vm.c drops them unconditionally) — the
Signal record exists exactly for that; class id rides the call as the
appended record operand (sm_record drives it even with no return)
- offerable: WINCH/CHLD/HUP/USR1/USR2; SIGTERM/SIGINT refused naming the
stop latch; shard-0-only registration; coalescing disclosed
- stdlib_modules gains `signal` (and `term`, next task)
- test_term: a real child kills the test process with USR1; the actor's
multi holds one coalesced delivery; refusal leg verbatim. 14/0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 14e03a6a4a343b97c9b47fab1a5e3c4bb69d8201)
- posix_openpt/grantpt/unlockpt/ptsname_r (plain libc, no -lutil); child
setsid + opens the slave as its controlling terminal, initial
TIOCSWINSZ from the call
- Child.stdin == Child.stdout = the master (caller's copy); the slot
keeps a private dup so resize survives the caller closing theirs
- proc.resize -> TIOCSWINSZ; refuses by name on a pipe child
- legs: test -t proves a real tty; stty size reads "24 80" then "40 120"
after a mid-sleep resize; refusal asserted; test_proc 193/0 ASan clean
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9836c9cd5197153517c054b243cab3b453d130a0)
- a child is fds: Child {id, stdin, stdout, stderr}, driven by the
existing net verbs (echo leg proves cat round-trip through write_dl/
read_dl); caller owns the fds, the runtime owns pid + pidfd
- wait_dl parks on the pidfd: code on exit, nil at the deadline with the
child untouched; one waiter per id, a second refuses by name; stale
ids refused via a generation counter in the handle
- proc.signal through pidfd_send_signal; actor_die kills the streaming
children the dying actor owns; dead fibers cannot linger as waiters
- ids 97-107 registered wholesale (wob.h, loader arities, dispatch
bound); Child + Signal predeclared records in types.ml; unimplemented
ids trap at the default case until their task lands
- test_proc 168/0 (echo, wait trio, one-waiter refusal, 200-round churn
fd-flat), suite ASan clean, woc-test green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9be87f159f1bf9cdd509ceed160e7ea518fde46c)
- spec 2026-09-01-runtime-v2-design.md: the one principle (PULL — a
child is fds, the net verbs drive them; runtime-v2 adds acquisition
verbs, never transport), the full surface (ids 97+: spawn/spawn_pty/
wait_dl/signal/resize, signal.on delivering the sig number, term.raw/
restore with runtime-guaranteed restore, send_fd/recv_fd/connect_unix),
actor-owned lifecycle, mechanics notes, refusals by name
- push transport rejected with reasons recorded (mailbox-cap collision,
new delivery machinery); death-notice verb refused (a two-line fiber
composes wait_dl)
- five stories flip readiness: ready; fork sections rewritten as settled
- graph section 6 remapped: pull broke the 1->2->3 chain — only 1->2
remains; 3, 4, 5 and the VTE grid startable alone today
- board section + registry follow; linkcheck 0 broken
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d313cdeebbe53c83b83f31f4480631568d9d0743)
- new docs/guides/updating-site.md: the developer loop deploying-site.md
deliberately does not cover — the submodule two-repo commit dance in
the order that cannot strand other clones (push writeonce-site first,
bump the pointer second), the gate living in the monorepo by design,
and framework changes being ordinary monorepo commits
- the schema section is measured against the built site, not inferred:
adding views: Int stopped the build with WO-E206 until all ten seed
inserts carried it (no field-default syntax — the seed cannot drift
from the schema), then the live WO_DATA migrated at boot, all ten
chapters rendered, and a live admin edit SURVIVED the migration;
retyping the field refused by name with the log intact
- states the one release combination that still needs the content wipe:
a schema change WITH new seed rows — migration handles the shape,
seeds still cannot reach a non-empty table
- deploying-site.md cross-links; site-update prefix registered
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a21a02f2c264a3fe1c31b3bfd9159415a594fa05)
- registry row corrected: it claimed "Not picked to master", and the
branches no longer differ structurally at docs/examples/site
- cherry-pick table gains the same row master's copy carries, so the
ledger reads identically from either branch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit ab7df69e762cd516d3016b7e2703cb6928c7d835)
- same 37-row dev-to-master map the master copy carries, so the ledger
reads the same from either branch
- registry rows for db2-keys, db2-delta, db2-chains, db2-chain-review
and site marked landed on master
- lang41 registered explicitly as on dev and not picked, so its absence
from master is a recorded decision rather than an oversight
- porch-store and query-corpus rows untouched: still dev-only
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 22b5675ed1c873135e8cb7dd10e010c4a00350b7)
- root cause: a worker's runtime is initialised lazily on first fiber
adoption, and rt.shard_id is stamped only there — but INBOX_READY[i]
is set at thread creation. A shard that never adopts is still settled
at shutdown, carrying rt.shard_id 0 from the memset
- it then impersonated shard 0: wo_drop_obj saw 0 == 0 for anything the
primary allocated, took the "we are home" branch instead of routing,
and called class_free against rt->classes, which lazy init never
filled. &rt->classes[class_id] off a NULL base is the faulting read
- fix: stamp the runtime's real identity at thread creation. An
uninitialised shard owns nothing, so its true id makes every payload
correctly foreign and routes it to an owner that can free it
- ASan could not name this: the arena is one hand-managed malloc block,
so intra-arena reuse is invisible and it surfaces as a bare SEGV
- pinned by tests/regress/lang-41, driven from db-actor-accept. Needs
multiple shards (the corpus runner pins WO_SHARDS=1) and the ASan
build. SEGVs twice per run unfixed, clean fixed
- the HANG is a separate defect and is NOT fixed: with this in place the
harness stops losing whole sections, but idempotent-stop-2 still
fires ~1 run in 6. The story records where to look
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 9dca0b4b4727b976d326b29cb4c6522b62d48a73)
- records every decision made without stopping to ask, with what each
costs if wrong, since the SDD workspace is deleted on completion
- R9 is marked WRONG and overturned by R14: WO-E222 fires on the class
Pool, not on multi, so an actor can hold slots: multi PoolSlot. My
ruling shipped a README prescribing a permanent 1-actor pool
- R6 records that my own brief caused a security bug: trust_proxy with
an absent XFF collapsed every client onto one shared bucket
- R15 parks the one residual: pool_slots/pool_of have zero call sites,
so real N-actor sharding is compile-proven but gate-unproven
- measured the gate over 10 runs: it is NOT stably green. Most runs
fail idempotent-stop; one lost 6 checks with 000 status codes
- traces the flake to the C-runtime hang/segfault, now localised by gdb
to wo_arena_alloc / wo_str_new / vm_run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a919ab104ce44754949d364e35c88b6964b81fee)
- bullet 2 wrongly told app authors to hold a bare actor handle and
re-wrap it as a forced ONE-slot Pool per connection -- that was my
own advice, not the previous implementer's, and the reviewer showed
WO-E222 fires on the class Pool, not on multi PoolSlot
- rewritten around the new pool_slots/pool_of pair: make_pool(n) once
at process start, multi PoolSlot held directly in connection-actor
state, a transient Pool rebuilt per use -- and states explicitly that
calling make_pool per connection restores the lost-increment race
- disclose that the gate's own ConnWorker fixtures still build a
deliberate one-slot Pool per leg, so no leg yet exercises real
N-actor sharding through pool_slots/pool_of
- soften the rate-limiting row: saturation-503 is gate-proven only via
Idempotent/pool_begin, not through Limiter's own try/catch arm
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6d48dbca98d4ea4c6d93f470ae3aad0b00acd2a1)
- new handlers/routes: casecheck (key_header: "Idempotency-Key", the
README's documented shape) and patha/pathb (include_body: false,
same key, different routes)
- 18g: capitalised key_header + capitalised wire header must still
dedupe (exec count, not status, is the load-bearing assertion --
pre-fix both calls answer 200 either way, but the handler reruns)
- 18h: same key on two different routes with include_body:false must
answer 200 then 422 (a digest mismatch, same as a body mismatch),
never replay route a's body under route b
- both legs run on a FRESH restart of the same binary, not piled onto
18a-18f's already-loaded server -- doing so measurably raised how
often this run hit the pre-existing, out-of-scope C-runtime
arena-allocator race (confirmed by gdb backtrace: SIGSEGV inside
wo_str_new, unrelated to this file's own .wo logic)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 86e7244dd97fb8ba940f8c0029faa05f70506e59)
- catch (e) nil could not distinguish a real store failure (e.g. a
mod-by-zero from Pool { actors: [] }) from ordinary saturation
- print_err the trap message before answering 503, matching the same
fix in idempotent.wo's pool_begin catch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c53ad583051abeeeb302301fc3d91073f0a15fcc)
- stored/replayed headers widen from content-type only to an allowlist
(content-type, location, etag, cache-control), matched case-insensitively
-- a redirect() lost its Location on its own first response, not just replay
- add pool_slots(Pool) -> multi PoolSlot and pool_of(multi PoolSlot) -> Pool
- Pool is demand-promoted to traced (WO-E222) and can't live in actor
state; PoolSlot/multi PoolSlot never is, the same shape chat/main.wo's
Room already holds directly -- this is what lets an app actually shard
across N actors per connection instead of a forced one-slot pool
- log a genuine pool_select trap instead of silently folding it into 503
- fix stale comments: the prune below IS a delete-then-insert (of a
fresh row, not the same one) contradicting the doc comment above it;
the catch shape referenced in two comments had changed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b738269314f01a95dee1341437c7661ce9e28730)
- normalise self.key_header via to_lower before the req.headers lookup
- req.headers keys are already lowercased on read (internal/parse.wo);
the documented key_header: "Idempotency-Key" never matched, silently
disabling idempotency (falls through to inner.handle) on every request
- key/digest lookups use the normalised name consistently
- digest now always includes method+path, body appended only when
include_body is set -- a bare "" digest under include_body:false
previously matched any other request reusing the same key
- log a genuine pool_begin trap instead of silently folding it into 503
- update the two doc comments describing the old, unsafe shape
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 91099cfbb2fb9a2d351894e444fed306b25557d5)
- Add a neutral note() helper (prints, touches neither pass nor fail)
- Use it for the saturation leg's unconditional teardown line, which
previously called ok() regardless of branch taken and inflated the
reported count with a line that could never fail
- New count: 78 checks, 0 failures (was 79) -- every number now is a
real assertion
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2ac1b8b6db360d4423dbb53d5d26b355e7b05e71)
- Add saturation leg (scripts/web-app-accept.sh): one-actor pool,
WO_MAILBOX=2, 15 concurrent requests, exactly 3 served + 12 answer
503; execution count matches the 200 count, retry-after + real
cause verified on the 503s
- Guard make_pool(n<1) by clamping in make_pool itself, not
pool_select's division -- that trap runs inside the middleware's
own try/catch and would be swallowed as ordinary saturation forever
- README: rate limiting + idempotency ledger rows moved to done,
scoped to what the gate proves; documented Handler-decorator
shape, Pool aliasing (WO-E222), call's scalar-only reply (WO-E226),
pool size as a capacity decision
- Story: Progress table filled with real hashes, 7/9 acceptance
criteria marked verified with citations, 2 marked verified by
construction (never gated even in the original plan), status: done
- Status board: standup entry, porch 1 pending row updated
- Recorded a pre-existing runtime hang (main() returns cleanly, OS
process sometimes hangs under concurrent call()-parked callers)
that also reaches the new leg's teardown; contained with kill -9
rather than asserted, so it can't flake the leg's actual subject
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 21934b18910070a3f24b8bd4367fcb9d397dc1fb)