Skip to content

Releases: lightconsen/syscity

v0.3.17

Choose a tag to compare

@github-actions github-actions released this 30 Oct 14:01

Full Changelog: v0.3.16...v0.3.17

v0.3.16-test1

v0.3.16-test1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 30 Oct 09:00

Full Changelog: v0.3.16...v0.3.16-test1

v0.3.16

Choose a tag to compare

@github-actions github-actions released this 29 Oct 18:55

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, and bench probe checks 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.
    OsworldDesktop implements ComputerAdapter over 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, /accessibility and
    (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|step joins OSWorld's evaluator to syscity's own observe/act loop,
    with --split/--domain/--ids/--resume and a per-run summary that keeps
    infrastructure errors out of the denominator. docs/computer-use-benchmark.md
    is the plan, docs/osworld-kvm-server.md the host runbook, and
    scripts/osworld-bench-setup.sh provisions a KVM host in one idempotent
    command (dependencies, Docker, OSWorld, the 12 GiB VM image, this release's
    binary and the bench/ 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-source still 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 escalate ax_action → pixel →
    keyboard, and only an observed SuspectedNoop escalates — Unverifiable
    stops instead, so a click that could not be checked never fires twice.
    ComputerSession is now the one step engine (settle, three-state
    verification, failure accounting, rollback, reflection, sensitive-UI
    scanning); ComputerUseLoop delegates 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_scorer is 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 in evals/ledger.md. See
    docs/benchmark.md §M2 for the method and the failure breakdown.

  • web_search can read the pages it finds. fetch_top: N fetches the top N
    results in the same call (through web_fetch, so the SSRF guard applies), and
    depth: "advanced" asks the provider for full page text where supported
    (Tavily search_depth + include_raw_content). The result cap moves from 10
    to 20.

  • image reads text out of images via the local RapidOCR models when the
    vision feature 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 by ratio·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 shell curl in an eval can reach sites that
    need it.

Changed

  • The x86_64 Linux CLI is built on ubuntu-22.04 from the new linux-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 plus cloud, minus
    vision; the arm64 Linux CLI and both macOS CLIs keep vision.

  • 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 the FINAL 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 years for 22, No — Earl cannot do it; … for No). Measured on the
    9-task subset drawn from those: 40.7% → 81.5% over 3 trials each, 7 tasks up,
    none down.

  • web_fetch retries 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.md and
    docs/build.md all 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 a Result<Cow<str>>; screen_recorder now 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's cache feature, which syscity never configures — it pulls
    wasmtime-internal-cache → zstd 0.13, a line with no patched release. Building
    wasmtime with default-features = false and without cache leaves zstd 0.14
    (via zip 9.1.2) as the only copy.
  • The three zip extract() waivers (RUSTSEC-2026-0367/0368/0369) are removed
    from deny.toml: zip 9 fixes them, and deny.toml's wasmtime@48.0.5
    duplicate-tree skip now names wasmtime@50.0.0.

Fixed

  • The x86_64 Linux CLI links, and starts on 22.04. Moving the build to
    ubuntu-22.04 exposed that the ONNX Runtime vision links against — pyke's
    prebuilt for x86_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-amd64 profile, which drops the ONNX-based OCR/UI detection on this one
    target. --features vision from source restores it on a 24.04+ host.
  • The cargo cache is keyed by runner image. Swatinem/rust-cache keys on
    Linux-x64, so the 22.04 job could restore a target/ 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

Choose a tag to compare

@github-actions github-actions released this 25 Oct 07:49

Highlights

  • CI is hardened for a public repository. Every workflow now declares permissions: contents: read (only the release job elevates, to contents: write); all 98 action references are pinned to commit SHAs rather than mutable tags; every job has a timeout-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 main only and the macOS arm of the build job is gone, so a pull request spends no macOS minutes at all. The release workflow is untouched.
  • Eval Nightly runs again. It had failed every night, not on an eval but on the build before it: cargo build --no-default-features --features plugins,sqlite-vec did not compile.

Added

  • Windows Check: cargo check --all-features on windows-latest, i.e. the x86_64-pc-windows-msvc target the release pipeline ships. Before this, the only Windows coverage prior to a tag was the mingw cross-check, which cannot link and skips ort — 57 cfg(target_os = "windows") sites were unexercised on the target users run.

Fixed

  • The crate compiles without simplex and browser. add_simplex_channel referenced crate::channels::simplex without a feature gate (every other channel pairs a gated implementation with a #[cfg(not(…))] stub), and InfraState's initializer set the browser-gated export_pool field unconditionally. That is what broke Eval Nightly nightly.
  • MSRV Check now checks something. It ran cargo check --all-features on toolchain 1.95 — the same compiler every other job uses, since rust-toolchain.toml, Cargo.toml rust-version and the CI toolchain: 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 pins elliptic-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; develop is 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 in ci.yml: no v* tag until the CI run for the commit it points at is green.

v0.3.13

Choose a tag to compare

@github-actions github-actions released this 22 Oct 17:25

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/react moves to 0.15.30 and @assistant-ui/react-markdown to 0.14.23, the first release on that peer range. The 0.15 line retired useComposerRuntime, so the composer is reached through useAui().composer.

Security

  • tokio 1.54.1 clears RUSTSEC-2026-0384, a race in time::Interval that 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-editor advisories in the web/ dev dependency.

v0.3.12

Choose a tag to compare

@github-actions github-actions released this 20 Oct 15:42

Highlights

  • The dependency graph is current across the board. The Rust side moved to sqlx 0.9, rand 0.10, sha1/hmac/sha2 on 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 in site/ 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 fallible SysRng; 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 --release failed with an undefined ___isPlatformVersionAtLeast: ort's prebuilt libonnxruntime.a carries Objective-C++ CoreML objects that call that clang compiler-rt helper, and rustc links with -nodefaultlibs. It only surfaced under the release profile's lto = true + -dead_strip, which walk the whole archive. build.rs now links compiler-rt on macOS when vision is enabled.
  • Two CI blind spots are closed. A Windows Cross Check job compiles x86_64-pc-windows-gnu on every pull request — tools/win_appcontainer.rs previously had no PR-time coverage at all — and a Release Link (macOS) job performs a real release link on pushes, because every other job runs cargo check, which never links.

Changed

  • Pairing codes are drawn with random_range rather 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::name is the Display impl now, and the debouncer's recommended cache is NoCache there — and under clippy.
  • The HMAC e2e helpers import KeyInit, which hmac 0.13 moved off Mac.

Removed

  • rustyline, declared since March but never referenced: interactive input moved to crossterm the same day it was added. Dropping it also removes clipboard-win, error-code and a duplicate unicode-width.

v0.3.11

Choose a tag to compare

@github-actions github-actions released this 18 Oct 10:08

Removed

  • Office document export — the pptx/docx/xlsx converters (src/office/) and the ?to= download target are gone, along with the four crates that existed only for them (docx-rs, rust_xlsxwriter, ppt-rs and the test-only calamine). The dependency graph is the point: it carried four copies of zip and, through ppt-rs → pdfrs, a whole PDF-rendering subtree (syntect, subsetter, ttf-parser, aes, cbc) for one feature. It now carries two zip copies — the direct one, still used by the plugin installer and the RAG loader, and the updater's — and ppt-rs/pdfrs are gone. write_report still writes and previews markdown and HTML, and HTML now downloads as PDF (?to=pdf) rendered by a real browser: the gateway holds one BrowserPool for 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 the browser feature 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 same generate_media abstraction 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_ratio and resolution travel 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), and video_analyze is 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, tts and stt each grew their own path: a hardcoded https://api.openai.com/... endpoint and their own read of OPENAI_API_KEY, which means media work ignores the configured provider, its credential and its fallback chain. src/providers/media.rs now 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 one Vec<u8> would make every caller guess. Provider::generate_media is 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 of OPENAI_API_KEY. Speech reads bytes from post_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 takes Fn, 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, tts and stt no longer create their own reqwest::Client, post to a hardcoded https://api.openai.com/..., or read OPENAI_API_KEY / DALL·E_API_KEY at all: they take the configured provider from the tool context and call generate_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 reading OPENAI_API_KEY regardless 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 — image size/style, speech voice/speed, transcription language/temperature/prompt — travel as documented vendor hints rather than being silently dropped. stt's response_format argument 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, and tts still falls back to the local say/espeak binary, 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 shell tool. A deployment declares them under [security.execution_backends.<name>] — type = "docker" with an image, or type = "ssh" with a host — 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 on shell, process and code_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 ("the docker backend cannot run: the docker command 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. The ssh options are built in one place shared with computer::remote_control, pinned by a test, so the two clients cannot drift.
  • Remote execution: a docker and an ssh backend behind the existing ProcessRunner seam (src/tools/process_runner/remote.rs). A request can now name where it should run — ExecutionBackend, defaulting to Local, 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_network becomes --network none, and the .git/.syscity carve-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 .git that 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_exec rlimits (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 makes require_mention mean "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...
Read more

v0.3.10

Choose a tag to compare

@github-actions github-actions released this 02 Oct 15:52

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's Announce delivery — 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 off security.auth_required (default false). That surface includes /v1/chat/completions — the operator's provider key — and POST /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 the connect frame 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 a tel:, mailto:, sms:, https: or http: 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.toml as well as rust-version, and every CI job follows.
  • Every WS refusal for a missing or unusable parameter answers INVALID_PARAMS — handlers previously read a missing id as "" and reported NOT_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].allow pre-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_exec wasm 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 stray SIGCHLD from a child spawn can interrupt it. Signal-based traps are off for both the plugin runtime and code_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 observe stores — was excluded by NULL != ?. 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

Choose a tag to compare

@github-actions github-actions released this 28 Sep 03:49

Full Changelog: v0.3.8...v0.3.9

v0.3.8

Choose a tag to compare

@github-actions github-actions released this 23 Sep 05:09

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), an allow_bypass switch (bypass is refused without it), and allow / deny / ask rule 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 /mode plus 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 permissions block 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; a clone carrying any CLONE_NEW* flag is refused; clone3 is forced back to a filterable clone), and an optional namespace view ([security] fence_namespaces, default auto): a user namespace plus a private mount namespace with a read-only root and a private /tmp, the working trees and /dev//run//proc re-bound read-write on top so 2>/dev/null and 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 leaves AF_UNIX alone, and withheld Windows capability SIDs. The .git and .syscity directories are unwritable inside any granted root on macOS.
  • Delegation is visible while it runs. Task rows carry parent_session and usage_tokens; the gateway pushes agent.usage and delegation.updated; the TUI renders live delegation task rows, a run token counter, and an /agents view.

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: /theme switches the palette and persists it in config; the palette is a Theme struct 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 52 copies the last answer to the clipboard, /retry resends the last prompt through the send gate, and /expand reprints the last tool call in full.
  • read_only is a declared tool capability, and SandboxedTool stops masking the capabilities of the tool it wraps.

Changed

  • Multi-user RBAC scaffolding that nothing wired up is removed; requires_approval now 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.