AgentOS 2026.8.11
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-sidex_searchon 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 aweb_searchbackend, and it does not enter the search provider registry. Adapted from NousResearch/hermes-agent (MIT);THIRD_PARTY_NOTICES.mdis updated from "reference material" to record the derived code. (Fixes #277)-
It is on the web permission surface.
x_searchjoinsgroup:web, so denying that group also cuts the route toapi.x.ai— a tool that reaches the network through a different name is a hole in the group. It sits inCRON_AGENT_ALLOWnext toweb_fetch/web_searchbecause 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_secondsbounds one attempt,total_timeout_secondsbounds 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_urlmust 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_generationpattern — aToolSurfaceCapabilitiesflag 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, andXAI_API_KEYin the env catalog.x_searchbills xAI directly and does not appear inagentos 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_searchagainst what they already pay for.agentos auth login xairuns the device-code flow againstauth.x.ai; tokens land in~/.agentos/auth.json(0600) and refresh themselves.agentos auth statusreports the login without ever printing a token,agentos auth logout xaiforgets it, OAuth is preferred overXAI_API_KEYat call time, andcredential_sourcein 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. Thetoken_endpointis 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 straybase_urlis a different class of mistake. -
A
403on 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 intoinvalid_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 asauth.xai.login.start/auth.xai.login.poll; the blockingdevice_code_loginbecomes the composition of the two and still backs the CLI, soagentos auth login xaiis 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 — answersexpiredso 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 onIntl.PluralRules.MessageKeyis 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 atscerror 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 viewlogic.tslabel 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_MIGRATEDESLint 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-inno-restricted-syntaxrules, 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, andAppShellsubscribes through auseLocale()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.tsimported 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 throughdefineNamespace()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 atregisterCatalog()rather than throwing later insidetPlural(). (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
c1even 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,trivialmoved fromc0toc1/c2in 13 of 14 languages, while a genuinely hard request in Chinese, Japanese or Thai dropped toc1. 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 atagentos_router.translate_ceiling_tier(defaultc0; settranslate_ceiling_enabled = falseto 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_streamingits stream policy resolved toadapter_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_finalandfinal_onlyadapters 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.logoutso a double click is not a failure. Signing out forgets local tokens only — nothing is revoked at xAI, andx_searchfalls back toXAI_API_KEYif 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