AgentOS 2026.8.9
A channels-and-internals release. Telegram replies stream instead of arriving all at once at the end; the seven GMGN skills stop reading as one skill listed seven times; a model's price and context window are declared once rather than restated in five places; and shell workspace lockdown stops missing a redirection that has no space around the >.
Added
-
Telegram replies stream. Telegram was the only major chat adapter without
send_streaming, soresolve_channel_stream_policypickedtyping_finalfrom method presence: a typing indicator for the whole run, then the entire answer dropped in at once. It now posts one message and edits it as text arrives, driven by the sameStreamThrottleSlack and Discord use — first post, then edits, never two concurrent flushes, accumulated text preserved when a flush fails.streaming_reply_kwargscarrieschat_idandthread_idfrom the inbound message, so a stream lands in the originating chat and forum topic rather than nowhere at all on a bot with nodefault_chat_id, which is the normal deployment. A missing target raises instead of returning a soft "unsupported": the runtime relay only replays undelivered chunks whenstream_erroris set, so a quiet return would drop the user's whole answer. (Fixes #141)-
Throttled to what Telegram actually allows. Edits default to
update_interval_ms=1200. Telegram's practical ceiling is around one edit per second per chat and the relay's own coalescing defaults to0, so without a throttle every text delta becomes an API call. -
A
429burst degrades instead of failing. Rate limits now surface asTelegramFloodErrorcarryingretry_after, read from both HTTP 429 andok: falsebodies withparameters.retry_after. That feedsFloodStrikeBackoff— defined for exactly this and until now with zero callers anywhere in the tree. Three floods in 30s stop the edit loop and the remainder is delivered as a single final message, full text, no lost chunks. -
Answers past 4096 characters roll over into a new message instead of failing, and the stream keeps editing the newest one. The budget is measured on the rendered HTML, since escaping and tags expand the text.
-
The manifest stops under-reporting.
ChannelCapabilityProfilegains astreamingfield mapped toChannelCapabilities.STREAMING, declared by every adapter that implementssend_streaming— Slack, Discord, Telegram, Microsoft Teams. The tag was previously reachable only through a magictransports=("streaming",)string that no adapter set, so the manifest and the Channels page claimed none of them streamed.
-
-
The GMGN skills are grouped, and wear their own mark. The Skills page already shipped an AgentOS Crypto Skills heading —
skillGroupKey()splits it out of the shipped group oncategory: crypto— but all seven GMGN skills declaredcategory: tradingand rendered under "AgentOS Normal Skills". The heading existed and the skills it was built for were not in it; onlysenior-unilp-managerever reached it. All seven now declarecategory: crypto, one line each, with no loader, wire, or snapshot-schema change sincecategorywas already parsed and serialized. Cards in that group also wore the generic package glyph, which for seven siblings from one upstream reads as one skill listed seven times: the GMGN mark now ships as a bundled asset badged with the emoji each SKILL.md already declared — 🔎 📈 💼 🛰️ 🧊 💱 🍳 — so the brand is shared and the icon is per-skill, across both the card and the detail dialog. Skill names are unchanged; renaminggmgn-*→crypto-*would churnNOTICE,THIRD_PARTY_NOTICES.md, four test modules, the docs and every user's invocation for no grouping benefit the category does not already provide. (Fixes #246)- The mark is chosen on provenance, not on the name. It resolves on
provenance.origin: gmgn-mit, never on thegmgn-prefix — a name is not a brand, which is the ruleskillPublisherIdexists to enforce. Provenance is self-declared frontmatter, so the trust sits in the crypto group key: it only resolves for a shipped/bundled skill, the same gate that keeps a hand-dropped directory out of a heading carrying the AgentOS name. Nothing routes throughpublisher, and the artwork is bundled, so no card fetches a remote image.
- The mark is chosen on provenance, not on the name. It resolves on
Changed
-
One declaration per model. A model's facts were restated in five places keyed five different ways with nothing tying them together, so bumping a tier default meant editing four or five files by hand. Worse, both lookup tables fail open in opposite directions — pricing prefix-matches down to an older model, the catalog falls through to a generic constant — so a forgotten entry produced a plausible wrong number rather than an error. That already happened once, in #139.
src/agentos/model_registry.pynow declares each model once: price, context window, max output, image support, and any provider-scoped window override.pricing.pyandmodel_catalog.pyderive their tables from it, andconfig.pybuilds every tier through a helper that raises at import time on a model that is not declared. Divergent id spellings are handled byDELIBERATE_SPELLING_DIVERGENCES— a bare id and its vendor-prefixed twin are different wire ids and may legitimately differ, but every difference must be declared with a written reason, and the test compares declared against observed in both directions so a forgotten edit and a stale allowlist entry both fail. (Fixes #140)-
No prices or windows change. The first commit, on the unmodified tree, freezes what 588 model ids resolve to — every pricing prefix, catalog key, tier default and migration id, each probed bare and with four suffixes. The suffixes are the point: the pricing table is an ordered
startswithscan, so an exact-id-only snapshot would miss a reordering regression. That golden file passes unchanged at every step, and tier default dicts were separately diffed across all ten profiles as byte-identical. -
One deletion worth reading closely. The eleven-row "Bankr LLM Gateway fallback" block in
_PRICING_TABLEis gone, along with the two "keep both lists in sync" comments guarding it. The block was unreachable —_lookup_static_priceconsults_PRICE_OVERRIDESfirst and every id in it was already an override — and three rows were doubly unreachable, sitting behind a shorter earlier prefix carrying a different price.
-
Fixed
-
Shell workspace lockdown no longer misses a redirection with no whitespace around it.
--workspace-lockdownandworkspace_write_deny_globsboth decide what a command writes through_shell_write_targets(), whose redirection regex was anchored with(?:^|\s). The operator had to be preceded by whitespace or start-of-string, soecho x>/etc/passwd— valid shell — parsed as containing no write target and passed both checks.>&was missing from the operator set entirely, socmd >&/etc/xslipped through even with a space, and the noclobber override>|was never handled. Theteeparser carried the same anchor bug and is fixed alongside it. Descriptor duplication and closing (2>&1,>&2,2>&-) are blanked before the scan, so the descriptor number after>&is never mistaken for a path — without that, dropping the anchor would turn every2>&1into a bogus write to a file named1.<,>,(and)are excluded from the target character class, socat<in>/etc/xresolves to/etc/xand process substitution>(cmd)does not produce a garbage target. (Fixes #197)-
Only two of the six reported repro lines were actually broken.
cmd 2>/tmp/leak.txt,true | cat >/tmp/out.txt,true; echo x >/tmp/out.txtand(echo x) >/tmp/out.txtall have whitespace before the operator and were already blocked. The real gaps were the no-whitespace form and>&, plus a third the issue did not list:cat<in>/etc/x. -
The docs now say what lockdown is. This is a regex over command text and will never be airtight: a command can still write outside the workspace through
python -c,cp,dd of=,sed -i,eval, or a path built from a shell variable. None of those are new and none are fixed here.docs/tools-and-sandbox.mddescribed lockdown as "writes must stay inside the workspace" with no caveat, and now states plainly that shell lockdown is defense in depth rather than a security boundary, pointing at the OS sandbox for real containment.
-
Full changelog: v2026.8.7...v2026.8.9