Skip to content

Releases: oftomorrowinc/byollm

0.1.2

Choose a tag to compare

@github-actions github-actions released this 29 Sep 05:10

0.1.2

A security release. Upgrade daemons; nothing else needs to move.

PROTOCOL_VERSION is still 2. A daemon on 0.1.2 talks to a hub or relay on
0.1.0 or 0.1.1, and the other way round. The wire, the on-disk shapes and the
hub's endpoints are unchanged. If you run the Supabase adapter on your own
site, there is one migration to apply — see below.

Three of the four changes come from a private review by David Sturgeon, who
read the code and the security document side by side and reported what did not
match. Thank you, David. SECURITY.md now says how to do what he did.

What changed

The claude backend is confined to the job's scratch directory. A
process-class backend runs somebody else's CLI, and that CLI has its own ways
of reading files. The fixed argument list the daemon passes to claude gains a
switch that confines the child's file surface to the empty directory a job
runs in; codex needed no change. This is not taken on trust: a new
adversarial test runs the shipped, signed-in binary against a canary file
outside that directory on every run, and reports inconclusive rather than
a pass when its control cannot reach the canary either. It is on by default
where a CLI is installed and signed in, and prints a line naming itself when it
skips. docs/security.md §3.2–§3.3 say which platforms each claim has been
proved on, and that this is a checked contract with the CLI rather than an
OS-level sandbox — that is the next piece of work, and the document says so.

The updater checks direction and authority. autoUpdate is still off by
default. When it is on, the daemon now refuses any version at or below the one
running, takes offers only from the update authority (updateAuthority, the
reference hub unless you set it — never another site you paired with, never a
direct-mode pairing), installs without scripts from registry.npmjs.org, checks
the package's SLSA provenance statement before the new version runs, and rolls
back if that check fails. docs/security.md gains §7a, Updates.

The Supabase adapter stops losing long cloud-lane jobs. A job a relay had
adopted carried the relay's lease clock, which nothing on your site renews, so
byollm_expire_due requeued it after one lease (60 s by default) and the
device's later complete was refused. Every cloud-lane job slower than one
lease lost its result, silently. The in-memory store was fixed before 0.1.0;
this is the same rule in SQL. If your site uses @byollm/server's Supabase
adapter and the cloud lane, apply

packages/server/supabase/migrations/20260928000000_adopted_leases_are_not_ours.sql
with supabase db push. Sites on the direct lane, or on the in-memory
store, have nothing to do. The cloud-lane store cases now run against Postgres
in CI as well as in memory.

SECURITY.md says how to report. Private vulnerability reporting is on for
the repository; the email door stays; the file says what a reporter can expect
and when. The security document's opening no longer says "impossible" — it
says what is designed out, what is detected, and what is neither.

Also

Four more of David's changes merged tonight, as pull requests #14–#16 and #13
— thank you again.

The docs say what the code does. A new site is pinned and served from its
first heartbeat; which sites reach your device is decided in the app's
dashboard, and the machine prints now serving <site> with the fingerprint
the first time a new site's work runs. The daemon README, SECURITY.md and
spec 009 had still described byollm approve and a waiting list, which the
code retired some time ago. Runner tokens, which the docs also described, were
dropped before 0.1.0 — every request after pair is signed by the device key.
And the Next.js mount: the daemon pairs with an origin and drops any path, so
the route has to live at app/byollm/[...route] — not under /api; the
READMEs, the next.ts docblock and the site snippet now show that shape.

A machine nobody has set up says so. With no config, byollm services
listed the built-in default as if somebody had chosen it, and byollm status
read running about a device on which nothing had ever run. Both now say
there is no config and name byollm setup. And byollm model <service> <name> works for HTTP services again — Ollama, LM Studio and vLLM had all
failed with "requires a baseUrl" before the probe could run.

secretsMatch is removed from @byollm/server. It was exported and
called by nothing: device codes are looked up by digest, never compared, so
the function only read as evidence of constant-time handling that does not
happen. Nothing in this repository imported it; if you did, there is nothing
to replace it with because nothing needed it.

zod is pinned exactly. byollm, @byollm/protocol and @byollm/relay
shipped it as a caret range, so a fresh install got a version CI never ran
against. All three name 4.4.3 now. The release workflow's actions are pinned
to commit SHAs and its tokens are least-privilege; the Next.js example moves
to 15.5.24 to clear its advisories. The unit suite no longer depends on how
your OS answers a SYN to a closed loopback port, which is why it now passes on
WSL2.

What this release does not change

No wire field, no on-disk shape, no hub endpoint, no dependency. The one
schema change is the migration above, and it only matters to a self-hosted
Supabase site on the cloud lane.

The warning at the top of the README stands: the protocol is version 2 and
settling; the software is early.

0.1.1

Choose a tag to compare

@github-actions github-actions released this 22 Sep 20:26

0.1.1

Documentation only. No code changed, and nothing needs upgrading.

PROTOCOL_VERSION is still 2. A party running 0.1.0 talks to a party
running 0.1.1, in both directions, without either being upgraded first — which
is the promise 0.1.0 made and this release is the first to keep. If you have a
daemon on 0.1.0, leave it alone.

What changed

The READMEs open with what the package is. Every package README began with
a stack of release notes — three markers deep, and 103 to 129 lines of them
above the package's own title. npm serves the tarball's README, so
npm view byollm@0.1.0 readme opened with "alpha.15 is a breaking wire
change, and it breaks daemons and relays"
, on the release whose entire
subject is that the wire is now locked. The root README put # BYOLLM at line
340, under 322 lines of alpha history.

Nothing was deleted. Twenty-two notes that existed nowhere else were recovered
into docs/release-notes/, and CHANGELOG.md indexes
all forty, newest first. Where a version had been written two ways — the root
README carried the product summary and a package carried its own — both are
kept.

The demo installs the version it demonstrates. examples/demo told
readers npx byollm@alpha connect, which resolves to 0.1.0-alpha.103. That
daemon speaks protocol 1; the hub serves protocol 2 and only 2. So the demo's
own instruction was refused by the service it demonstrates, with an error that
read like the reader's fault. It says @latest now, and the gate that was
written to prevent exactly this now walks examples/ — it had been walking
three hardcoded paths.

Sentences that stopped being true at 0.1.0. CONTRIBUTING.md said the
protocol was v0 and would change without a deprecation path — the opposite of
what 0.1.0 ships. The bug template asked for a version and suggested an alpha.
The docs site dated facts to 0.1.0-alpha.44, .58 and .62 for readers who
never saw one; they say before 0.1.0 now.

What this release does not change

No dependency, no schema, no wire field, no on-disk shape. The tarballs differ
from 0.1.0 by their README.md and their version number.

The warning at the top of the README stands, in the words it was written in:

0.1.0 — early. The protocol is version 2 as of 0.1.0; the software is
still early. These packages run one hosted service — byollm.cloud — and a
small number of integrations; beyond that they have little mileage, and most
of what we know about the failure modes we learned from the first people to
try.

0.1.0

Choose a tag to compare

@toddsampson toddsampson released this 22 Sep 16:05

0.1.0

The wire is locked. What that means, and what it deliberately does not.

This is the first release with a promise attached. Everything before it was
0.1.0-alpha.N and said so at the top of every README: the protocol was v0 and
would change without a deprecation path.

That changes here, for three surfaces and no others.

What is locked

A party running 0.1.0 can talk to a party running any later 0.1.x, in both
directions, without either being upgraded first.

That covers the envelope, pairing, and the claim wire — the three surfaces
@byollm/conformance proves, and it is a stricter promise than "we will not
remove fields". The full statement, including what breaks it, is
docs/schema-lock.md.

The part that surprises people is in there too, so it belongs here: every
wire shape is .strict(), so an unknown field is refused rather than ignored.
Which means, in the lock document's own words:

Adding an optional field is a breaking change — on the side that has not
upgraded.

Inside 0.1.x a locked surface takes no new fields at all.

That is a real constraint on how fast the wire can grow, and it is the price of
a lock that means anything.

What is deliberately not locked

Stated here rather than somewhere quieter:

  • The console protocol is EXPERIMENTAL. Hosted-only at 0.1.0, so both ends
    are ours. ConsoleFrame and everything under /console/* may change shape in
    any 0.1.x.
  • known-models lists are fluid. The model namespace moves faster than our
    releases; a list shipping in a release is a snapshot, never a contract, and
    free text is always accepted where a model is named.
  • Refusal MESSAGES are not locked, though refusal CODES are. The words
    change as we learn to say them better; the code a program branches on does
    not.
  • Everything in @byollm/server, @byollm/relay and @byollm/daemon that
    is not one of the three surfaces above.
    Those packages are implementations
    of the wire, not surfaces of their own: they hold to the shapes, not to their
    own exports, and an API you call directly can move in any 0.1.x. If you
    npm install one and program against its functions, you are outside this
    promise.

Three shapes reserved so later work stays additive

Because a lock that takes no new fields is only bearable if the fields a known
feature will need are already there. None of these is read by anything yet:

  • Purpose.routing on the site manifest — user-choice | site-fixed | user-first-with-fallback, validated and not acted on. The manifest is the
    one locked surface a third party writes by hand, and it is .strict(), so
    the window for this field closed here.
  • ~/.byollm/config.json carries version: 1, with one writer and
    migrate-on-load. Absent means 1, permanently.
  • 0.1.0 speaks protocol 2, and only 2. Protocol 1 was the alpha wire and is
    refused by name from here. The multi-version wire — a later version served
    alongside this one, routed by the version a daemon declares — is the
    operating model for the next protocol change, not a property of this
    release; its mechanism (a union of literals, version-parameterised schemas,
    or a second endpoint family) is a ruling still owed, and B291's test refuses
    any half of it landing silently.

What a conformance red means, and what it does not

@byollm/conformance is published as the thing that tells you whether you
speak this protocol, so its own limits are written into its README: a red says
this target did not satisfy that check, on this run. It does not say the
fault is yours — and if the kit is ever the flaky one, you have no way to tell
our defect from yours. The kit's own flake rate is measured rather than
assumed, and an unreliable check leaves the verdict rather than having its
assertion loosened.

What the number does not promise

The version number is a promise about the wire, not about mileage. These
packages run one hosted service — byollm.cloud — and a small number of
integrations; beyond that they have little mileage, and most of what we know
about the failure modes we learned from the first people to try. A 0.1.0 does
not change any of that.

What the number does change: you can build against those three wire surfaces —
the envelope, pairing, the claim wire — and expect them to still be there. Not
the packages that implement them; that distinction is the carve-out above, and
it is the one worth re-reading before you pin anything.

If you are upgrading from an alpha

One field left the wire in this release — paused on HeartbeatRequest,
which nothing ever set — and it is the last field that will, until 0.2.0.

It is required on a .strict() object, so there is no version of that removal
with an overlap: a 0.1.0 daemon is refused by an older hub, and an older daemon
is refused by a 0.1.0 hub. Upgrade the hub and the daemons together.

PROTOCOL_VERSION moves from 1 to 2 with it, which is what makes that
break legible: a mismatched pair now gets a refusal naming both versions and
the upgrade command, instead of a schema error naming nothing.

byollm pause was removed in B043 a fortnight ago — use byollm stop — and
the field outlived the feature only because both ends parse strictly. 0.1.0 is
the one release that can move both, which is why it goes here rather than
never.

The three renamed verbs go with it. byollm install is byollm start,
byollm uninstall is byollm stop, and byollm models is byollm services.
Each has printed "the old name still works for now" since byollm_020; now they
answer "unknown command".

Everything else is the alpha warning coming off the surfaces that earned it,
and the lock document arriving beside them.

0.1.0-alpha.102

0.1.0-alpha.102 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Sep 00:45

0.1.0-alpha.102

A hosted console was landing in /bin/sh. There is no default now.

byollm console-agent read BYOLLM_CONSOLE_SHELL and fell back to /bin/sh
when it was unset. On the hosted box that variable was set nowhere — not in the
image, not at first boot, not by the supervisor that starts the listener — so
the fallback was not a fallback. It was the behaviour, on every console, since
the feature shipped.

The box ships a restricted shell: an allowlist, a tested one, with its own
suite attacking it. Nothing ever spawned it. ls answered with the filesystem
while the page's greeting said the box "runs a fixed list of commands".

What changed

console-agent takes its shell from --shell <path> and refuses to open a
console without one
, naming what is missing. An unrestricted shell is still
available for local work, by asking for it in words that cannot be typed by
accident:

byollm console-agent --shell /opt/byollm-box/shell    # the fence
byollm console-agent --unrestricted-shell             # a bare shell, on purpose

The shell in use is printed at startup, every time. The variable is gone.

A default reached by forgetting is not a default. The permissive path had to
stop being the one you get by omission, and saying which shell is running had
to stop being something nothing anywhere did.

Exposure

Owner-only: a console already required the owner's authentication, a burn-once
grant, and an end-to-end encrypted channel. Nobody else could reach the shell
that was running. What was lost was the fence a box promises its own owner, and
the honesty of the sentence on the page.

If you drive console-agent by hand

Pass --shell. The one-argument form is unchanged — its session description
already carries its own shell, explicitly, which is why it was never affected.

0.1.0-alpha.99

0.1.0-alpha.99 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 21:45

0.1.0-alpha.99

The console dropped every keystroke's echo. Two ends, two base64 alphabets.

The box encoded a console frame's data with Node's standard base64. The
browser read it with a strict base64url decoder. The two alphabets disagree on
three characters — +, /, and the = padding — so the decoder returned
nothing, and the branch that handled nothing returned in silence.

No fault was raised anywhere, because nothing was wrong anywhere. The envelope
sealed, opened, verified against the pinned identity, and arrived in order. The
frame was received and counted as received. Then it decoded to nothing.

What an operator saw was the arithmetic of padding. One typed character is one
byte, and one byte always pads, so every keystroke's echo vanished. A longer
chunk survived only when its length happened to be a multiple of three and its
bytes happened to avoid + and / — roughly one chunk in three. The report
that found it described exactly that: "some characters show… just never mine,
and only some of the other ones from the box."

What changed

The protocol owns the codec now, and both ends call it instead of spelling it
out for themselves. data is checked rather than described: it had carried
the note base64 and no validation since it was written, which is the reason no
test on either side could see the two ends disagree about what it meant.

Decoding deliberately accepts both alphabets. That is a compatibility
decision, not a lax one: a box is software running on somebody else's machine,
and a reader that accepted only the canonical spelling would fix the console for
whoever upgraded first and for nobody else.

For anyone running an older box

You do not need this release to get your console back. The fix that matters for
a box already in the field is on the browser's side, and it reads what your box
sends today. This release makes the box's own spelling canonical.

Wire compatibility

The frame format is unchanged. A payload is spelled in the canonical alphabet
now instead of the standard one, and both are read by both ends, so a box and a
browser on either side of this release understand each other.

0.1.0-alpha.98

0.1.0-alpha.98 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 17:06

0.1.0-alpha.98

The box signs its console data connection. It never has.

0.1.0-alpha.97 taught the box's CONTROL socket to dial and sign, and it
worked: a box holds its connection and receives a console announcement. Then the
agent opened the DATA socket — the one the shell actually runs over — with

new WebSocket(url)

bare, at a door that requires a runner id, an issued-at, and a signature over
the session id, and refuses before the handshake without them. So the console
got as far as being announced and no further.

It signs now. Over the SESSION rather than the endpoint, deliberately: the
control socket signs over its own endpoint because there is no session yet, and
this one must not, or a signature captured from one console could be replayed
to join another.

For anyone driving the agent by hand

byollm console-agent <session> now requires the device to be paired, because
signing needs the runner id the pairing holds. It says so plainly if it is not.
That path exists so the route a real console takes is the one a person can walk
when something has gone wrong — which is only true if it authenticates the same
way.

Nothing else changed

No wire change, no protocol change, no new dependency. The daemon proper has
been serving jobs correctly throughout; every one of these releases has been the
console sidecar and nothing else.

0.1.0-alpha.97

0.1.0-alpha.97 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 15:58

0.1.0-alpha.97

The box now opens its console control socket. It never has before.

If you run a hosted box, byollm console-agent printed "listening for consoles"
and then held a socket it had never opened. The hub was never told the box was
there, so a console opened from the dashboard waited for a device that — from
the hub's side — had never arrived.

The cause is one line. The listener's dial() began:

if (deps.connect === undefined) return;

connect was documented "Injected in tests", and only ever was. The production
caller passes url, runnerId, keys, run and log, and no connect — so
the function returned immediately, having opened nothing, registered with
nobody, and reported no fault. It is now defaulted to a real implementation.

Nothing else was wrong with the box. The daemon proper has been serving jobs
correctly throughout; only the console sidecar was affected, and only for
consoles.

Why the logs looked healthy

0.1.0-alpha.96 fixed a crash loop in the same file by giving the listener a
keepalive of its own. That was correct and it is still correct — but with no
socket ever dialled, the process went from dying every few seconds to holding a
socketless calm indefinitely. "One clean listening line, zero redials" was the
bug wearing the appearance of health.

Also

A closed control socket now reports its CLOSE CODE rather than only that it
closed. A door refusing a signature and a network drop are the same sentence
without it, and they send an operator to different places.

No wire change, no protocol change, no new dependency — Node's global
WebSocket carries the signed headers, measured in the node:22 image a box
actually runs rather than assumed from a developer's Node.

0.1.0-alpha.96

0.1.0-alpha.96 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 05:17

0.1.0-alpha.96

The console listener stays up, and onNoRunner accepts the handler its own
documentation promised.

The box's console sidecar was crash-looping

If you ran byollm console-agent — the process a hosted box runs so a browser
console can reach it — it restarted every few seconds, and a console opened
against it died mid-session with no closing message.

Two defects, one cause, and the cause was the shape rather than any line. The
process stayed alive only for as long as its socket did.
The command parked on
a promise that never settles and references nothing, so it could not keep Node
running; the open websocket was doing that by accident. An accident that ends
whenever the socket closes is also a process that can never reconnect, because
reconnecting is what you do after the socket is gone.

The listener now owns a handle of its own and holds it until it is stopped —
lifetime is a decision rather than a side effect — and it dials again with a
1s→30s backoff, reset on success. That covers a broker that CLOSES the socket
and a broker that REFUSES the door; the second used to be the same dead end by
another road.

It also says why, every time. The old one printed one optimistic line and then
died silently on a loop, reporting what it intended and never what happened to
it.

The daemon proper was unaffected throughout — a box kept advertising its
services normally. Only the console sidecar looped.

onNoRunner accepts a handler that returns nothing

// Both of these were type errors before this release. Neither should have been.
await job.result({ onNoRunner: () => { showConnectModal(); } });
await job.result({ onNoRunner: async () => { await showConnectModal(); } });

The docstring has always said "return nothing and NoRunnerAvailableError is
thrown", and the type only accepted the first half of that. A function returning
nothing is () => void, which TypeScript will not assign to a signature
returning string | undefined; the async form failed the same way, and that is
the one most people want, since prompting somebody to connect their device and
waiting for them is asynchronous.

void is now part of the return union. Nothing about the runtime changed and
nothing about the wire changed
— this only stops rejecting handlers that were
always meant to work.

Worth stating plainly, because the README had it wrong: return a value and the
wait RESOLVES with it, stamped fallback: true by the wait itself so a hosted
answer can never be reported as the user's own compute. Return nothing and it
THROWS, so catch it.

Also

Windows CI no longer fails on a timing accident — a synchronous test was being
starved past a 5s limit on a runner whose import phase alone takes 28s. Test
infrastructure only; nothing in the package changed.

0.1.0-alpha.101

0.1.0-alpha.101 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 22:41

0.1.0-alpha.101

The box greets an empty room. Whoever arrives now hears it.

A console opens the shell, then dials a socket, then builds the session that
consumes the shell's output — in that order, deliberately, so a box with no pty
never dials for nothing. A pty starts producing the instant it spawns, and
onData only subscribed when it was finally called.

So everything the shell said in that window went to a listener that did not
exist yet. What the shell says in that window is its greeting and its first
prompt — and the first prompt is the entire signal that it is safe to type. An
operator opened a console, saw [connected] and an empty pane, and had no way
to tell a ready shell from a dead one.

The subscription now happens at spawn, and what arrives is held until somebody
comes for it. Bounded, because "nobody ever attaches" is a reachable state and a
shell left talking to itself must not grow without limit; the bound keeps the
beginning, which is the part that had to survive.

An exit is held the same way. A shell that died before the session existed — a
bad command, a missing binary — used to leave that session waiting on an exit
it had already missed.

The third time

The browser's hello went into this gap. Then the console's stylesheet. Now the
box's own greeting. The contract is written on ConsoleShell.onData now rather
than in one implementation's history: whatever the shell produced before the
subscription must be delivered to it.

0.1.0-alpha.100

0.1.0-alpha.100 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 22:02

0.1.0-alpha.100

0.1.0-alpha.99 shipped the console's shared codec through one door. The
browser comes in the other one.

.99 gave the protocol encodeConsoleData/decodeConsoleData so the box and
the browser would stop each spelling base64 for themselves. They were exported
from the barrel, which is the entry the daemon imports. The browser imports
@byollm/protocol/portable — the entry that carries everything a tab needs and
nothing that needs Node — and the codec was not in it.

The daemon compiled. The dashboard could not, and could not have: an export the
consumer cannot import is the same as no export, arriving one release later.

portable.ts has warned about this in its own header since it was written —
"the door had to be built twice" — so the fix is not only the two lines that
add the names. The two entries are now held to the same console surface by a
test: a name added to one and forgotten in the other fails in the protocol's
own suite rather than in a bundler in another repository.

Upgrading from .99

Nothing to change. .99 is not wrong, it is incomplete, and only for a browser
importing the codec — which nothing shipped could do. If you are on .98 or
earlier, read .99's note: that is the release with the console fix in it.