Repository navigation
Releases: lightconsen/syscity
Release list
v0.3.17
Full Changelog: v0.3.16...v0.3.17
v0.3.16-test1
v0.3.16
Highlights
- Computer Use can be scored on a public benchmark.
syscity bench run
drives a remote desktop over OSWorld's HTTP desktop contract,bench serve
steps one action at a time so the harness's own loop decides when the next
observation is taken, andbench probechecks a VM's endpoints in seconds
instead of three model rounds into a scored task. The OSWorld glue, the plan,
the host runbook and a one-command KVM provisioning script land together. - The desktop tools act on structure, not just pixels. Elements are
addressed by snapshot id and by platform id, the action ladder escalates from
accessibility to pixels to the keyboard only when a step is observed not to
have taken effect, every step is verified, and a fourth rung acts inside the
conversation's own browser page by role and accessible name. - The x86_64 Linux CLI is built on Ubuntu 22.04. The release binary's glibc
floor drops from 2.39 to 2.35, so it starts on every 22.04 machine — the
benchmark host included — instead of only on 24.04.
Added
-
syscity bench run: one Computer Use task against a remote desktop.
OsworldDesktopimplementsComputerAdapterover the VM's HTTP service
(/screenshot,/accessibility,/screen_size,/execute), so the action
ladder, element ids and effect verification run unmodified — they are what the
benchmark is supposed to measure. The agent's tool set is only
computer/screen_state/time/todo(shell, file, browser and web act on
the host, not the VM) and pre-approved, because no human answers approval
prompts; the result file is the schema the Python glue reads. A fake VM covers
the cache-and-invalidate rule, AT-SPI parsing, stale ids and action codegen. -
syscity bench serve: the step protocol. syscity sends one pyautogui
snippet over stdout, blocks until the harness has run it, and only then looks
at the screen — the ordering OSWorld's own loop has. In step mode/execute
is never called, and a test asserts it. Logging for bench subcommands now goes
to stderr, because stdout is the protocol's channel; the test holds that line
with the real binary. -
syscity bench probe:/screen_size,/screenshot,/accessibilityand
(with--execute) a read-only snippet through/execute, reported per
endpoint in seconds. Failures name the endpoint and exit non-zero; an empty
accessibility tree and a screenshot whose size disagrees with/screen_size
are warnings, because neither stops a run but both change how the agent works. -
OSWorld glue and a runbook (
bench/osworld/):run.py --mode replay|stepjoins OSWorld's evaluator to syscity's own observe/act loop,
with--split/--domain/--ids/--resumeand a per-runsummarythat keeps
infrastructure errors out of the denominator.docs/computer-use-benchmark.md
is the plan,docs/osworld-kvm-server.mdthe host runbook, and
scripts/osworld-bench-setup.shprovisions a KVM host in one idempotent
command (dependencies, Docker, OSWorld, the 12 GiB VM image, this release's
binary and thebench/glue, then the container self-check and probe). It
downloads the published tarball and verifies its checksum, so the host needs
no Rust toolchain;--from-sourcestill builds instead. -
Element addressing and an action ladder (
computer/element_index.rs,
computer/session.rs): snapshots carry a monotonic id and every element a
s{snapshot:08x}:{path}id; ids from an earlier snapshot are rejected as
stale rather than remapped. Actions escalateax_action→pixel→
keyboard, and only an observedSuspectedNoopescalates —Unverifiable
stops instead, so a click that could not be checked never fires twice.
ComputerSessionis now the one step engine (settle, three-state
verification, failure accounting, rollback, reflection, sensitive-UI
scanning);ComputerUseLoopdelegates to it instead of keeping a copy. -
The ladder reaches the page, the role and the background. A fourth rung
asks the conversation's own browser tab, by role and accessible name, through
a fresh ARIA snapshot — no name match means no action, never a neighbouring
control. Screenshots report the display geometry they were taken on and every
coordinate-taking tool says which space it is in, so a click on a scaled or
secondary display lands where the box was.ActionEffect::{Partial, Refused}
separate a half-done step from one the platform declined; refusal is terminal.
delivery: "background"acts through accessibility scripting without taking
the screen, and refuses with the reason when only an input-layer mechanism
exists. -
GAIA Level 1 benchmark. The official
question_scoreris wired through the
eval harness. The 53-task × 3-trial baseline was 70.4% (112/159); after the
determinism, retrieval and OCR work below, a repeat run scores 78.0% (124/159),
with tasks passing all three trials rising from 29/53 to 33/53 and zero-pass
tasks falling from 8 to 3. Both runs are recorded inevals/ledger.md. See
docs/benchmark.md§M2 for the method and the failure breakdown. -
web_searchcan read the pages it finds.fetch_top: Nfetches the top N
results in the same call (throughweb_fetch, so the SSRF guard applies), and
depth: "advanced"asks the provider for full page text where supported
(Tavilysearch_depth+include_raw_content). The result cap moves from 10
to 20. -
imagereads text out of images via the local RapidOCR models when the
visionfeature is on: the tool now returns recognized text in reading order
alongside the existing metadata, instead of dimensions only. The OCR models are
resolved on their own, without pulling the OmniParser detector. -
OCR decodes with the model's own dictionary. The recognition model emits
class indices, so the english PP-OCRv3 dictionary is downloaded and pinned to
the model; a class count that does not match it is an error, not a guess.
Detection boxes expand byratio·area/perimeter(the standard DBNet unclip)
and long lines are no longer squeezed into a 1600 px recognition input, which
is what made dense document screenshots read as fragments. -
Suite summaries report task-level outcomes: all trials passed / at least one
/ zero, with the zero-pass task ids listed. Trial-level rates hide flakiness. -
Eval runs inherit the caller's proxy through the new
AgentConfig.tool_environment, so shellcurlin an eval can reach sites that
need it.
Changed
-
The x86_64 Linux CLI is built on
ubuntu-22.04from the newlinux-amd64
feature profile, which pins the binary's glibc floor at 2.35. Built on
ubuntu-latest(24.04) it required GLIBC_2.39 and could not start on Ubuntu
22.04 at all — including the OSWorld benchmark host, whose runbook installs
the released binary. The profile is default features pluscloud, minus
vision; the arm64 Linux CLI and both macOS CLIs keepvision. -
Eval runs are deterministic and less budget-bound: temperature 0.0 (was
0.7), 60 tool iterations (was 30) with a soft "wrap up" reminder at 70%, and a
system prompt built from the registered tool list (it previously named five
tools that were not registered). -
Eval runs state the answer contract explicitly. The appended prompt now
says theFINAL ANSWER:line is read by an automatic checker, not a person:
the value alone, no unit, no explanation, no parentheticals, lists in the
requested order with repeats kept. On the GAIA run that motivated it, ~10
failing trials were answers the model knew but wrapped in prose or units
(22 yearsfor22,No — Earl cannot do it; …forNo). Measured on the
9-task subset drawn from those: 40.7% → 81.5% over 3 trials each, 7 tasks up,
none down. -
web_fetchretries transient failures once and tells the model when a page
looks JavaScript-rendered, instead of returning an empty body as if the page
had nothing to say. -
MSRV is now Rust 1.97 (was 1.95), required by wasmtime 50.
Cargo.toml,
rust-toolchain.toml, the CI workflows, the CI Dockerfile,README.mdand
docs/build.mdall move together — the MSRV job asserts they agree. -
wasmtime 48 → 50, zip 2 → 9 (locked at 9.1.2). zip 9's
ZipFile::name()
returns aResult<Cow<str>>;screen_recordernow propagates that error instead
of comparing the name directly.
Security
- The zstd 0.13 advisories are gone (RUSTSEC-2026-0391/0398/0403). They came
from wasmtime'scachefeature, which syscity never configures — it pulls
wasmtime-internal-cache→ zstd 0.13, a line with no patched release. Building
wasmtime withdefault-features = falseand withoutcacheleaves zstd 0.14
(via zip 9.1.2) as the only copy. - The three zip
extract()waivers (RUSTSEC-2026-0367/0368/0369) are removed
fromdeny.toml: zip 9 fixes them, anddeny.toml'swasmtime@48.0.5
duplicate-tree skip now nameswasmtime@50.0.0.
Fixed
- The x86_64 Linux CLI links, and starts on 22.04. Moving the build to
ubuntu-22.04exposed that the ONNX Runtimevisionlinks against — pyke's
prebuilt forx86_64-unknown-linux-gnu— is compiled against glibc ≥ 2.38
headers. On 22.04 the link fails outright (undefined symbol: __isoc23_strtoll,std::__cxx11::…::_M_replace_cold); on 24.04 it links but
produces a GLIBC_2.39-only binary. The release therefore builds the
linux-amd64profile, which drops the ONNX-based OCR/UI detection on this one
target.--features visionfrom source restores it on a 24.04+ host. - The cargo cache is keyed by runner image.
Swatinem/rust-cachekeys on
Linux-x64, so the 22.04 job could restore atarget/produced on 24.04.
(It was not this run's failure — the fix landed and a cold build failed the
same way — but the two jobs should not be able to share compiled objects.)
v0.3.14
Highlights
- CI is hardened for a public repository. Every workflow now declares
permissions: contents: read(only the release job elevates, tocontents: write); all 98 action references are pinned to commit SHAs rather than mutable tags; every job has atimeout-minutes; the expensive jobs need the lint job, so a formatting failure no longer spends an hour of macOS runner time; and a new push to a pull request cancels the run it supersedes. - macOS runner time cut. macOS is 89% of this repository's metered usage — $714.61 of $801.90 for the 25 days in October's report, at ten times the Linux rate. The macOS test suite now runs on
mainonly and the macOS arm of thebuildjob is gone, so a pull request spends no macOS minutes at all. The release workflow is untouched. Eval Nightlyruns again. It had failed every night, not on an eval but on the build before it:cargo build --no-default-features --features plugins,sqlite-vecdid not compile.
Added
Windows Check:cargo check --all-featuresonwindows-latest, i.e. thex86_64-pc-windows-msvctarget the release pipeline ships. Before this, the only Windows coverage prior to a tag was the mingw cross-check, which cannot link and skipsort— 57cfg(target_os = "windows")sites were unexercised on the target users run.
Fixed
- The crate compiles without
simplexandbrowser.add_simplex_channelreferencedcrate::channels::simplexwithout a feature gate (every other channel pairs a gated implementation with a#[cfg(not(…))]stub), andInfraState's initializer set thebrowser-gatedexport_poolfield unconditionally. That is what brokeEval Nightlynightly. MSRV Checknow checks something. It rancargo check --all-featureson toolchain 1.95 — the same compiler every other job uses, sincerust-toolchain.toml,Cargo.tomlrust-versionand the CItoolchain:input all say 1.95. It is now a consistency assertion between those three declarations, which is the property that silently breaks when a value drifts.
Security
- ctutils 0.4.5 clears RUSTSEC-2026-0385 (non-constant-time mask generation).
- RUSTSEC-2026-0387 (crypto-bigint, secret-dependent branches) is waived. The patched line is 0.7.6, but
jsonwebtoken 11.1.0— the latest release — still pinselliptic-curve 0.13/p256 0.13, which are built against crypto-bigint 0.5. The waiver follows the reasoning already recorded for RUSTSEC-2023-0071 on the same chain: the runtime path only verifies inbound bearer tokens against public keys, and the private-key side is a dev-dependency minting a token for a mock JWKS. Both files say when to revisit it.
Changed
- Work lands directly on
main;developis gone. Actions minutes are free on a public repository, so the integration branch only parked unverified commits until a pull request opened. The one rule that matters is written down inci.yml: nov*tag until the CI run for the commit it points at is green.
v0.3.13
Highlights
- The chat input works again. Typing a single character into the composer threw
Maximum update depth exceeded. The result of getSnapshot should be cached to avoid an infinite loop.and swallowed the keystrokes. The guard is assistant-ui's Tap store; the defect is@assistant-ui/core@0.2.23, which recomputed the thread-list state into a referentially unstable object while the list was unconnected, so every store read looked like a change (upstream assistant-ui/assistant-ui#6440).@assistant-ui/reactmoves to 0.15.30 and@assistant-ui/react-markdownto 0.14.23, the first release on that peer range. The 0.15 line retireduseComposerRuntime, so the composer is reached throughuseAui().composer.
Security
- tokio 1.54.1 clears RUSTSEC-2026-0384, a race in
time::Intervalthat arrived with 0.3.12's tokio 1.54.0 bump. - rustls-webpki 0.103.16 clears RUSTSEC-2026-0382 (certificate verification could accept distrusted intermediates); webpki-roots and webpki-root-certs 1.0.10 clear RUSTSEC-2026-0381 and -0383 (bundled roots trusting distrusted emSign intermediates).
- vite 6.4.5 clears three
launch-editoradvisories in theweb/dev dependency.
v0.3.12
Highlights
- The dependency graph is current across the board. The Rust side moved to sqlx 0.9, rand 0.10,
sha1/hmac/sha2on the digest 0.11 stack, ed25519-dalek 3, notify 8, cpal 0.18, nix 0.31, toml 1.1, ort 2.0.0-rc.15 and pdf-extract 0.12 (lopdf 0.42); the frontend to TypeScript 7, React 19.3, Vite 8 insite/and vite-plugin-pwa 2. Two of those carry security fixes: wasmtime 48.0.7 clears RUSTSEC-2026-0378 and RUSTSEC-2026-0379 — the latter a Cranelift aarch64 miscompile, CVSS 9.3 — and pdf-extract 0.12 moves lopdf to 0.42, clearing GHSA-6fj4-rcr3-r7rr. rand 0.10 splits the OS entropy source into a fallibleSysRng; the session-token, PKCE and gateway-token paths keep using the OS source, and session creation now reports a failure instead of panicking. - macOS release builds link again.
cargo build --releasefailed with an undefined___isPlatformVersionAtLeast: ort's prebuiltlibonnxruntime.acarries Objective-C++ CoreML objects that call that clang compiler-rt helper, and rustc links with-nodefaultlibs. It only surfaced under the release profile'slto = true+-dead_strip, which walk the whole archive.build.rsnow links compiler-rt on macOS whenvisionis enabled. - Two CI blind spots are closed. A
Windows Cross Checkjob compilesx86_64-pc-windows-gnuon every pull request —tools/win_appcontainer.rspreviously had no PR-time coverage at all — and aRelease Link (macOS)job performs a real release link on pushes, because every other job runscargo check, which never links.
Changed
- Pairing codes are drawn with
random_rangerather than%over a full-range value, removing the modulo bias. - The stale
RUSTSEC-2026-0187(lopdf) audit ignore is gone: pdf-extract 0.12 moved to lopdf 0.42, so the advisory no longer matches anything.
Fixed
- cpal 0.18 and notify-debouncer-full 0.7 compile on Linux —
DeviceTrait::nameis theDisplayimpl now, and the debouncer's recommended cache isNoCachethere — and under clippy. - The HMAC e2e helpers import
KeyInit, which hmac 0.13 moved offMac.
Removed
rustyline, declared since March but never referenced: interactive input moved to crossterm the same day it was added. Dropping it also removesclipboard-win,error-codeand a duplicateunicode-width.
v0.3.11
Removed
- Office document export — the
pptx/docx/xlsxconverters (src/office/) and the?to=download target are gone, along with the four crates that existed only for them (docx-rs,rust_xlsxwriter,ppt-rsand the test-onlycalamine). The dependency graph is the point: it carried four copies ofzipand, throughppt-rs→pdfrs, a whole PDF-rendering subtree (syntect,subsetter,ttf-parser,aes,cbc) for one feature. It now carries twozipcopies — the direct one, still used by the plugin installer and the RAG loader, and the updater's — andppt-rs/pdfrsare gone.write_reportstill writes and previews markdown and HTML, and HTML now downloads as PDF (?to=pdf) rendered by a real browser: the gateway holds oneBrowserPoolfor export, shared with the debugging bridge when it is enabled, and each export gets a fresh tab closed whether or not the render succeeds, with a 30-second ceiling. A browser is the honest engine here — the document is arbitrary agent-authored HTML, and a partial CSS renderer would not fail but produce a plausible-looking PDF with a broken layout. Without thebrowserfeature the export answers 501; with it and no pool yet, 503.
Added
video_generate, as the fourth media kind — the gap the hermes comparison actually records ("完全没有视频"). It rides the samegenerate_mediaabstraction as image, speech and transcription, with the configured provider's credential. Video is the first kind that is not one request: the vendor accepts a job, reports progress and serves the result when it is done, so the submit → poll → fetch loop lives in the provider, with a five-second interval and a ten-minute deadline (a job that never finishes must not hold an agent turn open for ever). The paths are the OpenRouter-style contract (POST /videos,GET /videos/{id},GET /videos/{id}/content), which is a real, already-configured syscity preset.duration,aspect_ratioandresolutiontravel as vendor hints, the way the image and speech knobs do. Two limits stated rather than implied: the path is not yet per-preset (a vendor whose media endpoint sits elsewhere needs that field before it works), andvideo_analyzeis deliberately absent — watching a video is a vision capability, not a generation one, and folding it in here would put two unrelated things behind one request type. The tool waits synchronously, so a turn that generates a video blocks for as long as the job takes.- A media capability on the provider layer — the vocabulary, not yet the implementations.
image_generate,ttsandstteach grew their own path: a hardcodedhttps://api.openai.com/...endpoint and their own read ofOPENAI_API_KEY, which means media work ignores the configured provider, its credential and its fallback chain.src/providers/media.rsnow defines the kinds (Image,Speech,Transcription,Video), one request shape, and the three ways a vendor actually answers — inline bytes, a URL, or text — because modelling those as oneVec<u8>would make every caller guess.Provider::generate_mediais defaulted to a refusal in words ("this provider does not support speech generation"): most providers do not do media, and that is a better answer than a missing method or an opaque failure. The OpenAI provider now implements image, speech and transcription on it, through the same gateway client chat uses — so media gets the configured credential, retry, timeout and rate limiting instead of a second read ofOPENAI_API_KEY. Speech reads bytes frompost_json_streaming, which already existed; no new client method was needed. Transcription now goes through the same path too, via a multipart method on the gateway client whose parts are described rather than built — the retry loop takesFn, so a one-shot form could not be replayed, and the form is rebuilt from owned bytes on every attempt. The three tools now go through it.image_generate,ttsandsttno longer create their ownreqwest::Client, post to a hardcodedhttps://api.openai.com/..., or readOPENAI_API_KEY/DALL·E_API_KEYat all: they take the configured provider from the tool context and callgenerate_media, so media uses the same credential, retry, timeout and rate limiting as chat. Two limits stated plainly: it uses the active provider, not the router's fallback chain, and only the OpenAI provider implements media so far — so a deployment whose chat provider is, say, Gemini now gets "gemini does not support image generation" where it previously worked by readingOPENAI_API_KEYregardless of the configured provider. That is the honest consequence of making media follow the configuration: the old behaviour was a second, unconfigured path to one vendor. The vendor-specific knobs the tools already exposed — imagesize/style, speechvoice/speed, transcriptionlanguage/temperature/prompt — travel as documented vendor hints rather than being silently dropped.stt'sresponse_formatargument is gone: the abstraction always returns text, so the parameter no longer selected anything. With no provider on the context a tool says so instead of falling back to an environment variable, andttsstill falls back to the localsay/espeakbinary, which is not a vendor. The three tools are not re-plumbed yet; until they are, they still reach OpenAI directly. - Named execution backends, reachable from the
shelltool. A deployment declares them under[security.execution_backends.<name>]—type = "docker"with animage, ortype = "ssh"with ahost— and the agent asks for one by name, never by image or host, so what is reachable is the deployment's decision and a call only chooses among those. The name travels to the tool through the same per-call context the fence posture uses. A name that is not configured is an error, not a quiet fallback to local: a command that meant to run elsewhere and ran here instead would have answered a different question, and the message names what is available so the next call can be right. A backend that cannot honour the fence (ssh) fails the call with its reason. The same argument and the same rule are onshell,processandcode_exec, so the three cannot disagree about what a missing backend means. A broken backend now says what is broken: a missing client program is reported as the backend being unusable ("thedockerbackend cannot run: thedockercommand was not found — is it installed and on PATH?") rather than as the local runner failing to spawn a program the caller never named, while a permission error keeps its own meaning; and a container is refused a working directory the fence did not grant, since it can only see what was mounted — the alternative is docker answering "no such file or directory", which reads like a typo. Thesshoptions are built in one place shared withcomputer::remote_control, pinned by a test, so the two clients cannot drift. - Remote execution: a
dockerand ansshbackend behind the existingProcessRunnerseam (src/tools/process_runner/remote.rs). A request can now name where it should run —ExecutionBackend, defaulting toLocal, so nothing that does not ask changes — and selection is per request rather than a property of the process. The two backends answer the write-fence question differently, and that difference is the whole design. Docker translates it: granted roots become read-write bind mounts,deny_networkbecomes--network none, and the.git/.syscitycarve-outs become read-only mounts emitted after the roots that contain them, because a nested mount is what shadows an outer one. The host filesystem outside those mounts is not read-only but absent, which is stronger than the local fence can say — and an absent carve-out is shadowed with an empty read-only tmpfs, so a.gitthat does not exist yet still cannot be created. That last case is one Linux's own posture does not cover, since Landlock cannot subtract a subtree. SSH refuses it: there is no far-side kernel a fence could be applied to, so a fenced request is rejected with the reason and told to run locally or on docker — never run unfenced, because quietly dropping the fence would turn the one guarantee this project is built around into a suggestion. An unfenced request runs with every argument quoted, since the command is reassembled into one string and parsed again by the remote shell. What a remote backend cannot do is documented rather than left to be discovered: the seccomp escape-vector deny list,pre_execrlimits (an opaque closure, so there is nothing a container flag could be built from — dropped with a warning rather than refused, because the shell tool sets one unconditionally), process-group kill at timeout, and the escalation path keyed on the local fence's stderr. - SimpleX Chat, as a channel — the fourth with no webhook route, and the one that is a client of a local daemon rather than of the platform: the gateway holds a WebSocket to
simplex-chat, and what travels on it is SimpleX's own command language wrapped in JSON ({"corrId": …, "cmd": "/_send @<id> json […]"}), with the message JSON-encoded inside the command string. Addressing is always by id — the display-name forms resolve at the daemon and silently drop when they do not, which is not something a reply should be able to do. Contact requests are accepted automatically, since otherwise nobody could ever reach the bot; that is not the access control, which runs on every message afterwards. SimpleX has no mention — in a group a sender cannot address the bot, so group messages arrive marked unmentioned, which makesrequire_mentionmean "say nothing in groups" (off by default, so a group the bot is in works). Only text is routed; images, files and voice notes are separate events carrying no text and are skipped, as are the file-t...
v0.3.10
Highlights
- Channel replies reach the channel. A message that arrived over Telegram, Slack, WhatsApp, Discord or any other channel was processed end to end — pipeline, agent turn, history,
AgentResponse— and then stopped: nothing bridged that event into the reply dispatcher, so the answer never left the gateway. Every configured channel user got silence. The inbound dispatch path now runs the final answer through the outbound pipeline like the queue path does, and cron'sAnnouncedelivery — which had the same shape of hole — now dispatches to the channel it names instead of only emitting an event. - Authentication was two switches, and only one of them was enforced.
auth_mode = "token"gated WebSocket upgrades but left the REST surface open, because the REST middleware keyed solely offsecurity.auth_required(default false). That surface includes/v1/chat/completions— the operator's provider key — andPOST /api/v1/ws-ticket, whose ticket clears the WebSocket gate: an unauthenticated caller could mint one and connect. A configured auth mode now requires credentials on both transports, and the preferred ticket flow works end to end (the handshake used to re-derive identity from theconnectframe alone, discarding what the upgrade had already validated, so ticket-authenticated clients landed with zero scopes). - MCP tools describe themselves. A server's
annotations(readOnlyHint,destructiveHint,idempotentHint) now drive a tool's capability: read-only tools stop prompting for approval and stay visible in plan mode, destructive ones keep it. Servers that publish nothing keep the conservative default, and tools whose names carry destructive verbs are classified when annotations are absent. Alongside:[permissions]rules can target MCP tools by name (ask = ["mcp__gmail__send*"]), and the Gmail preset — which pointed at a package that does not exist on npm — now names one that does.
Added
device_open_url: hand atel:,mailto:,sms:,https:orhttp:URL to the operating system on mobile — the dialer opens pre-filled, the mail composer opens with the recipient, and the user confirms in the target app. Schemes are allowlisted in Rust and again natively.- MCP tool capabilities are derived from the server's own annotations, with a name-based fallback for servers that publish none.
[permissions]bare-name glob rules reach tools with no primary invocation argument, which is what MCP tools are:ask = ["mcp__gmail__send*"]now works.- The desktop window title is "Syscity Desktop"; mobile keeps "Syscity".
Changed
- MSRV is 1.95 (from 1.94), pinned by
rust-toolchain.tomlas well asrust-version, and every CI job follows. - Every WS refusal for a missing or unusable parameter answers
INVALID_PARAMS— handlers previously read a missingidas""and reportedNOT_FOUND, indistinguishable from a resource that does not exist. - An approval-gated tool in a context with nobody to ask (cron, heartbeat, standing orders, goal runs, delegated sub-agents) now fails closed with a deny naming the
[permissions].allowpre-approval path, instead of running silently on one path and hanging five minutes on another. - A policy hook that matched a call and allowed it is authoritative; a hook configured for other tools no longer disables the approval fallback for every MCP and high-risk tool.
- Tests: the e2e tree gains journey coverage for the HTTP surface (probes, OpenAI-compatible endpoints, the ticket exchange, signed webhooks), approval and permission rules, MCP tools, hooks
ask, channel round trips, heartbeat and standing orders, the admin WS method surface, plus integration coverage for the model router, RAG ingestion, the planner DAG, office conversion, attachments and the eval harness.
Fixed
- wasmtime 46 → 48.0.3 (RUSTSEC-2026-0314, a guest-triggerable host panic reachable from the
code_execwasm path, and RUSTSEC-2026-0316). The old version has no backport except an LTS line three majors back, which is why the MSRV moved. - A signal could abort the daemon on macOS. wasmtime's default signal-based traps install a Mach exception port whose handler calls
libc::abort()when a receive is interrupted — and any straySIGCHLDfrom a child spawn can interrupt it. Signal-based traps are off for both the plugin runtime andcode_exec. - A memory the agent observed was invisible to every conversation. Recall is conversation-scoped, and all three memory stores filtered with strict equality, so an unbound (user-level) memory — which is what
observestores — was excluded byNULL != ?. The manager's own observe → session-context loop could not close. - A reply reaching the channel is the assistant's answer, not an echo of the prompt — pinned by test.
v0.3.9
v0.3.8
Highlights
- A permission rules engine in front of every tool call. A
[permissions]config block carries the gateway-wide mode (default/accept_edits/plan/bypass), anallow_bypassswitch (bypass is refused without it), andallow/deny/askrule lists — each"tool"or"tool:glob"matched against the call's primary argument (shell → command, file tools → path,web_fetch→ URL), evaluated in a locked order: deny → ask → allow → mode → hooks. A session can override its mode (sessions.set_mode, ephemeral until restart), the TUI gets/modeplus a status-line mode tag, and approvals offer approve / approve-and-remember (the invocation or its parent directory becomes a persistent allow rule) / deny. Plan mode hides and refuses every tool that has not declared itself read-only. - The Linux fence grows two layers and the macOS one grows an escape hatch. A fenced command may ask to run outside its fence — declared up front via a reserved
permissionsblock on the call, or failure-driven when the output says the fence refused it — with a human approving before the unfenced re-run, which is announced in the result. On Linux, the Landlock write fence is now stacked with a seccomp escape-vector deny list installed on every fenced run (ptrace,process_vm_*,bpf, keyring, module, mount and namespace syscalls are refused; aclonecarrying anyCLONE_NEW*flag is refused;clone3is forced back to a filterableclone), and an optional namespace view ([security] fence_namespaces, defaultauto): a user namespace plus a private mount namespace with a read-only root and a private/tmp, the working trees and/dev//run//procre-bound read-write on top so2>/dev/nulland AF_UNIX IPC keep working. The network posture ([security] fence_network) is a three-platform clause: Seatbelt(deny network*), a Linux seccomp socket filter that leavesAF_UNIXalone, and withheld Windows capability SIDs. The.gitand.syscitydirectories are unwritable inside any granted root on macOS. - Delegation is visible while it runs. Task rows carry
parent_sessionandusage_tokens; the gateway pushesagent.usageanddelegation.updated; the TUI renders live delegation task rows, a run token counter, and an/agentsview.
Added
- The gateway publishes the WS method table and the dispatcher is held to it — every registered method must be in the table and every table entry must dispatch.
- TUI:
/themeswitches the palette and persists it in config; the palette is aThemestruct ported to Claude Code's dark theme; the terminal's background is detected via OSC 11 and the palette degrades to the terminal's color depth. - TUI markdown rendering: inline code spans, consecutive pipe rows aligned as tables, block quotes with a dim gutter, OSC 8 hyperlinks for URLs in scrollback, list items with an accented marker, and ATX headings rendered as bold titles without the marker.
- TUI: multi-line paste inserts content one character at a time (no bracketed-paste surprises),
OSC 52copies the last answer to the clipboard,/retryresends the last prompt through the send gate, and/expandreprints the last tool call in full. read_onlyis a declared tool capability, andSandboxedToolstops masking the capabilities of the tool it wraps.
Changed
- Multi-user RBAC scaffolding that nothing wired up is removed;
requires_approvalnow gates a call where there is someone to ask. - The TUI event loop is a module directory rather than one file.
- The app version is 0.3.8 across the workspace, the desktop crate, and the web client.
Fixed
- A reply that was only markup (no text after formatting) no longer completes the turn early.
- Browser: pages are reused across tool calls instead of pooled and abandoned; the empty-object argument shape the schema advertises is accepted; keys reach the page in a form the browser recognizes.
- The planner database URL had a space in it.
- The CORS example in the config guide taught a pairing the gateway refuses; the permissions system is documented in the tools module doc.