Skip to content

AgentOS 2026.8.11

Choose a tag to compare

@github-actions github-actions released this 11 Aug 03:31
· 478 commits to main since this release
23e5812

An X-search and internationalization release. AgentOS can search X (Twitter) through xAI, and a SuperGrok subscription pays for it instead of an API key nobody with a subscription holds. Every Web UI view now reads its copy from a catalog rather than having English written into the JSX, so a second language is a catalog away rather than a rewrite of every view. Translation turns stop drifting a price tier based on the language they were written in.

Added

  • x_search: search X (Twitter) from an agent turn. A built-in tool backed by xAI's server-side x_search on the Responses API. xAI searches X's post index and returns a synthesized answer with citations — a different shape from the ranked pages a search provider returns — so this is its own tool rather than a web_search backend, and it does not enter the search provider registry. Adapted from NousResearch/hermes-agent (MIT); THIRD_PARTY_NOTICES.md is updated from "reference material" to record the derived code. (Fixes #277)

    • It is on the web permission surface. x_search joins group:web, so denying that group also cuts the route to api.x.ai — a tool that reaches the network through a different name is a hole in the group. It sits in CRON_AGENT_ALLOW next to web_fetch/web_search because it is read-only.

    • Deadline-aware, unlike upstream. Upstream runs synchronously with no outer bound; every AgentOS tool call is capped at EngineConfig.tool_timeout (60s). timeout_seconds bounds one attempt, total_timeout_seconds bounds the whole call, and the tool declares a static ceiling above both so the engine never cuts an attempt the tool still considers live.

    • base_url must be HTTPS and is refused if it resolves to a metadata endpoint. The request carries a bearer token.

    • An install with no xAI credential never pays the tool's schema. Visibility follows the image_generation pattern — a ToolSurfaceCapabilities flag detected from credential presence.

    • Surfaces: [x_search] config with hot-apply, a Setup page card, onboarding.x_search.configure, agentos configure x-search, agentos onboard catalog x-search, and XAI_API_KEY in the env catalog. x_search bills xAI directly and does not appear in agentos cost; the docs say so rather than leaving it to be discovered.

  • xAI OAuth, so a SuperGrok subscription can pay for it. xAI sells API credit and subscriptions separately: a SuperGrok / X Premium+ subscriber holds no API key and has no way to spend the subscription through one. OAuth is the only path that bills x_search against what they already pay for. agentos auth login xai runs the device-code flow against auth.x.ai; tokens land in ~/.agentos/auth.json (0600) and refresh themselves. agentos auth status reports the login without ever printing a token, agentos auth logout xai forgets it, OAuth is preferred over XAI_API_KEY at call time, and credential_source in the result says which one ran. A broken OAuth login is reported rather than skipped — silently falling through to an API key would turn "your xAI login expired" into "no credentials configured".

    • Discovery and inference origins are pinned to HTTPS on x.ai/*.x.ai. The token_endpoint is cached on disk, so one MITM at login would otherwise receive the refresh token on every future refresh — a permanent leak from a single interception. The check runs again on the refresh path, since the store may predate it. The inference origin is pinned the same way: an API key is something the operator pasted knowingly, but a self-refreshing subscription bearer sent to a stray base_url is a different class of mistake.

    • A 403 on refresh is a tier gate, not a re-login prompt. xAI restricts API access to certain SuperGrok tiers, and logging in again cannot change that. A terminal refusal (400/401) quarantines the dead tokens so the next call fails locally instead of making another doomed request.

    • Availability never touches the network, and refresh is serialized. Upstream's check refreshes on every tool-surface rebuild, i.e. once per turn; has_oauth_credentials() reads local state only, so a revoked token surfaces as a call error rather than a tool that vanishes mid-session. Refresh is async and holds a lock, because xAI's refresh tokens are single-use and two concurrent refreshes race each other into invalid_grant.

  • Signing in from the Setup page, without holding a request open. The card used to report which credential was in play and then tell the operator to open a terminal — a dead end for a Web UI, and the first thing someone with a SuperGrok subscription and no key hits. The device-code flow is now split into start_device_login / poll_device_login, exposed as auth.xai.login.start / auth.xai.login.poll; the blocking device_code_login becomes the composition of the two and still backs the CLI, so agentos auth login xai is unchanged. The split is what makes a non-blocking caller possible at all: approval takes as long as it takes, so a single method would hold a request open for minutes and still never get the code in front of the person who has to approve it. The pending grant lives in the gateway process under an opaque id and is swept when it expires; a poll for an id the process does not know — after a restart, or past expiry — answers expired so the UI restarts the flow rather than waiting forever. The device code itself never crosses to the browser: only the approval URL and the user code do, and neither is usable without the operator's own xAI session.

  • Every Web UI view now resolves its copy through the i18n seam. Every user-facing string used to be written into the JSX that rendered it — no catalog, no t(), no i18n dependency — so adding a second language meant touching every view and every new view added more of them. The seam landed with the shell and five views, and the remaining eleven followed one at a time; the shell and all sixteen views now read from per-namespace catalogs. English stays the default and the per-key fallback, so a single-locale build renders byte-identical copy. There is no runtime dependency: pluralisation rides on Intl.PluralRules. MessageKey is derived from the catalog and required interpolation vars are parsed out of the English value, so an unknown key, a missing var, or a renamed placeholder is a tsc error rather than a {message} rendered raw. (Fixes #138, #257)

    • t() is a plain module function, not a hook — most of the copy lives outside components, in view logic.ts label maps, module-scope route titles and the chat transcript's imperative DOM builders, none of which a hook could serve. The chat view alone is 215 strings of which only 70 are JSX.

    • The I18N_MIGRATED ESLint ledger is the migration record. A view joins the list in the same change that extracts its strings and can never silently regress, while un-migrated views stayed quiet — the ledger is now complete. Built-in no-restricted-syntax rules, no new devDependency.

    • Strings resolve at render time, never at module load. t() reads the active locale at call time, so a module-scope call froze the string for the life of the page. A sweep found 73 such sites across 8 files — the shell nav, route titles, health labels, the connection pill, env filters, approval mode options and overview session labels were all frozen. Every frozen constant is now a function, the route table holds catalog keys rather than resolved strings, and AppShell subscribes through a useLocale() hook. A lint rule makes the invariant permanent, so the bug class cannot return silently when a locale picker ships. (Fixes #258)

    • Catalogs register per namespace, so view copy stays out of the entry chunk. en/index.ts imported every catalog eagerly, so each migrated view's copy moved out of its lazy chunk and into the shell — five views cost 5.3 KiB gzip, with eleven still to come. The fallback is now assembled by import: each namespace registers itself through defineNamespace() and the bundler places it in whichever chunk imports it. Initial JS went 140.1 → 136.3 KiB gzip against the 180 KiB budget and no longer tracks the migrated-view count. (Fixes #261)

    • Numeric placeholders format through Intl.NumberFormat, and malformed locale tags are rejected at registerCatalog() rather than throwing later inside tPlural(). (Fixes #259, #260)

Changed

  • Translation turns are capped at the cheapest tier. The router scores reasoning difficulty rather than task type, so an ordinary "translate this" landed on c1 even in English — and because the Pilot corpus is English-only, the same request drifted a tier in either direction depending only on the language it was written in: measured against the English baseline, trivial moved from c0 to c1/c2 in 13 of 14 languages, while a genuinely hard request in Chinese, Japanese or Thai dropped to c1. A deterministic detector now recognises a translate verb in the first or last paragraph of a turn across English, Vietnamese, Chinese, Japanese, Korean, Thai, Indonesian, French, Spanish, German, Portuguese, Russian, Arabic and Hindi, and caps the turn at agentos_router.translate_ceiling_tier (default c0; set translate_ceiling_enabled = false to turn it off, or pick the tier in the setup wizard's Translation cap field).

    • Every detected translation is capped, extras and all. A complaint upgrade, the large-context floor, and a programming language named as the target ("translate this Python module to Rust" — a request to write code) are the only things that override it.

    • Verb matching is word-bounded and guarded against overloaded stems, so Vietnamese giao dịch / dịch vụ, English "address translation bug" and Thai แปลก are not mistaken for translation work.

Fixed

  • Streaming channels show the typing indicator again while the model is still thinking. Since Telegram gained send_streaming its stream policy resolved to adapter_stream, which suppressed the indicator for the whole turn — and nothing can be streamed before the first token, so a user waiting out model latency and tool calls saw nothing at all. Telegram and Discord now type until the first chunk reaches the chat and drop the indicator the moment it lands, rather than either suppressing it for the run or letting it flicker back under a message that is already being edited. typing_final and final_only adapters are unchanged. (Fixes #255)

  • A rejected sign-out is reported as a sign-out failure. Both the sign-in and sign-out paths rendered through one label, so a rejected logout RPC said "Sign-in failed" — pointing the operator at the wrong thing entirely, right after a successful sign-in. Relatedly, the login control never saw the auth status and offered "Sign in with xAI" to someone already signed in, directly under a line saying they were. Signed in, the control is now "Sign out of xAI", backed by an idempotent auth.xai.logout so a double click is not a failure. Signing out forgets local tokens only — nothing is revoked at xAI, and x_search falls back to XAI_API_KEY if one is set; the toast and the docs both say so, because "sign out" otherwise implies more than it does.

  • External links in the transcript open in a new tab instead of replacing the chat.

  • Writing the auth token store no longer fails on platforms without POSIX mode bits.


Full changelog: v2026.8.9...v2026.8.11