Skip to content

lua: Lua 5.5 as a crate, with io, os and kore.fs - #23

Merged
micro-perceptron merged 13 commits into
alphafrom
ft/lua-stdlib
Oct 7, 2026
Merged

micro-perceptron merged 13 commits into
alphafrom
ft/lua-stdlib

Conversation

@SnowCheetos

@SnowCheetos SnowCheetos commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Overview

crates/lua (kore-lua): the Lua 5.5 engine, its freestanding host and the kore.* standard library, moved out of Kore's user/apps/utils/lua so any libkore consumer can embed it. Beside kore-wasi, on the same seam.

A script sees two surfaces over one implementation. io.open and kore.fs.open return the same file object; one failure is rendered two ways, both derived from FsError. io/os answer what POSIX can ask, with the capabilities defaulted from the mode string; kore.fs answers what the store can. docs/lua.md is the reference.

io and os are written here, not vendored: Lua's liolib.c and loslib.c are libc over POSIX and this target has neither.

Capabilities

A DagFS route is reached through a View, which is affine. Only one of the four surfaces can hold one, because only one has a language that enforces it.

Surface What you hold What stops a second use Rights you can name
io / os a file object __close/__gc, plus a generation check none; inferred from the mode string
kore.fs the same file object the same r, w today; n, s as their verbs land
C (kore-ffi) a KoreFsHandle the adapter, at run time uint32_t; KORE_FS_READ, KORE_FS_WRITE
Rust (kore::fs) the View itself the borrow checker, at compile time the whole Rights type, and fs::dag

Lua and C are in the same position for the same reason: a Lua value can be copied into a table or captured by a closure, and a C struct can be memcpy'd, so neither can be handed something affine. Both get a handle — an index and a generation into a table thread 0 owns — and a stale one is invalid-handle. Rust holds the capability itself; src/fs.rs keeps a compile_fail doctest to prove it.

No surface forges authority: the store resolves every route through a generational slot. What differs is how much of the vocabulary each can say, what it costs to say it, and how early a mistake is caught.

io is not a smaller filesystem. It is the same one with the capability questions answered by convention, so it cannot ask for less, ask for more, or find out what it got:

local f = assert(io.open("/var/log/app", "a"))        -- rights: READ WRITE, create, append

local g = assert(kore.fs.open("/etc/hosts", "r"))     -- least authority, on purpose
local held = assert(kore.fs.rights("/var/log"))       -- and what did this task actually get?
if not held.write then return nil, "not endowed with write authority there" end

kore.fs expresses every route io can, plus three things it has no spelling for: rights asked for by name, rights reported back, and behaviour chosen apart from authority.

-- io's "w" is three decisions fused into one letter. Taken apart, you can take two:
local f = assert(kore.fs.open(path, "rw", {create = true}))   -- keep what is there

Lifetime is deterministic on both surfaces, __gc only a backstop. revision and id are visible on neither io nor POSIX: one moves on every commit, so a script can tell a write landed underneath it; the other is volume-stable, so it answers "same object" where a path answers "same name".

do
  local f <close> = assert(kore.fs.open("/tmp/scratch", "rw", {create = true}))
  f:write(payload)
end   -- retired here, whether the block ended or threw

docs/lua.md has the worked version.

kore.fs

Each answers nil, err on failure, where err is {code, kind} with a __tostring of the code.

Verb Answers
open(path [, rights [, options]]) a file object
read_file(path) the whole file; "" for an empty one
write_file(path, bytes) the byte count written
resize(file, bytes) true; takes an open file, not a path
stat(path) the object's record
entries(path) the directory's edges
rights(path) the rights held there
list(path) the directory's names
exists(path) a boolean; no failure spelling
mkdir(path) true
remove(path) true
rename(from, to) true; the store's own move, across directories, keeping identity and grants

open takes authority and behaviour separately:

local f = assert(kore.fs.open("/etc/hosts"))                         -- rights default to "r"
local g = assert(kore.fs.open("/var/log", "rw", {create = true}))

rights is a subset of the store's rwns spelling, each letter once, parsed by kore_dagfs::rights_parse rather than in C. READ is implied, so "w" is a readable write route. n and s are refused by name until the verbs that use them exist. options are create, truncate, append, exclusive.

Mistakes raise: a malformed spelling, an unreachable right, an unknown option key, truncate or append without w, exclusive without create. A refusal from the store arrives as nil, err.

stat answers the object's whole record. None of it is authority.

Field
size_bytes integer
kind, directory "file" or "directory", and the same fact as a predicate
revision monotonic; moves on every commit
id volume-stable; survives a service restart
links, grants names pointing at the object, grants outstanding on it
subtype "plain", "executable-image", "package-manifest", or absent when this build cannot name the byte
subtype_bits the raw byte, always present

entries answers {name, kind, directory, subtype?, subtype_bits} per edge, where list answers names. rights answers {spelling, read, write, mutate_namespace, share}; spelling is what a later share would accept, and the booleans are derived from it.

The file object

One userdata, whichever surface opened it.

Method Answers
f:read(...) "a", "l", "L", "n", or a byte count
f:write(...) the file, so writes chain
f:lines(...) an iterator over the read formats
f:seek([whence [, offset]]) the new position
f:flush() true; a durability barrier, not a buffer drain
f:setvbuf(mode) true; accepted and inert
f:close() true; idempotent
f:closed() a boolean, without raising

The stream position and its read-ahead live in the file object, not beside the view, so there is one authority for it; the host side is purely positional. A handle is an index and a generation, so a use after close is invalid-handle rather than a read of whatever took the slot. __close and __gc both retire the handle, deterministic close first.

io and os

Twenty of the twenty-two members the manual lists, and all seven file: methods. Where the manual leaves a detail to the system, this build chooses and records it in KORE-VENDOR.md:

Member
os.getenv nil for every name; a task's authority arrives as capabilities, not strings
os.date, os.time one clock, UTC; isdst false; civil time computed, there being no gmtime or timezone database below
os.clock monotonic since the VM started
os.exit records the code and unwinds by raising, so <close> handlers run
os.setlocale "C", nil for anything else
os.tmpname, io.tmpfile the name is created exclusively; io.tmpfile removes its own on retire
io.lines(name) four values, the last to-be-closed

Absent: os.execute and io.popen, both of which launch a process — a service face the embedding program holds and this crate does not, so they arrive through Extension. Also string.format (no hosted printf) and load/loadfile/dofile/string.dump (no bytecode verifier upstream).

The rest of kore.*

Module
kore.time now(), mono_ns(), wall(), wall_ns(), sleep(seconds)
kore.random bytes(n) for n <= 4096, integer(lo, hi), available()
kore.proc args, a lazy 1-based array fetched once
kore has(name), caps(); an unheld module is nil, so if kore.fs then is the idiom
require the task's tree then libs/; <name>.lua over <name>.so; source bounded at 64 KiB

Errors

local f, err        = kore.fs.open("/nowhere", "r")   -- err.code == "not-found", err.kind == "refused"
local g, why, errno = io.open("/nowhere", "r")        -- why == "not-found", errno == 2

kind is "refused" when the service named a reason, "unavailable" when the call could not be made at all. Expected failures answer nil, err; programming errors raise.

The embedding API

Item
Host::start::<E>(transport) spawn the VM worker
Host::eval::<E>(services, extension, script, chunk_name, repl) run a chunk, serving every require; answers (Outcome, stdout, stderr) or Fatal
Host::quit(self) retire the worker
Services::new(rt, transport, args) the thread-0 half: filesystem session, open files, launch arguments
Services::{exit_request, close_open_files, fs} the code os.exit recorded, teardown, the attached face
Outcome Ok, Failed, Exited, Incomplete, ResourceRefused, Startup
pub trait Extension {
    const LIBRARIES: &'static [Library] = &[];
    fn serve(&mut self, op: u32, args: [u64; 3]) -> impl Future<Output = Answer>;
}

Library::new(name, opener) registers one module beneath kore. Answer is Value(u64, u64), Bytes(u64, Vec<u8>), Refused(String) or Unavailable(String). Ops at or above OP_EMBEDDER_BASE (64) reach serve; below it the vocabulary is this crate's. kore_lua::api is the Lua C API subset an opener calls, and files::code is public so an embedder's own filesystem failures render in the same words.

Extension carries no 'static bound. It forbade the one shape an embedder has — a face borrowed from the Rt and transport its main owns — and bought nothing; the worker reads only LIBRARIES.

The transport grows one borrowed byte window so a path or a write's bytes cross without a copy. No new word state, and a verb is still a constant and an arm.

native/

Re-exported at Kore 9e4172bc2. The export grows kore-ld and kore-late, which require "<name>.so" needs to check a store object stamped EXECUTABLE_IMAGE, place it in the task's JIT arena and bind it against the exported Lua C API (Kore res/IMAGE.md §8).

Not implemented

Deriving a view, delegating one, and the grant vocabulary. Everything here reports rather than confers: a StableObjectId conveys no authority, a subtype gates nothing, rights reports what a walk already found. Conferring or moving authority waits on MicroPerceptron/kore#986 (grant boundary, route shape, rights vocabulary) and #991 (whether a userland authority domain gets a generic capability type).

Index publication on save waits on kore-dagfs's index feature, which pulls kore-svc-extract-worker-rust; enabling it is an image-size decision for this target.

search, grant listing, create with a subtype and scoped commit batching are planned. docs/kore-fs-vocabulary.md has the order, docs/pack-backed-reads.md the storage-transport half.

Gates

make fmt test check docs owners cross miri bindings-check and tools/check-local.py clean. make cross covers x86_64, aarch64 and riscv64gc and now includes this crate, so a target-only regression in the amalgamation meets a compiler instead of a keytest.

The host smoke runs the whole surface on every build against the in-memory store in tests/host_fs.c, which models the refusal vocabulary, the graph facts, short reads and short writes. Each behavioural claim has a negative control: stubbing out the large-read bypass, the short-write retry, the unknown-subtype carry or the unreachable-rights refusal fails that claim's assertion and no other.

The smoke also gets the right prototype for kore_lua_new: it declared three parameters against a definition taking five, so every run passed two garbage registers that luaopen_kore then walked as a library array.

Not proven on metal

Kore's user/apps/utils/lua still carries its own copy of the engine and pins libkore at 861cef90, from before this crate existed, so a Kore image builds a different program. Making that app a thin embedder over kore-lua is the follow-up. Until it lands, every measurement here is a host build.

Wasmtime

Two bumps unrelated to the Lua work, 47 → 48.0.3 and 48.0.3 → 48.0.5, for RUSTSEC-2026-0315, -0316, -0325, -0326 and -0327. The Advisories workflow fails on all five. Neither bump needed a source change.

Authored by @julian-avar

The engine, its freestanding host and the `kore.*` standard library move
out of Kore's `user/apps/utils/lua` into a crate any libkore consumer can
embed, beside `kore-wasi` and on the same seam.

`io` and `os` are written here rather than vendored: Lua's `liolib.c` and
`loslib.c` are not compiled at all, because both are libc over POSIX and
this target has neither.  They are a veneer over one implementation, not a
second one — `io.open` and `kore.fs.open` answer the same file object, and
one failure is rendered two ways, `nil, message, errno` on the stock
surface and `nil, {code = "not-found"}` on the kore one, both derived from
`FsError` rather than invented.

Twenty of the twenty-two members the manual lists are present, with all
seven `file:` methods.  `os.execute` and `io.popen` are the two absent:
both launch a process, which is a service face the embedding program holds
and this crate deliberately does not, so they arrive through `Extension`
like `kore.accel`.

The stream position and its read-ahead live in the file object rather than
beside the view, so there is one authority for it; the host side is purely
positional.  A handle is an index and a generation, so a use after close is
`InvalidHandle` rather than a read of whatever took the slot.

Where the manual leaves a detail to the system this build chooses and says
so in KORE-VENDOR.md: `os.date` computes civil time directly, there being
no `gmtime` or timezone database below it, so the one clock reads UTC;
`os.clock` is monotonic since the VM started; `os.getenv` answers nil for
every name, a task's authority arriving as capabilities rather than
strings; `os.tmpname` creates its file exclusively, and `io.tmpfile`
removes its own when the handle retires.

The transport grows one borrowed byte window so a path or a write's bytes
cross without a copy; no new word state, and a verb is still a constant and
an arm.  `Extension` loses its `'static` bound, which forbade the one shape
an embedder has — a face borrowed from the `Rt` and transport its `main`
owns — and bought nothing, the worker reading only `LIBRARIES`.

The host smoke now runs the whole surface on every build, including the
calendar, which is pure computation and needs no machine.  It also gets the
right prototype for `kore_lua_new`: it declared three parameters against a
definition taking five, so every smoke run passed two garbage registers
that `luaopen_kore` then walked as a library array.

Gates: host smoke, host clippy `-D warnings`, unit tests and `cargo fmt`
clean; `make cross` clean on x86_64, aarch64 and riscv64gc, and now
includes this crate, so a target-only regression in the amalgamation meets
a compiler instead of a keytest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 24, 2026 17:00
@SnowCheetos
SnowCheetos marked this pull request as draft September 24, 2026 17:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job was not started because a GitHub Actions budget is preventing further Actions use.

@julian-avar
julian-avar marked this pull request as ready for review September 29, 2026 13:03
julian-avar and others added 3 commits September 29, 2026 11:26
`kore-lua`'s build script queried Zig before anything else, so every host
`cargo test`/`cargo miri test` of the workspace failed on a machine with
no Zig — including CI's `host` and `miri` jobs, which install no Zig at
all. The engine exists only where `src/lib.rs` compiles its C half
(`target_os = "none"`), so a host build has no object to link and no
reason to need Zig: return before the toolchain query when the target is
not Kore's.

The C smoke moves ahead of that early return and becomes skippable: it
needs only a host C compiler, and a consumer with neither toolchain must
still be able to build this workspace. A compiler that is present but
fails is still fatal.

`tests/host_fs.c` gives the smoke an in-memory store, so the whole
`io`/`os`/`kore.fs` surface — `stat` included — runs without a machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 9e4172bc2 re-export added both crates to `native/Cargo.toml` without
their lockfile entries, so every `--locked` command against the native
workspace refused to run — `make test`'s second invocation, and Audit's
lockfile-currency gate. Resolution adds `elf 0.8.0`; everything else the
two crates want was already locked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two advisories landed against Wasmtime 47 on 2026-09-24: `call_ref` and
exception `catch` can drop fuel accounting (RUSTSEC-2026-0315, medium),
and dynamic record lifting can allocate past the hostcall fuel limit
(RUSTSEC-2026-0316). Both are fixed in 48.0.3. `cargo audit --deny
warnings` fails the Audit workflow until the bump, and it needs no source
change: the runtime, component-model and async surfaces this host uses are
unchanged across 47 -> 48.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SnowCheetos

Copy link
Copy Markdown
Contributor Author

@julian-avar Can you confirm that the fs API you're using is compatible with https://github.com/MicroPerceptron/kore/pull/1064's upgraded dagfs? I foresee we transition fs fully to be pack backed, the performance upside is too high not to.

julian-avar and others added 8 commits October 4, 2026 12:43
…them

The facade streams a read through this launch's storage ring only when it
is handed a destination of at least 256 KiB (`FACADE_MIN_BYTES`). Every
read this surface made was 4096 bytes — `KORE_LUA_BUFSIZE`, one page per
bridge crossing — so no file, however large, was ever read through a ring.
`f:read("a")` and `kore.fs.read_file` were that same page loop.

Three changes, and the sizes that decide them:

- `kore.fs.read_file` is one host call (`OP_FS_READ_FILE`) over
  `Fs::read_file`, which opens, streams and closes under one pinned
  version. Assembled here from positional reads it could not be: each of
  those pins and releases its own version, so a concurrent writer could
  leave the answer a mix of revisions, and no call was large enough to
  stream besides. Bounded by `MAX_FILE_BYTES`, refused `range`.
- A read of at least `KORE_LUA_STREAM_MIN` bypasses the page buffer and
  asks for the whole remainder at once. Both `f:read(n)` and `f:read("a")`
  go through it; the page buffer takes over again once the remainder is
  small, so both halves run in one large read. The buffer itself is not
  grown — it is a fixed array in all 256 open-file slots.
- `KORE_LUA_FACADE_STREAM_MIN` and `files::FACADE_STREAM_BYTES` pin the
  facade's threshold on each side of the bridge, `kore_lua_read_geometry`
  reports what the engine actually compiled, and the store records the
  largest destination any read asked for. A cap lowered for an unrelated
  reason now fails a build or a test rather than silently costing every
  read its ring.

Both positional loops move behind one `Transfer` seam, which is what makes
the rule they encode testable: a refusal answers *beside* the prefix that
already moved, never instead of it. Reaching that state needs a device
under the session, so it was previously unreachable from a host build and
untested in either direction.

With it comes the correction `Fs::read_at` now requires: a streamed read
writes into caller storage as it lands, so the acknowledged prefix bounds
what was read and says nothing about which bytes past it are still the
caller's. `TransferError::completed` and `docs/filesystem.md` say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects on the write path, none of them pack-backed — S8's write
grants have not landed upstream and nothing here streams a write. These
are what the store already owns and this surface was not using.

`kore.fs.write_file` was open + one positional write + close, and a
positional write commits once per window: a save cost one
image/flush/root/flush chain *per window*, not one for the save. It is now
`OP_FS_SAVE` over `Fs::write_bytes` under `hold_commits`, which is the
store's own `BeginReplace` transaction — one commit however many windows
it takes. The release is the commit, so a refused release is the caller's
answer even when every byte was acknowledged.

The open no longer truncates. The replacement sets the whole contents, so
truncating first was a wasted round trip — and it meant a save that then
failed had already destroyed the previous contents. They now survive a
refusal intact.

`f:write` guessed at a short write. A short acknowledgement is a fact, not
a refusal, and the old code turned every one of them into `out-of-storage`
— a reason it did not know. It now asks again from where the store
stopped, so a real refusal arrives in the store's own vocabulary on the
next call. Only a store that acknowledges nothing without refusing has no
reason to give, and that is the one case still spelled in C.

The harness grows a short-write cap, because the store wrote everything or
refused and so could not produce the state at all.

`Fs::replace_file` also publishes the index for what it wrote and this
does not: that verb is behind kore-dagfs's `index` feature, which pulls
kore-svc-extract-worker-rust, and this crate enables only `runtime`.
Turning it on is an image-size decision for the Lua target, not a side
effect of this change. A save therefore leaves the index stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Written to answer whether the filesystem API this surface uses is
compatible with the DagFS upgrade in MicroPerceptron/kore#1064
(`ft/pack-tickets`). It is — proven by syncing a scratch export to the PR
head and compiling — and that was the problem: the change is additive at
the `Fs` facade, so nothing broke and nothing improved either.

Records the evidence, why `res/STORAGE_TRANSPORT.md` §9's S8 makes this a
deadline rather than a missed win, and the slices. S-L0 through S-L3 and
the write slice are implemented and marked; S-L4 (a `pack-ring`-gated
witness) and S-L5 (sync `native/`, flip it on, measure on Gigatron)
are not. Nothing here has run on metal, so §7's acceptance is unmet.

Two claims an earlier draft got wrong are corrected in place rather than
quietly dropped: `io.lines(name)` reads lines and `require` reads through
a 64 KiB `Loader::read_source` buffer, so neither was ever going to reach
the threshold, and the whole-file cap is spelled `range` rather than
`out-of-storage` — the file is readable, and it is having all of it at
once that is refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`kore.fs` was `io` with a different error table. Its whole vocabulary was
open/read_file/write_file/remove/rename/mkdir/resize/stat/exists/list —
POSIX verbs, kore spelling, and nothing of the store underneath. Meanwhile
`src/fs.rs` already re-exports the entire graph vocabulary as `fs::dag` for
Rust consumers, so Lua was the surface that had it worst.

`Stat` carries eight facts and this surface passed on two of them. A script
could not see the revision it was racing, the links keeping an object
alive, the grants outstanding on it, or what its bytes were meant to be.
`stat` now answers the whole record over a fixed little-endian layout whose
length the C half checks before decoding a field.

`entries` answers a directory's edges where `list` answers its names — each
name with its target's class and subtype, so telling a native module from a
plain file costs one call rather than a stat per name. The target's
`ObjectId` is deliberately not in it: a service-local slot key is not a
stable identity, and a script that stored one would be wrong in a way
nothing would tell it. The stable one is `stat`'s, named as such.

`rights` answers what this task may do at a path, in the store's own `rwns`
spelling — `rights_label` printing what `rights_parse` reads — with the
booleans derived from the letters rather than beside them. A second
vocabulary in C would be a second authority over one set of bits.

An unknown subtype is carried, never guessed: the store's rule is that a
newer writer's subtype is data an older reader passes through, so `subtype`
is nil where this build cannot name one and `subtype_bits` still reports
it.

None of this is authority. A stable id conveys none, a subtype gates
nothing, and `rights` reports what a walk already found. Deriving a view,
delegating one, and the grant vocabulary stay absent until
MicroPerceptron/kore #986 and #991 rule the capability model for a userland
authority domain; a surface invented here first would have to be retracted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`docs/filesystem.md` documents the Rust facade and `languages.md` the C/Zig
ABI; the Lua surface had only its crate's module header, which is the wrong
place to look something up from a script.

Organised around the thing that is actually true of it: `kore.*` is the
store's vocabulary spelled for Lua, and `io`/`os` is the same code with the
capabilities defaulted. So every verb is listed in both spellings, and the
mode-string-to-rights mapping is stated once as a table rather than left to
be inferred — including the two rights this surface never requests, `SHARE`
and `BIND_EXECUTABLE`, which are the ceiling on everything a Lua script can
reach.

Per-verb sections carry the shapes a caller needs and the reasons where a
reason exists: why `resize` takes a handle and not a path, why `entries`
omits the target's object id, why an unknown subtype is absent rather than
`"plain"`, why `exists` is the one verb with no failure spelling.

Deviations from stock Lua are tabulated rather than scattered — `os.getenv`
always nil, `os.clock` measured from VM start rather than process CPU,
`os.exit` recording and unwinding, `f:flush` a durability barrier. Both
error spellings and the full errno mapping are in one place.

Marked aspirational, per AGENTS.md: a closing section lists what is not
exposed and what blocks it — unblocked and wanted, blocked on
MicroPerceptron/kore #986/#991, or blocked on the `index` feature. Storage-ring
streaming is described as the shape that can carry it rather than as present
behaviour, because the pinned export has no ring yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`kore.fs` has answered the graph since `stat`, `entries` and `rights`
landed, but it still took authority in POSIX's spelling. `kore.fs.rights`
prints `"rwn"`; `kore.fs.open` would not accept it. The surface reported
rights in one vocabulary and accepted them in another.

A mode string fuses two questions. `"w"` is write *and* create *and*
truncate, which is a projection of a right set onto three behaviours. So
they separate: authority is the rights spelling, behaviour is a trailing
options table (`create`, `truncate`, `append`, `exclusive`).

The spelling is not interpreted in C. It crosses packed into one argument
word as ASCII and `kore_dagfs::rights_parse` — the parser whose output
`rights_label` prints — does the mapping. A copy of that mapping in C would
be a second authority over one set of bits, which is what `kore.fs.rights`
already avoids by deriving its booleans from the store's own letters.

`n` and `s` are refused by name. A route carrying `MUTATE_NAMESPACE` or
`SHARE` is useless while no verb here can link or share, and accepting the
letter would claim a reach this surface does not have. They unlock with
their verbs, not before.

`io.open` converges onto the same path: its mode is translated to a
spelling and a set of options in C, so the capability default documented in
`docs/lua.md` lives in one place rather than being derived again on the far
side. Nothing about `io` changes, and its suite is the witness — it passes
unaltered.

The break is loud by construction. `"r"` is unchanged; `"w"` becomes a
read-write route that does not create, so a missing file is `not-found`;
`"a"`, `"r+"`, `"w+"` and `"rb"` raise, because `a`, `+` and `b` are not
rights letters. The argument error names the syntax.

`route_rights` is unit-tested directly — the host smoke reaches only the
harness's model of the rule — and the harness now models a spelling it will
not open a route with, so `usage` is reachable from a host build. Verified
by negative control: accepting an unreachable right fails the open suite at
its `'rn'` assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`lua.md` is the reference and `pack-backed-reads.md` is the storage-transport
plan; the arc that turns `kore.fs` from POSIX-with-kore-errors into the
store's own surface had no written shape.

States the rule the whole arc turns on — authority is a rights string,
behaviour is an options table, and the surface never offers a right it
cannot exercise — and why the third clause is what bounds the work: a
letter unlocks when its verb lands, and until then it is refused by name
rather than silently ignored.

V1 is marked done and its break tabulated. V2 (create with a subtype, which
is what a Lua installer needs to write what `require` loads) through V6 are
aspirational, as is the blocked set: grant creation and projection on
kore#986/#991, index publication on the `index` feature. Acceptance is
unmet — nothing here has run on metal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three advisories landed against Wasmtime 48.0.3 on 2026-10-02: mis-typed
WebAssembly tag imports can corrupt the GC heap (RUSTSEC-2026-0325,
medium), rooting for GC values live across `try_call` may be missing
(-0326, medium), and a component async-lifted callback's result count is
unvalidated, overflowing a native stack buffer (-0327, **critical**). All
three are fixed in 48.0.4; 48.0.5 is the current patch of that line.

`cargo audit --deny warnings` fails the Advisories workflow until the bump,
over both this lockfile and `native/`'s. It needs no source change: a patch
release within 48.0.x, and the runtime, component-model and async surfaces
this host uses are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@julian-avar julian-avar changed the title lua: Lua 5.5 on Kore, with io and os over the task's own filesystem lua: Lua 5.5 as an embeddable crate, with a filesystem surface that speaks the store Oct 5, 2026
@julian-avar julian-avar changed the title lua: Lua 5.5 as an embeddable crate, with a filesystem surface that speaks the store lua: Lua 5.5 as an embeddable crate, with io, os and kore.fs over the task's own filesystem Oct 5, 2026
@julian-avar julian-avar changed the title lua: Lua 5.5 as an embeddable crate, with io, os and kore.fs over the task's own filesystem lua: Lua 5.5 as a crate, with io, os and kore.fs Oct 6, 2026
`lua.md` stated the mode-to-rights convention but never showed a script
using one, and never placed the Lua surfaces against the other two. A
reader could not tell why `kore.fs` exists beyond nicer error tables.

A DagFS route is reached through an affine `View`, and only Rust can hold
one: a Lua value can be copied into a table or captured by a closure, and a
C struct can be memcpy'd, so neither language can be handed something
affine. Both get a generation-checked handle instead, which is why `io`,
`kore.fs` and `kore-ffi` all deal in handles while `kore::fs` deals in the
capability itself. That is one table, and it explains the whole shape.

Then what `io` cannot say, in code rather than prose: asking for less
authority on purpose, asking what the task actually holds, and taking
`"w"`'s three fused decisions apart so only two are made. Plus the two
things only the kore surface can see — a `revision` that moves under you,
and an `id` that answers "same object" where a path answers "same name".

No surface can forge authority; the store resolves every route through a
generational slot. What differs is how much of the vocabulary each can
say, what it costs to say it, and how early a mistake is caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@julian-avar

Copy link
Copy Markdown
Contributor

@SnowCheetos Take a look if its good now

@micro-perceptron
micro-perceptron merged commit 3bd47ad into alpha Oct 7, 2026
7 checks passed
@micro-perceptron
micro-perceptron deleted the ft/lua-stdlib branch October 7, 2026 22:27
SnowCheetos pushed a commit that referenced this pull request Oct 14, 2026
Brings in Lua 5.5 (#23, #25) and the Wasmtime 48.0.3/48.0.5 RUSTSEC bumps,
so kore can pin one revision carrying both its kore-lua worker and this
branch's kore-wasi changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
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.

4 participants