Skip to content

kxbox: the bridge API on both backends, checked by signature - #65

Merged
tamnd merged 1 commit into
mainfrom
kxbox-bridge-api
Sep 6, 2026
Merged

kxbox: the bridge API on both backends, checked by signature#65
tamnd merged 1 commit into
mainfrom
kxbox-bridge-api

Conversation

@tamnd

@tamnd tamnd commented Sep 6, 2026

Copy link
Copy Markdown
Owner

The M1 row is "The bridge API: boot, sh, read, trace, insmod". All five already existed. Two of them did not work the same way on both backends, and nothing was checking, which is the part this PR fixes properly rather than case by case.

The five calls

call what it does on a recording
kxbox.boot(profile) a session, live if there is one picks the recorded backend and says why in the banner
box.sh(line, recipe=) one shell line, waited for the recorded standard output and exit status
box.read(path, recipe=) a file inside the guest the recorded snapshot of that file
box.trace(name, do, ...) the function graph tracer around something the recorded tape, and the callable is not run
box.insmod(path) load a module the recorded reply to having loaded it

insmod had never worked on a recording

It was a branch inside Box rather than a call on a backend:

def insmod(self, path):
    if not self.live:
        return self.sh(f"insmod {path}", recipe=f"insmod {Path(path).name}")
    return self.backend.insmod(path)

The recorded side turned a module load into a shell line and looked it up under a recipe named insmod abba.ko. That naming convention was invented at that one call site and written down nowhere. No recipe has ever been named that way and none ever would be, so on the branch most readers are on, box.insmod raised NotRecorded every single time.

Corpus has an insmod now. It matches on a module field on the recipe rather than on the path, because the path a lesson writes is wherever the rootfs build happened to put the file and only the last part of it means the same thing on both sides. Box.insmod is a straight delegation like the other four, so the difference between the backends lives in the backend that has it.

max_depth could not be reached from a lesson

V86.tape has had it since it was written, and its docstring says what it is for:

It is worth reaching for when the thing being traced is not a program that owns its window, because set_graph_function records everything underneath what you named and a busy window fills the ring buffer faster than a serial line can drain it. That failure looks like the guest hanging rather than like too much output.

Corpus.tape did not take it and Box.trace did not forward it, so the documented fix for a guest that appears to hang was unreachable from any lesson. Both take it now.

The check that keeps this true

Two tests, and they are the point of the PR more than any individual fix above.

@pytest.mark.parametrize("call", ("sh", "read", "insmod", "tape"))
def test_the_two_backends_take_the_same_arguments(call):
    live = inspect.signature(getattr(bridge.V86, call))
    recorded = inspect.signature(getattr(corpus.Corpus, call))
    assert list(live.parameters) == list(recorded.parameters), call

and one asserting that every backend argument is forwarded by Box, since Box is the only way in and a parameter it does not pass on is a parameter that exists in two places and nowhere a reader can type it.

An argument one backend has and the other does not raises TypeError on whichever machine the reader is on rather than on the machine the lesson was written on, which is the worst place to find out. Both of the problems above are that shape and both would have failed these tests on the day they were introduced.

write is excluded on purpose and there is a test saying so. The live backend has one and drives the tracer with it. A recording can say what a file contained and can do nothing at all about a lesson writing to that file, so a cell that wrote would take effect for the few readers with an emulator and quietly do nothing for everybody else. That is worse than a cell that cannot be written.

An ambiguous read now refuses to guess

Corpus.read took the first recipe in file order that had recorded the path. Two readings of /proc/lockdep_stats either side of an insmod is the ordinary case rather than a strange one, and a lesson asking without saying which would have been handed the before or the after depending on nothing it could see. Both are plausible readings of the same file, so it would have looked right about half the time.

`/proc/lockdep_stats` is recorded against more than one recipe (load-abba, lockdep-before), so this
has to say which: box.read(path, recipe=...). They are different readings of the same file and
picking one for you would pick the wrong one about half the time.

Naming a recipe that did not record the path you asked for now says what it did record, instead of falling through to the generic message.

Two new recipes, and the first for a profile other than teaching

Both are real captures that were already in the corpus from the C09 work, taken off one boot of D-lockdep with the module load between them:

lockdep/lockdep-before  real  /proc/lockdep_stats read on a fresh boot, before anything has loaded a module, with debug_locks still 1
lockdep/load-abba       real  loading a module that takes two locks in two orders, which lockdep reports without either thread ever blocking

Until now, boot("lockdep") gave a session that knew how to do nothing at all, because every recipe in the list was teaching. The banner said so honestly and that was the whole of it.

Adding them turned up one more thing. Corpus.evidence looked at a recipe's trace and nothing else, so a recipe whose evidence is a /proc snapshot rather than a tape came out marked handwritten however its metadata was written, and both of these are that shape:

lockdep/load-abba  handwritten  loading a module that takes two locks ...   <- before
lockdep/load-abba  real         loading a module that takes two locks ...   <- after

That would have put "nothing here is evidence" in the banner of every lesson using a snapshot backed session. It looks at every file a recipe points at now. The test fixture grew the .meta.toml beside its /proc snapshot that it should always have had, which is the same gap in miniature.

One name collision, resolved rather than documented

kxbox/web/first-tape.py did this:

box = V86.find(profile=os.environ.get("KXBOX_PROFILE", "A-full"))

KXBOX_PROFILE names a build, which is what the headless harness picks an image by. V86(profile=...) wants a boot profile. They are different namespaces for good reasons, set out in #63, and this was quietly mixing them. profiles.for_build() turns one into the other, and it can answer None, because three of the six builds have nobody standing on them: A-gzip, B-btf-external and C-longterm exist to be measured against A-full rather than to be booted by a lesson. The build name still goes out in the report, so nothing is lost.

V86 also validates its profile name now, for the same reason boot() does. It is the other way in.

Checks

Every gate, locally: ruff check and format, 1487 pytest tests with 7 skipped, 63 node tests, lintprose across 37 files, and the check runs of diagrams, nbbuild, sitebuild, kxbox, vendor, baseline, claimledger, coverage, bpc, kconfig, refcheck, lintnb and kxmanim. kxbox --check reports 5 recipes clean.

The five calls a lesson writes are boot, sh, read, trace and insmod. All five
existed. Two of them did not work the same way on both backends and nothing was
checking.

insmod was a branch inside Box rather than a call on a backend. On a recording
it turned a module load into a shell line and looked it up under a recipe named
`insmod abba.ko`, a naming convention invented at that call site and written
down nowhere. No recipe has ever been named that way, so the recorded path had
never once worked. Corpus has an insmod now, matched on a `module` field, and
Box is a straight delegation like the other four.

max_depth was on the live backend from the day it was written, was on nothing
else, and Box.trace could not forward it. The bridge docstring calls it the
thing to reach for when the guest would otherwise hang, and no lesson could
reach it. Corpus.tape takes it, Box.trace forwards it, and a test now compares
the two backends' signatures argument by argument so the next one shows up as a
failure rather than as a reader's TypeError.

Corpus.read used to take the first recipe that recorded a path. Two readings of
/proc/lockdep_stats either side of an insmod is the ordinary case and not a
strange one, and a lesson asking without saying which would have been handed the
before or the after depending on nothing it could see. It raises now and lists
the recipes.

Corpus.evidence looked at a recipe's trace and nothing else, so a recipe whose
evidence is a /proc snapshot came out marked handwritten however its metadata
was written. Both new recipes are that shape. It looks at every file a recipe
points at now, and the test fixture grew the .meta.toml it should always have
had.

Two new recipes, both real captures already in the corpus from the C09 work, and
the first for any profile other than teaching. Until now a lesson asking for the
lockdep kernel got a session that knew how to do nothing at all.

V86 validates its profile name the same way boot() does, since it is the other
way in. kxbox/web/first-tape.py was passing a build profile name where a boot
profile goes, which profiles.for_build now translates, and the build name still
goes out in the report.
@tamnd
tamnd merged commit 0387786 into main Sep 6, 2026
3 checks passed
@tamnd
tamnd deleted the kxbox-bridge-api branch September 6, 2026 09:31
@tamnd tamnd mentioned this pull request Sep 6, 2026
20 tasks
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