AgentOS 2026.9.1
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_listreturned every project's knowledge text to the model, not just the caller's.projects_updateaccepted anyproject_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_sessionaccepted 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-dimensionalCounter,GaugeandHistogramtypes with a standard text exposition format, wired toTaskRuntimeso the numbers track in real time. - OTLP span export.
OtlpTraceSinkshipsTraceEventrecords 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_requestcaps 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 nocontent-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, reportingdownload_capped. A timeout bounds time, not bytes. (#508)- Credentials in URL userinfo are blocked at the egress guard.
_sensitive_url_markerchecked path segments and query values only. httpx turnshttps://user:sk-…@host/into anAuthorization: Basicheader on the wire, so a vendor-shaped credential parked in userinfo egressed to whatever host the URL named — throughhttp_request, throughweb_fetchon every redirect hop, and through the mediaimagetool. Username and password are now percent-decoded and matched, raising asensitive_url_userinfomarker. Percent-decoding matters: the client decodes before it encodes the header. (#499) - Gateway token auth compares in constant time. Four token gates —
resolve_authfor WebSocket/RPC, the HTTPAuthMiddleware, the upload route and the audio-transcription route — used==/!=, which short-circuits on the first differing byte. The Slack adapter already usedhmac.compare_digest; these now route through a sharedtoken_matcheshelper that does the same and fails closed on a missing or empty configured token. The auth contract is otherwise unchanged. (#498) - Scheduler
timeout_secondsis bounded. It was accepted unvalidated on both cron create and update. A value<= 0makesasyncio.wait_forrun 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 singleadd/updatecall. 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 whenPromptAssemblerStageresolves the final model, but the metadata kept advertising the route as applied. The Web UI router HUD, theDoneEvent, 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 theobserverollout 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 duringobserve. (#586) - OpenCAP requests carry
x-api-key. Theopencapandbankrprovider kinds failed chat completions and model listing withHTTP 401: API key required for remote API access. (#519) - Gemini reasoning models keep their
thought_signature. Tool calls came back withHTTP 400: Function call is missing a thought_signature. The signature is now captured from streamed and non-streamed deltas, carried onToolUseEndEvent,ContentBlockToolUseandToolCallthrough the turn loop, session sanitization and history deserialization, and echoed back when messages are rebuilt. (#519) agentos upgradekills the whole process tree on Windows._kill_process_group()calledproc.kill(), which terminates only the direct child — grandchildren (compilers, downloads, nested Python runs) were orphaned and kept file locks on the virtualenv. Windows now usestaskkill /T /F /PID, falling back toproc.kill()only if that fails. POSIX is unchanged:os.killpg, SIGTERM then SIGKILL. (#536)
Thanks to @keyKQ, @Carlys17, @bukeeastrey, @tejajakarulloh and @s97472091-pixel.