Skip to content

AgentOS 2026.9.1

Choose a tag to compare

@github-actions github-actions released this 31 Aug 18:07
· 140 commits to main since this release
5b740f3

A release about the blast radius of a shared surface. Projects turned out to be one — shared knowledge text, editable by any member session, injected into every member's system prompt — and three of the fixes below are about drawing that boundary properly. Alongside it, the gateway learns to say what it is doing while it runs.

Projects: one prompt injection should not reach every session

A project's knowledge text is injected into the system prompt of every member session, every turn. That makes it the highest-value write target in the product, and the projects_* agent tools handed it out too freely:

  • projects_list returned every project's knowledge text to the model, not just the caller's.
  • projects_update accepted any project_id, so a session in project A could rewrite project B's knowledge — text that then runs inside the system prompt of every session in B, on their very next turn.
  • projects_move_session accepted arbitrary session keys, letting the same hand-off happen by dragging a victim session into an already-poisoned project.

One prompt-injected instruction in any member session was enough. The three tools are now scoped to the calling session: projects_update edits only its own project, projects_list includes knowledge only for that project, and projects_move_session moves only itself. Cross-project management stays on the Web UI, CLI and RPC surface, which is control plane, not model-reachable.

Project knowledge is also capped at 24,000 characters on write — the same ceiling the per-turn injection already applied, down from 32,000. Text between the two caps used to save fine and echo back intact from the API, then be silently truncated out of every turn, with only the model able to see the marker. Rows already above the cap keep working (validated on the next write, truncated at injection until then), and the Web UI knowledge editor now shows a counter for the real limit.

Two clients, one project

projects.update read the whole row, applied the change, and wrote every column back. A rename holding a stale row silently reverted a concurrent knowledge save — no error, no conflict, just the other person's work gone.

Updates now write only the fields passed, and the Web UI sends the updatedAt it last read, so a lost race returns a project.conflict error — draft kept, latest version loaded — instead of clobbering. Same-millisecond writes get distinct updated_at values, and a unique index on project names (V012) backstops the duplicate-name check under concurrent creates.

The Projects page also stopped needing a Refresh: it listens to the gateway's projects.changed / sessions.changed broadcasts, so another client's create, rename, delete or session move shows up on its own. Moving a session between projects via sessions.patch now broadcasts projects.changed too, keeping other clients' session counts fresh, and the move can no longer be reverted by a simultaneous field patch on storage-only session managers. Loading and error states render as a spinner and an error-with-Retry rather than the "No projects yet" empty state with a create button, back/forward can no longer leak one project's unsaved draft into another's editor, and a saved knowledge edit no longer flashes the stale pre-save text. (#652, #654)

The gateway can now be watched

Two operability gaps for a gateway that stays up for months:

  • Prometheus GET /metrics. Thread-safe multi-dimensional Counter, Gauge and Histogram types with a standard text exposition format, wired to TaskRuntime so the numbers track in real time.
  • OTLP span export. OtlpTraceSink ships TraceEvent records to any OpenTelemetry collector over HTTP/JSON at /v1/traces.
  • Log retention. A sweeper prunes ~/.agentos/logs/** by TTL age and against a maximum total disk budget, so the thing that was supposed to help you debug an incident stops being the one that fills the disk. (#367)

Bytes, timing, and userinfo

Four security fixes, each one a check that ran after the thing it was checking:

  • http_request caps what it downloads, not just what it returns. The request went out non-streaming, so httpx buffered the entire body into memory before the 1 MB model-facing limit was applied. A chunked response with no content-length — or a lying one — was read fully into RAM: one attacker-influenced URL could exhaust the process. Measured before the fix, a 50 MB chunked body streamed 52,428,800 bytes into memory to return 1 MB. The response is now streamed and accumulation stops at a hard ceiling, reporting download_capped. A timeout bounds time, not bytes. (#508)
  • Credentials in URL userinfo are blocked at the egress guard. _sensitive_url_marker checked path segments and query values only. httpx turns https://user:sk-…@host/ into an Authorization: Basic header on the wire, so a vendor-shaped credential parked in userinfo egressed to whatever host the URL named — through http_request, through web_fetch on every redirect hop, and through the media image tool. Username and password are now percent-decoded and matched, raising a sensitive_url_userinfo marker. Percent-decoding matters: the client decodes before it encodes the header. (#499)
  • Gateway token auth compares in constant time. Four token gates — resolve_auth for WebSocket/RPC, the HTTP AuthMiddleware, the upload route and the audio-transcription route — used ==/!=, which short-circuits on the first differing byte. The Slack adapter already used hmac.compare_digest; these now route through a shared token_matches helper that does the same and fails closed on a missing or empty configured token. The auth contract is otherwise unchanged. (#498)
  • Scheduler timeout_seconds is bounded. It was accepted unvalidated on both cron create and update. A value <= 0 makes asyncio.wait_for run the handler with no wait at all; a huge one holds a model turn open for years. Either is a scheduler denial of service from a single add/update call. Values below 1 second or above 24 hours are now rejected on both paths. (#570)

Fixes

  • Router metadata no longer names a model the provider never ran. An explicit model — a durable config.agents[].model, a session pin, or a per-call override — beats the Pilot Router's pick when PromptAssemblerStage resolves the final model, but the metadata kept advertising the route as applied. The Web UI router HUD, the DoneEvent, per-turn usage and the savings figures all named the routed model and credited savings for a route the turn never took. The decision is now demoted the way the observe rollout phase already does it (routing_applied=false, tier and model kept on the record as advice), and per-turn savings are priced from the model that actually ran — which also corrects the cost basis reported during observe. (#586)
  • OpenCAP requests carry x-api-key. The opencap and bankr provider kinds failed chat completions and model listing with HTTP 401: API key required for remote API access. (#519)
  • Gemini reasoning models keep their thought_signature. Tool calls came back with HTTP 400: Function call is missing a thought_signature. The signature is now captured from streamed and non-streamed deltas, carried on ToolUseEndEvent, ContentBlockToolUse and ToolCall through the turn loop, session sanitization and history deserialization, and echoed back when messages are rebuilt. (#519)
  • agentos upgrade kills the whole process tree on Windows. _kill_process_group() called proc.kill(), which terminates only the direct child — grandchildren (compilers, downloads, nested Python runs) were orphaned and kept file locks on the virtualenv. Windows now uses taskkill /T /F /PID, falling back to proc.kill() only if that fails. POSIX is unchanged: os.killpg, SIGTERM then SIGKILL. (#536)

Thanks to @keyKQ, @Carlys17, @bukeeastrey, @tejajakarulloh and @s97472091-pixel.