Skip to content

feat(email): agent-led mailbox onboarding — the agent sets up its own access - #2496

Merged
kovtcharov-amd merged 54 commits into
mainfrom
claudia/task-272d716d
Jul 27, 2026
Merged

feat(email): agent-led mailbox onboarding — the agent sets up its own access#2496
kovtcharov-amd merged 54 commits into
mainfrom
claudia/task-272d716d

Conversation

@kovtcharov

Copy link
Copy Markdown
Contributor

Closes #2469.

Driving the TUI end to end today: preflight went green, I asked "triage my inbox", and got back CONNECTOR_ERROR: All connected mailboxes failed during pre-scan: google:… (credential problem) — followed by well-written, completely unactionable advice to leave the terminal, run gaia connectors connect google --scopes <scopes> --grant-agent installed:email, and come back. Now the agent works out which of the four problems it actually has (nothing connected / credentials stopped working / a missing scope / connected but not granted to this agent), says something specific about that one, and offers to fix it right there in the conversation. The connected-but-not-granted case is repaired with a local grant write — no browser, no re-sign-in at all. Connecting Google still needs the user's own OAuth client ID and secret (GAIA ships no first-party client), so the agent now says that up front with a link and asks for the secret behind a masking flag, rather than failing on a token refresh three steps later.

Captured live against lemond + a dev-mode sidecar at 80x24, from exactly the failure above:

  [ok] pre_scan_inbox
  [..] setup_mailbox_access: google
╭────────────────────────────────────────────────────────────────────────────╮
│ ? Your Gmail sign-in has stopped working — the saved credentials were      │
│   rejected, which usually means the access was revoked or expired. I can   │
│   take you through signing in again.                                       │
│ > (*) [1] Reconnect Gmail                                                  │
│       Opens your browser to sign in again. Your mail is untouched.         │
│   ( ) [2] Not now                                                          │
│       Change nothing. I'll ask again next time.                            │
│ 1-2 pick · up/down or tab move · enter answer · esc cancel the turn        │
╰────────────────────────────────────────────────────────────────────────────╯

Answering continues the same run — no second request, no replayed context.

Three threads, shipped together

Splitting these is how a feature ends up built-but-unreachable, so all three land here.

  • A mid-run structured-input primitive (contract 2.5 → 2.6, additive). /v1/email/query could pause but never continue — a step needing input emitted an event and then deliberately killed the run. New non-terminal canonical event needs_input carries the question, 2-4 mutually exclusive options each with a label and a description of what choosing it does, and a free-text escape; POST /v1/email/query/{run_id}/respond resumes the original stream. Callers declare can_answer_questions (default false), so a one-shot or batch run gets an immediate actionable refusal instead of parking on a question nobody can see. Stale request_id → 409, unknown run → 404, unanswered → the run ends with an error. Never a hang. needs_confirmation is deliberately untouched — it stays terminal and deny-by-default; that is a security control, and folding the two into one event would have made it conditional on an optional field. Resolves spec §9 Q3.
  • A TUI question surface. Inline in the transcript (a modal would cover the conversation the question is about). ASCII-only state markers, number shortcuts, free text, masked input for a credential, 80x24. Built generically so a Phase 3 approval is just a question whose options are Approve and Deny — not a second mechanism.
  • The agent actually using it. check_mailbox_access classifies the state; the live token probe is the only thing that catches the third-party revocation behind "(credential problem)". setup_mailbox_access walks the fix, asking only what it cannot determine itself. The conversation is scripted in Python, not improvised by a 4B local model.

Test plan

  • PYTHONPATH=$PWD/src:$PWD/hub/agents/email/python python -m pytest hub/agents/email/python/tests/ -q — 1024 pass; the single failure (test_agent_version_matches_package_metadata) is pre-existing on an env without the wheel installed.
  • Resume seam, against a real uvicorn server so a second request lands while the first stream is open and blocked: pytest hub/agents/email/python/tests/test_query_resume_2469.py -q — answered run completes, label-instead-of-value resolves, unanswered times out and errors rather than hanging, unknown run 404s, stale request_id 409s (and the real one still works), stream heartbeats while parked, and a caller that didn't set can_answer_questions is refused in <15s.
  • Four mailbox states → four different opening questions, plus the no-browser grant path, the honest-limit OAuth path, and the daemon forwarded-credentials mode: pytest hub/agents/email/python/tests/test_mailbox_onboarding_2469.py -q.
  • cd tui && make lint && go test ./... -count=1 -race — all green.
  • python util/lint.py --all.
  • Blast-radius regression check: every test file touching sse_handler / sidecar_harness / connectors / the canonical vocabulary (85 files, 1453 tests) run with and without this branch — identical 12 pre-existing failures, zero new.
  • Live: ./bin/gaia --control-port 8771 against a dev-mode sidecar (api: v2.6), driven over the control API at 80x24 — the capture above, then answering to completion (3 steps, 2 tools, terminal final), then the OAuth branch through the client-ID prompt and the masked secret prompt.

Worth a reviewer's eye

  • src/gaia/ui/sse_handler.py gains three optional params on request_user_input_blocking plus a lock around the pending-input maps (/respond is a second thread on state only one ever touched). Semantics are inherited from that primitive rather than forked.
  • The browser sign-in wait passes an explicit 150s budget: run_sync defaults to 30s while the flow itself waits 120s, so the default would abandon a sign-in that then succeeds and skip the grant — landing the user in the connected-but-ungranted dead end this PR exists to remove. The test asserts the budget, not just the call.
  • A first-party OAuth client would delete two questions from this flow (the client ID and the secret) and is the single biggest remaining friction. A device-code flow would remove the local-browser requirement. Both are noted as follow-ups in docs/plans/email-agent-tui-port.md; neither blocks this.
  • Under the daemon the sidecar holds no credential of its own (feat(connectors): OAuth forward-out to sidecars (V2-14) #2154 forward-out), so a connect made here reports "try again shortly" rather than claiming success it can't yet back — the daemon's hand-over is on a 300s timer. Forwarding on demand right after a connect would close that gap and needs a daemon-side change.

The TUI can only talk to agents over a stdin/stdout pipe, but the email
agent has been a daemon-managed HTTP sidecar since #2191 — so it shows up
in the hub greyed out and unusable. This plan scopes what it takes to make
it a first-class TUI agent: install/run/uninstall from the hub and the
command line, interactive-and-autonomous by default, and a settings screen.

Also records two findings that shape the design: the CLI console
auto-approves every gated tool (send, forward, permanent delete), and the
canonical /query SSE contract cannot complete a confirmation-gated action.
Once the TUI can install, run, and configure the email agent, the gaia email
subcommand is a second front door to the same sidecar with none of the UI.
Scopes its removal, and pins the sequencing risk: it must land last, after the
replacement is verified, or we delete the only working entry point first.

Only the email subcommand is in scope — the rest of the legacy Python CLI is
retired separately.
The user-journey design challenged ten points in the port plan; seven are
accepted here. The biggest: preflight was scoped as late polish, but without
it every transport failure reaches the user as a raw HTTP status — so it
becomes Phase 1's exit test instead.

Also drops 'always allow' on irreversible actions, replaces the three-level
autonomy enum with fixed approval behaviour plus a background-work checklist,
and moves background scanning from a TUI timer to launch time. Settings
shrink to what actually persists, with the config and jobs APIs filed as
#2470 and #2471.
…rt plan

The plan told the decommission phase to keep src/gaia/daemon/agent_query.py
because it was 'the generic daemon thin-client used by more than email'.
That is false: run_query and ConsoleRenderer have exactly two callers, both
inside the email handler being deleted, so removing the subcommand orphans
all 346 lines. Left uncorrected, that phase would have kept dead code on a
premise nobody would re-check.

Reframed as an explicit choice — keep it as the reference implementation the
Go transport mirrors (and say so in the module), or delete it with the
subcommand — rather than a false statement of fact.

Also ignores .claudia-worktrees/, which held six full checkouts one 'git add
-A' away from being committed, and fixes two drifted line references.
The sidecar's QueryRequest.context is a required List with extras forbidden,
so a null is a 422. A Go nil slice marshals to null, which means the obvious
implementation fails on the very first turn — the one case with no history —
and works on every turn after. A mocked 200 hides it entirely.

Calls for initializing the slice and asserting the serialized body contains
"context":[] rather than asserting the mock was called.
The TUI could only talk to agents it spawned itself as a subprocess, so the
email agent — a long-lived HTTP sidecar the GAIA daemon starts, supervises and
reverse-proxies — was visible in the hub but unreachable. The TUI is now a
second thin client of the same relay `gaia email` uses: it discovers the daemon,
ensures the sidecar, and streams the canonical seven-event `/query` contract.
The subprocess transport is untouched, so the bash agent keeps working.

Daemon discovery mirrors src/gaia/daemon: instance.json is trusted only after
BOTH the recorded pid is alive AND a token-authed /daemon/v1/status probe
answers with our service id and that same pid (a recycled port after a crash is
a real case). Start-or-attach takes the same flock so two callers yield one
daemon. The client token is re-read on any 401 — it rotates on every daemon
restart — and never cached, logged, or printed; the sidecar's own bearer is
discarded with the ensure response and never leaves the daemon.

A stream that ends without a terminal final/error is reported as a failure, not
an empty answer, and a turn is appended to the pushed transcript only when it
terminated in `final` so a failed turn cannot poison the next one. Unknown event
types and unparseable frames are surfaced instead of dropped.

Also fixes three latent bugs found while in here:
- an agent path containing a space was re-split on whitespace and never found;
- cancelling a turn abandoned the read without stopping the child, so the tail
  of that turn's output surfaced as the next turn's events;
- ChatModel held its streaming buffer in a strings.Builder, which panics the
  moment a copied non-zero Builder is written again — that would have crashed
  the TUI on the second streamed token of every answer.
Found by pointing the new client at a real running GAIA daemon rather than the
test fake. Three fixes, none of which the fake server could surface:

`Client.Do` defaulted to the status-probe HTTP client, whose 1.5s timeout is
deliberately tiny because a live daemon answers loopback in milliseconds. Any
relay call taking longer than that timed out: `GET /v1/email/init` does real
work (probing Lemonade, the model and connectors) and failed with a misleading
"context deadline exceeded" instead of returning its readiness report. Nothing
shipped today relied on the default — every current caller passes its own
client — but the Phase 2 install/catalog calls and the Phase 5 preflight screen
would each have walked straight into it. Do() now defaults to a general 60s
client and the probe timeout is confined to the probe.

A cancel POST for a run that is no longer in flight answers 404 by design, which
is exactly the case when we abandon a stream that already ended. That was being
logged as a failed cancel; it is now reported as "already finished".

Also: a `data` line with no colon carries an empty value per the SSE spec, which
the reference implementation's str.partition yields but strings.Cut discarded;
and Close() landing while a query request is in flight no longer registers an
un-cancellable run.
…try point

Two independent reviews of the Phase 1 transport, plus a run against a real
daemon. Everything below was found after the first commit was already green on
106 tests, which is the argument for the review pass rather than against it.

Cancelling a subprocess turn emitted a spurious "agent process exited with code
-1" bubble under the "cancelled" line about half the time: killing the child made
the reader see a non-zero exit and a closed pipe, and it reported both as agent
failures. A deliberate kill is not an error. The `emit` helper also selected over
a ready channel send and a ready ctx.Done() simultaneously, so Go picked at
random — it is now deterministic. Reproduced at 5/12 runs before the fix; a
regression test now runs the cancel path eight times.

The same cancel path could panic the TUI. `Send` read the stdin pipe outside the
mutex while the cancel goroutine nil'd it, so a turn started right after a cancel
could write to a nil pipe or scan a nil scanner. Turn state is now captured under
one lock acquisition. Relatedly, killing also reaped the child while the reader
was still reading its stdout pipe, which os/exec explicitly forbids — reaping is
now the reader's job alone. A second test drives ten cancel-then-send cycles.

The turn after a cancel could also silently produce nothing: the early-return
paths skipped the reset, so the next turn reused the child that had just been
killed. Found by the new test, not by review.

A late-arriving event from a cancelled turn could tear down whichever turn was
running by then — Bubble Tea cannot cancel a dispatched Cmd, so the old
waitForEvent goroutine delivers whenever its channel finally closes. Events now
carry the channel they came from and a superseded turn is ignored. The SSE client
no longer holds that channel open behind a 10s cancel POST either.

Nothing bounded the wait for response headers, so a daemon that accepted the
connection and never answered hung the caller forever with a spinning cursor —
the Python reference gets this for free from its read timeout. A recycled pid
whose port was answered by an unrelated process permanently blocked daemon start,
even though the probe had already proved the record was garbage; stale records are
now classified so only our own wedged daemon is left alone. A 401 from a relayed
call now blames the sidecar instead of sending the user to restart the daemon.

Finally, the transport had no user-reachable entry point at all — roughly 1,700
lines that a reviewer could not exercise, and that Phase 2 would have been the
first thing to drive. `gaia tui chat --agent <id>` now runs any catalog agent over
whatever transport it declares, and with `--query` it is a genuine one-shot:
answer on stdout, progress on stderr, exit 0/1, no alt screen. Driving it against
the live daemon showed a bare "HTTP 404", so that case now explains that the
installed sidecar predates the canonical /query contract.

Transport selection moved off the Bubble Tea model into client.ForAgent so the
headless paths do not need a UI, and the cancel path moved out of the daemon
control-plane client into the SSE transport that owns the agent wire contract.
The legacy event vocabulary is now marked frozen so new UI features are not
built twice.

Correcting the first commit's message: it said the daemon token is "never
cached", which is stronger than the code. The token is re-read on every 401,
never logged and never printed, and the sidecar bearer is discarded — but the
last verified instance is held in memory so a run can be cancelled.
Teaches the TUI to reach agents through the daemon relay instead of only
spawning a local binary over a pipe — the prerequisite for the email agent,
which has been a daemon-managed sidecar since #2191 and so could never work
over the subprocess path.
There was no way to exercise the TUI without a human at the keyboard, and no
way for a user to watch an assistant use it. `gaia tui --control` now exposes a
loopback HTTP API that injects keys into the *running* Bubble Tea program and
serves back the frame it just rendered, and an MCP server wraps that as tools —
so Claude Code can navigate the hub, filter, type, and launch agents while the
user watches the same terminal update in real time.

Go side (tui/internal/control/): a Recorder wraps the root model so every
View() is cached, and named keys map to real tea.KeyMsg values. The existing
smoke tests send tea.KeyMsg{Type: KeyRunes, Runes: []rune("tab")} — three
letters, not the Tab key — so the mapping is contract-tested by asserting
KeyMsgFor(name).String() round-trips to the name the handlers switch on.

POST /wait is what makes automation reliable: it blocks on a broadcast channel
until the screen or the reported model state matches, and a timeout reports
what the screen actually contained. Callers no longer busy-poll.

Injection is synchronous end-to-end: tea.Program.Send only queues, so a
trailing sentinel message is used to answer /keys only once every key in the
batch has been handled and drawn. Without it a four-key batch returned with one
key applied.

Python side (gaia.mcp.servers.tui_mcp, `gaia mcp tui`): discovery reads
~/.gaia/tui/control.json (0600) and trusts it only when the recorded pid is
alive and a token-authed probe answers with a matching pid — a recycled port
after a crash is a real case. Loopback-only hosts are enforced client-side too,
since the bearer token rides every request.

Test plan:
- cd tui && make lint && go test ./... -race
- pytest tests/unit/test_tui_mcp.py
- pytest tests/integration/test_tui_control_e2e.py  (boots the real Go server
  and drives it with the real Python client; skips without a Go toolchain)
Rebasing onto the transport work collided two same-named helpers in package
ui — the one-shot test's local run() and the control server's run(). Renamed
the test helper to captureOneShot; the production name stays.

Also corrects which server owns port 8766 (the MCP bridge, not agent_ui_mcp).
Lets an assistant drive the live TUI the user is watching — read the rendered
screen, send keys and text, wait on conditions — so the terminal UI can finally
be exercised and regression-tested without a human at the keyboard.

Its own review pass caught six real bugs, including a key batch that was
acknowledged before it was applied and stdio logging that corrupted the
JSON-RPC stream (the latter also affects the shipped Agent UI MCP — #2472).
Before: an agent driven from a CLI or a subprocess executed every
confirmation-gated tool with no prompt at all. `OutputHandler.confirm_tool_execution`
returned True unconditionally and no CLI console overrode it, so the entire
mechanism was inert outside the Agent UI. In practice that meant `gaia email -i`
could send, forward, RSVP to, and permanently delete mail on the model's say-so,
and any agent could run shell commands and overwrite files, with no prompt and no
event the user could see.

After: gated tools prompt on the terminal — the tool name, its literal arguments,
and a yes/no answer that defaults to no (Ctrl-C and EOF also mean no). "Always
allow" is per tool and per console, and is not offered for run_shell_command /
run_cli_command, where the name says nothing about the next call. With no terminal
to ask on — piped stdin, redirected stdout, CI, a subprocess host — the tool is
denied with an actionable error naming both ways forward, instead of running
silently. The agent loop still returns {"status": "denied"}, so the model can
explain itself rather than the run crashing.

Details worth knowing:

- Silence covers narration, not consent. `gaia chat` builds a SilentConsole even
  for a user sitting at a terminal, so the prompt lives in a mixin shared by both
  CLI consoles; otherwise the flagship path would have gone from "runs gated tools
  unasked" to "refuses them unasked".
- The gate itself never raises. A malformed tool_args from the model, a detached
  tty that errors on read, or a closed stdout all resolve to a logged denial —
  because the tool used to just run, an exception escaping here would be a new way
  to kill a working session, and `Agent._execute_tool`'s callers do not catch.
- The unattended opt-in (GAIA_AUTO_APPROVE_TOOLS=1, or auto_approve_gated_tools on
  the console) is read from the environment as it was at startup, so a
  project-local .env cannot switch off a user's prompts. Every approval taken that
  way is logged with the tool name.
- The Agent UI path is untouched: it still emits permission_request, blocks on the
  modal, and denies on timeout; a new test pins that end to end through the gate.
  Background/autonomous ticks keep their existing auto-deny.
- The OpenAI-compatible API server and scheduled UI runs have no channel to ask a
  human, so they now deny rather than approve. Documented in the CLI reference,
  troubleshooting, the console spec, and the API-server and output-handler pages.

Denial reasons are bound to the tool they belong to, so a handler that denies
without recording one can never report another tool's explanation. Argument
shortening is display-only — an approved write receives the full content.
Confirmation-gated tools — send, forward, permanent delete, RSVP, shell,
file write — executed with no prompt and no record whenever an agent was
driven from a CLI or subprocess, because the base handler answered yes on
the user's behalf. It now denies by default and the terminal console
genuinely asks.

Unattended hosts opt in with GAIA_AUTO_APPROVE_TOOLS=1, which deliberately
cannot be granted by a .env file — a project file travelling with a
directory is not an operator's decision. The SSE approval path is
unchanged.
`gaia_agent_email/cli.py` was an in-process argparse entry point that
nothing imported and no console script exposed. Its docstring still claimed
`gaia.cli.handle_email_command` invoked it, which stopped being true at
#2191 when that handler became a thin daemon relay — so the file was both
dead and actively misleading about how a query reaches the agent.

The sidecar (`gaia-agent-email serve` -> POST /query) is the only supported
entry point; an in-process CLI would bypass its caller auth, trust gate, and
SSE translation. Adds a guard test so the module can't creep back silently.
…orecard

The npm package shipped two different scores for the same version: README and
EVALUATION advertised 83.4/100 while the co-located SCORECARD.md — machine-
generated from a real 3-run eval for v0.5.0 — reported 84.53. 83.4 is the
0.3.0-era number; the prose was never refreshed when the scorecard was
regenerated, so an integrator comparing the two pages saw the package
contradict itself about its own benchmark.

SCORECARD.md's aggregate.value is the source of truth, so the prose moves to
it. The CHANGELOG's 83.4 under 0.3.0 stays as-is: per-version entries are a
historical record, and rewriting one would falsify the release history.

Adds a guard that fails whenever a prose score claim drifts from the generated
aggregate again, since prose does not regenerate with the eval.
…f hardcoding it

The generated capability matrix contradicted itself inside one file: the
Definitions section said the REST API has "16 functional verbs" while the
matrix header five lines below computed 21 from the live OpenAPI spec. Anyone
reading the doc to size the REST surface got the wrong number from the more
prominent of the two.

The 16 was a literal inside TOOLS_COUNT_DEFINITION, so the existing
byte-identical drift guard could never catch it — regeneration reproduced the
same stale literal. The blurb is now a template fed by the derived counts, and
the two stale literals that had spread to a module comment and the
gaia-agent.yaml comment are gone with it. Regenerated: 16 -> 21.
…nswer

Two swallowed failures on the same SSE path made an internal crash look like
the agent simply had nothing to say. The queue drain caught bare `Exception`
where it meant `queue.Empty`, so any non-Empty error read as "queue drained";
the stream then synthesized `{"type": "run_complete", "answer": ""}` — a
well-formed *successful* completion — whenever the worker thread had died
without emitting a terminal event. A client could not tell the two apart.

The drain now catches `queue.Empty` only, matching the sibling query route,
and a dead worker emits an `error` event naming the failure and the sidecar log
to check before the stream closes.
…profile

Two degradations that left no trace at all.

A malformed GAIA_EMAIL_TRIAGE_MAX_MESSAGES silently reverted to the
100-message default, and a negative or zero value was clamped to 1. The eval
harness sets that variable to cover the whole labelled corpus, so a typo
produced a quietly smaller scan and therefore a quietly different score with
nothing to notice — exactly the regression-hiding the rule exists to stop. It
now raises ConfigurationError naming the bad value and the way out, matching
config._undo_window_seconds, which already handled the identical shape this way.

An unreadable sender-profile row was reset to an empty record with no log,
throwing away the reply-latency and interaction history that drives
priority-sender promotion, while the sibling read paths log the same
corruption class. The reset stays fail-soft — one bad row must not kill
behavioral learning — but now says which sender lost what.
…CLI claim

CONTRACT.md — the doc that calls itself the contract reference — declared
schema 2.3 while contract.py has been on 2.5 since #2154. Both additive bumps
(2.4's /v1/email/query agent loop, 2.5's /v1/connections OAuth intake) shipped
without a history row, and every copy-pasteable JSON example told integrators
to send the stale version. The same file still explained that the interactive
CLI loads the model at 32K via an `agent_context_sizes` registry in
src/gaia/cli.py; that registry no longer exists and no CLI loads a model at all
since #2191 made every front-door relay to the sidecar.

npm/SPEC.md stated the contract version as 2.4 without saying whose — true of
the client, wrong for the sidecar it documents. Now says both and why 2.4
requests still work.

Adds guards tying CONTRACT.md's headline, examples, and history table to
contract.SCHEMA_VERSION, so the next bump cannot land doc-less.
…tops startup

Follow-up to the fail-loudly pass: raising from _inbox_scan_ceiling() achieved
nothing. Both scanning tools call it inside their own
`except Exception -> _envelope_err`, so the ConfigurationError was caught two
lines later and handed to the LLM as a per-call error string. An eval run with
a typo'd GAIA_EMAIL_TRIAGE_MAX_MESSAGES would have failed every triage call,
apologised, and still written a scorecard — worse than the silent default it
replaced.

The resolver moves to config.py beside default_undo_window_seconds and becomes
an EmailAgentConfig field, so a bad value fails construction, where an operator
sees it. Blank now means "unset" on both, matching that precedent.

Also from review: the profile-reset warnings logged a raw sender address at
WARNING, which gaia diagnostics bundles into user-attached bug reports — they
log the store row id instead. CONTRACT.md's new version-policy sentence
contradicted the 2.3 row it introduced (a breaking change shipped as a MINOR,
which clients gate on MAJOR alone and never saw), and its ctx note overreached:
gaia eval benchmark does still build the agent in-process. The docs site
repeated the stale 2.3 schema claim and is now guarded with CONTRACT.md.
… environment

The config field added in the previous commit was validated at construction and
then never read: both scanning tools still called the env resolver on every
invocation. `EmailAgentConfig(inbox_scan_ceiling=500)` was accepted and
silently ignored, and because the resolution happened per call the ceiling
could move underneath a live agent when the environment changed mid-run.

The registrar now resolves it once, preferring the config and falling back to
the same env resolution for the duck-typed configs some hosts pass (the
existing `debug` flag reads the same way). Tests cover all three: explicit
config wins, a config without the field still works, and the value is stable
across calls.

Also drops the ✅ the debug playground printed on an empty `run_complete` —
a dead worker now emits an error event immediately before it, and a green
check after that read as success.

Reverts drive-by reformatting of 13 untouched files from a whole-tree black run.
Fixes a case where the agent could stream a blank answer as success after
its worker had died, and makes a bad inbox scan ceiling stop startup rather
than degrade quietly.

Also settles two doc contradictions at the source: the package advertised an
eval score its own scorecard disagreed with, and the capability matrix
hardcoded a verb count that its own generated table contradicted. Deletes
the dead in-package CLI module retired by #2191.
The email→TUI port had an engineering scope but no user experience: a list of
capabilities, not a journey. This walks all eleven stages a person actually
passes through — from `gaia tui` on a bare machine to a triaged inbox — with
ASCII mockups for every screen state, a keybinding map checked against what the
hub and chat models already bind, and an explicit build order.

The finding it argues: the port's hard problem is the four-precondition
readiness gate (local LLM, model, daemon, mailbox), not the transport. Every one
of those is already reported by an existing endpoint with an actionable hint, so
seven of the ten highest-value fixes need no backend change at all.

Pushes back on the engineering plan in ten places, notably: preflight is Phase
1's acceptance criterion rather than later polish, "always allow" on irreversible
actions should not ship, the three-level autonomy enum should be two plain
controls, and background timers do not belong inside a foreground TUI.
Review pass against the code found four things the doc asserted that aren't
true as written, each of which would have sent an implementer down a dead end:

- `ctrl+b` and `ctrl+t` were proposed as free chat keybindings. Both are taken
  by bubbles/textarea's default keymap, which receives every key the chat model
  doesn't handle. Moved to `ctrl+g` / `ctrl+r` and documented the full set of
  chords the textarea reserves, since checking only chat/model.go misses them.
- The quit message told users to stop the background service with `gaia kill`,
  which kills a process by port and cannot stop the daemon. It is
  `gaia daemon stop`.
- `GET /daemon/v1/catalog` was referenced as if it exists; it is proposed in the
  engineering plan's Phase 2.
- `gaia tui` itself does not exist — no `tui` subparser in the CLI, and the Go
  binary's cobra root is already named `gaia`, so installing both yields two
  different `gaia` commands.

Adds a "what exists today vs. what this design assumes" table so every proposed
surface is labelled in one place, and tightens the `diagnose()` line range.
Defines the end-to-end experience the port is building toward, using email as
the driving case: what a first-time user sees, where the walls are, and which
fixes are worth the most per hour. Its pushback reshaped the engineering plan
— most importantly, the readiness gate moved from late polish to Phase 1's
exit test, because without it every transport failure surfaces as a raw HTTP
status.
The plan's email paths predated the #2060 restructure, so every reference
pointed at a directory that no longer exists. Also splits the bug list by
status: two are fixed and merged, two remain open, and four are closed as
moot because they are flag behaviour on the subcommand Phase 5b deletes —
reading them as open would send someone to fix a command being removed.

Adds the hub tab-cursor bug found while the control API drove the TUI.
Until now the only way to install a GAIA agent was the web UI's FastAPI
server on port 4200 — no CLI, no daemon API, so the TUI and any scripted
setup were blocked. The daemon now owns install, which means the TUI, the
CLI and the Agent UI share one installer, one SHA-256 check and one
install lock instead of three forks of the logic.

New routes (all behind the existing daemon client token):

  GET    /daemon/v1/catalog                     hub catalog + installed state
  POST   /daemon/v1/agents/{id}/install         202, queue an install
  GET    /daemon/v1/agents/{id}/install-status  progress + terminal error
  DELETE /daemon/v1/agents/{id}                 stop, verify, remove the dir

Safety semantics that are not negotiable and are covered by tests:

- The install dir IS the sidecar's binary cache, so install and uninstall
  stop the sidecar first and hold it stopped for the whole mutation via a
  new SidecarRegistry.hold_for_mutation(): an ensure arriving mid-download
  waits instead of respawning the process from the directory being
  rewritten. A pid that survives the tree-kill aborts the operation.
- Install and uninstall of one id share a slot, so a DELETE can never
  report success while a download is still writing into that directory.
- Integrity is the hub manifest's server-computed SHA-256, verified before
  anything is written; a mismatch leaves nothing installed and there is no
  "use it anyway" path.
- Reserved built-ins are refused, and ids are validated before they become
  a path segment under ~/.gaia/agents.
- The catalog only offers agents the daemon has a sidecar spec for, and
  reports the ids it hid rather than dropping them silently.

Also drops the daemon's dead dependency on the committed
binaries.lock.json (placeholder SHAs, and not shipped in the wheel): a
missing lock now means "not installed yet" and names `gaia hub install`
as the remedy instead of blaming a broken install.
There was no way to install an agent from a terminal at all — setup meant
opening the Agent UI. `gaia hub` drives the daemon's install routes, so a
scripted or headless machine can now provision agents, and it shares the
UI's installer rather than forking it.

`list` shows the catalog with installed versions and an update marker;
`--installed` answers from the local .installed sentinels and needs no
network; `--refresh` bypasses the 5-minute catalog cache. `install` polls
and prints each phase, exiting 1 with the reason on failure. `uninstall`
stops the agent's sidecar and verifies its pid is gone before removing
anything. None of these renderers can print the sidecar bearer token.
The registry only verifies the pid it still tracks, so a sidecar the
daemon lost track of would have its binary replaced underneath it. That
is not hypothetical: on this machine a daemon-spawned email sidecar
(pid 31647, port 57920) has been serving /health for an hour while
`GET /daemon/v1/agents` reports the agent as `stopped` — the daemon
spawned a second process 12 seconds after the first without killing it
and dropped the reference.

Install and uninstall now also ask the OS what is actually running out of
the agent's directory and abort with the surviving pids and a remedy if
anything is. exe/argv[0]/argv[1] only, so a log path passed to another
tool cannot block an install.
@github-actions github-actions Bot added agent::email Email agent changes tui Go terminal UI (gaia-tui) daemon Daemon supervisor / sidecar control plane sidecar Agent sidecar contract / harness labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Verdict: Approve

This lands the #2469 mailbox-onboarding feature end to end: when the email agent can't reach a mailbox, it now works out which of the four distinct problems it has (not connected / credentials stopped working / missing scope / connected-but-not-granted-to-this-agent), says something specific, and offers to fix it in the conversation — including a no-browser local-grant repair — instead of telling a user in a terminal to go run a shell command. Under the hood it adds a genuinely new, additive capability: a mid-run needs_input SSE event that pauses a /query run and resumes the same stream via POST /query/{run_id}/respond, kept deliberately separate from the terminal deny-by-default needs_confirmation security control. The daemon gains one shared Agent Hub install/uninstall path (used by CLI gaia hub, TUI, and Agent UI) with trust-gating and stop-the-sidecar-before-mutating safety, plus a new gaia mcp tui control server.

The bottom line: I found no blocking issues. The code is unusually careful about the two things that break this class of feature — concurrency on the resume path and not leaking a credential secret into model context — and both are handled correctly. The change is large but each thread is coherent and the docs are synced across README/SPEC/SKILL/CHANGELOG/CONTRACT.

Real-world evidence

evidence-bundle.md is present and substantial — real processes on a no-inference ubuntu-latest runner, not mocks:

  • CLI gaia hub list fetched the live hub catalog; gaia hub install email returned the clean trust-gate refusal naming --trust (no stack trace).
  • HTTP API — the new /v1/email/query/{run_id}/respond returned 404 for an unknown run and 422 for a missing value; daemon install routes returned the documented 403/404. GET /version reports 2.6 matching the SCHEMA_VERSION bump.
  • MCPgaia mcp tui registered all 9 tools over the real wire protocol; the "no TUI running" and out-of-range-resize error paths produced the documented structured errors.

Legitimately deferred to the strix-halo lane (no real inference / no Go TUI build on this runner): the full needs_input → /respond → run-resumes round trip, the conversational onboarding flow against a live 4B model, and the Go TUI question surface. This is the expected CI-lane deferral — the underlying routes were exercised — so it's adequate for merge-time CI; the rendered onboarding capture in the PR description covers the pixel side. Verdict rests on static review + the route/CLI/MCP evidence above; the Go TUI (~25k lines) was not deeply reviewed here and rests on its go test ./... -race gate.

🔍 Technical details

Strengths

  • Resume-path concurrency is correct (src/gaia/ui/sse_handler.py). The three pending-input maps are now guarded by _user_input_lock; registration moved before the emit (mirroring confirm_tool_execution) so a fast client answering before the worker registers can't be spuriously rejected; and the timeout cleanup + resolve_user_input are each single locked steps, so a resolve landing at the deadline either wins or is cleanly rejected (→ 409) rather than accepted into a slot about to be dropped. resolve_user_input returning False is what lets /respond answer 404-vs-409 distinctly.
  • Credential-secret containment (tools/onboarding_tools.py:898). The catch-all in setup_mailbox_access deliberately returns only type(exc).__name__, never str(exc), precisely because this path can hold an OAuth client secret in locals — the one place a credential would otherwise leak into model context and then the answer text. question.ask also refuses to echo a sensitive answer back in its ValueError.
  • needs_input kept distinct from needs_confirmation (sse_translation.py). Folding them would have made the terminal, deny-by-default security behaviour conditional on an optional field; instead needs_input is answerable and explicitly excluded from TERMINAL_TYPES, with a heartbeat (query_routes.py _HEARTBEAT_SECONDS) keeping a parked run under the 300s read-idle watchdog. Fail-loud throughout: a caller without can_answer_questions is refused at ask-time rather than parked (question.py), and an unanswered question raises rather than inventing an answer.
  • Daemon install safety (daemon/sidecars/install.py). assert_no_live_process_in backstops the registry's pid check by scanning the OS for any process running out of the install dir; install/uninstall share one _claim_slot; _ID_RE re-validates the id before it becomes a path segment under shutil.rmtree; trust is never inferred (trusted defaults false at every layer).
  • TUI MCP hardening (mcp/servers/tui_mcp.py). Loopback-only host enforcement on the control file (a non-loopback host is treated as tampering), NO_PROXY so the bearer token/screen text can't be handed to a proxy, token scrubbing in error bodies, and two-check discovery (pid alive AND token-authed probe with matching service+pid) against pid/port recycling.

Minor (nit)

🟢 Redundant __import__("sys") (src/gaia/cli.py, in handle_mcp_tui). sys is already imported at module level (line 9), so the two file=__import__("sys").stderr calls can just use sys.stderr.

            route_logging_to_stderr()
            print(
                "Starting GAIA TUI Control MCP Server (stdio mode)...",
                file=sys.stderr,
            )

(Second occurrence is the same swap.)

One thing to keep an eye on (not blocking)

mailbox_state.py couples its STATE_* strings to AuthRequiredError.Reason values ("wherever one exists") — _probe_credentials returns exc.reason.value straight through as the state that REPAIRABLE/_confirm_repair branch on. If a future connectors refactor renames a Reason, a real state would silently fall through to the not_connected branch. The docstring calls this out and test_mailbox_onboarding_2469.py covers the four states, so it's fine today — just worth a guard/enum-mapping assertion if the connectors enum ever drifts.

@kovtcharov
kovtcharov changed the base branch from main to claudia/task-5fcce79e July 25, 2026 13:00
Every query failed against the sidecar users actually have installed. The
published 0.5.0 binary is contract 2.4; its request model is strict
(`extra="forbid"`), so the `can_answer_questions` field this branch added was a
422 on every request — interactive and one-shot alike, `true` and `false` alike:

  HTTP 422: [{"type":"extra_forbidden","loc":["body","can_answer_questions"],
              "msg":"Extra inputs are not permitted","input":false}]

The sidecar is right to reject it. The client is the side that must not send a
field the peer never agreed to. The two halves of the feature reach users on
different clocks — the TUI ships when the TUI builds, the agent only when a new
sidecar is published AND installed — so a current TUI routinely talks to an
older peer, and the earlier code had no gate at all.

The client now asks first: `GET /v1/<agent>/version` once per launch, and
`can_answer_questions` is sent only to a peer at contract >= 2.6. The field is a
pointer with `omitempty`, so below the floor the key is absent entirely;
`omitempty` alone would not have helped, because an interactive session needs
`true` and that is exactly the value that 422s.

Both directions now hold. A new TUI against an old sidecar works and degrades to
"cannot ask questions" — and says so once, in the transcript, because the
capability it loses is the in-conversation mailbox fix and a feature that
silently never appears reads as broken rather than as out of date. An old TUI
against a new sidecar works because the absent field already defaults to false.
A missing or unreadable `/version` is treated as an old peer: the query stays
valid, and the probe failure is logged rather than swallowed.

Verified against the real installed binary, not the source tree — the trap
CLAUDE.md names. Reproduced the 422 first, then confirmed negotiation reads
`{"apiVersion":"2.4"}` off that binary, omits the field, and the POST is
accepted with no 422 logged. The new tests model BOTH peers' accepted field sets
in a strict decoder, so the fake cannot agree with the client by construction;
reverting the gate fails three of them.
@kovtcharov

Copy link
Copy Markdown
Contributor Author

Fixed in 20611a25 — pushed to the same branch, no force-push, base left at claudia/task-5fcce79e.

You were right on all three counts: no version gate existed, omitempty alone would not have been enough, and my tests only ever saw the Python source where the field exists.

The fix

The client asks before it sends. GET /v1/<agent>/version once per launch (cached — one probe, not one per turn), and can_answer_questions goes out only to a peer at contract ≥ 2.6. The field is now a *bool with omitempty, so below the floor the key is absent entirely rather than present-and-false.

Both directions you asked for hold:

old sidecar (≤ 2.5) new sidecar (≥ 2.6)
new TUI field omitted → works; degrades to "cannot ask questions" field sent explicitly, true/false per surface
old TUI unchanged field absent → defaults false

And the degradation is stated, once, in the transcript — since the capability it loses is the in-conversation mailbox fix, and a feature that silently never appears reads as broken rather than as out of date:

the installed 'email' agent speaks contract 2.4, so it cannot ask questions mid-task — in-conversation mailbox setup needs 2.6 or newer. Update it with gaia uninstall email then gaia install email.

A one-shot doesn't get it (it can't act on it). A missing or unreadable /version is treated as an old peer: the query stays valid and the probe failure is logged, not swallowed.

Verified against the installed binary, not my source

Ran ~/.gaia/agents/email/email-agent (the published 0.5.0 artifact) standalone and reproduced your 422 first:

$ curl ... -d '{"query":"say hi","run_id":"…","context":[],"can_answer_questions":false}'
HTTP 422
{"detail":[{"type":"extra_forbidden","loc":["body","can_answer_questions"],…}]}

Note it reports {"apiVersion":"2.4"}, not 2.5 — so the floor had to be a real comparison, not a != "2.5" check.

Then drove the real SSEClient code path (negotiate → Send → consume) against that same binary through a thin proxy:

sse: 'email' speaks contract 2.4 (mid-run questions: false)
proxied: GET /v1/email/version -> 200

can_answer_questions absent from the body, and zero 422 lines in the sidecar log — where the reproduction logged one instantly. The POST is accepted and the sidecar proceeds into the agent loop.

One thing I could not get to a green end-to-end, and it isn't this

That published 0.5.0 binary never returns response headers for /query on this machine — it accepts the request and then hangs before the first SSE frame. I isolated it away from my change with plain curl, no TUI, no daemon, a body it fully accepts:

$ curl -m 120 -X POST .../v1/email/query -d '{"query":"reply with exactly: hi","run_id":"…","context":[]}'
HTTP 000 after 120.007s     # 0 completed /query lines in its log, for any caller

The daemon-relayed path fails the same way, and its log is full of OAuthClientNotConfiguredError — this box has no Google OAuth client, and the forward-out path appears to block on it. The 2.6 dev-mode sidecar completes queries here fine (that's the onboarding capture in the PR body), so this is specific to the published artifact in this environment, not to queries and not to this branch. Flagging rather than chasing it — it looks like its own issue, and I'd rather you decide whether it is.

Because of that, the published-sidecar end-to-end evidence stops at "the request is valid and accepted". If you can complete a query against 0.5.0 on your box, that's the last mile worth re-checking.

Tests that would have caught it

The new fake relay models both peers' accepted field sets in a DisallowUnknownFields decoder keyed off the peer's own declared version — so the fake cannot agree with the client by construction, which is exactly how the 422 shipped. Reverting the gate fails three of them:

--- FAIL: TestOldStrictPeerGetsNoUnknownField
--- FAIL: TestOneShotAgainstOldStrictPeerWorks
--- FAIL: TestMissingVersionRouteDegradesQuietlyButWorks

Plus: a 2.6 peer is told the truth both ways (true and false — omitting false there would silently let a one-shot be asked something), no /version route at all still works, the notice fires once and only when interactive, the probe happens once per client, and contractAtLeast handles 2.10 > 2.6 numerically rather than lexically.

cd tui && make lint && go test ./... -count=1 -race — 12 packages green. Email suite still 1024 pass / 1 pre-existing.

Also

Documented the rule in the contract spec §7 as a general receiving/sending obligation, not an email footnote — a client must not send an optional field the peer has not agreed to, omitempty is insufficient, read /version and gate. Same guidance added to the npm SPEC.md / SKILL.md / CHANGELOG.md for integrators, who hit this the moment they pin an older sidecar.

One heads-up: while chasing the daemon-side hang I ran a pkill -f gaia.daemon that was broader than intended and took down the daemon running from your main checkout as well. It re-attaches on demand, so nothing to restore, but worth knowing if you saw one disappear.

@github-actions

Copy link
Copy Markdown
Contributor

🟡 The TUI upgrade notice gives users wrong commands — they'll get an error trying to follow them.

noticeForMissingCapability tells an interactive user whose email sidecar is too old to run `gaia uninstall email` then `gaia install email`. Neither of those commands manages hub agents: gaia install installs Lemonade/GAIA itself, and gaia uninstall removes the entire product. The actual hub-agent commands are gaia hub uninstall email and gaia hub install email. The test that guards this string only checks for "gaia install email", so it doesn't catch the wrong prefix.

🔍 Technical details

tui/internal/client/negotiate.go line 4446–4447 (approximate in the full file):

// current — wrong subcommand prefix
return fmt.Sprintf(
    "the installed '%s' agent speaks %s, so it cannot ask questions mid-task — "+
        "in-conversation mailbox setup needs %d.%d or newer. "+
        "Update it with `gaia uninstall %s` then `gaia install %s`.",
    agentID, have, questionsContractMajor, questionsContractMinor, agentID, agentID)

The CLI dispatches hub-agent lifecycle through gaia hub install / uninstall; gaia install takes no agent-id argument. Fix:

    "Update it with `gaia hub uninstall %s` then `gaia hub install %s`.",

The companion test TestInteractiveUserIsToldWhenThePeerIsTooOld in negotiate_test.go checks for "gaia install email" — update that assertion to "gaia hub install email" so it guards the right string going forward.

…d command

Two defects in the notice added by the previous commit, both of which cost the
user the actionable half of an actionable message.

The notice was clipped at exactly the terminal width, mid-word:

  the installed 'email' agent speaks contract 2.5, so it cannot ask questions
  mid-task — in-conversation mailbox setup n

What got cut was "needs 2.6 or newer. Update it with ..." — the version that
fixes it and the entire instruction. The viewport does not soft-wrap, so any
message rendered as a bare line rather than inside a width-constrained block
loses its tail; status and user lines were both rendered bare. They now wrap to
the pane, and the shared wrapper the question panel already used is exported
rather than duplicated.

The remedy itself named the wrong commands. `gaia install <id>` and
`gaia uninstall <id>` reject the agent argument outright, because they are not
agent commands at all: `gaia install` is the Lemonade/first-run helper and
`gaia uninstall` is the tiered cleanup of the GAIA install itself, one flag from
`--purge`. A user handed `gaia uninstall email` sees an argparse error and may
reasonably retry without the argument — landing on the GAIA-wide cleanup. Both
verbs DO exist on the standalone TUI binary and take an agent there, which is
exactly why the string read as correct when it was written.

It now names `gaia hub uninstall email` / `gaia hub install email`, verified
against the real CLI: both take an agent_id, both are agent-scoped, and they
resolve the same whether the TUI was launched standalone or through the Python
CLI. A test pins the class by asserting the scoped form and rejecting the bare
verbs, so the next remedy string cannot regress to a command that means
something else.

Each defect has a test that fails without its fix: the notice must wrap and keep
its remedy on screen at 80/100/120 columns, and the remedy must name the scoped
command for any agent id.
@kovtcharov

Copy link
Copy Markdown
Contributor Author

Both fixed in 85092547 — same branch, no force-push, base untouched.

Defect 1: the notice wraps now

The viewport doesn't soft-wrap, so anything rendered as a bare line rather than inside a width-constrained lipgloss block gets clipped — status and user lines were both bare. They now wrap to the pane, reusing the wrapper the question panel already had (exported rather than duplicated).

Rendered through the real View() path at 120 cols, the width you captured:

LEN=120 |  the installed 'email' agent speaks contract 2.5, so it cannot ask questions mid-task — in-conversation mailbox
LEN=120 |  setup needs 2.6 or newer. Update it with `gaia hub uninstall email` then `gaia hub install email`.

Where it previously stopped at setup n, the remedy now survives — both halves of it.

I also wrapped RoleUser, since a free-text answer to a mid-run question (a pasted OAuth client ID is ~72 chars) is echoed into the transcript on a bare line and would clip the same way.

Defect 2: the remedy names gaia hub …

You were right, and I verified rather than took your word for it:

$ gaia install email     → exit 2   $ gaia uninstall email   → exit 2
$ gaia uninstall --help  → --venv / --purge / --purge-lemonade / --purge-models …

The string now reads gaia hub uninstall email then gaia hub install email. Checked against the real surface — gaia hub install <agent_id> and gaia hub uninstall <agent_id> both take the agent id, and they resolve the same whether the TUI was launched standalone or through the Python CLI. I picked hub over tui deliberately: gaia tui doesn't exist on my branch (it's yours, from the sweep), whereas gaia hub is correct on both trees today, so the string can't be right only after a merge.

On the irony — it's worse than it looks, and that's the useful part. The standalone Go binary does have bare install/uninstall taking an agent id:

$ ./bin/gaia --help
  install     Install an agent from the Agent Hub
  uninstall   Remove an installed agent

So when I wrote gaia uninstall email I was looking at a command tree where it is exactly right. Two binaries, same name on PATH, same verbs, different meanings — one of them a --purge away from wiping the GAIA install. That's not a typo class, it's an ambiguity class, and eyeballing will never catch it. Hence the test asserting the literal, as you asked: it requires the scoped form and rejects a remedy that opens with a bare verb, for any agent id.

Tests that fail without each fix

--- FAIL: TestLongStatusLineWrapsInsteadOfLosingItsTail      (defect 1)
--- FAIL: TestRemedyNamesTheAgentScopedCommand               (defect 2)
--- FAIL: TestRemedyIsScopedForAnyAgent                      (defect 2)
--- FAIL: TestInteractiveUserIsToldWhenThePeerIsTooOld       (defect 2)

That last one is my own earlier test, which had asserted the wrong command — it was pinning the bug in place. Worth noting as its own small lesson: a test written from the same mistaken assumption as the code doesn't protect anything.

The wrap test runs at 80/100/120 cols and asserts two things: no line exceeds the width, and 2.6 or newer plus gaia hub install email are still on screen. Width-only would have passed on a truncating renderer.

cd tui && make lint && go test ./... -count=1 -race — 12 packages green.

One caveat on live verification

I could not reproduce your end-to-end capture here: the published 0.5.0 sidecar won't stay up on this box (the pre-existing hang from my last comment — preflight never gets past "installed, not started"), so I can't drive a real 2.4 peer to the chat surface to photograph the notice. The 120-col output above is the real View() render, not a terminal screenshot. Since you have a working published sidecar, a glance at the wrapped notice in your merged tree would close that last gap.

Also, twice now while chasing the sidecar hang I've run a pkill broader than intended and taken down daemons beyond my own isolated one. They re-attach on demand so there's nothing to restore, but I've stopped using that pattern.

kovtcharov-amd
kovtcharov-amd previously approved these changes Jul 27, 2026

@kovtcharov-amd kovtcharov-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — agent-led mailbox onboarding handles the paths that usually break this class of feature: no credential leakage (the one catch-all that can hold an OAuth secret returns only type(exc).__name__, and a sensitive answer is refused into its own ValueError and masked end-to-end through the SSE/TS/Go/TUI chain), fail-loudly rather than guessing (a transport blip during the token probe returns STATE_ERROR, not a false revocation; success is not reported on a still-unusable mailbox), and the cold-state "no connector yet" path is the primary, tested flow. The resume-seam concurrency is locked correctly, and docs are synced across CHANGELOG/CONTRACT/SPEC/SKILL/README/openapi with the 2.5→2.6 bump.

Nit: map the connector Reason enum to STATE_* explicitly (or assert the mapping) so a future enum rename fails loudly instead of silently routing to not_connected.

…ogress

Three defects found driving the agent against a real Gmail account.

The setup option promised a browser it could not open. "Reconnect Gmail —
opens your browser to sign in again" is chosen purely from the mailbox state,
so a user with no OAuth client accepted it and was asked to paste a client ID
instead. _oauth_client_gap() already detects exactly that and was only
consulted later, inside the flow. The option now says what actually happens
next; when the client is missing it says credentials come first, then the
browser. Same reports-ready-then-fails shape the mailbox row was built to
remove.

Connecting through the agent produced a weaker connection than connecting from
a shell. The agent authorized only its two mail scopes, so the token could not
name the account and every surface showed the mailbox as "default" instead of
the address just signed in with — while `gaia connectors connect` requests the
provider's identity scopes alongside. _connect_scopes() now merges the
provider's default_scopes with the agent's, deduped. The grant stays narrow:
identity is for the connection, not for the agent.

A 25-message triage sat silent for ~50s. One LLM follow-up costs 9-31s
locally, so the scan looks hung long before it is. triage_inbox_impl now takes
an optional progress callback and narrates per message through the console
that already reaches the live stream — verified as 25 updates on a real inbox.
Hosts that do not accept the parameter are detected by signature, not by
catching TypeError, which would have swallowed a genuine TypeError raised
inside triage and blamed the callback.

Also: triage_inbox's docstring now sends "triage my inbox" to pre_scan_inbox.
pre_scan_inbox already returns the typed card envelope the chat surface draws;
triage_inbox returns an unrendered verdict list a model can only paraphrase,
which is why a 25-message inbox came back as three bullet points.
…owed

Two UX defects found driving the agent against a real inbox.

A follow-up referring to something on screen resolved against nothing. Cards
are drawn by this client from tool_result.render, so their rows never reach the
sidecar's history — the transcript recorded only the model's one-line framing.
Asked "when is the summer holiday held?" right under a card listing that exact
email, the agent answered that it has no access to public calendars. The
assistant turn now carries a compact "[shown to the user]" index (sender,
subject, message id) alongside the answer, capped at 40 rows. Retested live:
the same question now answers from the email.

A 200-second turn told the user almost nothing. The activity area accumulated
"Step 1/50", "Thinking", "Step 2/50", "Thinking" and the model name — repeated
on every message — while the one informative line, the tool call and its query,
scrolled away. Status is now a single line replaced in place; loop mechanics,
the per-message model name and bare "Thinking" are dropped; completed tool
calls stay as evidence of work.

Tests cover both: that the transcript carries card rows and leaves ordinary
turns untouched, and that loop mechanics are filtered while real progress
("Triaged 7/25 — …", "Opening your browser…") still reaches the user.
Base automatically changed from claudia/task-5fcce79e to claudia/task-a140d608 July 27, 2026 09:03
Base automatically changed from claudia/task-a140d608 to claudia/task-fd276692 July 27, 2026 09:08
Base automatically changed from claudia/task-fd276692 to main July 27, 2026 09:18
@kovtcharov-amd
kovtcharov-amd dismissed their stale review July 27, 2026 09:18

The base branch was changed.

# Conflicts:
#	docs/guides/email-integration.mdx
#	docs/plans/email-agent-tui-port.md
#	docs/reference/cli.mdx
#	hub/agents/email/python/CAPABILITY_MATRIX.md
#	hub/agents/email/python/CONTRACT.md
#	src/gaia/daemon/sidecars/fetch.py
#	src/gaia/daemon/sidecars/install.py
#	src/gaia/mcp/servers/tui_mcp.py
#	tests/unit/test_daemon_hub_routes.py
#	tests/unit/test_tui_mcp.py
#	tui/internal/client/client.go
#	tui/internal/client/factory.go
#	tui/internal/client/sse.go
#	tui/internal/client/sse_test.go
#	tui/internal/event/canonical.go
#	tui/internal/event/canonical_parser.go
#	tui/internal/event/canonical_parser_test.go
#	tui/internal/ui/app.go
#	tui/internal/ui/chat/canonical.go
#	tui/internal/ui/chat/model.go
#	tui/internal/ui/oneshot.go
#	tui/internal/ui/root/model.go
@kovtcharov-amd
kovtcharov-amd added this pull request to the merge queue Jul 27, 2026
Merged via the queue into main with commit ee54b77 Jul 27, 2026
44 of 51 checks passed
@kovtcharov-amd
kovtcharov-amd deleted the claudia/task-272d716d branch July 27, 2026 09:30
kovtcharov added a commit that referenced this pull request Jul 27, 2026
Same stale-guard family as the schema ones: #2496 added check_mailbox_access
and setup_mailbox_access, and the exact-set assertion was not updated, so
Test Email Agent fails on main.
@itomek itomek mentioned this pull request Aug 10, 2026
8 tasks
pull Bot pushed a commit to bhardwajRahul/gaia that referenced this pull request Aug 13, 2026
# GAIA v0.23.0 Release Notes

GAIA v0.23.0 makes the agents easier to get, safer to run, and easier to
extend. You can now browse, install, and run agents straight from the
terminal with `gaia hub`, and add new capabilities to an agent as
signed, auditable skills. Under the surface it's a security release: the
local API and MCP bridge no longer expose themselves to the network by
default, the confirmation prompt that pauses an agent before it sends
mail, writes a file, or runs a command now works from the terminal and
over the local API and MCP — not just inside the graphical app — and an
agent can no longer quietly reach into your `~/.gaia` config or slip
crafted SQL into the database agent. Connecting a Microsoft account is
now an explicit Personal-or-Work/School choice with a zero-setup
sign-in.

**Why upgrade:**
- **Get agents from the terminal** — `gaia hub` browses, installs
(behind a trust prompt for unverified agents), runs, and removes agents
without leaving the shell.
- **Every agent asks before it acts** — the confirmation gate for
sending mail, writing files, and running commands now works in the
terminal, over the local API, and across MCP tools, not only in the
Agent UI.
- **Safer by default** — the MCP bridge binds to localhost, the local
API refuses credentialed cross-origin requests from arbitrary sites, MCP
servers launch without a shell, and an agent can't write into `~/.gaia`
or reach the database with crafted SQL.
- **Build and share skills safely** — `gaia skill` makes skills
first-class: create, import, sign with trust tiers, and audit them
before sharing. They're opt-in — you add the ones you want.
- **Connect a Microsoft account without a secret** — Personal and
Work/School are now separate connectors with device-code sign-in and no
client secret required.

<Note>
**The email agent is beta and CLI-first this release.** It runs locally
and never sends,
forwards, or deletes without your confirmation — that safety gate is
verified. This cycle
was mostly robustness and correctness: sturdier Outlook and calendar
handling, honest
reporting when a scan is truncated, and a long list of fixes (see Bug
Fixes). It's still
early — a full inbox triage can currently time out on larger mailboxes,
and autonomy is
experimental and not yet wired up in the packaged sidecar. Treat its
output as a draft to
review, and please report what you run into.
</Note>


## Breaking Changes

### `GAIA_MICROSOFT_TENANT` is gone

The Microsoft connector was split into two explicit connectors —
Personal and Work/School — each with its own hard-coded tenant, so the
`GAIA_MICROSOFT_TENANT` environment variable no longer does anything and
has been removed (PR [amd#2729](amd#2729)). If
you set it to work around the old single-connector tenant guessing, drop
it and pick the connector that matches your account instead (see
*Microsoft accounts* below).


## What's New

### Install and run agents from the terminal — `gaia hub`

Getting an agent used to mean the graphical app or a manual pip install.
Now the hub is in your shell: `gaia hub list` shows the catalog, `gaia
hub install <agent> --trust` installs one (the `--trust` is required for
an unverified agent — it will not install silently), and `gaia hub
uninstall <agent>` removes it. The install → run → uninstall round-trip
works end to end against the live catalog, with the trust prompt
actually enforced (PRs [amd#2484](amd#2484),
[amd#2530](amd#2530),
[amd#2708](amd#2708)). Try it: `gaia hub
list`.


### Every agent asks before it acts — beyond the Agent UI

The confirmation prompt that pauses an agent before a consequential
action — sending or deleting mail, writing a file, running a shell
command — used to work only inside the Agent UI; from a terminal, the
local API, or an MCP tool call those actions could run unprompted. This
release closes those paths: the gate now fires from a plain terminal,
through the `gaia api` server, and across MCP tool calls, classifying a
tool as read-only or mutating and failing closed when unsure. The agent
stops and asks before the action, declining leaves nothing changed, and
setting `GAIA_AUTO_APPROVE_TOOLS=1` in your environment is the explicit
way to opt out (PRs [amd#2475](amd#2475),
[amd#2544](amd#2544),
[amd#2846](amd#2846),
[amd#2854](amd#2854)).


### Safer by default — a security-focused release

Several local exposures are closed this release. The MCP bridge binds to
`127.0.0.1` by default instead of every interface, so it isn't reachable
from other machines on your network unless you pass a bind-all host, and
it can now require an `--auth-token` that is actually enforced rather
than ignored. The local API server no longer echoes an arbitrary origin
back with credentials allowed — a cross-origin request from a site that
isn't allow-listed is refused. MCP servers are launched without going
through a shell, so a server name can't smuggle shell metacharacters; an
agent can no longer write into your `~/.gaia` configuration; and SQL
supplied by the model is blocked from reaching the database agent's
statements rather than being executed (PRs
[amd#2246](amd#2246),
[amd#2238](amd#2238),
[amd#2344](amd#2344),
[amd#2844](amd#2844),
[amd#2847](amd#2847),
[amd#2860](amd#2860)).


### Build and share skills — `gaia skill`

A skill gives an agent a new capability from a folder with a manifest —
no new Python, no forking the agent. `gaia skill create <name>`
scaffolds one, `gaia skill import` adds a skill so an agent can discover
it, and `gaia skill list` / `info` show what's installed and the
permissions each one declares. Sharing is guarded: skills carry
signatures with trust tiers — an unsigned or untrusted skill is capped
at the lowest tier, and tampering is caught by checksum — a pre-publish
audit rejects a skill that attempts prompt injection or
`shell=True`/`eval`, and `gaia skill migrate` converts skills authored
in other formats. Skills are opt-in: no agent loads them automatically
yet, so you add the ones you want (PRs
[amd#2669](amd#2669),
[amd#2692](amd#2692),
[amd#2702](amd#2702),
[amd#2693](amd#2693)). Try it: `gaia skill
list`.


### Microsoft accounts: Personal and Work/School, no secret required

Connecting a Microsoft account is now two clear choices instead of one
connector guessing your tenant. `gaia connectors list` shows Microsoft
as two distinct connectors — Personal and Work/School — each with a
zero-setup device-code sign-in (a code and a URL to visit) and no client
secret required for a public app registration (PRs
[amd#2718](amd#2718),
[amd#2364](amd#2364)).


### Lemonade Server 11.5.0

This release runs against Lemonade Server 11.5.0 — the version installed
by `gaia init` and pinned across CI and the installer.


## Bug Fixes

A selection of the user-visible fixes this release — the full list is in
the changelog below.

- **Triage now paginates large inboxes and reports truncation honestly**
(PR [amd#2646](amd#2646)) — no more silently
dropping mail past a hidden limit.
- **Thread messages come back sorted and numbered** (PR
[amd#2570](amd#2570)) — "reply to 3" hits the
message shown at position 3, not raw backend order.
- **The inbox pre-scan stops reporting a guess as a verdict** (PR
[amd#2587](amd#2587)) — an uncertain
classification is surfaced as uncertain.
- **An email conversation survives its turns** (PR
[amd#2837](amd#2837)) — a follow-up question
keeps the session context instead of starting over.
- **A bare reconnect no longer guts a mailbox** (PR
[amd#2733](amd#2733)) — reconnecting an
account keeps its existing grants instead of wiping them.
- **Restore from Trash anytime** (PR
[amd#2542](amd#2542)) — undo an archive/trash
without a narrow time window; the dead permanent-delete path was
removed.
- **A low-priority sender no longer forces a promotional label** (PR
[amd#2774](amd#2774)) — sender priority stops
overriding the actual content classification.
- **The agent survives an OpenMP double-init** (PR
[amd#2508](amd#2508)) — a mid-conversation
native-library clash no longer kills the run.
- **Large tool results are truncated to valid JSON** (PR
[amd#2645](amd#2645)) — an oversized result no
longer produces unparseable output.
- **A missing model surfaces as a real 404** (PR
[amd#2245](amd#2245)) — the builder names the
missing model instead of a generic placeholder.
- **GPU is detected on all platforms and default_device is honoured**
(PR [amd#2244](amd#2244)).
- **A sidecar that is alive but has stopped serving is now detected**
(PR [amd#2707](amd#2707)) — a wedged agent
process is caught instead of hanging.
- **The model-slot lease is held across inference, not just the load**
(PR [amd#2394](amd#2394)) — a second agent
can't evict the model mid-generation.
- **A browser that never launched is surfaced** (PR
[amd#2507](amd#2507)) — a failed OAuth browser
open reports an actionable error instead of hanging.
- **Stop actually aborts in-flight streaming** (PR
[amd#2166](amd#2166)) — the Agent UI Stop
button ends generation immediately.


## Full Changelog

**331 commits** since v0.22.0:

<details>
<summary>Expand full changelog (331 commits)</summary>

- `96ce0d1e` — feat(email): recognize the third mailbox connector —
Gmail, Outlook personal, Microsoft work (amd#2896)
- `88ecc18e` — fix(email): scoped 'anything suspicious?' query no longer
dumps the full triage report (amd#2910)
- `1c75ccee` — fix(skills): reject version pins GAIA cannot read instead
of matching them (amd#2928)
- `29a5c364` — docs(plans): assess readiness for the generic gaia-agent
(amd#2926)
- `e3b28653` — fix(tui): stop cancel-then-resend from racing the
daemon's session lock (amd#2912)
- `b38f5e60` — fix(tui): anchor TTFT on first inference token, use real
token counts (amd#2911)
- `779d7b00` — fix(email): reply/draft/send actions no longer report
failure after they already succeeded (amd#2908)
- `f8d4610c` — ci(eval): queue eval runs instead of cancelling the
pending one (amd#2921)
- `611b1928` — feat(hub,skills): pre-publish security audit gate for
marketplace skills (amd#2702)
- `c83047b9` — feat(skills): gaia skill migrate — OpenClaw/Hermes skills
to GAIA format (amd#2693)
- `ca17067a` — test(skills,hub): cover the signing and lane checks
amd#2668/amd#2692 shipped untested (amd#2907)
- `a951567a` — fix(eval): drop the judge temperature pin the model now
rejects (amd#2905)
- `236f8a58` — feat(skills): gaia skill publish/install with
signature-backed security tiers (amd#2692)
- `d26b15da` — ci(eval): trigger the Gemma eval on the PR diff, not the
push (amd#2897)
- `be15258a` — fix(cpp): normalize CRLF so Windows-authored skills parse
identically (amd#2906)
- `84415b06` — feat(cpp): text extraction and chunking (amd#2822)
- `6352f264` — fix(database): make the SQL read-only authorizer disarm
on Python 3.10 (amd#2904)
- `4709a728` — feat(hub,skills): publish and serve skills as a
first-class hub catalog lane (amd#2668)
- `245cb72e` — fix(api): refuse approval-gated tools instead of
auto-approving them (amd#2854)
- `e803103a` — fix(mcp): enforce --auth-token on the MCP bridge instead
of ignoring it (amd#2844)
- `a6dc1fa3` — fix(ci): run the Gemma-4-E4B agent eval in PowerShell on
its Windows pool (amd#2773)
- `75b61f4f` — feat(cpp): SKILL.md format parser and validation (amd#2824)
- `b89c6414` — feat(cpp): gaia::HttpClient — a general HTTP client
abstraction (amd#2809)
- `5e6b1d43` — fix(security): launch MCP servers without a shell,
protect ~/.gaia from agent writes (amd#2847)
- `768be2e2` — feat(skills): ship a ten-skill starter pack with a guide
and honesty guards (amd#2697)
- `340bf2c2` — feat(cpp): interactive TUI — event loop, streaming
render, modals (amd#2825)
- `acaad161` — feat(cpp): native OpenAI tool calling and conversational
response mode (amd#2821)
- `23ae5d63` — feat(cpp): SQLite integration and gaia::Database (amd#2816)
- `4f2a6686` — feat(cpp): gaia::VectorIndex — flat vector index with
persistence (amd#2807)
- `b1a7e8df` — fix(email): ship with Agent Skills off until the eval
gate covers them (amd#2848)
- `34c26577` — ci(webui): run the Agent UI Vitest job on Node 22 so the
suite executes (amd#2898)
- `20036474` — docs: escape MDX-breaking literals so Mintlify validation
passes (amd#2903)
- `07b0858e` — ci(tui): run on stacked PRs, add -race, lint, and a
per-OS test matrix (amd#2696)
- `e19d46be` — feat(cpp): harden the coding toolbelt — stale-write
rejection, ignore-aware search, persistent shell (amd#2823)
- `7621b223` — ci(claude): move every Claude workflow to Opus 5 and run
the audits nightly (amd#2859)
- `d086faa4` — fix(claude): correct the agent/skill prompts and make
plain language the default (amd#2862)
- `990d19ff` — fix(cpp): gate MCP tools behind user confirmation in the
C++ SDK (amd#2851)
- `878d473f` — fix(mcp): gate MCP write tools behind the user
confirmation prompt (amd#2846)
- `c2c68064` — fix(security): stop LLM-supplied SQL reaching
DatabaseAgent statements (CWE-89) (amd#2860)
- `e936d3d9` — docs(release): rebuild v0.23.0 notes to hardware-verified
features only
- `be03c7c8` — fix(init): stop reporting success gaia init did not
deliver (amd#2889)
- `75804a15` — docs(dev): require --extra-index-url for every uv pip
install on Linux (amd#2878)
- `4a24d663` — ci(email): run the triage eval on PRs that touch the
email agent (amd#2849)
- `c3d9ec11` — fix(code): route orchestrated tool calls through the
confirmation gate (amd#2853)
- `c59e5d8a` — fix(hub): stop the terminal hub publishing against a core
that cannot serve it (amd#2712)
- `04f18294` — chore(deps-dev): bump the agent-ui-dependencies group in
/src/gaia/apps/webui with 6 updates (amd#2750)
- `ce4fcbf3` — fix(web): bracket pinned IPv6 addresses in URLs (amd#2739)
- `e7e93287` — fix(installer): install the terminal hub on macOS and
Windows (amd#2708)
- `3cda9f77` — feat(cpp): MCP server registry — resolve server ids from
mcp.json (amd#2820)
- `c09764da` — fix(ci): validate the STX CMake cache instead of trusting
bin/cmake.exe (amd#2818)
- `616014d7` — docs(plans): scope C++ framework parity for
domain-specific agents (amd#2806)
- `f6433039` — ci(security): audit allowlist soundness + add PSIRT/CVSS
triage skill (amd#2752)
- `5c9f2b60` — chore(deps): bump the github-actions group with 2 updates
(amd#2751)
- `c103f6cd` — feat(discovery): real macOS and Linux branches for
day-zero scanners (amd#1956) (amd#2747)
- `e7e362e1` — chore(deps-dev): update mcp requirement from
\<2.0,>=1.1.0 to >=1.1.0,\<3.0 in the python-dependencies group (amd#2749)
- `ca9e5461` — chore(deps): bump the root-npm-dependencies group with 2
updates (amd#2748)
- `f62ca240` — fix(website): unbreak the Railway deploy, red since July
31 (amd#2855)
- `88d130ec` — feat(email): render the triage list from the scan, not
from the model (amd#2858)
- `bf9eb183` — ci(windows): pin ffmpeg version to drop the gyan.dev
dependency (amd#2852)
- `c531ca69` — fix(email): a conversation now survives its turns —
session_id on /query (amd#2837)
- `bcde95ee` — fix(email-agent): stop meeting/invite answers from
inventing what tools never said (amd#2833)
- `162274a5` — fix(tui): 'triage my inbox' draws its card under the
question again (amd#2845)
- `bbf69fd6` — feat(email,skills): bundled skills + account-keyed
skill-set selection (amd#2695)
- `abed9f1c` — fix(security): close find -exec / write side-doors in
shell command whitelist (CWE-184) (amd#2740)
- `0d1258a1` — fix(email-agent): get_thread renders a table card instead
of relying on model prose (amd#2788)
- `300163dd` — fix(email-agent): search_messages defaults to
metadata-only, fixing context overflow on counting questions (amd#2782)
- `cc331b78` — fix(email): low-priority-sender match no longer forces
PROMOTIONAL (amd#2774)
- `68736511` — fix(email): meeting proposal in a confidently-classified
message no longer vanishes from needs_you (amd#2779)
- `e135b8ef` — fix(ci): post PR reviews from the workflow instead of
hoping the model does (amd#2719)
- `efd3812b` — feat(email/tui): resolve "reply to 1" to the message the
card actually shows (amd#2761)
- `ed70db73` — fix(email-agent): search_messages states an exact, stable
message count (amd#2760)
- `42fdfdcd` — fix(email/tui): one triage card that tells you what to
do, not what was classified (amd#2757)
- `3803aa06` — fix(email): stop leaking classifier internals into triage
card rationale (amd#2754)
- `2c11ac0b` — fix(tui): sanitize agent error text (amd#2753)
- `9bf0042a` — docs(tui): rewrite the terminal hub README for newcomers
(amd#2717)
- `1cdd9746` — docs(skills): implementation spec for Agent Skills v2
adaptive skills (amd#2685)
- `50a5b862` — fix(website): pin the deploy toolchain so CI and
production build alike (amd#2691)
- `500cdac6` — fix(mcp): cap the mcp dependency below 2.0 (amd#2694)
- `3686e069` — docs(guides): document the terminal hub, and how to
actually get it (amd#2698)
- `99508ec5` — docs(plans): preserve the TUI packaging design and
binary-distribution plan (amd#2699)
- `21c02f69` — fix(hub): mark the email agent verified so first install
is not refused (amd#2703)
- `8d07ee3d` — docs(website): correct every deployment instruction in
the README (amd#2689)
- `dff8f1b2` — refactor(memory): rename synthesis Skill dataclass to
DistilledProcedure (amd#2684)
- `d2832fd5` — # feat(email): opt-in on-device SLM classifiers for
phishing and triage category (amd#2568)
- `a02f2f0c` — fix(connectors,email): a bare reconnect no longer guts a
mailbox (amd#2733)
- `bc0d4632` — fix(website): reach the hero terminal, marquee, and code
blocks by keyboard (amd#2706)
- `82635362` — fix(website-router): track apex Worker, fail loudly on
origin errors (amd#2688)
- `5d269094` — feat(skills): SKILL.md loader, validator, discovery +
gaia skill CLI core (amd#2669)
- `0ae019c8` — fix(daemon): detect a sidecar that is alive but has
stopped serving (amd#2707)
- `4551ec5b` — fix(website): stop offering Intel Macs a DMG they cannot
run (amd#2701)
- `bc8a7e29` — docs(spec): Gatekeeper blocks the browser download, not
curl | sh (amd#2732)
- `0d170869` — fix(website): make the hub agent rows readable in both
themes (amd#2711)
- `d5ae430d` — fix(tui): a consequential readiness check now holds the
screen (amd#2731)
- `d6bd047f` — fix(email): stop Gmail 429-ing every scan — chunk at 25
and retry the rate limit (amd#2727)
- `93216bc0` — chore(connectors): remove GAIA_MICROSOFT_TENANT — dead
since the connector split (amd#2729)
- `e3a6958a` — fix(tui): show a failed tool's own error instead of an
"Invalid card" box (amd#2726)
- `6efc520b` — fix(email-agent): surface connector errors from autonomy
runs instead of a bare HTTP 500 (amd#2640)
- `f0543406` — feat(connectors): split Microsoft into Personal +
Work/School connectors (amd#2718)
- `b8a97cbb` — fix(connectors): stop requiring a client secret from
secretless public PKCE clients (amd#2630)
- `70e4eb12` — fix(website): stop a published agent rendering twice on
the hub page (amd#2690)
- `282e30e0` — fix(tui): name the binary the user actually invoked, and
fix the setup hint (amd#2700)
- `d95866ff` — fix(installer): repair the Lemonade download URLs and add
macOS support (amd#2704)
- `3bc0b612` — fix(hub): stop components publishing against a core that
cannot serve them (amd#2705)
- `c068166d` — Email Triage draft/proposal SDK (amd#2551)
- `cb90bab3` — fix(email): priority senders never force urgent;
informational tail auditable (amd#2658)
- `ab41b115` — fix(email): stop the assistant from narrating what the
turn's tools don't support (amd#2659)
- `7a93bfbe` — fix(email-agent): propagate the autonomy kill switch to
the scheduler (amd#2657)
- `4de65cff` — fix(email-agent): require the conflict tool for conflict
verdicts (amd#2656)
- `84404a6e` — perf(email): metadata-first scan + read-mail pre-scan
coverage (amd#2661)
- `733822e2` — fix(tui): restore the attention card on direct chat
--agent launches (amd#2655)
- `d4d5a7cc` — fix(mcp): cap the mcp dependency below 2.0
- `1d867eed` — fix(email): strip infrastructure banners from bodies
before the prompt (amd#2650)
- `22d8b2d4` — fix(email-agent): kill a running autonomy cycle and keep
partial reports (amd#2652)
- `a4f4b39b` — fix(email): paginate the triage scan and report
truncation honestly (amd#2646)
- `7adc4882` — fix(tui): dedup attention rows and stop the card
interrupting turns (amd#2648)
- `24e8d5ab` — fix(email): thread summaries keep the newest message's
open asks (amd#2644)
- `c3ef3796` — fix(agents): truncate large tool results to valid JSON
(amd#2645)
- `d633b387` — docs(connectors): document which Google scopes to declare
in the Console (amd#2612)
- `4ad367a9` — fix(connectors): derive --grant-agent scopes from the
agent's own declaration (amd#2610)
- `f7d5d7d9` — chore(deps): bump the root-npm-dependencies group across
1 directory with 3 updates (amd#2503)
- `c84d9a79` — fix(tui): make the terminal hub readable on a light
terminal background (amd#2611)
- `bcffbc8d` — feat(email): recover the attention view and
waiting-on-you detector onto main (amd#2604)
- `b63a9f0d` — feat(email): guided Outlook mailbox setup — walk, verify,
and answer questions in chat (amd#2598)
- `e0d5014c` — fix(daemon): dev-mode start-agent refuses a checkout
mismatch instead of silently serving a stale build (amd#2592)
- `29dc0f9f` — feat(email): find meeting proposals during the inbox scan
(amd#2589)
- `22b0fa04` — fix(email): pre-scan stops reporting a guess as a verdict
(amd#2587)
- `c211e870` — fix(daemon): start the daemon clock so scheduled work can
fire (amd#2586)
- `358fd6e1` — fix(email-agent): add preference removal tools and a
truthful read-back (amd#2520) (amd#2541)
- `d01b9d43` — fix(email): don't cancel a retrying agent for a
recoverable tool error (amd#2572)
- `7fdcd6b2` — fix(email): surface degraded memory state and diagnose
the real cause (amd#2577)
- `1c42d842` — fix(email): autonomy /run refuses while off; add gaia
email autonomy CLI (amd#2578)
- `25b6cacd` — fix(email): draft_reply/draft_forward compose the body,
don't ask for it (amd#2576)
- `a050c64d` — fix(email): briefing carries a structured breakdown, not
one sentence (amd#2575)
- `8da002f0` — fix(email): resolve relative snooze/schedule times
agent-side (amd#2574)
- `ee4af04b` — fix(email): get_thread returns messages sorted and
numbered, not raw backend order (amd#2570)
- `9269a306` — fix(email): give list_inbox/search_messages a combined
envelope budget (amd#2546)
- `c4495a0f` — fix(agents): dispatch Python-call-style embedded tool
syntax (amd#2573)
- `f6103a03` — feat(email-agent): broaden autonomy candidates, add undo
surface and per-message decisions (amd#2545)
- `d975853c` — fix(email): normalize calendar time bounds to RFC 3339
before Google (amd#2579)
- `fdf665f0` — feat(website): redesign landing, Agent Hub, and agent
detail pages (amd#2566)
- `000ab88e` — fix(ui): reject null bytes in upload-path; unrot 8 stale
UI/journey tests (amd#2565)
- `0d6ca966` — feat(tui): tool-confirmation modal for
destructive/external actions (amd#2544)
- `4131420a` — fix(agent): recover from context overflow on
NPU/FastFlowLM (amd#2543)
- `35062526` — fix(email): restore from Trash anytime, drop dead
permanent_delete (amd#2542)
- `2a1767e0` — chore(release): bump the hub component manifests to
0.23.0
- `b5d5bf54` — feat(hub): publish the terminal hub and Agent UI as R2
hub packages (amd#2530)
- `014cbcbc` — fix(email): catch the contract guards up to schema 2.6,
and de-race the heartbeat test (amd#2549)
- `9f70be13` — fix(ui): serve the Lemonade start hint instead of
hardcoding a dead command (amd#2510)
- `26127e80` — feat(release): publish the terminal hub binary and
install it (amd#2522)
- `59374508` — fix(tui): unbreak build_tui on main — allowlist the
bare-host remedy (amd#2548)
- `1b189215` — fix(daemon): unbreak Unit Tests on main — remedy
docstring names an unparseable command (amd#2534)
- `9bdd7eb3` — docs(release): name the Lemonade version v0.23.0 actually
ships (amd#2509)
- `21d40c33` — fix(llm): detect Lemonade on macOS; stop printing
commands that don't work (amd#2497)
- `e272fbae` — fix(tui): keep a valid hub selection when switching tabs
(amd#2482)
- `1d0454b6` — fix(tui): prove the mailbox is usable before the gate
clears a launch (amd#2494)
- `ee54b779` — feat(email): agent-led mailbox onboarding — the agent
sets up its own access (amd#2496)
- `cc73e244` — fix(tui): stop the hub offering agents it cannot run or
launch (amd#2492)
- `5b378ebe` — fix(tests): make unit suite hermetic by blocking real
network connections (amd#2500)
- `238fe9ac` — feat(tui): install, run and uninstall agents from the TUI
(amd#2484)
- `ce730876` — feat(tui): draw tool_result render cards, starting with
the inbox pre-scan (amd#2485)
- `7fdd43ea` — chore(deps-dev): bump electron from 43.1.1 to 43.2.0 in
/src/gaia/apps/jira/webui in the jira-app-dependencies group (amd#2501)
- `c940fd0a` — chore(deps-dev): bump electron from 43.1.1 to 43.2.0 in
/src/gaia/apps/example/webui in the example-app-dependencies group
(amd#2502)
- `7f569605` — feat(daemon): install, uninstall and catalog hub agents
from the daemon (amd#2477)
- `61ac101c` — docs(plans): design the TUI user journey around the email
agent (amd#2480)
- `fd66ba95` — fix(email): fail loudly on a dead worker and reconcile
the package docs (amd#2479)
- `1340b3b7` — fix(agents): actually ask before running
confirmation-gated tools (amd#2475)
- `5f283e10` — feat(tui): control API + MCP server for driving the live
TUI (amd#2478)
- `12016d5d` — feat(tui): stream agents over the daemon HTTP/SSE relay
(amd#2476)
- `554ef27c` — chore(deps): bump electron from 43.1.1 to 43.2.0 in
/hub/agents/emr/python/gaia_agent_emr/dashboard/electron in the
emr-dashboard-dependencies group (amd#2504)
- `4822e2ed` — chore(deps-dev): bump the agent-ui-dependencies group in
/src/gaia/apps/webui with 6 updates (amd#2505)
- `3fc0d2e1` — chore(deps): bump the github-actions group with 4 updates
(amd#2506)
- `47c2d1b2` — fix(connectors): surface a browser that never launched
(amd#2507)
- `1763606f` — fix(agents): stop the OpenMP double-init from killing the
agent mid-conversation (amd#2508)
- `56215070` — fix(hub): carry requirements.min_lemonade_version through
the manifest parser (amd#2493)
- `02cf9984` — fix(mcp): keep console logs off stdout in stdio
transports (amd#2473)
- `d6c02c2f` — chore(deps): bump Lemonade Server to v11.5.0 (amd#2424)
- `2898f1b5` — fix(agents): don't dedup errored mutation retries (amd#2464
batch dead-end) (amd#2465)
- `867bc677` — fix(email): recall last archive batch so undo reaches
across turns (amd#2458)
- `9073ec20` — fix(email): strip LLM quoting from ARCHIVE_MESSAGE_BATCH
ids (amd#2457)
- `529d11b1` — ci(review): allow fork-PR checkout under
pull_request_target (checkout@v7) (amd#2461)
- `524d3282` — fix(email-agent): make undo window configurable for
chat-speed bulk ops (amd#2449)
- `61dc3a1f` — fix(email-agent): surface actionable Lemonade-down copy
in gaia email -q (amd#2453)
- `d78115a9` — fix(memory): guard against self-supersede hiding recalled
preferences (amd#2452)
- `6bf96e56` — fix(email-agent): isolate per-provider failures in read
fan-out (amd#2451)
- `acd20400` — fix(agents): reject unexpected tool kwargs with a
structured error (amd#2450)
- `f99ccaea` — fix(daemon): drop --reload from dev-mode email sidecar
spawn (macOS) (amd#2442)
- `c2337178` — fix(email-agent): verify archive left inbox + fix
same-day search miss (amd#2438)
- `3f6af0ff` — fix(email): don't misclassify timeouts as Lemonade-down
(amd#2139 follow-up) (amd#2454)
- `9386e1b1` — fix(email-agent): resolve draft/reply target from sender
or topic (amd#2403) (amd#2437)
- `229e61da` — fix(email-agent): never auto-archive IMPORTANT /
security-sender mail (amd#2426) (amd#2435)
- `e6ac57f9` — fix(email-agent): persist preferences to state.db so they
survive without the embedder (amd#2427) (amd#2434)
- `4301c897` — fix(agent-email): actionable copy for Lemonade-down
/query errors (amd#2432)
- `1bb2cccd` — fix(daemon/email-agent): dev-mode sidecar 'Empty module
name' on macOS (bad PYTHON_KEYRING_BACKEND) (amd#2443)
- `7a44bfd5` — fix(email): bulk-archive undo survives the whole run via
a per-turn batch handle (amd#2439)
- `5101b853` — fix(daemon): re-forward OAuth tokens on expiry so
sidecars self-recover (amd#2436)
- `55010368` — fix(email-agent): applying an existing label fails with
'Invalid label' (T14) (amd#2433)
- `ce83feb9` — fix(email-agent/ui): de-jargon the send confirmation
surface (amd#2407)
- `4bb196fb` — fix(connectors): name consumers-tenant migration on
personal-account app rejection (amd#2391)
- `6b29a4d6` — fix(email-agent): construct with zero connectors instead
of 502 (amd#2423)
- `f3af7bff` — fix(agent-ui): treat ctx_size=0 as unknown, not a
too-small window (amd#2402)
- `f22b0f4a` — fix(ci): run evidence stage via direct claude CLI, not
the GitHub-coupled action (amd#2430)
- `60334202` — fix(email-agent): add live mailbox connection-status tool
(amd#2405)
- `4c370b23` — fix(ui): surface actionable sidecar HTTP errors instead
of generic crash card (amd#2422)
- `7fc559af` — test(ci): evidence lane exercises UI-backed routes +
spot-regresses adjacent ops (amd#2421)
- `9be31036` — security(ci): harden the evidence stage against env dumps
/ credential flows (amd#2417)
- `0f709733` — fix(daemon): validate custody rag/query 'k' to a bounded
positive int (amd#2390)
- `ca50bf8a` — ci(review): broaden evidence gate + require a
verdict-linked evidence section (amd#2415)
- `59c736df` — ci(review): fold gaia-testing evidence into the PR review
comment (Phase 1: CLI/API/MCP) (amd#2414)
- `1a233d94` — test(connectors): fix main red — amd#2408 install test vs
amd#2410 trust gate (amd#2412)
- `78de5bfd` — fix(electron): resume periodic update checks after a
no-feed start (amd#2389)
- `a20c04b4` — fix(agent-ui): left-align installed-agent hub cards on
home screen (amd#2398)
- `236ba233` — fix(connectors): register hub-installed sidecar agents so
email grant works on fresh install (amd#2411)
- `b343f396` — fix(security): harden install trust gate and
analyze_data_file sandbox (amd#2410)
- `765cb544` — fix(daemon): hold the model-slot lease across inference,
not just the load (amd#2394)
- `27be002e` — fix(onboarding): neutral NPU wording in first-run
Hardware check (amd#2399)
- `c86a46e5` — docs(release): note email is Linux + API/CLI only this
release (Windows amd#1648)
- `e658cb15` — docs(testing): bind real-world evidence contract to the
changed surface (amd#2376)
- `feb00155` — docs(email): correct earn-trust claim — positive-outcome
accrual not yet wired (amd#2392)
- `7aee219a` — fix(email): commit autonomy dedup INSERT so it survives
headless teardown (amd#2393)
- `6b6a8dab` — fix(daemon): map NotGrantedError to 403 in forward_all
route (amd#2395)
- `5b2e6be8` — docs(release): keep only verified user-facing features in
What's New
- `e11740e7` — docs(release): fix Agent UI heading — drop 'update'
(auto-update was trimmed)
- `a011e33d` — docs(release): trim v0.23.0 notes to features verified
working
- `ce5ddd84` — docs(release): correct v0.23.0 notes to match verified
behavior
- `c62a3e27` — Release v0.23.0
- `5f624706` — fix(hub): importable CLI wheel agents + chat distribution
via gaia init (amd#2373)
- `3ad39e60` — fix(sidecar): actionable user-mode binary error (amd#2347)
(amd#2357)
- `bfc0f5b8` — fix(lemonade): recognize grouped amd_gpu/nvidia_gpu
device keys in validation (amd#2368)
- `0553c8b0` — feat(email): full autonomy — earn-trust engine, learning
loop, scheduled driver (amd#2363)
- `0284300b` — feat(connectors): support work/school Outlook +
zero-setup device-code sign-in (amd#2364)
- `1aaba5cc` — feat(lint): require security suppressions to be reviewed
in an allowlist (amd#2343)
- `19b1a4c5` — test(daemon): de-flake sidecar stop test on the
pid-liveness check (amd#2349)
- `174e9d0c` — refactor(chat): extract ProfileSpec, honest manifest,
lazy RAG — one class → separable profiles (amd#2323) (amd#2362)
- `57f970b0` — chore(audit): drop the security dimension from the weekly
audit (amd#2348)
- `23181040` — feat(security): proactive Claude security-audit workflow
+ CVSS/SARIF tooling (amd#2346)
- `d605caec` — fix(security): enforce --allowed-paths sandbox on file
read tools (amd#2344)
- `1c8a91c9` — fix(security): remove pre-existing bandit HIGH findings
and enable the HIGH gate (amd#2350)
- `4b5c16b7` — ci(labeler): add tui/daemon/sidecar auto-label rules
(amd#2356)
- `a905b057` — chore(deps): bump the github-actions group with 2 updates
(amd#2341)
- `b3f793af` — fix(routing): default unknown language to TypeScript, not
a process kill (amd#2337)
- `48286dbd` — fix(init): stop Rich eating bracketed tokens in gaia init
output (amd#2340)
- `638a7643` — docs(skills): add porting-agent-to-hub — the legacy-agent
port flow (amd#2338)
- `d2c00b55` — fix(hub): harden agent-archive extraction against path
traversal (amd#2342)
- `a4417656` — fix(ci): repair the startup-failing GAIA CLI aggregate
workflow (amd#2307)
- `0835a250` — fix(ci): make the weekly eval and runner heartbeat
monitor actually run (amd#2306)
- `977c158f` — fix(cli): add --layout hub to gaia agent init for the
agent-first hub tree (amd#2295)
- `558a73e6` — chore(deps): bump the github-actions group with 3 updates
(amd#2294)
- `f8bff000` — chore(deps-dev): bump electron from 43.1.0 to 43.1.1 in
the root-npm-dependencies group (amd#2292)
- `fb179e32` — ci(eval): gate the Gemma-4-E4B consolidation on agent
evals (amd#2283)
- `6b2aa2db` — chore(deps-dev): bump electron from 43.1.0 to 43.1.1 in
/src/gaia/apps/jira/webui in the jira-app-dependencies group (amd#2291)
- `5837bdac` — chore(deps-dev): bump electron from 43.1.0 to 43.1.1 in
/src/gaia/apps/example/webui in the example-app-dependencies group
(amd#2290)
- `3ce4f790` — test(agents): stub live Lemonade probe in
context-overflow tests (amd#2288)
- `1886a92f` — chore(deps): bump electron from 43.1.0 to 43.1.1 in
/hub/agents/emr/python/gaia_agent_emr/dashboard/electron in the
emr-dashboard-dependencies group (amd#2289)
- `0a308449` — feat(agents): consolidate every agent onto Gemma-4-E4B at
one context size (amd#2284)
- `4392e06a` — chore(deps-dev): bump the agent-ui-dependencies group in
/src/gaia/apps/webui with 3 updates (amd#2293)
- `d9b11ec7` — refactor(hub): agent-first layout —
hub/agents/\<id>/\<lang> (amd#2060)
- `5f15b333` — feat(daemon): broker-wire the remaining direct model-load
surfaces (amd#2286)
- `3affe149` — fix(ui): detect GPU from real Lemonade payload shapes
(amd#2285)
- `4f888b78` — fix(hub): report file-based custom agents' real health,
not always 'error' (amd#2277)
- `36c29873` — fix(cli): detect GPU on all platforms and honour
default_device (amd#2244)
- `6c6f8a34` — test(audit): coverage for schedule CLI, perf-vis, VLM
extraction, PDF gen/export; fix silent table-row loss (amd#2259)
- `2425afe8` — fix(builder): surface model-not-found (404) instead of a
generic placeholder (amd#2245)
- `3a0eebe7` — ci(hub): wire nine hub package test suites into CI; fix
.cjs docs-link guard gap (amd#2258)
- `3e82ca61` — docs(connectors): correct the client_id_hash claim in the
OAuth runbook (amd#2264)
- `9f7dca84` — fix(ci): fix Lemonade startup and bash-on-PATH in the doc
walkthrough (amd#2281)
- `53f4b529` — fix(daemon): owner-only DACL for the Windows launch
secret (amd#2250) (amd#2282)
- `ee8825fa` — fix(security): require allowed_dir in compute_file_hash
(amd#2280)
- `46fb7687` — spec(factory): define the dogfooding loop — Claudia as
live validation runtime (amd#2234)
- `1a679f02` — feat(ui): always-available OAuth client field in Settings
(amd#2104 interim) (amd#2265)
- `45c796b1` — test(jira): unit tests for JiraAgent HTTP boundary and
config discovery (amd#1991) (amd#2263)
- `bed702eb` — feat(ci): execution-based weekly doc walkthrough (amd#2278)
- `f8f309e6` — fix(ui): stop silently accepting unimplemented agent_mode
'autonomous' (amd#2257)
- `3b0938ed` — fix(ci): weekly audit cross-links the prior parent
instead of auto-closing it (amd#2254)
- `78306480` — test(audit): risk-bearing coverage — jira, trust-gate,
flag-precedence, amd#1655 boundaries (amd#2253)
- `1aa39ca2` — fix(observability): real system-metrics polling + real
rollbackAction (amd#2251)
- `384b3ca8` — fix(security): bind MCP bridge to loopback, not all
interfaces (amd#2246)
- `037f4370` — fix(cli): implement/gate stubbed api-status, eval flags,
schedule --skill (amd#2247)
- `2b6ed53e` — fix(security): rate-limit exposed routes, harden JS
ReDoS/XSS/cleartext-logging (amd#2237)
- `6c6b3baf` — fix(packaging): stop the amd-gaia[agents] extra from
downgrading the core wheel (amd#2262)
- `8744a51a` — fix(security): validate user-influenced file paths
(py/path-injection) (amd#2252)
- `c921fa7b` — feat(email): quality + robustness batch
(amd#2110/amd#2113/amd#2114/amd#2115/amd#2116) (amd#2192)
- `b9391ec2` — fix(security): parameterize SQL, redact sensitive logs,
harden ReDoS regexes (amd#2239)
- `09e0bac4` — fix(security): stop leaking stack traces at API
boundaries + least-privilege workflow permissions (amd#2236)
- `3df5a3db` — fix(security): tighten API CORS — no wildcard origin with
credentials (amd#2238)
- `a3db04ef` — feat(connectors): OAuth forward-out to sidecars (V2-14)
(amd#2203)
- `e0886cbf` — refactor(daemon): reconcile the clocks into one
daemon-owned scheduler (V2-15) (amd#2199)
- `15459153` — feat(daemon): /host/v1 custody API v1 with per-agent
scoping (V2-12) (amd#2197)
- `e226d584` — feat(webui): group session sidebar by agent +
session-state polish (amd#2193)
- `6e48cb22` — feat(daemon): host-owned model-slot broker serializes
loads (V2-11) (amd#2194)
- `c42bb354` — fix(website): correct hub install command and clarify
agent availability (amd#2207)
- `2f18de85` — fix(tests): align email CLI dispatch test with amd#2191
thin-client contract (amd#2209)
- `a2ad4eff` — fix(tests): repair stale gaia.ui.email_sidecar.manager
import after amd#2144 (amd#2208)
- `2b89643c` — fix(webui): repair broken main — AgentHubView imported
deleted AgentHubGrid (amd#2206)
- `68773d46` — test(eval): sidecar eval harness + distributed-seams
suite (V2-19) (amd#2202)
- `d7cffc72` — feat(agent-ui): first-run onboarding wizard — hardware
pre-flight, in-app model download, connect-on-install (amd#2204)
- `5bc7d325` — feat(electron): in-app install, R2 auto-update feed,
gaia:// deep links (amd#2196)
- `eb6b34bf` — feat(daemon): one-time versioned migration of ~/.gaia
state (V2-13) (amd#2200)
- `73b8f98c` — refactor(api): remove the last in-process email mount;
relay via daemon (amd#2176) (amd#2205)
- `e4ea0e33` — feat(connectors): grant the mailbox to the email agent in
the same connect flow (amd#2195)
- `2bf405db` — feat(webui): in-app Hub page with catalog lanes + install
trust gate (amd#2201)
- `459efba9` — feat(api): relay /v1/\<agent>/query through the daemon
(V2-17) (amd#2198)
- `ada5b95a` — feat(cli): gaia email attaches to the daemon — thin
client (V2-8) (amd#2191)
- `a49d257d` — fix(webui): reachable Agent Hub + installed-agent
discovery and per-session picker (amd#2190)
- `7c98b51a` — docs(connectors): rewrite the Google client-ID
walkthrough for the current console (amd#2189)
- `a94a8126` — feat(daemon): deliver sidecar launch secret via 0600
file, not bare env (amd#2149) (amd#2186)
- `5601e2a5` — feat(daemon): streaming SSE reverse-proxy for agent
routes (amd#2150) (amd#2188)
- `1465bc73` — feat(agents): proactive lifecycle hooks with
approval-gated proposals (amd#1484) (amd#2187)
- `2c4ff395` — test(chat): mirror the amd-gaia floor guard from amd#2169;
harden version parsing (amd#2184)
- `2271278c` — fix(llm): make swallowed model-load failures loud in
_ensure_model_loaded (amd#2185)
- `2e0294f9` — feat(hub): add multi-component type discriminator to the
manifest (amd#1716) (amd#2183)
- `b78b71d1` — test(chat): guard the amd-gaia dependency floor against
amd#2112 regression (amd#2174)
- `33caec54` — refactor(ui): extract _best_effort_cancel helper for the
relay cancel paths (amd#2173)
- `ba58e31a` — feat(email): fast dev-iteration loop for the email agent
SDK (amd#2083)
- `f79c4177` — fix(webui): pin explicitly-set session titles — stop
auto-retitle churn (amd#2165) (amd#2171)
- `ae741690` — fix(email): error on an explicitly-targeted unconnected
mailbox instead of substituting (amd#2164) (amd#2172)
- `4dafa091` — fix(email): normalize date operators in search_messages
(amd#2161) (amd#2170)
- `dd2fa328` — fix(email): raise amd-gaia floor to match
get_embedding_model_for_device (amd#2112) (amd#2169)
- `011b6e3c` — fix(email): default calendar list to a forward window
when range args are absent (amd#2162) (amd#2168)
- `ed7d2969` — fix(ui): propagate cancel to the email sidecar on relay
timeout/crash (amd#2158) (amd#2167)
- `d8daf66d` — fix(agent): abort in-flight streaming generation on
Agent-UI Stop (amd#2166)
- `c907f6c4` — feat(memory): adaptive, review-gated onboarding
conversation (amd#1955) (amd#2143)
- `def5e979` — fix(ui): stop blaming Lemonade for cancelled/empty chat
turns (amd#2141)
- `553cb541` — fix(email): consolidate Agent UI pre-scan across every
connected mailbox (amd#2129)
- `5446af2c` — fix(agent-email): drop $orderby from Outlook get_thread
to avoid Graph InefficientFilter (amd#2140)
- `8ba66f4d` — fix(email): REST triage honors the LLM's is_spam verdict
(amd#2125)
- `aa44c2c8` — refactor(daemon): daemon-supervised agent sidecars (V2-6)
(amd#2144)
- `ac3cc4cd` — fix(eval): fail the briefing eval loudly on a
zero-case/zero-judged run (amd#2123)
- `c9566b19` — chore(deps): bump Lemonade Server to v11.0.0 (amd#2130)
- `47c1142a` — feat(ui): route email chat through the sidecar
/v1/email/query loop (V2-10) (amd#2136)
- `b340427d` — docs(skill): correct the release skill against what
v0.22.0 actually did (amd#2133)
- `9f1ece0f` — feat(webui): render→component map + generic render
primitives (V2-9) (amd#2131)
</details>

Full Changelog:
[v0.22.0...v0.23.0](amd/gaia@v0.22.0...v0.23.0)

---

## How these notes were verified

Every **What's New** entry was exercised on real hardware, not inferred
from green CI. Re-verified end to end after merging current `main` (334
commits since v0.22.0), because the delta touched the MCP bridge, hub,
skills, and connectors — the exact surfaces the notes assert.

<details>
<summary>Verification results, what broadened, and what was
excluded</summary>

**Re-verified against the current pin and kept:** `gaia hub`
install/run/uninstall (trust gate enforced); the confirmation gate now
covering terminal + `gaia api` + MCP tool calls (fail-closed classifier
proven model-free — amd#2846/amd#2854); localhost binding + stricter CORS +
enforced MCP `--auth-token` + shell-less MCP launch + `~/.gaia` write
guard + CWE-89 SQL block (amd#2844/amd#2847/amd#2860, live probes); `gaia skill`
create/import/list/info plus signed-tier + pre-publish-audit + migrate
(amd#2692/amd#2702/amd#2693); the Microsoft connector split (two connectors,
secretless PKCE — amd#2896's third connector is Gmail, so the wording is
not stale).

**Broadened this cycle:** the confirmation-gate claim (blocking gap
amd#2846 merged) and the security section (four verified hardening items).
The skills section is stated **opt-in** (amd#2848: no shipped agent loads
skills by default).

**Removed:** the onboarding / day-zero discovery headline — the scanner
works on macOS, but the first-run onboarding flow it feeds is not ready
to ship, so it is not claimed (the underlying commit amd#2747 remains in
the changelog).

**Excluded (not claimable):** a skills-by-default claim (false — amd#2848);
the C++ SDK (real and CI-green but ships no user-facing artifact and
predates this release); the prebuilt terminal-hub binary ("not yet
distributed"). Claims that failed live verification remain filed as open
bugs, not shipped: amd#2883, amd#2884, amd#2885, amd#2893, amd#2894. Email ships as a
beta note whose only asserted behavior — never sending or deleting
without confirmation — is verified.

</details>

## Release checklist
- [x] `util/validate_release_notes.py` passes
- [x] `docs && npx mintlify validate` passes
- [x] `src/gaia/version.py` → `0.23.0`; `LEMONADE_VERSION` → `11.5.0`
- [x] webui `package.json` / `package-lock.json` → `0.23.0`
- [x] Navbar label → `v0.23.0 · Lemonade 11.5.0`
- [x] All 334 commits in range represented in the changelog
- [x] Branch merged current with `main`; `setup.py` keeps `mcp<2.0`
- [ ] Review from @kovtcharov-amd addressed

---------

Co-authored-by: Kalin Ovtcharov <kalin@extropolis.ai>
Co-authored-by: k <k@e>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent::email Email agent changes agents cli CLI changes daemon Daemon supervisor / sidecar control plane devops DevOps/infrastructure changes documentation Documentation changes eval Evaluation framework changes mcp MCP integration changes performance Performance-critical changes sidecar Agent sidecar contract / harness tests Test changes tui Go terminal UI (gaia-tui)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(email): agent-led connector onboarding — let the agent set up mailbox access itself

2 participants