Skip to content

AgentOS 2026.8.27

Choose a tag to compare

@github-actions github-actions released this 26 Aug 17:45
· 240 commits to main since this release
a90b786

A release about ceilings and about what a file hands back. Money gets a hard stop that survives a restart, a skill zip stops being a way to take the gateway down, a credential read out of a config file stops landing in the transcript, and three subsystems that shipped in the wheel while doing nothing are gone.

A budget you can actually hit

A new [budgets] config section sets money ceilings — per session, per UTC day, per agent, per channel:

[budgets]
session_limit_usd = 5.00
session_warn_usd = 4.00
daily_limit_usd = 50.00
daily_warn_usd = 40.00

A turn that starts at or above a hard limit is refused before any provider call, with a budget_exceeded error naming the scope and the number — not a generic failure the operator has to go read logs to explain. A matching *_warn threshold raises a one-shot budget_warning and lets the turn run.

The part that matters is where the check happens. A ceiling that is only read at the top of a turn is not a ceiling: one turn with a long tool loop can spend all day inside it. Ceilings are re-checked between iterations within a turn, so a runaway loop stops mid-turn rather than at the start of the next one.

And spend is persisted to ~/.agentos/state/spend_ledger.db, in its own database rather than through the async session writer. A budget kept only in process is a budget you clear by restarting — a runaway overnight loop that crashes and respawns would come back with a clean slate. It comes back to the same number.

Nothing is enforced until an operator sets a number. Changing [budgets] requires a gateway restart; the config RPC reports restart_required rather than pretending the new number is live. UsageTracker.check_warning() is removed — it had no callers, and this replaces it with a configurable, enforced equivalent. (#363, #473)

Secrets stop riding out in file content

read_file, read_spreadsheet and grep_search now mask credentials in the content they hand back to the model, and so do the two channels that quote file content alongside them: edit_file's closest-match hint, and terminal output from a command that reads a credential file (cat ~/.aws/credentials).

The sensitive-path denylist used to be the only thing standing between a secrets file and the model — and that denylist is lifted entirely under elevated-full mode, which cron agent_turn jobs run in by default. So read_file ~/.aws/credentials on a scheduled turn returned the raw keys straight into the persisted transcript, where they stay.

Masking uses a non-reusable «redacted:…» sentinel, DSN and URL passwords included, so a value read out of a config file cannot be written back over the working one by an agent that edits what it just read.

How much of the pass runs depends on the file. Shape-matched credentials (sk-…, JWTs, PEM blocks) are masked everywhere. The name-driven pass — the only one that catches a shapeless secret like aws_secret_access_key — runs everywhere except source code, where it would mask ordinary identifiers and hand back code that no longer matches the file. (#355, #459)

Masking itself got stricter about what it is allowed to touch. It was rewriting ordinary source: header names matched as substrings ("requiresApiKey": False), a value ran past the closing bracket ({"xi-api-key": api_key} lost its }), a bare number was masked as a credential, a vendor prefix matched mid-base64 inside an embedded font blob, and a PEM span across two adjacent string literals swallowed the code between them. Header names now match on a segment boundary, values stop at the punctuation that closes them, prefixes need a left boundary, and a PEM span must have a base64 body. A PEM block inside a read_file window is masked line by line, so the line numbers the reader computes its next offset= from stay correct. Quoted keys ("client_secret": "…") are recognised too, so credentials in JSON and YAML config are masked as the docstring always claimed.

A skill zip can no longer take the gateway down

ClawHubSource.fetch read every entry of a downloaded skill archive into memory with no cap on entry count or uncompressed size. A few tens of KB of nested deflate — a classic zip bomb — could exhaust the gateway's memory and kill the process.

The download is now streamed against a size ceiling, because httpx gunzips a Content-Encoding body with no limit of its own: a buffered read could be filled before any zip-level cap got a say. The archive is then refused past an entry count, a per-entry size, and a total uncompressed size, with each entry decompressed in chunks against a running total — so an archive that understates ZipInfo.file_size is caught mid-read rather than believed.

A hostile archive also fails closed rather than raising through the installer. An entry flagged encrypted, an unsupported compression method, or a truncated deflate stream reaches the caller as "no bundle", not as an exception that aborts a whole lockfile sync. Entry paths are checked against Windows-style escapes (..\, C:\), which posixpath.normpath leaves intact and only the installer's resolve check used to catch. (#357, #471)

aero-stock-lp

aero-stock-lp joins the Bankr skill hub. It range-LPs Coinbase tokenized equities (NVDA, AAPL, GOOGL, META) and AERO/USDC on Aerodrome Slipstream (Base) — opening, recentering and exiting concentrated-liquidity positions, reporting pool status, NAV, yields and P&L, and routing each position to whichever side pays more at this epoch: staked for AERO emissions, or unstaked for trading fees.

It ships as a directory in BankrBot/skills, so it browses and installs through the existing repo half of the Bankr source with no new code path. The Bankr user-skill allowlist — the half carrying skills published from a wallet on bankr.bot — is now empty; stock-premium-lp-manager is retired in its favour. Copies already installed keep working; the slug is no longer offered for browse or install.

Bankr catalog cards also wear the Bankr brand mark again. aero-stock-lp is the one entry in BankrBot/skills whose catalog.json ships a logo, so it rendered that artwork while every other card in the partner tab showed the Bankr symbol. The source now ignores the payload's logo entirely — membership in the catalog is the brand, and a repository-side edit can no longer repaint a partner card's identity.

Fixed — the gateway

  • The Control UI bootstrap endpoint no longer leaks host details to any website the operator visits. {control_ui.base_path}/api/bootstrap sat inside the prefix exempt from the loopback Origin guard, so with the default cors.allowed_origins = ["*"] any page could fetch() it cross-origin and read the absolute config file path — which reveals the OS username — along with the configured auth_mode. The payload no longer carries config_path at all; the console reads it from the authenticated doctor.status RPC. The Origin guard's Control UI exemption now stops at {base_path}/api/, and AuthMiddleware is narrowed the same way, with a single carve-out for /api/bootstrap itself, which the console must read before it holds a token. (#351, #452)
  • auth.mode = "password" no longer admits the gateway unauthenticated. The mode was advertised and env-bound (AGENTOS_AUTH_PASSWORD) but had no branch in AuthMiddleware.dispatch, so it fell through to the unauthenticated pass and left the whole non-RPC surface — /api/system/status, /api/config, /api/v1/files/upload, /api/audio/transcribe — open. Any typo'd mode did the same. auth.mode now validates at load time against the modes the gateway implements (none, token, trusted-proxy) and refuses anything else with a message naming the fix; the middleware fails closed with 401 on any mode without an enforcement branch, reading the config object live so a runtime mutation cannot reopen the hole. The value is case- and whitespace-normalized (" TOKEN " loads as token). (#352, #458)
  • Multipart uploads are capped before the body is buffered, rather than after. (#436, #445)
  • Channel approval prompts resolve the channel kind correctly. (#435, #441)

Fixed — the CLI and the tools

  • agentos chat, agentos sessions, agentos skills and agentos env send the resolved gateway token when they open their own WebSocket, and honour AGENTOS_GATEWAY_URL consistently. resolve_auth grants no loopback exemption in token mode, so auth.mode = "token" broke all four commands even on a purely local install; the token resolver existed and these call sites never used it. chat additionally ignored AGENTOS_GATEWAY_URL and always dialled the hardcoded ws://localhost:18791/ws. (#461)
  • edit_file's fuzzy matcher runs off the event loop, so a large file no longer stalls the gateway. (#439, #446)
  • The download install kind is hardened, and its hint agrees with the executor when bins is absent. (#451, #464, #465)
  • The sandbox runs the command when the resource module is missing, instead of failing on Windows. (#437, #443)
  • Deleting a session deletes its agent tasks and memory receipts with it. (#440, #444)
  • The router applies the cost-aware override after the routing decision, not before it. (#438, #442)
  • Cron delivery honours account_id when resolving its target. (#359, #382)
  • Turn, compaction and tool hooks are wired into the engine, and a failing hook is logged rather than swallowed. (#361, #386)

Removed

Three subsystems that shipped in the wheel while being dead or permanently-failing (#362, #472):

  • The onboard_agent wizard — wizard.start / wizard.next / wizard.cancel / wizard.status and their state machine — had no caller in the frontend or the CLI and no side effect: its terminal step returned the collected answers and never created an agent, while its hardcoded model list had gone two generations stale. The Agents view already creates agents through agents.create.
  • The canvas and nodes built-in tools validated their action argument and then raised ToolError unconditionally. No node runtime exists anywhere in the tree to configure; only exposed_by_default=False kept them from failing in front of a model.
  • tools/visibility.py no longer exports filter_by_profile (returned its input) or profile_allows_tool (returned True), and the dispatch chain no longer runs the ProfilePolicy that only called them. Profile enforcement has one home in tools/policy_config.py. ToolProfile and resolve_profile stay — they are the live seam.

The agentos dist install inventory also stops advertising built-in tools that are not in the wheel. bundled_tools listed nodes (deleted above) and agent (no such module for some time), so an inventory diff across releases showed capability that was not there. A new parity test asserts every name in BUNDLED_TOOLS resolves to a module under agentos.tools.builtin.

Docs

The scheduling guide drops a stale claim about the read-only tool surface. (#422)

Install

uv tool install --python 3.12 "use-agent-os[recommended]==2026.8.27"

Full changelog: https://github.com/use-agent-os/agent-os/blob/v2026.8.27/CHANGELOG.md