Skip to content

cartridge substrate item 5: L1 wired over L3 — abi table, host trait, rustlite extern/pub/if/intrinsics, cartridge lifecycle - #16

Merged
compusophy merged 2 commits into
mainfrom
agent/cartridge-lifecycle
Aug 25, 2026
Merged

cartridge substrate item 5: L1 wired over L3 — abi table, host trait, rustlite extern/pub/if/intrinsics, cartridge lifecycle#16
compusophy merged 2 commits into
mainfrom
agent/cartridge-lifecycle

Conversation

@compusophy

Copy link
Copy Markdown
Owner

CARTRIDGE_PLAN §11 item 5: the L1 ABI wired over the L3 runtime. A cartridge written in rustlite can now be loaded, initialized, and handed messages, talking to the host through the ABI.

what landed

  • src/cartridges/abi.rs (new) — the ONE ABI table: HostFn ×5, GuestFn ×3, pack/unpack, the Host trait (bytes only, never pointers). Compiler, runtime, and lifecycle all read it, so a signature cannot drift between guest and host.
  • rustliteextern "C" { … } imports, pub fn exports, if / else / else if, eight inline intrinsics (load_u8 store_u8 load_i32 store_i32 memory_size pack unpack_ptr unpack_len), Program {externs, fns}, an every-path-returns checker rule.
  • wasm.rs — import / memory / export sections; check_program for the module-level contract (unique names, reserved intrinsics, externs vs the ABI, lifecycle exports vs the ABI).
  • runtime.rs — decodes the new sections, refuses non-custom unknown sections instead of skipping them, if/else via the existing branch-patching discipline, memory ops, Trap::{MemoryOutOfBounds, CallDepthExceeded, Unreachable}, MAX_CALL_DEPTH, host dispatch with bounds-checked copies, bounded host→guest re-entry for store_get.
  • src/cartridges/lifecycle.rs (new)Cartridge::loadinithandle, every refusal at the door named.

three real bugs the new evals caught

  1. Void calls were unwritable as statements (log(…); refused by the checker).
  2. Item 4's decoder underflowed r.pos - body_end on a short body — found by the cartridge-shaped fuzz on its first run.
  3. Every-path-returns bodies failed wasmparser validation; closed with a trailing unreachable that the runtime names as a trap.

Also: Cargo.lock refreshed (stale since #14 added wasmparser), and boot_reconcile's source-shape guard made CRLF-tolerant (false red on Windows checkouts).

verification

  • tests/cartridge_lifecycle.rs (new, 14): echo cartridge through the real pipeline against a recording fake host; every host fn, every refusal, corruption fuzz across the host boundary.
  • runtime_evals +12, rustlite_emit +7 (cartridge validates under wasmparser), rustlite_front +3.
  • Shared gate run natively: every suite green (217 tests), clippy --deny warnings clean, cargo check --target wasm32-unknown-unknown --lib --bins clean.

Memory updated on this branch before opening (taskboard: item 4 done, item 5 done, item 6 next; string literals / data segments flagged before item 8).

🤖 Generated with Claude Code

compusophy and others added 2 commits August 25, 2026 12:51
… rustlite extern/pub/if/intrinsics, cartridge lifecycle

a cartridge can now receive a message and answer it. ONE ABI table
(src/cartridges/abi.rs) is read by the compiler, the runtime, and the
lifecycle alike — HostFn ×5 (log/now_ms/store_get/store_set/emit),
GuestFn ×3 (cart_init/cart_handle/cart_alloc), pack/unpack for the
`ptr<<32|len` result, and the `Host` trait, which takes BYTES only.

- rustlite: `extern "C" { fn …; }` imports (only "C"; string tokens exist
  for this one purpose and are refused as expressions with the workaround
  named), `pub fn` = export, `if / else / else if` (else-if desugars to a
  nested If), eight inline intrinsics (load_u8 store_u8 load_i32
  store_i32 memory_size pack unpack_ptr unpack_len — rustlite has no
  pointers, casts, or shifts by design). `Program {externs, fns}`.
  every-path-returns is now a checker rule that names the fix.
- wasm.rs: import/memory/export sections in spec order; a fixed 16-page
  memory exported as "memory" whenever the module imports, exports, or
  touches memory; function index space = imports then defined (the
  runtime applies the identical rule). check_program = the module-level
  contract: unique names, intrinsics reserved, externs vs the ABI table,
  lifecycle exports vs theirs, lifecycle entry points must be pub.
- runtime.rs: decodes the new sections (custom skipped; every OTHER
  unknown section now REFUSED — silently dropping a data segment is the
  D4 failure in binary form); if/else through the same unresolved-site
  patching as blocks and loops; memory ops with u64 effective-address
  math; Trap::{MemoryOutOfBounds, CallDepthExceeded, Unreachable};
  MAX_CALL_DEPTH 1024; host dispatch that bounds-checks and COPIES every
  (ptr, len) before the host sees it; guest_alloc = the host→guest hop
  for store_get, bounded at MAX_HOST_REENTRY 4.
- lifecycle.rs: Cartridge::load refuses at the door (manifest, unknown
  import, import shape, missing or mis-shaped lifecycle export, no
  exported memory — each naming the fix); init (nonzero = Refused(code),
  verbatim); handle (packed answer bounds-checked, bytes copied out).
  store_set refusal = status 1 to the guest; emit failure = a trap.

three real bugs the new evals caught before the first commit:
1. void calls were unwritable as statements — `log(…);` was refused as
   "used as a value". check_call now serves both positions.
2. item 4's decoder underflowed `r.pos - body_end` on a body that ended
   SHORT of its declared size. the pure-module fuzz never produced that
   shape; the cartridge-shaped fuzz did on its first run.
3. every-path-returns bodies failed wasmparser (empty stack at the
   function's end after an if/else whose arms both return). a trailing
   `unreachable` closes them; the runtime names it as a trap, and a
   compiled test proves it never executes.

Cargo.lock: stale since PR #14 added wasmparser (the browser agent cannot
run cargo); refreshed.

tests: tests/cartridge_lifecycle.rs (new, 14) — an echo cartridge in
rustlite through the REAL pipeline against a recording fake host, every
host fn observed through guest memory both ways, every refusal named,
the corruption fuzz across the host boundary; runtime_evals +12,
rustlite_emit +7, rustlite_front +3. taskboard/status/plan updated
(item 4 marked done — PR #15's board edit was one item behind — item 5
done, item 6 next; string literals / data segments flagged as the
language gap to close before item 8).

verification: the shared gate run natively — every tests/*.rs suite
green (217 tests), clippy --deny warnings clean, cargo check --target
wasm32-unknown-unknown --lib --bins clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`a_failed_delete_keeps_its_index_entry` searches src/agent/tools.rs for
a literal `{\n` shape. a windows checkout with core.autocrlf=true serves
the same bytes as `{\r\n`, so the guard went red on a tree ci passes — a
false red that blocks running the shared gate locally at all. the
source() helper now normalizes line endings before any guard looks.

verification: boot_reconcile 7/7 on a CRLF checkout; a no-op on LF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vanish Ready Ready Preview Aug 25, 2026 6:54pm

Request Review

@compusophy

Copy link
Copy Markdown
Owner Author

live boot verification (the assertion smoke-preview could not make): the preview of head b488766 was opened in a Vercel-authenticated browser — #status = "ready", #feed/#prompt/#run mounted, /build.json = {"build":"b488766"}, no console errors.

smoke-preview is red because the preview 302s to vercel.com/sso-api (Deployment Protection) and ci/e2e.mjs's interstitial markers do not match the "Login – Vercel" page, so it misreports "app did not mount". it has been red on every PR since it landed (#12#15) for the same reason; follow-up PR makes the diagnosis honest and honors a VERCEL_AUTOMATION_BYPASS_SECRET.

@compusophy
compusophy merged commit 55fa83d into main Aug 25, 2026
4 of 5 checks passed
compusophy added a commit that referenced this pull request Aug 25, 2026
… secret (#17)

smoke-preview has been red on every pr since it landed (#12#16). the
cause was never the app: preview deployments sit behind vercel deployment
protection, the preview url 302s to vercel.com/sso-api, and ci/e2e.mjs's
interstitial markers did not match the "Login – Vercel" page it landed
on — so it reported "#feed never appeared — the app html did not mount",
the exact D4 misdiagnosis its own header says it exists to prevent. and
main carries no branch protection, so the red check never blocked a
merge; the board's "merge_pr refuses until booted-green" was never true
in practice.

- ci/e2e.mjs: the protection check now fires on a redirect off the
  preview host onto vercel.com AND on the login page's text, exits 3
  with the fix named (bypass secret or relaxed preview protection), and
  says explicitly whether a secret was supplied. with
  VERCEL_AUTOMATION_BYPASS_SECRET set, every request from the browser
  context carries x-vercel-protection-bypass (+ set-bypass-cookie), so
  index.html, worker.js, and the wasm all pass the wall.
- .github/workflows/e2e.yml: passes the secret through when it exists.
- memory: the finding, the owner action (generate the bypass secret in
  vercel, add it as the repo secret), and the lesson — a gate nobody has
  seen pass is not a gate; witness one green and one red.

verification: node --check on the script; tests/ci_gate.rs guards still
green. the pr for this branch is ITSELF the red-side witness: its
smoke-preview must now fail with "EVIDENT-CAUSE FAILURE … redirected to
https://vercel.com/sso-api" instead of the false "did not mount". the
green side needs the owner's secret.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant