Agent UI Agent Capabilities Plan
Branch:kalin/agent-uiDate: 2026-03-06 Two Milestones:
- Milestone A — Agent UI: Wire Existing SDK Capabilities (#15) Expose existing GAIA SDK features to the Agent UI. No new SDK code — just wiring, MCP integration, and UI work.
- Milestone B — GAIA Agent SDK: New Capabilities (TBD) Enhance the core GAIA Agent SDK with capabilities that don’t exist yet: guardrails framework, screenshot capture, computer use, voice, etc.
Milestone Scope Summary
Milestone A — Agent UI: Wire Existing SDK Capabilities
Goal: Make ChatAgent as capable as possible using what the SDK already has.Milestone B — GAIA Agent SDK: New Capabilities
Goal: Build new capabilities in the core SDK that don’t exist anywhere today.0. v2 Architecture: Sidecar-First (supersedes the in-process model below)
Realizes the Architecture (v2 — sidecar-first) section ofagent-ui.mdx. The Milestone A/B wiring below assumes agents run in-process inside the UI backend; v2 inverts that — agents are out-of-process sidecars and the UI is a thin host. Where a section below wires a capability into in-processChatAgent, read it as “a capability the sidecar exposes over REST and the host relays,” not “Python loaded into the UI backend.” PR #1910 (in-UIEmailProxyAgent) is superseded.
0.§ Reading map
§0 grew comprehensive; here’s the shape so you can jump to what you need.- Foundations: §0.0 the host is a headless daemon (not the web UI) · §0.3 the sidecar supervisor.
- The agent contract: §0.1 REST surface (fixed-function +
/query) · §0.2/querySSE event schema · §0.4 mid-workflow confirmation · §0.18 dispatch · §0.32 multi-agent orchestration · §0.33/queryon thegaia apiREST server. - Autonomy: §0.22 the scheduler clock · §0.34 autonomy-readiness (the human-in-the-loop → policy-driven gap).
- Structure & framing: §0.35 architecture-review refinements (two-tier custody contract, naming, module seams, v1-hardening scope, render primitives) · §0.36 third-party integration ergonomics (the tiered capability matrix) · §0.37 pluggable custody (one sidecar = rich and stateless; refines §0.9) · §0.38 memory/resource footprint · §0.39 custody-modes comparison table · §0.40 hybrid custody (per-store shared/private — the embedded↔delegated spectrum) · §0.41 the unified capability-mediation model (collapses the modes — the capstone).
- Auth & security: §0.6 OAuth forward · §0.11 the three auth legs + per-agent authorization · §0.24 third-party containment (signing, tiers, egress, encrypt-at-rest, audit integrity, taint) 🔒.
- Shared resources (host custody): §0.9 memory/RAG/sessions/audit/MCP/model table · §0.12 the model-slot broker · §0.19 audit sink · §0.29 store consistency.
- Data & contracts: §0.28 the agent manifest schema · §0.29 custody-store
consistency · §0.30 identifier catalog · §0.31 the
/host/v1/*callback API · §0.15 contract evolution. - Lifecycle & ops: §0.5 install (+model provisioning) · §0.13 concurrency / cancel / failure / reaper · §0.14 one daemon per machine · §0.15 version negotiation · §0.16 dev-mode · §0.20 uninstall · §0.21 offline/footprint · §0.22 autonomy clock · §0.25 daemon lifecycle · §0.26 on-disk layout.
- Delivery & context: §0.7 CLI parity · §0.8 what’s superseded · §0.10 migration (strangler-fig + data) · §0.17 eval/test · §0.23 feasibility & build order · §0.27 relationship to sibling plans.
- Open decisions needing sign-off: confirmation model (§0.4) · memory/RAG home (§0.9) · third-party trust-root + containment (§0.24) 🔒.
0.0 “The host” is a headless daemon, not the web UI
Throughout §0, the host is a headless custody/supervisor daemon — a distinct, always-on machine process — not the web UI. It serves/host/v1/*
(custody: OAuth/memory/RAG/sessions/audit — §0.9, §0.11, §0.19), supervises
sidecars (§0.3), and holds the scheduler clock (§0.22). The thin web UI and the
gaia <agent> CLI are both thin clients that attach to this one daemon
(machine-wide, discovered per §0.14). This split is load-bearing:
- CLI-only / no-UI works.
gaia email "what did I say about X last week"and its audit writes reach/host/v1/*because the daemon — not the browser tab — is the custody server. Without this split, any sidecar spawned while the UI is closed would have no callback target (the exact hole §0.11/§0.19 exist to prevent). - The daemon is the always-on component that survives the web UI closing, owns the machine-wide single-instance registry, and is the wake-up owner for autonomy (§0.22). Starting the UI or a CLI command auto-starts the daemon if not already running.
0.1 The agent REST contract (one per sidecar)
Every agent sidecar exposes the same HTTP contract, presenting the agent’s full capability set (specification.html, schema 2.x, is the reference — the
deterministic core and the advanced tier: task extraction, follow-up tracking,
daily briefing, scheduled send/snooze, sender prioritization, inbox profiling,
persistent preferences). Two co-equal surfaces:
- Fixed-function endpoints — the deterministic core as one call each
(
POST /v1/<agent>/<capability>: triage, search, pre-scan, archive, calendar- create, …). No LLM in the loop; for scripted/integrator use. Not every capability is a fixed-function call — the spec’sCapability → surfacetable marks the advanced/personalization/detect-and-reason tier “agent only”, so those are reached via/query+ the §0.22 scheduler, not a deterministic endpoint. (So “exposes ALL capabilities” is true of the sidecar, split across the two surfaces — not “every capability has a fixed endpoint.”) POST /v1/<agent>/query— the agent loop, exposed over REST like any other endpoint. NL request in; the sidecar reasons and chains tools into multi-step workflows; response istext/event-stream(SSE). This is the surface the UI’s chat experience and thegaia <agent>CLI both drive.GET /health,GET /version— readiness + contract-version handshake.- Connector surfaces: self-service OAuth routes (bare integrators) and
forwarded-connection intake
POST /v1/connectionsgated byX-Gaia-UI: 1(the Agent UI host path — see §0.6).
/query request body (define it — currently only the response is specced):
{ query, run_id, context, model?, provider?, max_steps? }. Two rules that avoid
real bugs:
- The host mints
run_id, not the sidecar. §0.13’scancel/{run_id}and the confirmation flow key off it; if the sidecar minted it, a “Stop” pressed before the first SSE event would have no id to cancel (a race window). Host-mintedrun_idis cancellable from the instant the request is sent. - Context is pushed in the body, not pulled. The host owns the transcript
(§0.9) and passes the relevant slice as
context; the sidecar stays stateless and never reads other sessions back over the callback (which §0.11’s scoping would forbid anyway). TheGET /host/v1/sessionscallback is host-internal, not a sidecar transcript-access path — so a pushed slice and a pulled one can’t disagree.
0.2 /query SSE event schema
One JSON object per SSE data: line, discriminated on type:
render replaces the in-process SSE “fence injection” hack: the sidecar declares
the card type explicitly, so the host needs no per-tool knowledge. Frontend
change required (be honest about “thin”): today MessageBubble.tsx renders
cards by fence-parsing the assistant text (STRUCTURED_PAYLOAD_LANGS); v2 moves
it to a render→component map keyed off tool_result events. The host stays
generic, but each render type needs a frontend component, so a new agent that
introduces a new card ships that component with it.
Build reality — the loop→SSE seam already exists; this table is a re-spec of it.
src/gaia/ui/sse_handler.py (SSEOutputHandler(OutputHandler)) already turns every
agent-loop console.print_* call into a typed JSON event on a queue.Queue a
streaming endpoint can drain — so /query is “run the agent with an SSE handler and
expose the queue as text/event-stream,” not net-new agent instrumentation.
BUT the existing handler emits its own vocabulary
(status/step/thinking/tool_start/tool_end/tool_result/chunk/answer),
which is not the vocabulary above. The table is therefore the target contract;
a small translation layer maps the handler’s events onto it. Spec the mapping
explicitly (it is the wire contract integrators depend on) rather than assuming the
two vocabularies already agree — they don’t.
0.3 Host: the per-agent sidecar supervisor
Generalize the emailEmailSidecarManager into an AgentSidecarManager
registry keyed by agent id. Per agent: mode (user frozen binary / dev
uvicorn-from-source), verified download (SHA-256 vs the agent’s lock), ephemeral
port (never 4001), lazy spawn on first use, health + /version handshake,
tree-kill on shutdown, one shared instance per agent. Host routes:
GET /v1/agents— installed + catalog (hub mirror, §0.5).POST /v1/agents/{id}/install·DELETE /v1/agents/{id}— one-click install/uninstall.ANY /v1/<agent>/*— reverse-proxy to that agent’s running sidecar (lazy-start), preserving SSE for/queryand the sidecar’s own status codes + actionable detail.
EmailSidecarProxy is a synchronous requests.Session that ends every call
in resp.json() (src/gaia/ui/email_sidecar/proxy.py) — it fully buffers and
has no streaming, no client-disconnect propagation, no cancel path. SSE passthrough
(§0.13 cancel-on-disconnect + the synthetic-crash error event) must be rebuilt on
httpx.AsyncClient(stream=True) + StreamingResponse. Standard work, but net-new —
the deterministic (buffered) proxy is what’s reusable; the streaming path is not.
0.4 Mid-workflow confirmation (over SSE, no WebSocket)
Destructive steps keep the single-use confirmation-token gate. Flow:- Sidecar reaches a gated step, emits
needs_confirmation{run_id, action, summary, confirm_url}and pauses the run (kept alive server-side, keyed byrun_id). - UI shows approve/deny with the literal
summary(recipient/subject/body, etc.). - On approve the host
POSTs toconfirm_url(/v1/<agent>/query/{run_id}/confirm) with the minted token; the sidecar resumes emitting on the same SSE stream. Deny ends the run with afinal“skipped” answer.
run_id→continuation map with a TTL), which cuts against the email contract’s
explicit “HTTP, stateless” ethos and is fragile across dev-reload / crash /
uninstall. The alternative — emit needs_confirmation, end the stream, let the
host call the deterministic endpoint (/send with the token) itself, then issue a
fresh /query carrying the prior context to continue — keeps the sidecar
stateless at the cost of the host stitching the workflow across calls.
Recommendation: stateless stop-and-hand-off for v1 (preserves the sidecar’s
stateless design; the host, which already holds session context, orchestrates the
continue). Promote to the resume model only if real workflows prove they need
many confirmations in one unbroken run and host-side stitching is too clumsy.
Decision pending sign-off.
Approve-what-you-saw invariant (critical). In the stateless model the sidecar
re-reasons on the continuation, so it can produce a plan that diverges from the
action the user just approved — the approved summary and the re-executed step are
no longer guaranteed identical. The payload-fingerprint token protects a single
/send, but not the workflow continuation. Resolution: the continuation must
resume the exact approved step, not re-plan it — either scope the confirmation
token to the whole approved multi-step segment, or have the host pass the sidecar
the exact prior tool-plan/step state so it executes rather than re-decides. This
constraint holds under either confirmation model and is a hard requirement.
Which artifacts belong to which model (avoid the mixed-model trap). run_id is
the streaming-run handle and exists under both models — it is what §0.13’s
cancel/{run_id} targets. The confirm_url = /v1/<agent>/query/{run_id}/confirm
in the numbered flow above is resume-model-only (it keeps a paused run alive).
Under the recommended stateless model there is no paused run to resume: a
needs_confirmation event ends the stream, the host performs the deterministic
call (/send with the token) itself, then issues a fresh /query carrying the
approved-step state to continue. So if stateless is adopted, drop confirm_url and
the server-side run-continuation store; keep run_id purely for cancellation.
0.5 Agent Hub mirror (install / uninstall)
The UI embeds a catalog mirror (existingagentHub.ts / AgentInstallDialog /
AgentHubGrid are the starting point). Install = resolve the agent’s
platform binary from its lock → verified download (SHA-256) → cache under
~/.gaia/agents/<id>/ → register so it appears as a launchable agent.
Uninstall = stop any running sidecar → remove the cached binary + registration.
Reuses the email sidecar’s verified-fetch + lifecycle primitives; no Node on the
runtime path. Install/update/forward are trust decisions — SHA-256 verifies
integrity, not authenticity; they must go through the signing + tier + least-
privilege + containment model in §0.24 before a third-party agent is trusted or
handed a live connection.
Install must provision the model, not just the binary (cold-start). A freshly
installed agent whose model (e.g. Gemma-4-E4B) isn’t downloaded spawns fine, then
its first /query dies deep in the broker/Lemonade path with model-not-found — the
hidden-cold-state failure CLAUDE.md warns about (#1655). The agent’s lock/manifest
declares its required model(s); install (or first spawn) pulls them through the
host broker (§0.12) with progress in the UI, before the agent is marked launchable.
Define the full cold sequence: open UI → no daemon (start it, §0.25) → no agents
(hub grid) → install agent (binary + model) → optional connector consent (§0.6,
§0.24) → first /query.
0.6 OAuth: host owns consent + refresh, forwards to the sidecar
grants.json keeps a single writer. The host runs the OAuth consent flow once and
forwards the pre-authenticated connection out to each agent sidecar via the
sidecar’s POST /v1/connections intake (X-Gaia-UI: 1; refresh-token/client-secret
are inputs, never returned). Sidecars never run the UI-facing consent write. A bare
integrator (no host) may instead use the sidecar’s self-service connector routes.
Two things the current code makes non-trivial (flag for the build):
- Role inversion. Today
/v1/connectionsis a host-side intake — external apps forward connections INTO gaia (src/gaia/ui/routers/connectors.py:94forwarded_router;src/gaia/connectors/api.py:266import_forwarded_connection). In v2 the host becomes the forwarder OUT, and the sidecar exposes the intake (it bundlesgaia.connectors). The primitive exists; the direction and the per-agent wiring are new. - Token refresh ownership. With N sidecars sharing one Google grant, each holding a refresh token would race to refresh and rotate each other out. The single-writer host should own refresh and forward short-lived access tokens (re-forward on expiry), so sidecars never touch the refresh token.
0.7 CLI ↔ sidecar parity
gaia <agent> becomes a thin client of the host daemon (§0.0), exactly like the
web UI: it auto-starts the daemon if none is running, then attaches to it — it
does not run AgentSidecarManager or spawn the sidecar itself. The daemon owns
sidecar lifecycle (§0.3) and custody; the CLI just calls the identical contract
through it — gaia email "triage my inbox" drives POST /v1/email/query exactly as
the UI does, via the same daemon. One contract, one supervisor, no second in-process
code path to keep in sync. (If the CLI spawned its own sidecar, that sidecar’s
callback base-URL would point at the ephemeral CLI process and its /host/v1/audit
- memory writes would vanish when the command exits — the §0.0 hole. The daemon being the sole spawner is what closes it.)
0.8 What’s superseded
- In-process agent construction in
src/gaia/ui/_chat_helpers.py(theagent_type=…factory) → replaced by proxying to sidecars. EmailProxyAgent+ the in-UI tool loop (PR #1910) → replaced by relaying to the sidecar’s/query.- Fat UI routers whose logic belongs to an agent (chat loop, scheduling) → migrate into the relevant sidecar; the host keeps UI serving, shared-user-data custody (§0.9), and sidecar supervision.
0.9 Shared services (host custody) — the biggest open call
Refined by §0.37: custody is a pluggable provider, not intrinsically the host. The rationale below holds for the multi-agent Agent UI (N sidecars sharing one user’s data ⇒ single writer ⇒ the host). A standalone sidecar is its own single writer and uses an embedded provider. So read this table as “the provider the GAIA host implements,” not “the only place custody can live.”Not everything in the fat backend is “agent logic.” Three chunks are user-scoped data that spans agents and would fragment/corrupt if each sidecar owned a copy — so in the Agent UI they belong to the host’s custody layer (same single-writer rationale as OAuth), queried by sidecars rather than duplicated:
Conversation history must be host custody (was a contradiction). An earlier
draft had the sidecar own message history — but §0.3 makes the sidecar ephemeral
(lazy-spawn, tree-kill, dev-reload, and uninstall deletes the cached binary),
and §0.4 leans on it being stateless. A disposable process cannot be the system
of record for the user’s chat log: uninstalling or crashing an agent would destroy
it, and the stateless
/query model needs the host to replay context anyway. So
the host owns the transcript (extending the session index it already owns); the
sidecar is fed the slice it needs per call and persists nothing durable itself.
Session ownership across clients. Run-isolation (§0.13) namespaces concurrent
runs inside the sidecar, but two clients (two UI tabs, or UI + CLI) pointed at the
same host-owned session would interleave writes into one transcript — braided
turns or a lost-update race. The host needs a session-focus model: one active
writer per session (others read-only/observing), or per-client sessions so two
tabs are simply two conversations. Cheap to decide now, confusing to retrofit.
Alternative considered: make user-memory and RAG their own dedicated
sidecars (purest “all logic in a sidecar”). Rejected for v1 — it multiplies
cross-process user-data writers (the exact problem the OAuth single-writer rule
avoids) and adds two more supervised processes before we’ve proven one. Revisit if
the host custody layer grows its own heavy reasoning. This is the single
decision that most shapes the migration — needs explicit sign-off.
0.10 Migration path (strangler-fig — no big-bang)
Retire the ~19K-LoC backend incrementally; the app stays shippable throughout. This is code migration — existing user data needs its own step:- Data migration (one-time, idempotent, versioned). The current
~/.gaiastate predates host custody: today’s transcripts (sessions.pystore) have no agent tag, and the single memory store has no host/agent-private partition. Migrate on first v2 launch: existing sessions → host session index with a default agent tag; existing memory → host user memory (the conservative default — it was cross-agent already), with a documented rule for what becomes agent-private. Stamp an on-disk schema version so the migration is detectable and runs once. Without this, upgraders lose past chats or see them reattach to a wrong/null agent, and memory either leaks cross-agent or is stranded. - Email first — already sidecar-shaped; add
/query(SSE) and route the UI’s email session through it. All other agents remain in-process, untouched. - Host supervisor — generalize
EmailSidecarManager→AgentSidecarManager- hub install/uninstall, so in-process and sidecar agents run side by side.
- Per-agent migration — move one agent at a time behind the proxy; delete its in-process router logic only after its sidecar ships.
- Extract shared services — lift user memory / RAG / session index into the host custody layer (§0.9) as agents stop owning them in-process.
0.11 Auth: three legs (client↔daemon, daemon↔sidecar, sidecar↔daemon)
The §0.0 daemon split creates three trust legs, all currently undefined in the code (email_sidecar/router.py/proxy.py do no auth; the port is loopback-only).
- Client → daemon (new with the §0.0 split — 🔒). The daemon is now a standalone
process exposing, on a loopback port, the whole custody API (
/host/v1/memory|rag| sessions|audit) and theANY /v1/<agent>/*proxy that can drive any sidecar. The UI and CLI are external clients of it, and loopback is not an auth boundary (same threat model as below) — so without a credential, any local process can read the entire cross-agent memory/RAG/transcript store or drive any sidecar through the proxy. The daemon mints a client-auth token at startup, stored0600in its~/.gaia/host/instance.json(§0.14); UI and CLI read it and present it on every daemon call. (A unix-domain socket with SO_PEERCRED is a stronger alternative where available.) This is distinct from the per-spawn secret below, which is daemon↔sidecar only. - Per-spawn secret (daemon → sidecar). Loopback binding is not an auth
boundary — any local process (another user on a shared box, a browser tab via
CSRF/DNS-rebinding, a malicious
npm postinstall) canPOST /v1/<agent>/query "archive everything"on an unauthenticated port that can send mail. The host mints a random secret per spawn, passes it to the sidecar via env at launch, and the sidecar requires it (bearer header) on every request — fixed-function,/query, and/confirm.X-Gaia-UI: 1is CSRF hygiene, not authentication. (In the daemon-less bare-integrator topology of §0.6, the process that launches the sidecar supplies this launch secret — the sidecar always requires it; only the injector changes.) - The reverse contract (sidecar → host). The custody model (§0.9) needs
sidecars to read host-owned data. Define a first-class host callback API —
GET /host/v1/memory,POST /host/v1/rag/query,GET /host/v1/sessions/{id}— that the sidecar calls, authenticated with the same per-spawn secret (the host injects its own callback base-URL + the secret at launch). Symmetric to the forward proxy; without it, “the host stores it, the sidecar queries it” has no transport and agents silently re-fork their own copies. - Authorization, not just authentication (🔒 the decisive one). The per-spawn
secret only proves “the host spawned me” — it does not say which agent, and
unscoped callback routes turn the custody store into a single-reader
exfiltration surface: a hub-installed third-party agent could read the entire
cross-agent user memory, the whole RAG corpus, and any session transcript by id
— including other agents’ conversations. The host must bind the secret to the
agent id at mint time and scope every callback per-agent:
/host/v1/memoryand/host/v1/ragreturn only rows tagged to that agent (or an explicitly user-granted shared scope), and/host/v1/sessions/{id}verifies the session belongs to the caller. Shared-memory/RAG read access becomes a per-agent grant surfaced at install, like connector grants. This is an architecture-level security decision — settle it before the callback contract is frozen. - Secret delivery. Passing the secret via env is readable by other same-user
processes on some OSes — the exact “another local process” threat the auth exists
to stop. Prefer a pipe/inherited fd or a short-lived
0600file over the environment.
0.12 Shared model backend — a host-owned model-slot broker
Lemonade is single-tenant per model slot (this is why evals must run serially — CLAUDE.md). N sidecars each loading models independently will race-evict each other exactly as concurrent evals do — chaotic ctx-size errors andmodel_load_error in production, not just CI. The custody layer must arbitrate the
most-contended shared resource: a host-owned broker that serializes model
loads and grants a model-slot lease per request/run. Agents request inference
through the broker (or the host proxies the backend), never contend directly. A
hard requirement for more than one concurrently-active agent.
- All model families, not just the chat LLM. Host-custody RAG needs the
embedder to serve
/host/v1/rag/query; a VLM agent needs the VLM; voice (host I/O) needs Whisper/Kokoro; SD needs SDXL. A host RAG call mid-/queryevicting the chat model is the exact silent-ctx-cap regression #1030 warns about. The lease must cover embedder + LLM + VLM (+ voice/SD) together for a run — host-custody RAG/VLM/voice are broker clients, not privileged bypass paths. - Priority + preemption. Serialization prevents corruption, not stalls. A Phase-C autonomous brief runs in-sidecar and also draws the slot, so it can block the user’s interactive turn behind it. The broker needs interactive > background priority (and preemption), or a background job silently makes a chat turn wait.
- Affinity + legibility. A hot-model affinity hint keeps agents sharing a
model from reloading on every switch; the host emits a
switching model…status event so an unavoidable evict+reload stall (seconds, worse on NPU) is a visible state, not a frozen UI.
0.13 Concurrency, cancellation & failure semantics
- Run isolation. §0.3 mandates one shared sidecar per agent, but a
/queryrun is a stateful multi-step loop and today’s confirmation store is a single process-wide dict. Two simultaneous callers (UI + a scheduled brief, two tabs) must not collide: namespace all per-run state byrun_id(token store, tool state), forbid shared mutable module state, and document a parallelism limit (or a per-run worker). “Shared instance” must not silently imply “safely reentrant.” - Cancellation. Define
POST /v1/<agent>/query/{run_id}/cancel; the host propagates a UI “Stop” and a dropped SSE connection to it (a relayed stream hides the client disconnect from the sidecar), and the agent loop checks a cancel flag between tool steps — so a stopped run stops burning the model slot and fires no further tools. - Failure semantics. On sidecar crash mid-SSE the host injects a synthetic
terminal
errorevent (the UI otherwise sees a truncated stream with nofinal), cleans up the orphanedrun_id/session-index entry, and surfaces the actionable cause. Download/spawn failure fails loud via the hub (§0.5). - Lifecycle/resource limits. Every installed agent is a long-lived process holding a port + memory + a model-slot claim. Lazy-spawn is not enough: add an idle-timeout / LRU reaper and a cap on concurrent live sidecars, or ten installed agents = ten resident processes.
0.14 One daemon per machine; the daemon holds one sidecar per agent
Because the daemon (§0.0) is the sole spawner/supervisor, the old “UI and CLI race to spawn rival sidecars” problem is structurally gone — no client spawns a sidecar. What remains is single-instancing the daemon itself and letting clients find it:- One daemon per machine. A daemon lockfile/registry
(
~/.gaia/host/instance.json→ pid + client-auth token — see §0.11) makes the first UI/CLI invocation start the daemon and every later one attach. Two daemons would re-create two writers intogrants.jsonand two forwarders, so the lock is what guarantees a single forwarder-of-record. - One sidecar per agent, owned by the daemon. The daemon keeps its own
per-agent registry (
AgentSidecarManager, §0.3) → pid + ephemeral port + per-spawn secret; UI and CLI never see it, they go through the daemon’s proxy. So “one shared instance per agent” is now a property of the single daemon, not a race between client processes.
0.15 Contract-version negotiation at install + render fallback
GET /version exists but the policy on mismatch does not. Installing an agent
built against contract 3.x into a 2.x host would silently diverge on the SSE schema,
render types, and /v1/connections shape. Define: the host advertises a
supported contract-major range; hub install rejects out-of-range agents with a
loud, actionable error (never a silent partial-compat install); and the frontend
renders an explicit “unsupported card” fallback for an unknown render type
rather than nothing.
Custom cards are first-party in v1. A sidecar binary cannot inject a React
component into the pre-built, signed thin-UI bundle without a dynamic
component-load path (itself a security surface) — so custom render types are
first-party / AMD-verified only in v1; a third-party agent’s novel card
gracefully degrades to the generic result card via the fallback above. Don’t plan a
dynamic-component-load path unless/until that’s explicitly sanctioned.
Evolution doesn’t stop at the install gate. Install negotiates only the contract
MAJOR; §0.25’s daemon self-update then makes skew inevitable (the daemon updates,
installed sidecars don’t). Cover the after-install cases:
- Reverse callback skew (new daemon → old sidecar). §0.25 versions the
client↔daemon API; the mirror case — an old installed sidecar calling a changed
/host/v1/*— is uncovered. Version the callback API too, symmetric to §0.25: refuse + prompt reinstall on a MAJOR the daemon can’t serve, or hold N-1 compat. - Unknown SSE event
type(§0.2). A newer agent emitting a new top-level eventtypeto an older host must surface a visible “unsupported event,” never silently drop it (the CLAUDE.md no-silent-fallback rule) — parity with therender-type fallback above. - Additive vs deprecation. A new endpoint/capability/event is a backward-compatible MINOR; removing one needs a stated deprecation/sunset window, not a silent break. (The MAJOR gate alone can’t express either.)
0.16 Dev-mode discovery for unpublished agents
GAIA_<AGENT>_MODE=dev runs a known agent from source, but AgentSidecarManager
is keyed by hub id + lock — a brand-new agent an author is building has no catalog
entry, no lock, no binary, so it can’t be registered or spawned, defeating “the UI
doubles as dev mode.” Add a local-agent registration path: point the manager at
a source dir + port (skip SHA/lock, no catalog entry) so an in-development agent is
launchable and appears in the UI before it is ever published.
0.17 Testing & eval strategy (/query + the distributed seams)
/query is a multi-step LLM loop — the most LLM-affecting surface, which CLAUDE.md
mandates evals for — but v2 also adds a daemon, a cross-process broker, an SSE
relay, three auth legs, and a data migration, each with its own failure modes. The
strategy spans four tiers:
/querybehavior (eval). A sidecar eval harness drives/queryover REST and asserts on the event sequence (tool_call→tool_result→needs_confirmation→final), not just a final string; eval baselines move into each agent’s hub package. Runs stay serial against the shared broker (§0.12) — the per-agent sidecar model must not reintroduce concurrent model-slot contention.- The distributed seams (deterministic unit/integration, no LLM):
- SSE relay — streaming passthrough (assert no buffering), cancel-on-disconnect
(§0.13), and the synthetic-crash
errorevent, against a fake sidecar. - Auth legs (§0.11) — assert all three reject a missing/wrong credential (client→daemon token, daemon→sidecar per-spawn secret, sidecar→daemon callback), and that callback authorization is per-agent scoped (agent A cannot read agent B’s memory/session) — the security property, tested as a boundary, not a mock that only proves “we called it.”
- Broker (§0.12) — a concurrency test proving two agents requesting different models serialize rather than race-evict (the property CLAUDE.md’s serial-eval rule protects), plus priority (interactive preempts background).
- Daemon lifecycle (§0.25) — stale-
instance.jsonreclaim after a killed pid, atomic write, and daemon restart-on-version-skew draining in-flight runs.
- SSE relay — streaming passthrough (assert no buffering), cancel-on-disconnect
(§0.13), and the synthetic-crash
- Data migration (§0.10 step 0) — idempotency + cold-state test (CLAUDE.md’s
“test from the user’s real initial state”): run the one-time migration twice, and
from a real pre-v2
~/.gaiafixture, asserting transcripts/memory land tagged correctly and a second run is a no-op. - End-to-end golden path — one on-hardware test per the
gaia-testingtiers: UI (or CLI) → daemon → sidecar → Lemonade, a real/queryinbox triage, confirming theemail_pre_scancard renders and a destructive step surfaces the confirmation gate. This is the “the call is valid, not just invoked” proof for the whole chain.
0.18 Dispatch — which sidecar answers a free-form message
/query “is what the UI’s chat drives,” but with N installed sidecars nothing
chooses the target. gaia email "…" is explicit; the UI’s single chat box is not —
the “host renders, sidecar reasons” split has a hole exactly where the user types.
Pick one model and put it in the call graph:
- (a) Explicit per-session agent selection (recommended v1) — the UI’s active- agent picker names the sidecar; matches the CLI; zero extra LLM cost.
- (b) A routing sidecar the host calls first — flexible, but routing is itself an LLM loop, so it draws the model slot (§0.12) and adds a hop before every turn.
0.19 Audit trail — a host-custody sink, not agent-private
The observability dashboard (security-model.mdx) promises “everything the agent did,” but v2 moves the acting into sidecars, so the host sees only what crosses its SSE relay. Three blind spots: fixed-function calls from the CLI/integrators bypass the host; Phase-C autonomous schedules fire in-sidecar with no host in the loop; and email’s action log is agent-private (§0.9) so the dashboard can’t read it and uninstall deletes it. Every consequential action (send, archive, calendar-create, autonomous or scripted) must be appended to a host-owned audit log viaPOST /host/v1/audit (per-agent scoped, §0.11) — not just host-relayed
/query. Otherwise the trail is a partial, self-erasing view.
0.20 Uninstall — data lifecycle
§0.5 uninstall stops the sidecar + removes the binary, but is silent on data. Define the policy explicitly (tie to §0.11 per-agent scoping so “this agent’s data” is a well-defined set):- Forwarded connection — the host withdraws/revokes the OAuth connection it forwarded, so a removed agent doesn’t retain live mailbox access.
- Host-custody transcripts tagged to the agent — offer keep or delete (they survive the binary; dangling “ghost” sessions that can’t reopen are the failure to avoid); on reinstall, keep = reattach.
- Agent-private state (
~/.gaia/agents/<id>/, e.g. the action log) — wipe by default (state a default), with an option to retain. - Shared user-memory rows the agent wrote — governed by its §0.11 grant scope.
0.21 Offline / sideload install + install footprint
- Sideload. Every §0.5 path downloads from the lock’s
baseUrl— but GAIA is privacy-first and targets air-gapped + OEM-preloaded deployments (a stated Phase-D goal). Add a local install source: point the manager at a pre-staged directory of verified binaries (still SHA-check against the lock, skip the network), covering the first agent too. This also unblocks OEM factory-image bundling — cheap to reconcile now rather than retrofit. - Footprint (honest downgrade). The email binary is ~90 MB only because its
freeze
--exclude-modules torch/transformers/faiss/scipy/pandas — legal solely because email runs no in-process ML (it calls Lemonade over HTTP;packaging/freeze.py). Any agent that needs an in-process embedder/VLM/SD (RAG, vision) cannot use those excludes → multi-GB per binary. And PyInstaller produces self-contained bundles with no cross-binary linking, so a “shared runtime layer the per-agent binaries link against” is not a PyInstaller capability — it needs a different packaging strategy (a shared base venv, a host-side model-runtime service the sidecars call, or non-frozen deployment). Treat §0.21 shared-runtime as aspirational, not a config tweak, and prefer keeping in-process ML out of sidecars (route it through the host model broker, §0.12) to preserve the lightweight-binary property.
0.22 Autonomy — the host holds the clock, the sidecar does the work
§0.9 runs autonomy (schedules/goals) in the owning sidecar, but §0.13 idle-reaps sidecars and caps live ones — so a reaped email sidecar has nothing alive at 8am to fire the daily brief, and the job silently never runs (passing every unit test). Resolve by splitting the clock from the work:- The host daemon (§0.0) owns the trigger registry + cron clock — always-on, it is the wake-up owner. Schedule metadata is host custody.
- At fire time the host spawns the owning sidecar (if not resident) and hands it
the job over
/query(or a fixed endpoint), then lets the reaper reclaim it after. - Alternatively, mark specific agents pinned-resident (exempt from the reaper + live-cap) when sub-minute latency matters. State which agents qualify; default is wake-on-fire, since holding every autonomous agent resident defeats §0.13.
0.23 Feasibility & build sequencing (grounded in the current code)
A feasibility pass against the codebase found no fatal blocker — the design is buildable — but the “thin” framing rests on some primitives that are reuse and others that are genuinely net-new. Naming which is which sets an honest build order. Already exists (de-risks the plan):- The agent-loop → SSE seam (
src/gaia/ui/sse_handler.pySSEOutputHandler) —/queryreuses it (§0.2 build note), not net-new instrumentation. - Mid-workflow confirmation over SSE — the pause/resume primitive §0.4 describes
(
_confirm_event,_user_input_queue,permission_requestevents) ships in-process today; the only new work is carrying the Event across the process boundary (which is exactly what makes a resumed run stateful — the §0.4 tension is real, not hypothetical). - OAuth forward + short-lived-token refresh —
import_forwarded_connectionandget_access_token(src/gaia/connectors/api.py) already exist and the refresh engine is client-neutral, so §0.6’s “host refreshes, forwards short-lived tokens” maps onto real code.
- Lemonade broker (§0.12) — the critical path. Today the only lock is an
in-process
_downloads_lock(lemonade_client.py); “one model slot” literally means last-writer-wins across processes. The host broker (serialized loads + cross-process lease + priority) has zero existing scaffolding and gates the plan’s headline (more than one concurrently-active agent). Build + prove first. - Streaming reverse-proxy (§0.3, §0.13) — net-new
httpxstreaming + cancel; the current proxy buffers. Prove with a one-endpoint freeze spike (SSE through a PyInstaller binary is unproven here, though low-risk — uvicorn loops/protocols are collected in the freeze) before committing. /queryendpoint + event-vocab translation (§0.1, §0.2) — de-risked by the SSE seam, but the sidecar has no/querytoday and the handler’s vocabulary must be translated to the §0.2 contract.- CLI rewrite (§0.7) —
gaia emailis fully in-process (cli.pybuildsEmailTriageAgentand callsprocess_query); converting it to a daemon client is a real rewrite that must stay behaviorally identical. - Forward-OUT intake on the sidecar (§0.6) — primitive exists; the sidecar
/v1/connectionsintake route + role inversion is new wiring (low risk).
/query + vocab → CLI → OAuth
forward-out, migrating email first (§0.10) as the reference.
0.24 Security & privacy — containing a sidecar that is itself the adversary
The prior auth work (§0.11) authenticates the sidecar↔host channels well, but the plan had no story for containing a hostile agent — critical the moment one-click, third-party hub agents (§0.5) are installable. A security review surfaced these. The first three are one coupled trust-root + containment decision; settle before the first third-party agent ships (🔒 @kovtcharov-amd).- Sign the lock/catalog — SHA-256 alone is not authentication (🔒). The hash
proves a binary matches the lock; it says nothing about who wrote the lock. A
controlled catalog/
baseUrl/CDN can serve attacker code + a matching hash, or a known-vuln older version. Sign the lock/catalog with an AMD key and verify the signature before trusting any hash inside it; add anti-rollback (record highest-installed version, refuse silent downgrade); third-party publishers get a publisher signature + TOFU pin. - Tier-gate + least-privilege the OAuth forward (🔒). §0.5 one-click + §0.6 forward means a third-party agent can be handed a live mailbox token. Nothing consults the Phase-D trust tiers (Verified/Community/Experimental) before forwarding, and the forward hands the whole grant. Make forwarding + shared scopes tier-aware, require an explicit install consent naming the exact OAuth scopes forwarded, and forward the minimum scope the agent declares.
- Constrain sidecar network egress (🔒 the decisive containment gap). A hostile
sidecar holds a mailbox token and, as an ordinary process, unrestricted outbound
network — it can read mail through the sanctioned connection and POST it anywhere,
invisible to the callback-authorization model (which guards host custody, not the
agent’s own sockets). This is the difference between “sandboxed agent” and
“trusted arbitrary code with your mailbox,” and it negates “100% local.”
No-network-by-default + a declared, install-surfaced egress allowlist (the
manifest names hosts, e.g.
googleapis.com) enforced via a host-controlled network namespace/proxy. Carve-out (do not sever the control channel): the daemon’s own loopback endpoints — the §0.11 callback API, the §0.12 broker, and the reverse-proxy — are an always-allowed control channel, separate from the external-host egress allowlist. The proxy enforcement variant satisfies this automatically (it is the host); a network-namespace variant must explicitly plumb the daemon socket into the namespace, or it would cut the very callback/broker paths the custody model depends on. - Encrypt data at rest — but separate the two secret classes (don’t break §0.11).
0600stops other users, not the threats that matter on a single-user desktop (stolen laptop, synced backup, same-user malware). Two distinct classes:- Durable custody secrets — OAuth refresh tokens (
grants.json) and the custody stores (memory/RAG/transcripts) → OS keychain (Keychain/DPAPI/ libsecret) + encrypt at rest. Pull Phase-C’s “encrypted credential vault” forward to whenever §0.6 lands, not v0.23. These are the high-value, long-lived secrets a stolen disk exposes. - The §0.11 client-auth token (
instance.json) — must stay client-readable (UI/CLI read it to call the daemon), so it legitimately remains0600ininstance.json: it is ephemeral, re-minted every daemon startup, so losing it is harmless. Do not move it to the keychain — that would break the read path §0.11 relies on. (The daemon↔sidecar per-spawn secret is likewise ephemeral; keychain is for durable secrets only.)
- Durable custody secrets — OAuth refresh tokens (
- Make the audit log tamper-evident. §0.19 appends actions to a host sink, but nothing enforces append-only — a compromised process can rewrite/truncate it, defeating the observability promise. Hash-chain / rolling-MAC each entry (sealing the prior), restrictive perms, write-only exposure to agents, gap/rewrite detection surfaced in the dashboard.
- Cross-agent prompt-injection taint (name it in §0.18 now). The email agent
hardens its own body-as-data, but the cross-agent path (orchestrator, “summarize
this and email it”) carries untrusted mailbox content from agent A into agent B’s
/query, where it can drive B’s destructive action — compounded by §0.4’s re-plan divergence. Taint must travel with the data across the boundary (mark cross-agent-sourced context untrusted so the receiver treats it as data), with the confirmation gate as backstop. A hard requirement for the orchestrator, not v1. - MCP auto-install must not reintroduce the supply-chain hole. §0.11 names “a
malicious
npm postinstall” as a threat, yet §13.1 proposes auto-installingmcp-server-*from npm/GitHub on demand — unpinned/unsigned npm runs arbitrary postinstall code. Restrict auto-discovery to a curated, version-pinned, integrity-checked allowlist; never auto-execute an unpinned package; disable lifecycle scripts; gate installs behind the same signature/tier model as agents. - Signed updates + re-consent. §0.5 specs install/uninstall but not update. Silent catalog auto-update is the classic vector (push malicious “update” to an agent already granted mailbox + memory scope). Make update a first-class, signature-verified op (reusing the signed lock), with anti-rollback and re-consent when the new version widens declared scopes/egress.
- Telemetry vs “100% local.” Any phone-home (incl. Phase-D “cost savings telemetry”) punctures the privacy promise and rides the same egress as above. Local-only aggregation by default; any network telemetry is explicit opt-in with a visible disclosure. State the default here.
0.25 The daemon’s OWN lifecycle — birth, death, control, update
§0 specs the daemon’s responsibilities (custody, broker, supervision, clock) and its singleton identity (§0.14) but treats it as a process that simply exists. It never says how it starts, recovers, is controlled, or updates — the single highest-leverage hole, because a daemon that can silently die strands the scheduler clock (§0.22), the broker (§0.12), and every sidecar, undercutting the “always-on agent” headline.- Birth + rebirth (fixes the §0.0↔§0.22 contradiction). §0.0 says the daemon “auto-starts on the first UI/CLI call,” but §0.22 makes it the always-on cron clock — after an overnight reboot with no human present, nothing starts it and the 8am brief never fires. Register the daemon with the OS process manager (launchd/LaunchAgent, Windows Scheduled Task/service, systemd user unit) so it starts at login/boot and restarts on crash — a supervisor-of-the-supervisor. The Phase-C system-tray app is the natural long-term home, but it lands two phases after the daemon ships, so name the interim manager. On restart, define whether pinned-resident sidecars (§0.22) are respawned or left to lazy-spawn.
- Stale
instance.jsonrecovery. On SIGKILL/OOM/power-loss the §0.14 lock file is left pointing at a dead pid / freed port; the next client either hangs or attaches to an unrelated process now on that port. On attach, liveness-check the pid and probe the port + auth token before trusting the file; if dead, atomically reclaim it. Write it temp-then-rename so a crash mid-write can’t corrupt it. - Control surface + cross-tier diagnostics. Add
gaia daemon status|stop|restart| logs— an always-on process needs a way to see/stop/recover it (gaia killis too blunt; it also kills the clock). Give each sidecar a log file under its state dir, stamprun_id(§0.1) into every daemon/sidecar/relay log line, and extendgaia diagnosticsto gather daemon + all sidecar logs correlated byrun_id— so a run spanning UI→daemon→sidecar→Lemonade is reconstructable and third-party-agent bug reports are actionable, not “it froze.” - Daemon↔client version skew on update. §0.15 negotiates the agent contract; the UI/CLI↔daemon boundary is uncovered. An app update replaces the CLI/UI while the old daemon keeps running, so the new client attaches to a stale host API. Version the host API; on attach, mismatch the client can’t speak → drain in-flight runs and cleanly restart the daemon into the new binary. Decide where the daemon binary ships (core wheel vs hub) so “update the daemon” has an owner.
0.26 On-disk state layout & update survival
The~/.gaia layout is specified piecemeal (host/instance.json §0.14, agents/<id>/
§0.5, custody stores + schema version §0.10, keychain secrets §0.24) with no single
map — and daemon config (pinned agents §0.22, broker priorities §0.12, egress
allowlists §0.24, reaper timeout + live-cap §0.13) has no assigned home. Add one
layout table classifying every path as runtime-ephemeral (rebuildable —
instance.json, caches, spawned-sidecar records), durable user data (must be
backed up — custody: OAuth/memory/RAG/transcripts/audit), or config
(daemon settings + per-agent settings), and state the update-survival guarantee:
custody + config survive an app update; ephemeral is rebuilt. One source of truth
prevents an update from silently orphaning grants, memory, or pinned-agent config.
0.27 Relationship to sibling plans (supersedes / depends / reconcile)
v2 is a fundamental architecture change, so several sibling plan docs — which still describe the in-process model — must be superseded or reconciled, or a reader following the wrong one builds the wrong thing. Supersedes (the sibling doc is now wrong for out-of-process agents):connectors.mdx+email-sidecar-agent-ui.md— “the sidecar readsgrants.jsondirectly” is superseded by §0.6. Those docs bundlegaia.connectorsinto the freeze so the binary can read the grant + keyring; v2 inverts this — the host owns consent+refresh and forwards a short-lived access token to the sidecar via/v1/connections; sidecars never touch the refresh token or the ledger. A sidecar readinggrants.jsonis exactly the cross-process-writer race §0.6 removes. (Most material — it changes what the email cutover builds.)security-model.mdx“localhost-only is the trust boundary; CLI+UI are TRUSTED” is superseded by §0.11/§0.24. v2 states loopback is not an auth boundary and mandates the three auth legs + per-agent authorization + egress containment. An implementer reading onlysecurity-model.mdxwould wrongly conclude localhost binding suffices.email-sidecar-agent-ui.mddecision 4 (fixed-call forwarding only, no/query) is superseded — v2 makes/query(SSE) the primary UI chat surface (§0.10 step 1). PR #1910’sEmailProxyAgentis already flagged superseded (§0 header); the standalone doc needs a top banner pointing here.email-sidecar-agent-ui-implementation.md“the UI backend spawns/owns the sidecar;EmailSidecarManagerlives insrc/gaia/ui/” is the interim (email-first, §0.10 step 1) shape, not the v2 end state. In v2 the supervisor relocates into the headless daemon (§0.0/§0.3/§0.14); the UI and CLI only attach.
autonomy-engine.mdx— the v2 host daemon IS the Autonomy Engine’s always-on background service. Both independently define an always-on process with a cron clock; they are the same process (recommended). The shipped in-UI/api/schedulesrouter (schedules.py, #550) moves per §0.9: the clock + trigger registry are host/daemon, the job executes in the owning sidecar spawned at fire time (§0.22). Self-scheduling writes to the host clock, never sidecar-local state.- MCP server ownership (
connectors.mdxmirrors servers for an in-processMCPClient). v2 split: the host owns the MCP server registry/config (user-configured, shared — §0.9); a sidecar spawns/manages its own client connections to the servers its agent needs. The host does not proxy MCP traffic; it owns the config the sidecar reads (per-agent scoped, §0.11). setup-wizard.mdxmodel download vs §0.5 per-agent install-time provisioning. Two model-download triggers exist (wizard first-run, profile-level; §0.5 per-agent at install). Decide ownership + sequencing: the wizard handles the base profile model at first run; §0.5 handles additional per-agent models at install, both pulling through the host broker (§0.12); daemon auto-start (§0.25) must compose with wizard first-run detection so they don’t both fight oversetup-state.json.
agent-ui-hub-publish.mdxPart 2 “in-app Agent Hub + dynamic install” IS §0.5. Same work — cross-link them. Its multi-component model also resolves “is the UI an agent?” cleanly: the UI is anapp, not an installable agent. Two requirements v2 layers on top that the hub docs must carry: install provisions the model, not just the binary (§0.5 cold-start), and sign the lock/catalog + anti-rollback (§0.24) — the hub docs specify SHA-256 + tiers, but SHA is integrity, not authenticity.
0.28 The agent manifest — the load-bearing artifact (was undefined)
~8 sections say “the manifest/lock declares X,” but no section defined it — and the artifact that exists today (hub/agents/email/npm/binaries.lock.json) is a
binary-integrity map only (schemaVersion, agentVersion, baseUrl, per-platform
{filename, executable, sha256, size}) — it carries none of the policy/capability
fields §0 leans on. So the plan conflated two different artifacts:
- The lock = binary integrity (exists). SHA/signature-verified (§0.24).
- The manifest = capability + policy declaration (new; define it). It drives
authorization decisions (egress, scopes, tier), so it must ride inside the
§0.24 signature envelope — the recommended shape is a separate
manifest.jsonreferenced by digest from the signed lock, so its security fields can’t be swapped independently of the binary. A manifest outside the signed envelope is forgeable and worthless for authorization.
Authoring + validation. The publisher checks
manifest.json into the hub package
next to the lock. The install-time validator fails loud on: unknown
contractMajor, missing requiredModels, a capability with no matching renderType/
endpoint, or egress/scope fields absent when trustTier != Verified. This one artifact
unblocks §0.5, §0.12, §0.15, §0.18, §0.20, and §0.24 at once.
0.29 Custody store consistency model (concurrent daemon + N sidecars)
§0.9 moves memory/RAG/sessions/transcript/audit into host custody, read/written by the daemon and N sidecars — but onlygrants.json got a single-writer rule (§0.6). The
same rule must generalize, and one case is a hard correctness requirement:
- Single physical writer. All custody writes go through the daemon; sidecars
request writes via
/host/v1/*, they never touch the files. This extends §0.6’s single-writer rationale to memory, RAG, sessions, and audit — so N sidecars writing “shared user-memory rows” (§0.20) still resolve to one physical writer. - Serialized audit appends (hard requirement of §0.24’s hash-chain). §0.24’s
tamper-evident chain needs a strict total order — “each entry seals the prior.”
N sidecars
POST /host/v1/auditconcurrently would both seal entry k and fork the chain, which the gap-detector reads as tampering. The daemon must serialize appends behind a single append queue, or the tamper-evidence guarantee is undefined. - Storage engine. The current UI stores are SQLite (single-writer;
SQLITE_BUSYunder concurrent writers). Specify WAL +busy_timeout(or a host-side write queue) and RAG read-during-index isolation, or “host stores it, sidecars query it” hits lock contention the first time two agents write.
0.30 Identifier catalog
run_id is the model (host-minted §0.1; cancel target §0.13; confirm key §0.4;
log-correlation stamp §0.25). The rest need the same rigor — minter / scope / lifetime /
consumer:
session id— host-minted, host-owned (§0.9); it is the authorization key the callback verifies (/host/v1/sessions/{id}, §0.11), so leaving its mint point + per-client-vs-per-session semantics undefined (the open §0.9 focus decision) leaves a security check keyed off an undefined id. Resolve with §0.9.action_id— mint on every consequential action; it’s the handle the §0.19 audit entry and §0.20 uninstall-scoping (“withdraw/revoke this action”) both need, and it appears nowhere in §0 today. Stamp it into the audit record.batch_id— batch-archive/undo handle (email spec); define its mint + lifetime.- per-spawn secret vs client-auth token — already cleanly separated (§0.11/§0.24); no gap.
0.31 The /host/v1/* callback API (the reverse contract, specified)
§0.11/§0.9/§0.19/§0.29 lean on the daemon’s reverse contract but never list it. Like the
manifest (§0.28), it’s load-bearing and must be pinned. Every route requires the
per-spawn secret and resolves the calling agent id from it (§0.11 authorization);
every response is scoped to that agent (or its granted shared scope).
Versioning: this API carries its own MAJOR, negotiated on the sidecar↔daemon leg and
subject to the §0.15 evolution rules (new-daemon/old-sidecar skew). Errors: every
route fails loud with an actionable, typed error (
403 unauthorized/wrong-scope, 409
audit-chain conflict, 429 no model slot, 503 store unavailable) — never a silent
empty result, per the no-silent-fallback rule. The daemon’s client-facing API
(UI/CLI → daemon: sessions list, agent install/uninstall, /query proxy) is a separate
surface under the client-auth token (§0.11), versioned per §0.25.
0.32 Multi-agent orchestration (the headline “complex workflow automation”)
The stated goal is workflow automation that “reasons and calls the necessary tools,” including cross-agent (“summarize this doc and email it to Bob”). §0.18 rightly ships a v1 explicit agent picker (one sidecar per turn), but the cross-agent case needs a design, or the headline capability has no home. Shape it now even if it lands after v1:- An orchestrator is itself an agent (a sidecar), not host logic. Keeping the host
thin (§0.0), the orchestrator is a first-class agent whose
/queryplans a multi-step, multi-agent workflow and invokes other agents. The host stays the router/custodian; the reasoning lives in the orchestrator sidecar. - Agent-to-agent calls go through the host, never sidecar-to-sidecar directly.
The host mediates so every hop is authorized (§0.11 per-agent scope), audited
(§0.19
action_id), broker-leased (§0.12), and taint-tracked — a direct sidecar→sidecar mesh would bypass all four. Add a host route (POST /host/v1/agents/{id}/invoke, client-auth + orchestrator-scoped) so the orchestrator reaches sub-agents under the same controls as any client. - Taint + confirmation are the safety backbone (§0.24 / §0.4). Untrusted content read by agent A (a mailbox body) that becomes agent B’s input is marked tainted so B treats it as data, not instructions (§0.24 cross-agent injection), and every destructive cross-agent step keeps the confirmation gate (§0.4 approve-what-you-saw) — the orchestrator cannot launder an injected instruction into an un-approved send.
- Cost is real and must be legible. A cross-agent workflow is N agent loops + M
model switches on one slot (§0.12) — the orchestrator’s own loop plus each sub-agent’s.
Surface it as streamed
status(which agent is working) and hold interactive priority so a foreground orchestration preempts background jobs (§0.12). - Sequencing: v1 = explicit picker (§0.18); the orchestrator is a later, first-class agent built on the same contract — not a special host mode. This keeps “the UI is thin, the sidecar reasons” intact even for multi-agent flows.
- Delegated (host present): full A2A as above —
invokethrough the host, which authorizes + audits + leases the slot + carries taint on every hop. - Embedded, single agent: no A2A — one agent, no peers, no coordinator needed.
- Embedded, multi-agent (a vendor runs several sidecars): needs a coordinator — run
the GAIA host or implement its
invoke+custody+broker interfaces. Direct sidecar→sidecar is possible but loses the guarantees (taint doesn’t travel → §0.24 injection risk; no unified audit; the two agents contend for the model slot → the §0.12 eviction bug). Fine for trusted simple chaining; unsafe as a general pattern. - Ephemeral: A2A is caller-orchestrated call-chaining — the caller invokes A, passes its output into B’s request; nothing shared or persisted; the caller owns all coordination.
0.33 The GAIA REST API agent server exposes /query too (src/gaia/api/)
/query is not UI-specific — it belongs on every agent-serving REST surface. GAIA’s
OpenAI-compatible API server (src/gaia/api/openai_server.py, gaia api) is a
first-class front-door for programmatic agent access; today it serves
POST /v1/chat/completions (agents-as-models), GET /v1/models, and an in-process
/v1/email/* mount. It must also expose the agentic /query loop.
- Add
POST /v1/<agent>/query(SSE) to the API server, the same contract (§0.1/§0.2) — so a REST consumer gets tool-calling, multi-step workflows, and the typed event stream, not only OpenAI-style chat. One agent-loop implementation: the API server proxies to the sidecar (via the daemon/broker, §0.3/§0.12), exactly as the UI and CLI do — it does not run its own in-process loop. - Relationship to
/v1/chat/completions. Keep both: chat-completions is the OpenAI-SDK-compatible surface (drop-in for existing tooling);/queryis the richer agentic superset (SSE tool events +needs_confirmation+ workflows). Consumers choose by need; neither is removed. - Supersedes the API server’s in-process email mount.
openai_server.py:143mountsgaia_agent_email.api_routesin-process — the same in-process pattern the UI cutover removed (this surface was explicitly left out of PR #1910’s scope). Under v2 it likewise becomes sidecar-backed (proxy to the email sidecar), so core stays lightweight and there is one email implementation. - Auth is stricter here than the loopback daemon (🔒).
gaia apiis a network-exposed surface (and reachable remotely via the tunnel), unlike the daemon’s loopback custody API — so/queryon it must sit behind the API server’s API-key auth and still enforce the §0.4 confirmation gate + §0.24 containment on every destructive step. Do not inherit the loopback trust assumptions of §0.11 here.
0.34 Autonomy readiness — infrastructure-ready, policy-hostile
GAIA’s roadmap is “always-on background agent” (Phase C), so assess v2 against full autonomy (the agent acting unattended, on its own initiative). The verdict: the architecture is a strong foundation but not yet a fit for full autonomy, because its safety + interaction model is human-in-the-loop by construction. Already fits (the substrate is right): the always-on daemon (§0.0/§0.25) + scheduler clock (§0.22); tamper-evident audit for unattended review (§0.19/§0.24); persistent memory (§0.9); interactive > background broker priority so autonomous jobs yield to the user (§0.12); and containment (egress/least-privilege/taint, §0.24) — the exact guardrails high-stakes unattended action needs. Fights full autonomy (all assume a human is present) — the autonomy layer to design:- Confirmation assumes a watcher. §0.4’s “approve-what-you-saw” pauses the SSE stream for synchronous approval — but unattended there’s nobody to approve. Full autonomy needs a policy / pre-authorization model: the user pre-grants categories of action (e.g. “auto-archive promotions,” “auto-decline conflicting invites”) with undo + audit, replacing per-action approval when unattended. This is the central missing piece.
- Cron-only triggering (§0.22). Autonomy is largely event-driven (“urgent mail arrived → act”); needs a trigger/event bus + mailbox-watch, not just a clock.
- No long-lived goals / self-initiation. The contract is request →
/query→ done; autonomy means the agent initiates from standing objectives it tracks over time (goals.pyis noted moving to a sidecar, §0.9, but autonomous goal-pursuit is undesigned). - No async escalation. An unattended agent that hits uncertainty should notify and resume later when the user answers — but §0.4 confirmation is synchronous. Needs a notify-and-resume path (push notification → deferred approval → continue).
- No autonomy levels. There are third-party trust tiers (§0.24) but no graduated autonomy (observe → suggest → act-with-undo → act-freely) governing what an agent may do unattended, per capability.
- Ephemeral vs. continuous. §0.13 reaps idle sidecars; a monitoring agent doesn’t fit “spawn-at-fire, reap-after.” Pinned-resident (§0.22) is the exception, not a first-class monitoring model.
/query/fixed-function calls
on the user’s behalf under pre-authorization, reusing audit (§0.19) + containment (§0.24)
- broker priority (§0.12) as its guardrails. It also reconciles with
autonomy-engine.mdx(§0.27): that engine is this layer, hosted in the daemon. Sequence it after the human-in-the-loop v1 — the v1 confirmation model is the safe default, and the autonomy layer is what lets the user progressively hand off.
0.35 Structural refinements (from the architecture review)
A holistic architecture review found the design structurally sound — the daemon is a coherent custodian (everything it owns needs a single writer, a single arbiter, or is the trust root — one responsibility, not ten; and §0.9’s rejection of dedicated memory/RAG sidecars is correct because decomposing would reintroduce the multi-writer problem the design exists to avoid), the sidecar↔host coupling is a versioned runtime cycle, not a build cycle, and the deferrals are honest. The remaining work is framing + trimming premature hardening. The refinements (priority order):- Sidecar independence is a two-tier contract, not a flat claim. The sidecar owns
no durable state (§0.9), so custody-backed
/queryneeds a host. Make it honest: publish/host/v1/*(§0.31) as a first-class, third-party-implementable custody interface (a third party can bring their own custodian), and formalize the bare-integrator degraded tier (§0.6) as a shipped, tested product with a capability matrix — works without a host: fixed-function + stateless/query; needs a host: memory, RAG, sessions/transcript, audit. Converts the biggest hidden coupling into an explicit contract tier. (Reflected in the mdx overview.) - Naming: never call the daemon “thin.” “The host” = the custodian daemon, the most responsibility-dense process; “thin” is the UI only. (Applied across the docs.)
- Assert the daemon’s internal module seams. One process is right; one module would rot into a monolith. Commit — even inside the process — to separated custody-store / broker / supervisor / proxy-router / clock modules with defined interfaces. Mark the broker (§0.12) as the one designed to be later-extractable to its own process: it arbitrates a hardware resource contended by non-agents (host RAG embedder, voice, SD), a different axis + lifecycle from data custody.
- Slogan: “one agent contract, many front-doors; two control-plane contracts behind it” (the callback §0.31 and the client↔daemon §0.11/§0.25) — so B/C get first-class versioning/auth, not footnote status. (Reflected in the mdx.)
- Trim three pieces of premature hardening to “third-party gate, not v1 build”
(all additive later; acceptable under the first-party single-user threat model):
- Audit: v1 = a plain append-only host-owned log (the single writer already gives order); the §0.24 hash-chain + §0.29 global single-append-queue are the third-party addition, not day-one.
- Egress: v1 = the proxy-enforced allowlist (§0.24); the full network- namespace sandbox is research-grade/per-OS — defer.
- Broker: v1 = priority queueing (foreground jumps the queue); defer mid-run preemption (§0.12) — you can’t cleanly pause a llama-server generation.
- Make the render boundary real (layering fix). The “thin UI” has a hidden compile-time dependency on first-party agents’ card set (§0.2/§0.15). Ship a small fixed set of generic render primitives (table / key-value / list / image / diff) once; agents default to them, a bespoke component is the first-party exception — so “the thin UI renders any agent” is true rather than an ever-growing first-party component map.
0.36 Third-party integration ergonomics (a first-class design goal)
“Agents as products” only works if a third-party vendor can digest a sidecar easily. The design is built for this, but ease is tiered, and the plan should treat that as a goal to hit, not an accident. Already digestible (keep): it’s a language-agnostic HTTP service (no Python import, no running our UI); it speaks familiar protocols — fixed-function endpoints for “just do X” and OpenAI-compatible/v1/chat/completions (§0.33) so a vendor
aims their existing OpenAI SDK at it; it ships as a self-contained frozen binary + npm
package via the Hub, each agent carrying README/SPEC/SKILL.md/specification.html
/OpenAPI; and the contract is versioned + negotiated (§0.15).
The two frictions that block “drop-in” for the full agent — design to remove them:
- Custody (the §0.35 #1 seam). The sidecar owns no durable state (§0.9), so the
rich agentic experience (memory/RAG/audit-backed
/query) needs a host. Make the tier a product: publish/host/v1/*(§0.31) as a first-class, third-party- implementable interface and ship the bare-integrator tier (§0.6) as a tested product with a published capability matrix (below). Then “run our custodian,” “bring your own custodian,” and “degraded standalone” are three documented choices, not a cliff. - The LLM backend. The sidecar needs a reachable model backend (Lemonade), and some
agents (email) force local-only by policy — so the artifact is “agent + LLM backend,”
not one binary. Make the backend an explicit, documented dependency with a
configurable
base_url, and state per-agent whether a cloud/remote backend is allowed (email = local-only by design; others may differ). A vendor must know “you also provide the model host” up front.
Treating this matrix as a shipped contract (not implicit) is what makes the sidecars
genuinely easy for vendors to adopt — they pick a tier with eyes open instead of hitting
the custody/LLM frictions by surprise.
0.37 Pluggable custody — one sidecar, both experiences (rich and stateless)
§0.36’s tiers can collapse from a cliff into a config switch, resolving the §0.35 #1 risk at its root. The key realization: §0.9 put custody in the host because N sidecars sharing one user’s data need a single writer — but that constraint exists only in the multi-agent Agent UI. A standalone single agent is trivially its own single writer. So custody is not intrinsically “the host”; it’s a pluggable provider, and host-custody is one deployment’s chosen backend. Design (ports & adapters). The sidecar depends on aCustodyProvider interface
(memory / RAG / sessions / audit / grant-read — the same surface as /host/v1/*, §0.31)
and ships three adapters, auto-selected at launch:
The sidecar calls the interface and doesn’t know which backend answers (auto: host
endpoint present → delegate; else embed). Same code path; the backend swaps by config.
Pros / cons per provider:
- Embedded (self-custody).
- Pros: fully self-contained rich agent — one binary, no external custody dep; trivially its own single writer (no coordination); works offline/air-gapped; simplest deployment; ideal for a third-party vendor who wants “rich, standalone.”
- Cons: state is siloed per agent — no shared cross-agent user memory (each agent has its own), so a multi-agent setup fragments the user’s memory + duplicates shared data (e.g. grants); the sidecar owns store lifecycle (schema, migration, backup, encryption); audit is local + per-agent (uninstall can lose it).
- Delegated (host custody).
- Pros: one coherent user — memory/RAG/audit unified across every agent; one place to encrypt-at-rest, back up, govern, and enforce uninstall lifecycle; the sidecar stays truly light (no store in the binary); enables cross-agent orchestration over shared memory (§0.32).
- Cons: requires a running host (the coupling — custody-backed features don’t work
standalone); a network hop + auth on every memory/RAG call; the host is a single point
of failure for all agents’ custody; carries the full cross-process contract cost
(
/host/v1/*+ auth legs + versioning).
- Ephemeral (stateless).
- Pros: nothing on disk — simplest, most portable, most secure at rest; horizontally scalable (any instance serves any request); no migration/backup/encryption concerns; easiest to test; ideal for one-shot/scriptable/serverless integration.
- Cons: no memory or learning — no personalization, follow-up tracking, or cross-session context; the caller must pass all context every request (bigger payloads, caller owns state); no persisted audit trail; it’s a function, not an assistant.
- One binary does both: a vendor gets a rich, self-contained agent or plugs in their own custody host; GAIA gets shared multi-agent custody. §0.36’s “high-effort” tier becomes low-effort (embedded is the default).
- Footprint stays light: embedded custody is storage (SQLite + index files), not ML libs — the embedder compute still runs through the external model backend, so §0.21 is untouched.
/host/v1/*is now doubly justified: it’s both the delegated-mode wire protocol and the third-party-implementable interface (§0.35 #1) — the embedded adapter is simply GAIA’s own reference implementation of it.- Cost: mode migration (embedded→hosted / export) reuses the §0.10 machinery; the embedded and delegated adapters must pass the same conformance tests (§0.17) so behavior is identical across backends.
0.38 Practical memory / resource footprint
The sidecar-first + pluggable-custody design has real RAM implications on a Ryzen laptop; two effects dominate, and the mode choice is a memory decision as much as a feature one.- The model dominates; the broker (§0.12) is the primary memory governor. A 4B GGUF is ~2–4 GB resident. The broker’s single model slot keeps ~one model resident regardless of agent count — without it, N agents wanting models = N models = OOM. So §0.12 is a memory-protection mechanism, not only a correctness one.
- Per-sidecar overhead (~50–150 MB RSS each) is bounded by reaping. N installed agents ≠ N resident processes — lazy-spawn + the idle-reaper + the live-sidecar cap (§0.13) keep only active agents in memory. Second governor.
- Custody mode changes the multiplier: embedded duplicates the store and the RAG index in RAM per agent (heaviest multi-agent); delegated holds shared custody + one RAG index in the host (leanest multi-agent; host RSS grows instead); ephemeral holds no store (lightest). The RAG/FAISS index (tens–hundreds of MB in RAM per query) is the sneaky duplicator — embedded = N copies, delegated = 1.
- Model switching is churn, not resident bloat — different-model agents evict+reload (~GB I/O + a stall), serialized by the broker; hot-model affinity (§0.12) reduces thrash.
- Steady state ≈ one model + the daemon + the active sidecars + (Electron UI ~300 MB, only when open) — not “sum of everything installed.”
0.39 Custody modes at a glance — consolidated comparison
One table across all three sidecar custody modes (§0.37), pulling together capabilities, A2A (§0.32), memory (§0.38), and pros/cons.
Selection: auto — host custody endpoint injected → Delegated; else → Embedded; Ephemeral by explicit flag. Same sidecar binary, same code path (§0.37); the invariant (single writer, never multi-writer) holds in all three.
0.40 Hybrid custody — per-store provider (the embedded↔delegated spectrum)
Embedded and delegated are not two points but the endpoints of a spectrum. Custody is not one blob — it’s several stores — so §0.37’s provider choice generalizes to a per-store choice: a compositeCustodyProvider where each store is independently
embedded or delegated.
A “store” = one named, independently-addressable custody data-domain — a coherent
slice of user state with its own data kind, its own interface (a specific set of
/host/v1/* routes / CustodyProvider methods), its own persistence + lifecycle, and its
own natural sharing scope. The custody stores:
Boundary test (what is a store): a data domain is its own store when it has (a) a
distinct single-writer requirement (it needs one clear owner) and (b) a coherent,
independently-swappable interface. State that always shares a lifecycle + writer is one
store (a transcript and its session-index entry travel together → one
sessions store) —
this keeps the list at ~5 stores, not per-field granularity. “Per-store” = each store
independently picks embedded or delegated.
Governing rule (preserves §0.9’s invariant, now per-store): the sharing scope of a
store decides its provider — a store shared across agents ⇒ delegated (host is its
single writer); an agent-private store ⇒ embedded (the agent is its single writer).
Never a shared store with multiple writers; the invariant just applies per store.
High-value configurations:
Why it’s better, not just more knobs: fine-grained sharing (share identity/coherent
memory, isolate per-agent memory/corpus — the “shared auth + private data” split that SSO
- local app state uses); and weaker coupling / graceful degradation — an agent needs the host only for its delegated stores, so a host outage degrades shared features while embedded ones keep working (partial-host becomes a real, tolerable state).
0.41 Separation of concerns — one clean authorization plane, not one god-grant
An adversarial stress-test (+ prior-art research, §0.42) showed an earlier draft here overreached: it relabeled five orthogonal mechanisms as “one grant.” That was a vocabulary, not an architecture — and it even contradicted §0.34 and §0.24. The simpler and correct model is clean separation of concerns: orthogonal single-purpose planes beat one kernel pretending to do five jobs. (Simplicity = orthogonality, not collapse.) What genuinely unifies (keep this): the capability grant is one clean thing — the authorization / isolation plane: which agent may touch which store (§0.40 scope), connector (§0.6), or peer (§0.32 routing). §0.11 per-agent authz + §0.40 per-store scope- §0.32 A2A-routing really are one primitive (a scoped, host-checked grant). Keep them one.
Consequences of getting the boundary right (all corrections to the overreach):
- The grant checks the tool boundary, not the reasoning. In an LLM loop the tools are the capabilities but the model chooses which to call over tainted context — a static “may A call tool T” cannot judge “should this call, with these args, happen.” That judgment is the confirmation gate + treat-input-as-data (the confused-deputy answer), not the grant. Don’t claim the grant secures the reasoning.
- Revocation (simple, concrete). Grants are mutable host state keyed by agent-id; the mediator re-reads the grant per mediated call (cheap, in-memory) — revoke = update the grant. An in-flight autonomous batch re-checks per op (already mediated); revoking A stops further A2A invokes while B’s in-flight call completes under B’s own grant. (No new machinery — it’s a lookup the mediator already does.)
- Embedded has NO mediation plane — say so. Embedded single-agent is a trust boundary of one — a storage adapter (§0.37), not a policy-enforcement point. The mediation plane exists only where multiple mutually-distrusting agents share a host (delegated). Drop the “embedded = a minimal host that enforces” claim (it would ship a stub PEP that enforces nothing). So the modes are not “one architecture, varying host” — they’re one contract, with a real PEP only in the multi-agent (delegated) case.
- Audit actions, not reads. §0.24/§0.29’s serialized hash-chain covers consequential actions (low-frequency, tamper-evident) — do not put a machine-wide append-lock on every store read. Audit is host-if-present, not part of the per-store hybrid (§0.40) freedom.
- Taint is a defense-in-depth hint, not a guarantee — it dies through LLM summarization (§0.42) and is non-durable across a daemon restart. The real backstops are isolation + egress + confirmation + audit. Treat cross-agent/untrusted input as data regardless of taint.
0.42 Prior art & learnings (OpenClaw · Hermes · MCP/A2A)
Benchmarking v2 against the field both validates the shape and yields concrete adopt-items. Convergent validation (the shape is sound):- Coordinator + one agent core. OpenClaw’s Gateway (one port multiplexing sessions,
tool dispatch, routing, orchestration) and Hermes’s single
AIAgentcore reused by CLI / gateway / ACP server / cron are the same pattern as our custodian daemon + one-contract-many-front-doors (§0.33). Independent convergence → sound. - OpenAI-compatible server (Hermes exposes
/v1/chat/completions) = our §0.33. - Shared-or-scoped skills (OpenClaw scopes skills global↔per-agent) = our shared/private custody axis (§0.40).
- Persistent memory is table-stakes — Hermes (“grows with you,” a GraphRAG “second brain”) validates our custody/memory direction; consider GraphRAG for RAG.
- Align with the MCP + A2A two-layer standard — don’t invent. MCP (Anthropic’s
tool layer; ~97M monthly SDK downloads; adopted by Anthropic/OpenAI/Google/Microsoft/
Amazon) is the vertical tool protocol → expose our fixed-function endpoints as MCP
tools (the email agent already ships
mcp_server.py— make it first-class). Google’s A2A protocol is the horizontal agent-coordination layer → align §0.32 A2A with A2A Agent Cards (capability discovery), signed payloads, and delegation control instead of a proprietary invoke route. Payoff: any MCP/A2A client consumes our agents out of the box → §0.36 third-party digestibility jumps. - Process isolation is our security advantage — lean in. OpenClaw’s documented core flaw — “a single compromised skill inherits ALL permissions” (plus multiple arXiv injection/trojan papers) — is a shared-runtime problem. Our per-agent process- isolated sidecars + per-agent grants contain a compromised agent to its own grant. Strictly better isolation; position it as a differentiator.
- Use a permission cascade to blunt consent fatigue. OpenClaw’s global→provider→ agent→session→sandbox cascade sets sensible defaults at each level so the user consents only to deviations. Adopt trust-tier defaults → user overrides (§0.24), far better than per-capability prompts.
- Nobody ships formal information-flow control — this confirms the §0.41 correction.
MCP’s own security guidance is “read-only for untrusted agents, scoped permissions,
network restrictions” — i.e. the same pragmatic stack (isolation + scopes + egress
- human approval + audit) we landed on. Don’t build formal IFC.
- A2A protocol’s “prevents unauthorized delegation” is the confused-deputy answer — adopt its delegation-control + payload signing for our A2A hops.
1. Current GAIA SDK Capability Inventory
1.1 Agents
1.2 ChatAgent Tools (Current — What the Agent UI Uses)
1.3 CodeAgent Tools (Available in SDK, NOT in Agent UI)
1.4 Other SDK Capabilities (Not Exposed to Any Agent)
2. Gap Analysis: Agent UI Agent vs. Modern PC Agent Expectations
2.1 Capabilities Users Expect Today
Based on the current landscape (Claude Computer Use, OpenAI Operator, Windows Copilot, etc.):2.2 What Can Be Added to ChatAgent NOW (Reusing Existing SDK)
These capabilities already exist in the codebase and just need to be wired into ChatAgent:2.3 What Requires New Development
These capabilities don’t exist anywhere in GAIA and need to be built:3. Implementation Plan
Phase 1: Quick Wins — Wire Existing SDK into ChatAgent (1-2 weeks)
ExtendChatAgent with existing mixins from CodeAgent and other agents. Minimal new code.
ChatAgent class after Phase 1:
Phase 2: Vision & Media (2-3 weeks)
Add image analysis, screenshot capture, and image generation.Phase 3: Web & System (2-3 weeks)
Add web browsing, clipboard, and basic system tools.Phase 4: Computer Use (4-6 weeks, separate milestone)
Full desktop automation. This is the most complex and security-sensitive phase.Phase 5: Audio/Voice (2-3 weeks)
Wire existing Whisper ASR and Kokoro TTS into Agent UI.4. Cross-Platform Requirements
All capabilities MUST work on Windows, Linux, and macOS:5. Security Considerations
6. Issue Tracker
Already Created (Milestone #15)
To Create (Phase 1-5)
7. MCP Server Integration
7.1 Current MCP Infrastructure in GAIA
GAIA already has a robust MCP client infrastructure:MCPClientMixin(gaia/mcp/mixin.py) — Any agent can connect to MCP servers and auto-register their toolsMCPClientManager— Manages multiple MCP server connections- Config file —
~/.gaia/mcp_servers.jsonfor persistent server configuration MCPAgentbase class —agents/base/mcp_agent.py- MCP Bridge —
gaia/mcp/mcp_bridge.pyexposes GAIA as an MCP server to external tools - Existing integrations — Docker MCP, Blender MCP already implemented
7.2 Most Popular MCP Servers (2026 Ecosystem)
Based on real usage data from MCP Directory and mcpservers.org:Tier 1 — Essential (High demand, directly useful for Agent UI)
Tier 2 — High Value (Popular integrations users commonly request)
Tier 3 — Windows Desktop Automation (Key for “Computer Use”)
Tier 4 — Microsoft Ecosystem (Enterprise)
7.3 Agent UI MCP Integration Design
A) MCP Server Manager Panel (Settings)
The Agent UI Settings modal gets an “MCP Servers” tab where users can:- Browse/add popular servers from a curated list (Tier 1-2 above)
- Add custom servers by providing command + args + env config
- Enable/disable servers per session or globally
- View connected server status (connected, tools available, errors)
- Configure server credentials (API keys, tokens) with secure storage
B) Backend API Endpoints
C) ChatAgent MCP Integration
- Calls
agent.connect_mcp_server(name, config) - Tools from the MCP server are auto-registered in the agent’s tool registry
- The agent can now use those tools in its planning/execution
- Tools appear in the Capabilities panel (#440)
D) Curated Server Catalog
Ship a built-in catalog (~/.gaia/mcp_catalog.json) with pre-configured popular servers:
7.4 MCP Issues to Create
8. Dependencies
9. Critical Capabilities Coverage Matrix
The following capabilities were identified as user-critical priorities. This matrix tracks exactly where each is addressed in the plan and flags gaps.9.1 Browser Control — Needs Priority Bump
The plan buries browser automation in Phase 4g which is 8+ weeks out. But the Playwright MCP server is a Tier 1 server that works TODAY with the existingMCPClientMixin. This should be promoted to Phase 1-MCP (first sprint):
9.2 Web Search — Needs Free Fallback
ExternalToolsMixin.search_web requires a PERPLEXITY_API_KEY environment variable.
If the user doesn’t have one, web search silently fails. The plan says “Low effort”
but doesn’t address this.
Fix: Prioritize Brave Search MCP (@anthropic/mcp-brave-search) as the
default web search. It’s free-tier capable and runs as a standard MCP server. Fall
back chain should be:
- Brave Search MCP (free, no API key for basic usage)
- Perplexity (if API key available, via ExternalToolsMixin)
- Fetch MCP (raw URL fetch + markdown conversion as last resort)
9.3 Shell Commands — Write Capability Gap
CurrentShellToolsMixin is read-only by design (whitelist: ls, cat, grep,
find, etc.). This is safe but limiting — users will want:
mkdir— create directoriescp/mv— copy/move filespip install— install packagesnpm install— install node packages
FileIOToolsMixinfor file write/create/edit (Phase 1a)CLIToolsMixinfor broader command execution (Phase 1, with guardrails)- Guardrails (#438) to confirm dangerous operations
9.4 Guardrails — The Critical Prerequisite
Issue: The plan lists guardrails (#438) as “NEXT SPRINT” but also addsFileIOToolsMixin (file write), TestingMixin (Python execution), and CLIToolsMixin
(arbitrary commands) in the same sprint. This means write/execute capabilities would
ship before the safety mechanism that protects against them.
Mandatory fix: Guardrails (#438) MUST be implemented BEFORE or simultaneously
with any write/execute capability. The implementation order should be:
9.5 Email & Calendar — New Capability Needed
Completely missing from the plan. This is a critical gap for a PC agent. Users expect to triage emails, manage calendar events, and get summaries of their day.MCP Servers Available
User Workflows (Email)
Priority
Add to Phase 1-MCP (Tier 2 servers) — these are MCP integrations, not native code. The ChatAgent just needsMCPClientMixin and the user adds the server in
Settings.
9.6 Application Control (CUA) & Popular App Demos
The plan covers low-level computer use (Phase 4: pyautogui, pywinauto) but misses the high-level application control pattern that users actually want. Users don’t say “move mouse to (432, 128) and click” — they say “play my Discover Weekly on Spotify” or “open my latest document in Word”.MCP Servers for Application Control
CUA (Computer Use Agent) Strategy
Two complementary approaches:- MCP-first (preferred): Use app-specific MCP servers for structured, reliable control. Spotify MCP is better than clicking the Spotify UI because it’s API-driven, reliable, and doesn’t break when the UI changes.
-
Vision + automation (fallback): For apps without MCP servers, use:
- Screenshot → VLM (identify UI elements) → pyautogui (click/type)
- This is Phase 4 in the plan and requires VLM + guardrails
Demo Workflows
10. Detailed Plan Critique & Issues Found
10.1 CRITICAL: FileIOToolsMixin Has Hidden Dependency
Plan says: “Add FileIOToolsMixin to ChatAgent — Low effort, just add mixin”
Reality: FileIOToolsMixin has a hard dependency on ValidationAndParsingMixin.
From src/gaia/agents/code/tools/file_io.py lines 26-31:
read_file processes a .py file (line 99), it calls self._validate_python_syntax(content)
which is defined in ValidationAndParsingMixin, not in FileIOToolsMixin. Without it,
reading ANY Python file will crash with AttributeError.
Impact: Effort is Medium, not Low. Options:
- Add
ValidationAndParsingMixinto ChatAgent (drags inCodeSymbol,ParsedCodemodels, validator classes) - Refactor
FileIOToolsMixinto make Python validation optional (try/except around_validate_python_syntax) - Create a lightweight
ChatFileIOToolsMixinthat strips out Python-specific features
10.2 CRITICAL: _TOOL_REGISTRY Is Global — Tool Count Explosion
The plan proposes adding 6+ mixins to ChatAgent. Each mixin registers tools into
_TOOL_REGISTRY which is a module-level global dict (src/gaia/agents/base/tools.py:16).
Current tool counts:
- ChatAgent: ~13 tools (12 @tool decorators across 3 files)
- CodeAgent: ~57 tools (69 @tool decorators across 12 files, minus register functions)
FileIOToolsMixin (11 tools), CLIToolsMixin (6 tools), ExternalToolsMixin
(3 tools), ProjectManagementMixin (4 tools), TestingMixin (3 tools), plus MCP
tools (variable), ChatAgent could have 40+ tools.
Problem: Every tool’s full docstring gets appended to the system prompt via
_format_tools_for_prompt() (agent.py:370-384). With 40 tools averaging 10 lines of
description each, that’s 400+ lines of tool descriptions in the system prompt.
The default context window is min_context_size: 32768 tokens (~24K words). Tool
descriptions alone could consume 15-25% of the context.
Impact:
- Reduced context for actual conversation history
- LLM confusion from too many tool choices (decision paralysis)
- Slower inference (more tokens to process)
- Lazy tool loading: Only register tools when their category is needed (e.g., don’t load Prisma tools if user isn’t doing database work)
- Tool description compression: Use 1-2 sentence descriptions in prompts, not full docstrings
- Tool categories: Group tools and let the LLM request a category expansion
- Per-session tool selection: Let users enable/disable tool categories from the UI (ties into #440 Agent Capabilities Discovery)
10.3 HIGH: ExternalToolsMixin — Silent Failure Risk
ExternalToolsMixin imports from gaia.mcp.external_services:
get_context7_service()— requiresnpxon PATH (Node.js installed)get_perplexity_service()— requiresPERPLEXITY_API_KEYenv var
10.4 MEDIUM: MCP-vs-Native Build/Buy Confusion
The plan proposes BOTH native implementations AND MCP server equivalents for the same capabilities:
The plan doesn’t resolve which to use. Having both creates:
- Duplicate tools in registry (LLM sees
read_fileANDfilesystem__read_file) - Conflicting behaviors (different error formats, different security models)
- Maintenance burden
- Native tools for core, always-available capabilities (file I/O, shell)
- MCP servers for external integrations and optional capabilities (GitHub, Spotify, email)
- MCP preferred when the MCP server is more capable (Playwright MCP > building our own)
- Never both for the same capability in the same session
10.5 MEDIUM: Missing Effort Estimates & Timeline Reality
The plan says “Phase 1: 1-2 weeks” but doesn’t account for:- Guardrails framework (#438) — design + implement + test = 1 week minimum alone
FileIOToolsMixinrefactoring (§10.1) — 2-3 days- MCP Server Manager UI — new Settings tab + API endpoints + state management = 1 week
- Testing all new tools on Windows + Linux = 3-5 days
10.6 LOW: Cross-Platform Testing Gaps
The plan’s cross-platform table (§4) lists tools but doesn’t mention:- CI/CD: No GitHub Actions matrix for Windows/Linux/macOS testing
- pyautogui on headless: Won’t work in CI without virtual display
- macOS permissions: Screenshot, accessibility, and automation all require explicit System Preferences permissions that can’t be automated
11. User Workflow Validation Tests
Every major capability should have a user workflow — a concrete end-to-end scenario that validates the capability works. These serve as acceptance criteria and demo scripts.11.1 File Operations Workflows
11.2 Web & Search Workflows
11.3 Document Analysis Workflows
11.4 Shell & System Workflows
11.5 Email & Calendar Workflows
11.6 Browser & App Control Workflows
11.7 Guardrails Validation Workflows
12. Updated MCP Server Catalog (Complete)
Adding the missing Tier 2+ servers identified in the critique:Tier 2+ — Communication & Productivity (NEW)
13. New SDK Capabilities: MCP Auto-Discovery & SKILL.md Support
13.1 MCP Server Auto-Discovery & Installation
Problem: When a user asks the agent to do something it can’t (e.g., “check my email”), the agent currently says “I can’t do that.” A modern agent should be able to find, recommend, and install the right MCP server to gain the capability. Design:
Curated Capability Map (ships with GAIA):
13.2 Anthropic SKILL.md Support for GAIA Agent SDK
What is SKILL.md? Anthropic’s specification for agents to document their learned skills — reusable procedures, workflows, and domain knowledge that persist across sessions. Skills are stored as markdown files that the agent can read, update, and reference. Why it matters: GAIA agents should be able to learn from experience. If a user teaches the agent a multi-step workflow (“here’s how to deploy our app”), that knowledge should persist and be reusable. Design for GAIA:
SKILL.md Format (Anthropic-compatible):
- Skills can reference RAG documents (“See indexed doc: architecture.pdf”)
- Skills can reference MCP servers (“Requires: gmail-mcp-server”)
- Skills can include tool sequences that the agent replays
- Skills directory is auto-indexed by RAG for semantic search
14. Summary: Recommended Priority Order (Revised, Split by Milestone)
Milestone A — Agent UI: Wire Existing SDK (Weeks 1-6)
Milestone B — GAIA Agent SDK: New Capabilities (Weeks 3-12+)
Milestone Dependency Map
FileIOToolsMixin (Milestone A, week 3) needs guardrails
(Milestone B, week 3) to be safe for write operations. These should be developed
in parallel with guardrails landing first or simultaneously.