Skip to content

Release v0.0.8 - #407

Merged
davidmckayv merged 3 commits into
mainfrom
release/publish/v0.0.8
Sep 6, 2026
Merged

Release v0.0.8#407
davidmckayv merged 3 commits into
mainfrom
release/publish/v0.0.8

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Release v0.0.8

Merging this publishes the image, tags the commit and creates the GitHub Release.

Before merging:

  • The notes below describe what a deployment does differently
  • The smoke journey passed against a licensed deployment, and the result is
    pasted in a comment: bash scripts/start.sh && bun run test:smoke

Merging runs the full suite against this commit before it builds, so there is
nothing to check about CI here. The journey is the part CI cannot do: it needs a
licence, and a licence belongs to the machine it was issued for.


### A person's Stop is recorded as a stop, not as a failed action

Pressing Stop mid-action aborts the request, and the gateway wrote that outcome beside the decision
row as a failure — the same `computer.action_failed` type it writes when a computer is unreachable or
times out. The row's message already said the action was stopped, but anything counting failures by
type, the natural way to watch for outages, read every Stop as one. A stop now writes its own type,
`computer.action_stopped`: the action did not happen and nothing broke. The audit page already groups
it with the other did-not-happen outcomes, and a policy dry-run skips it the way it skips a failure,
since both sit beside a decision row that is already scored.
### A malformed `DATABASE_URL` is refused without printing the password

`DATABASE_URL` is taken apart before it reaches Bun, and the string most likely to fail that parse is
one with a stray character in the password. The refusal for an unparseable value quoted the whole
string back to name the fault, which wrote the database password into the log line that reported it.
It now names the variable and the shape it expects, the way the other refusals beside it already do,
and never echoes the value.

### Wiping a computer is recorded even if clearing its stored state fails

Resetting a computer destroys the profile first and wrote the audit row last, after two Postgres
deletes. A connection reset, a failover or a statement timeout in either delete threw before the row
was written, so the most destructive button in the product could leave a wiped computer -- every
login gone, no undo -- with nothing on the trail to say who wiped it or when. The row is now written
as soon as the profile is gone, which is the point after which nothing can be put back. A failure in
either delete is still reported to the caller.
### Pressing Stop is recorded as a stop, not as a computer that is not running

The computer transport answered a Stop correctly only when it arrived before the request left. The
caller's signal is handed to `fetch` precisely so a Stop can also land mid-action, and a fetch
aborted that way rejects with an `AbortError`, which fell through to the message for a computer that
cannot be reached. The person was told their own click had failed because the assistant's computer
was not running, and the gateway wrote that sentence into the action's audit row as its failure --
so a deliberate stop read back as an outage. A genuinely unreachable computer and a timeout still
say what they said.
### A component the server refused is no longer drawn anyway

A sandboxed component asks the server at call time whether the Bot may still use it, and a refusal
is recorded so the drawing can be replaced with a card saying so. The renderer looked that refusal
up under `props.toolCall.id`, which is the shape a tool HANDLER is given; a renderer's props carry
the id flat, as `toolCallId`. The lookup key was therefore always undefined, the refusal was never
found, and the component rendered as though it had been allowed -- so revoking a component from a
Bot did not take effect on screen until the five-second grant poll caught up, and a failed decision
request showed nothing at all.
### A component whose name has a stray space is the same component

The catalogue announcement asked whether each component's `name`, `title`, `kind` and `description`
were more than whitespace, and then published the untrimmed strings. A `name` is a component's
identity -- it is what `syncCatalogue` compares against what is already published, what `decide` and
`listForAgent` look up, and what a grant names -- so a build shipping `" weatherPanel "` added a
second catalogue row beside `weatherPanel`: published, ungranted by anybody, and impossible to hold
a Bot back from under the name people use. The four fields are now stored as the strings the guard
approved.
### A password with a `%` in it says so, instead of failing as `URI error`

`DATABASE_URL` is taken apart before it reaches Bun, and each part is percent-decoded. A part
holding a `%` that starts no escape -- `postgres://openbot:100%pure@host:5432/openbot`, which a
generated password produces often enough -- is a string `new URL` accepts and `decodeURIComponent`
rejects, so the server stopped with `URIError: URI error` and named neither the variable nor the
part. It now refuses with the same kind of sentence as every other malformed address: which part is
wrong, and that a literal `%` must be written `%25`.

A correctly encoded password is unaffected.
### An empty Bot `PORT` is unset, so NaN never reaches Bun.serve

`PORT=` on `agent-bot` and `agent-langgraph` used to parse as `NaN` (`??` does not treat empty as absent) and `Bun.serve` bound an ephemeral port while compose still published 4200/4201. A prefix typo (`42o0`) started on 42. Empty now means the shipped default; anything that is not a whole port number refuses to start.

### The server connects to Postgres on Windows, and `localhost` is no longer a coin toss

Two separate faults, both of which stop a deployment reaching its own database and neither of which
says so.

The connection address went to Bun as a URL. Bun reads such a URL's path, the database name, as the
path of a unix socket, ignores the host and the port, and fails to open a socket Windows does not
have (oven-sh/bun#27713). The server could not reach Postgres there at all, while `psql` inside the
container and a plain TCP connection from the same machine both worked, which makes it look like a
network fault rather than a parsing one. The address is now passed in parts, and `DATABASE_URL` is
removed from the environment as it is read, because Bun prefers that variable to the parts it was
handed and would otherwise put the address straight back through the same parser. A URL with no host
or no database is now refused by name instead of connecting somewhere nobody chose.

Separately, Compose published its loopback ports on `127.0.0.1` only. `localhost` resolves to `::1`
and `127.0.0.1` in an order the platform decides, and a client handed `::1` first does not fall back
to the other, so the same configuration worked on one machine and failed on the next for a reason
nothing in the error mentions. Every loopback port is now published on both addresses. Both are
loopback, so nothing became reachable from another host.

### A bad `COMPUTER_MEMORY_BYTES` refuses to start the supervisor, instead of capping a computer at 512 bytes

`COMPUTER_MEMORY_BYTES=512m` used to parse as `512` via `parseInt`, which Docker accepts as a memory
cap Chromium cannot live in. Empty `COMPUTER_MEMORY_BYTES=` is still unset (no cap). A value that is
not a whole number of bytes now exits before any computer is created.
### An IPv6 address in `AGENT_ENDPOINT_ALLOWED_HOSTS` now matches however it is written

The endpoint check compares the list against the address as the URL parser spells it, compressed
and lower-case, while the list kept each IPv6 entry as the operator wrote it. `[0:0:0:0:0:0:0:1]:8443`
was therefore a line that silently never matched, the failure the list's other refusals exist to
prevent. Stripping the brackets on both sides also folded two different names into one, so naming
`[fd00::1:8443]`, an address, admitted `[fd00::1]:8443`, another address on a port, and the other way
round. Bracketed entries are now stored in the parser's spelling, with the port kept as written, and
compared with their brackets on; an entry the parser does not read as an address is refused at boot,
naming the entry, as a URL or a wildcard already was. Names and IPv4 entries are unaffected.
### A Bot's own decline is only recorded against a Bot the caller may reach

A Bot reports that it declined a request through the person's session, and the audit row says
`reportedBy: the Bot itself`. The route wrote that row for any agent id in the path, without asking
whether the caller could reach that Bot, so any signed-in person could put a decline, in any words,
against any coworker, one they cannot see included, and an administrator reading the trail would take
it for something the Bot said. The route now asks the store first, as every other route on a Bot
does, and answers not found for a Bot the caller cannot reach, writing nothing.

### The engine socket the supervisor is given can be pointed somewhere else

Compose mounted `/var/run/docker.sock` into the supervisor as a fixed path. That is correct for
Docker, and for Podman on macOS, where `podman machine` symlinks it to the rootless socket inside the
virtual machine. It is wrong for rootless Podman on Linux, where the path is either absent or, with
`podman-docker` installed, a symlink to `/run/podman/podman.sock`, the rootful socket, which is not
the one running. The supervisor held a dead socket and every attempt to give a Bot a computer failed
with a message about not reaching Docker.

The mount source is now `ENGINE_SOCKET`, defaulting to `/var/run/docker.sock`, so nothing changes
unless it is set. On rootless Podman on Linux, set it to `$XDG_RUNTIME_DIR/podman/podman.sock`.

### A Bot's computer is waited for properly on Podman, and the supervisor can reach the engine there

Two things stopped OpenBot running on Podman, which nothing had tried before.

The supervisor could not reach the engine at all: `The supervisor could not reach Docker`. The socket
is there and the mount is right, but Podman's virtual machine runs SELinux and labels the socket in a
way a container is not allowed to read. The supervisor now declares `label=disable`, which is what
that needs and which changes nothing on Docker.

Then every cold start of a computer raced the first request to it. Readiness was read off the image's
`HEALTHCHECK`, and Podman does not report one: its images are OCI-manifest, the OCI image config has
no healthcheck field, and the instruction is dropped both when Podman builds an image and when it
pulls one that has it. With no health to read, the supervisor fell back to accepting a container that
was merely running, and a running container is not a browser that is answering, so the first request
arrived at a port nothing was listening on and came back as a computer that is not running. The
supervisor now states the healthcheck when it creates a computer instead of inheriting it, so
readiness no longer depends on how the image was built. On Docker the behaviour is unchanged.

@github-actions github-actions Bot added the release Release PR: merging publishes label Sep 6, 2026
@github-actions github-actions Bot added the release Release PR: merging publishes label Sep 6, 2026
@davidmckayv
davidmckayv merged commit fd4fb49 into main Sep 6, 2026
14 checks passed
@davidmckayv
davidmckayv deleted the release/publish/v0.0.8 branch September 6, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Release PR: merging publishes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant