@amd-gaia/gaia 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -23,7 +23,8 @@ Two ways in:
23
23
  > playbook: how *you* wire this package into an app. The agent separately loads
24
24
  > **Agent Skills** into its own prompt at runtime from
25
25
  > `gaia_agent/skills/<name>/SKILL.md`. Same filename, different artifact —
26
- > don't ship this one as an agent skill. See [Skills](#10-skills--opt-in-and-empty-in-010).
26
+ > don't ship this one as an agent skill. See
27
+ > [Skills](#10-skills--one-always-on-the-rest-opt-in).
27
28
 
28
29
  ## 1. Install
29
30
 
@@ -51,7 +52,10 @@ built-in `fetch`. Use `import`, not `require`; from CommonJS use
51
52
  each against the lock**.
52
53
  4. Installs the sidecar into `~/.gaia/agents/gaia/` and the TUI into
53
54
  `~/.gaia/npm-cache/gaia-<version>/`.
54
- 5. Execs the TUI, whose exit code becomes ours.
55
+ 5. Writes `~/.gaia/agents/gaia/.installed` — the record the daemon and the TUI
56
+ both read to decide the sidecar is installed. Written on a cache hit too, so
57
+ an install staged by an earlier release repairs itself.
58
+ 6. Execs the TUI, whose exit code becomes ours.
55
59
 
56
60
  `run` deliberately does **not** spawn a sidecar. The TUI reaches agents through
57
61
  the GAIA daemon's relay and never holds a sidecar token, and the daemon is what
@@ -123,9 +127,22 @@ for the version you have.
123
127
  The agent thinks with a model hosted by **Lemonade Server**, which this package
124
128
  does not install. Required before any query succeeds:
125
129
 
126
- 1. Lemonade **10.2.0 or newer**, running (`lemonade-server serve`).
127
- 2. The default model downloaded (`gaia download Gemma-4-E4B-it-GGUF`, or
128
- `gaia init`).
130
+ 1. Lemonade **10.2.0 or newer**, running. GAIA's daemon starts and supervises
131
+ one, so `gaia daemon start` is normally all that is needed (`gaia init`
132
+ also installs and starts it on first run). Qwen3.8 Flash Next needs
133
+ **v2026.39.1 or newer**, the version `gaia init` installs; older servers
134
+ cannot load it.
135
+ 2. The machine's default chat model downloaded — run `gaia init`. It picks
136
+ `Qwen3.6-35B-A3B-GGUF` where a GPU holds it (~27 GB for models,
137
+ weights plus context cache — a 64 GB+ Strix Halo or a 32 GB GPU) and
138
+ `Gemma-4-E4B-it-GGUF` everywhere else, CPU-only PCs included, and records the pick as
139
+ `default_model` in `~/.gaia/config.json`. On 128 GB-class PCs the user may
140
+ switch to the multimodal `user.Qwen3.8-Flash-Next-GGUF` with `gaia config set
141
+ default_model`. `model.id` below names whichever this machine uses.
142
+ `gaia download` takes **no** model argument — naming one makes it exit 2. To
143
+ pull a single model instead, repeat the command `GET /v1/gaia/init` gives
144
+ you: it names the Lemonade client this machine actually has. Do not invent
145
+ one.
129
146
 
130
147
  Do not guess — ask the sidecar. `GET /v1/gaia/init` is a read-only preflight
131
148
  (it never pulls or loads) that probes Lemonade, compares its version to the
@@ -146,7 +163,9 @@ alone:
146
163
  "min_version": "10.2.0", "compatible": null },
147
164
  "model": { "id": "Gemma-4-E4B-it-GGUF", "present": false,
148
165
  "loadable": null, "ctx_size": null },
149
- "hint": "Local Lemonade Server is not reachable at … — start it with `lemonade-server serve`, or set LEMONADE_BASE_URL to a running server."
166
+ // The manual fallback ("Otherwise: …") is resolved for the host's Lemonade
167
+ // install (tray app, macOS app, systemd service, or CLI) — render it verbatim.
168
+ "hint": "Local Lemonade Server is not reachable at …. GAIA starts it automatically — run `gaia daemon start` if the background service is not running. Otherwise: Start the Lemonade app from Applications, then retry. Or set LEMONADE_BASE_URL to a running server. See https://amd-gaia.ai/docs/guides/gaia."
150
169
  }
151
170
  ```
152
171
 
@@ -187,6 +206,36 @@ await shutdown(proc); // tree-kill; auto-cleanup also reaps on exit
187
206
  on `exit`, `SIGINT`/`SIGTERM`/`SIGHUP`, `uncaughtException`, and
188
207
  `unhandledRejection`. A `SIGKILL` of *your* process is the one case nothing
189
208
  in-process can catch.
209
+ - **Mint a caller token, or you are running the sidecar unauthenticated.** The
210
+ sidecar requires `Authorization: Bearer <token>` on every `/v1/gaia/*`
211
+ request, and skips the check only when neither token env var is set — dev
212
+ mode, which is what `startSidecar` gives you, because this package mints
213
+ nothing. Loopback binding is not the boundary: this agent has shell and file
214
+ tools. Mint your own and pass it through the `env` option (`StartOptions`
215
+ extends `SpawnOptions`, so it merges over `process.env`):
216
+
217
+ ```ts
218
+ import { randomBytes } from "node:crypto";
219
+ const token = randomBytes(32).toString("hex");
220
+ const proc = await startSidecar({
221
+ binaryPath: sidecar.binaryPath,
222
+ env: { GAIA_GAIA_SIDECAR_TOKEN: token }, // or ..._TOKEN_FILE, a 0600 path
223
+ });
224
+ ```
225
+
226
+ Then send `authorization: "Bearer " + token` on every `/v1/gaia/*` call.
227
+ `/health`, `/version`, and `/v1/gaia/version` are exempt, so `startSidecar`'s
228
+ own health-and-version handshake works either way. **If you did not spawn the
229
+ sidecar** — you are talking to one the GAIA daemon started — the token is the
230
+ daemon's, delivered to the sidecar as `GAIA_GAIA_SIDECAR_TOKEN_FILE` (a `0600`
231
+ file path); read it from there, don't invent one. A 401 whose `detail` names
232
+ both env vars means you sent the wrong token or none.
233
+
234
+ The programs the agent starts (shell commands, MCP servers, CLI installs and
235
+ sign-ins, native hub agents, media tools) never inherit this token
236
+ or GAIA's other internal credentials. To withhold your own variables from
237
+ them too, list the names in `GAIA_CHILD_ENV_DENY` (comma or space separated);
238
+ `GAIA_NO_DOTENV=1` stops the sidecar loading `.env` files.
190
239
 
191
240
  Or skip the code entirely and let the CLI own it:
192
241
 
@@ -198,7 +247,7 @@ curl http://127.0.0.1:8141/health
198
247
  ## 7. Call `POST /v1/gaia/query`
199
248
 
200
249
  This is the whole agent surface. There is **no typed query client** in this
201
- package — call it with plain `fetch`. Contract version **2.12**; the stream is
250
+ package — call it with plain `fetch`. Contract version **2.17**; the stream is
202
251
  `text/event-stream` terminated by **exactly one** `final` or `error`.
203
252
 
204
253
  Request body (`extra: "forbid"` — an unknown field is a **422**, not ignored):
@@ -208,10 +257,11 @@ Request body (`extra: "forbid"` — an unknown field is a **422**, not ignored):
208
257
  | `query` | yes | Non-empty. |
209
258
  | `run_id` | yes | **You mint it**, and it must be a UUID (non-UUID → 422). It is the cancel handle, valid from the instant the request is sent. |
210
259
  | `context` | yes | Transcript slice, pushed in the body — may be `[]`, never absent. Each item `{ role, content }`; `role` ∈ `user` / `assistant` / `system` / `tool`. |
211
- | `session_id` | no | Contract ≥ 2.12. **Pass it.** The agent persists its indexed-document set per session — without it, it forgets a document between the turn that indexed it and the next question. |
260
+ | `session_id` | no | Contract ≥ 2.12. **Pass it.** The agent persists its indexed-document set per session — without it, it forgets a document between the turn that indexed it and the next question. 1–128 characters from `A-Z a-z 0-9 . _ -` (a UUID works); anything else is a **400**. |
261
+ | `documents` | no | Contract ≥ 2.16. Absolute paths of the files the user attached to this conversation — send the whole set **every turn**. The agent indexes any it has not read yet before the turn runs, granting each exact file and not its folder. A relative path is a **422**. A missing file, a folder, or a refused secret is named as not indexed in the turn's `index_documents` step, and the turn still runs. |
212
262
  | `can_answer_questions` | no | Set `false` for one-shot / batch runs so the agent resolves ambiguity itself instead of parking on a question nobody can see. |
213
- | `model` | no | Overrides the model id for this run. |
214
- | `provider` | no | Local inference only — anything but `"lemonade"` is a **400**. |
263
+ | `model` | no | Overrides the model id. On a retained `session_id` a different model is **switched in place** (contract ≥ 2.14), keeping the conversation and any loaded skills; a switch that fails is a **409** and leaves the session on its previous model. |
264
+ | `provider` | no | `"lemonade"` (the default backend) or `"claude"`, which sends the conversation to Anthropic's API instead of the local server. Anything else is a **400**. When you name it, `model` must agree: under `"claude"` it must be a `claude-*` id, under `"lemonade"` it must not be. A mismatch is a **400**, on a new session or an existing one. Omit it and the `model` decides instead — a `claude-*` id alone reaches Anthropic, any other id runs locally — so there is nothing to mismatch. On a retained `session_id`, naming a different provider switches the session to it in place, onto `model` or, without one, the provider's default model; omitting both leaves the session where it is. |
215
265
  | `max_steps` | no | ≥ 1. |
216
266
 
217
267
  ```ts
@@ -220,7 +270,11 @@ import { randomUUID } from "node:crypto";
220
270
  const runId = randomUUID();
221
271
  const res = await fetch(`${proc.baseUrl}/v1/gaia/query`, {
222
272
  method: "POST",
223
- headers: { "content-type": "application/json", accept: "text/event-stream" },
273
+ headers: {
274
+ "content-type": "application/json",
275
+ accept: "text/event-stream",
276
+ authorization: `Bearer ${token}`, // required unless the sidecar is in dev mode — §6
277
+ },
224
278
  body: JSON.stringify({
225
279
  query: "Summarize the PDFs in ~/Documents/reports",
226
280
  run_id: runId,
@@ -263,11 +317,11 @@ The canonical event shapes, as emitted:
263
317
 
264
318
  | Event | Shape |
265
319
  |---|---|
266
- | `status` | `{ type, message }` — progress and reasoning narration |
320
+ | `status` | `{ type, message, phase?, words?, chars? }` — progress and reasoning narration. `phase` (`loading_model`, `downloading_model`, `reading`, `reasoning`, `tool_call`) names what the model is doing; show it as the live line, not as narration |
267
321
  | `token` | `{ type, delta }` — answer text to append |
268
322
  | `tool_call` | `{ type, tool, args }` |
269
323
  | `tool_result` | `{ type, tool, data, render? }` |
270
- | `needs_confirmation` | `{ type, run_id, action, summary }` — no `confirm_url`; see §8 |
324
+ | `needs_confirmation` | `{ type, run_id, action, summary, confirm_id?, always_scope? }` — answer it, or it ends the run; see §8 |
271
325
  | `needs_input` | `{ type, run_id, request_id, question, options[], allow_free_text, sensitive, respond_url, timeout_seconds? }` |
272
326
  | `final` | `{ type, answer, usage? }` — terminal |
273
327
  | `error` | `{ type, detail, status }` — terminal, surface `detail` verbatim |
@@ -281,7 +335,7 @@ Rules a client must respect:
281
335
  guarantees one; a close without one means something broke on your side.
282
336
  - **Answer `needs_input`, don't restart.** The run is parked on the *same*
283
337
  stream. `POST /v1/gaia/query/{run_id}/respond` with
284
- `{ request_id, response }` and keep reading the existing stream — a fresh
338
+ `{ request_id, value }` and keep reading the existing stream — a fresh
285
339
  `/query` POST abandons the paused run. Unknown run → **404**; a `request_id` that
286
340
  is no longer pending → **409** (both loud, never a silent drop). Render each
287
341
  option's `description`, and mask the input when `sensitive` is set.
@@ -289,36 +343,116 @@ Rules a client must respect:
289
343
  `{ run_id, cancelled }` — an unknown id reports `cancelled: false` with a
290
344
  **200**, not a 404, because a cancel racing a normal completion is expected.
291
345
  Dropping the HTTP connection also cancels the run.
346
+ - **Add to a running turn with `POST /v1/gaia/query/{run_id}/followup`**
347
+ (contract ≥ 2.15, body `{ text }`). The run is not interrupted and no second
348
+ turn starts: the agent folds the text in at its next step boundary and
349
+ answers it alongside what it was already doing. Unknown run → **404**, an
350
+ agent that cannot take one → **409**; both are loud, so hold the message
351
+ rather than telling the user it was sent. `/query` is stateless, so put a
352
+ delivered follow-up into the next turn's `context` yourself, between that
353
+ turn's question and its answer.
354
+
355
+ ## 8. Over `/v1/gaia/query`, a gated tool asks — when you can answer
356
+
357
+ Read this before you design a workflow around it. This section is about the HTTP
358
+ surface — the agent's other transport can collect an approval; see SPEC §5.5.
359
+
360
+ Twelve of the agent's 95 tools mutate the machine and need explicit approval
361
+ before they run. Nine sit in the base `TOOLS_REQUIRING_CONFIRMATION` set —
362
+ **`write_file`**, **`edit_file`**, **`run_shell_command`**,
363
+ **`wait_for_condition`**, which re-runs a shell command until it succeeds,
364
+ **`execute_python_file`**, **`run_python`**, **`notify_desktop`**, which spawns
365
+ a PowerShell child on Windows to draw the notification, and **`install_cli`** /
366
+ **`sign_in_cli`**, which install software and sign a CLI in to the user's
367
+ account — and the flagship adds three of its own
368
+ (`CONFIRMATION_REQUIRED_TOOLS`): **`install_skill`**, **`capture_skill`**, and
369
+ **`remove_skill`**, because installing or capturing a skill writes third-party
370
+ content under `~/.gaia/skills` and removing one deletes it. A capture that does
371
+ land is additionally **code-inert**: its instructions load, but any
372
+ `tools.py`/scripts stay unregistered until a human runs
373
+ `gaia skill promote <name>` in a terminal. Everything else — reading,
374
+ indexing, querying, web fetching, memory, and the read-only `check_cli_setup`
375
+ — runs without asking. A write or edit inside the agent's own scratch
376
+ folder (`gaia-scratch-…` under the system temp folder) runs without asking too,
377
+ so running a script from it asks every time and carries no `always_scope`. So does a shell command run there or naming a path there, and a script handed a scratch path as an argument.
378
+
379
+ The eight live-browser tools are gated by a **hook**, not a set, so a name
380
+ alone does not tell you whether a call will ask. `browser_login` always asks.
381
+ `browser_click` / `browser_type` ask when the call acts inside a session the
382
+ run signed in to, or when the target element's label reads irreversible, and
383
+ run unasked on the open web. The hook **fails closed**: if the browser cannot
384
+ be asked what state it is in, the call is treated as authenticated and prompts.
385
+ Design a workflow around the approval path, not around a fixed tool list.
386
+
387
+ **Contract ≥ 2.14 can answer one.** Send a `session_id` and leave
388
+ `can_answer_questions` unset (or `true`). The stream emits `needs_confirmation`
389
+ carrying a `confirm_id` and then **stays open** while the agent waits:
292
390
 
293
- ## 8. Confirmation-gated tools are **refused, not prompted**
391
+ ```
392
+ data: {"type":"needs_confirmation","run_id":"…","action":"run_shell_command","summary":"python -m pytest -q tests/\nCan run any code, with your permissions.","confirm_id":"…","always_scope":"pytest (runs the test code it finds)","risk":"execute"}
393
+ ```
394
+
395
+ Answer it on the same run, then keep reading the stream:
396
+
397
+ ```
398
+ POST /v1/gaia/query/{run_id}/tool_decision
399
+ { "decision": "allow" | "deny" | "always" | "timeout", "confirm_id": "…" }
400
+ ```
294
401
 
295
- Read this before you design a workflow around it.
402
+ `summary` is the command or call itself, shown once — your UI supplies the
403
+ question. For a shell command it can carry two more lines: `in <folder>` when
404
+ the command runs outside the workspace, and one plain sentence on what a `write`
405
+ or `execute` command can do. Show it as-is. A CLI a skill would grant (`git
406
+ commit`, `python script.py`) with no such skill loaded arrives here as a prompt,
407
+ not a refusal. So does a command GAIA's own checks don't cover (`git push`,
408
+ `python -c`, a path outside the project): its `summary` is the exact line the
409
+ shell will run, unshortened, then `in <folder>`, then a risk sentence, and it
410
+ carries no `always_scope` — offer allow-once and deny only. `allow` runs that
411
+ line once; the next call asks again. `risk` says what the call does (`read`, `write`, `execute`,
412
+ `destructive`); label by it rather than by tool name, since `run_shell_command`
413
+ is `pytest` on one call and `rm -rf` on the next. `always` grants the family
414
+ named in `always_scope` for the rest of the session (`pytest` covers every
415
+ spelling of a test run) — say so in your UI, because it stops asking; no
416
+ `always_scope` means don't offer it. If your prompt expires unanswered, send
417
+ `timeout`, not `deny`: it still refuses, but the agent tells the user the
418
+ request timed out instead of claiming they said no. Unknown run → **404**; a
419
+ prompt that is no longer pending → **409**; any other decision → **422**. All loud,
420
+ never a silent drop. Send `confirm_id`: without it a late answer resolves
421
+ whichever prompt replaced the one it was typed against.
422
+
423
+ To stop being asked for a whole session:
424
+
425
+ ```
426
+ POST /v1/gaia/sessions/{session_id}/bypass
427
+ { "enabled": true }
428
+ ```
296
429
 
297
- Three of the agent's 55 tools write to disk or execute a command, and sit in the
298
- base `TOOLS_REQUIRING_CONFIRMATION` set: **`write_file`**, **`edit_file`**, and
299
- **`run_shell_command`**. Everything else — reading, indexing, querying, web
300
- fetching, memory — runs without asking.
430
+ It applies to the very next gated tool, including one in a turn already running,
431
+ and an unknown session is a **404** rather than a new one. A malformed
432
+ `session_id` is a **400**, same as on `/query`.
301
433
 
302
- Over `/v1/gaia/query` there is **no way to collect an approval**, so the stream
303
- does not prompt. When the agent reaches one of those tools it emits a
304
- `needs_confirmation` event, and the server **immediately follows it with a
305
- terminal `final`** whose `answer` says it stopped before running that action,
306
- then cancels the run. There is no `confirm_url`, no resume, and no
307
- `/query/{run_id}/confirm` endpoint — it is a deliberate deny-by-default stub, not
308
- an oversight.
434
+ **That bypass stops the prompts; it does not open the shell.** The stdio
435
+ transport's `--bypass-permissions` does both — it runs gated tools unasked *and*
436
+ lifts the shell tool's guardrails (redirection and the other shell-only
437
+ operators, the read-only binary allowlist, the rate limit). That second half is
438
+ arbitrary code execution, appropriate for one local parent on a private pipe and
439
+ not for a bound socket, so it stays on stdio: HTTP sessions never lift the shell
440
+ gates, and the request body rejects unknown fields so a client cannot ask. See
441
+ SPEC §5.5.
309
442
 
310
- Concretely, your client sees:
443
+ **A run nobody can answer is still refused.** With `can_answer_questions: false`,
444
+ or with no `session_id`, the server emits `needs_confirmation`, follows it
445
+ immediately with a terminal `final` saying it stopped before running the action,
446
+ and cancels the run:
311
447
 
312
448
  ```
313
- data: {"type":"needs_confirmation","run_id":"…","action":"write_file","summary":"Run 'write_file'?"}
314
449
  data: {"type":"final","answer":"I stopped before running 'write_file' because it needs your explicit approval, and this streaming surface cannot collect that yet. …"}
315
450
  ```
316
451
 
317
- So: **`/query` cannot write files, edit files, or run shell commands.** If your
318
- integration needs that, drive the agent from a surface that can prompt (the
319
- terminal UI or the Agent UI), or perform the mutation yourself from your own code
320
- and let the agent do the reading and reasoning. Treat `needs_confirmation` as an
321
- early warning that the run is about to end, not as a question you can answer.
452
+ That is deny-by-default, not an oversight: parking a batch run on a prompt
453
+ nobody will ever see reads as a hang. So a one-shot integration cannot run those
454
+ nine tools — pass a `session_id` and answer, or perform the mutation from your
455
+ own code and let the agent do the reading and reasoning.
322
456
 
323
457
  ## 9. File-access scope
324
458
 
@@ -328,10 +462,31 @@ for a personal document agent, and it is still a real boundary — system
328
462
  directories, program files, and other users' homes are refused, with the check
329
463
  run against the *resolved* path so a symlink out of scope doesn't slip through.
330
464
 
331
- **In 0.1.1 narrowing it is a construction-time setting only.** The packaged
332
- sidecar exposes no flag or env var for `allowed_paths` (its CLI accepts only
333
- `--host` and `--port`), so restricting the scope means embedding `GaiaAgent` in
334
- your own Python process:
465
+ **Being in scope is not the same as being safe, and two denylists apply inside
466
+ it.** Reads refuse secrets — `.env`, `id_rsa`, `credentials.json`, `.netrc`,
467
+ `.pem`/`.key`, and everything under `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube` —
468
+ even in an allowed directory. Writes additionally refuse anything that executes
469
+ on its own: shell startup files, PowerShell profiles, `~/.config/autostart/*`,
470
+ systemd user units, `LaunchAgents`, and a repo's `.git/` (hooks and config). Both
471
+ come back as a structured error naming the file and the reason, so do not plan an
472
+ integration around reading a credential file or editing a shell rc — perform
473
+ those from your own code.
474
+
475
+ The agent also gets its own scratch directory for throwaway scripts and
476
+ intermediate files, so they stay out of the user's project. It is created per
477
+ agent under the system temp dir, deleted when the agent closes, and is the only
478
+ part of the temp dir the agent may use.
479
+
480
+ **To narrow it for a run, set `GAIA_ALLOWED_PATHS`** in the environment the
481
+ sidecar starts in — absolute folders joined with `;` on Windows and `:` elsewhere.
482
+ The scope becomes the overlap with the default; an entry outside it is ignored
483
+ with a warning and never widens it, and out-of-scope paths are refused without a
484
+ prompt, even under full access. The scratch directory stays writable. An empty,
485
+ relative or missing entry stops the sidecar at startup. It confines the agent's
486
+ tools, not code they run: a `run_python` script or shell command can still read
487
+ anything the OS user can, so isolating a run from files it must not see needs a
488
+ separate OS user, container or VM. Embedding `GaiaAgent` lets you pass an explicit
489
+ list instead (the variable still narrows it):
335
490
 
336
491
  ```python
337
492
  from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
@@ -339,7 +494,7 @@ from gaia_agent.agent import GaiaAgent, GaiaAgentConfig
339
494
  agent = GaiaAgent(config=GaiaAgentConfig(allowed_paths=["/home/me/Documents"]))
340
495
  ```
341
496
 
342
- ## 10. Skills — opt-in, and empty in 0.1.1
497
+ ## 10. Skills — one always-on, the rest opt-in
343
498
 
344
499
  The agent is built to host **Agent Skills** (short playbooks loaded into its own
345
500
  prompt, grouped into named sets, one set active per launch), and its bundled
@@ -351,23 +506,75 @@ matching bodies in full — the rest collapse to a one-line menu entry, and the
351
506
  model (or the user) re-activates one by calling `load_skill` on it again.
352
507
  `GAIA_DYNAMIC_SKILLS=0` disables the per-turn selection (every loaded body
353
508
  renders every turn); `GAIA_DYNAMIC_SKILLS_TAU=<float>` overrides the match
354
- threshold. Manifest `skills:` entries are always-on and never collapse. If the
509
+ threshold. Manifest `skills:` entries are always-on and never collapse, and a
510
+ turn with only always-on skills loaded embeds nothing. If the
355
511
  embedder is unavailable, selection disables itself for the session and every
356
512
  body renders — capability is never silently lost to a failed match.
357
513
 
358
- **Nothing ships enabled in 0.1.1.** The bundled skill library is empty, and
359
- `gaia-agent.yaml` ships its `skills:` / `skill_sets:` / `default_skill_set:`
360
- blocks **commented out** — following the email agent's precedent, because skill
361
- bodies cost prompt tokens and no eval has measured that trade for this agent yet.
362
- Re-enabling is uncommenting two blocks; no code change.
363
-
364
- So today: no skill set loads, and there is nothing for `GAIA_SKILL_SET` to
365
- select — leave it unset. Once a release declares sets, `GAIA_SKILL_SET` is the
366
- selection channel for the packaged sidecar (its CLI has no `--skill-set` flag),
367
- and an undeclared name raises naming the valid sets rather than falling back to a
368
- default. Do not document or design around skills being on by default.
369
-
370
- ## 11. Ports
514
+ **One skill ships enabled: `gaia-voice`.** It is a manifest `skills:` entry, so
515
+ it is always on, always rendered in full, and paid on every LLM call of every
516
+ turn — budget for it. It is not a task recipe but the agent's honesty floor: do
517
+ not claim work you did not do, do not present empty output as a result, do not
518
+ substitute a near-miss and report success. Those failures corrupt an answer
519
+ whatever the task is, which is why it cannot live in an opt-in bundle. It
520
+ declares no tools, and its body measures 790 tokens (tiktoken `cl100k`).
521
+
522
+ **`document-extract` ships bundled but not enabled.** `gaia-voice` routes a
523
+ request for every item in a document to it, and it drives
524
+ `extract_document_items` and `save_extracted_items` so a long transcript yields
525
+ a complete, source-quoted inventory rather than a summary. A skill turns that
526
+ inventorying on only by listing `extract_document_items` in `tools_required`;
527
+ wording such as "find every call site" in another skill does not. Loading it
528
+ registers the tools at once, so `load_skill` never reports them missing; coverage
529
+ is enforced only when the request itself names a source document or asks for
530
+ every item, and a file the user named as a deliverable is never a source.
531
+
532
+ **No skill *set* loads.** `gaia-agent.yaml` ships its `skill_sets:` and
533
+ `default_skill_set:` blocks **commented out** — following the email agent's
534
+ precedent, because loading several skill bodies into every prompt costs tokens
535
+ and no eval has measured that trade for this agent yet. Re-enabling is
536
+ uncommenting two blocks; no code change.
537
+
538
+ So today there is nothing for `GAIA_SKILL_SET` to select — leave it unset; any
539
+ value stops the agent at startup (the sidecar exits non-zero before binding its
540
+ port). Once a release declares sets, `GAIA_SKILL_SET` is the selection channel
541
+ for the packaged sidecar (its CLI accepts only `--host` and `--port`), and an
542
+ undeclared name stops startup naming the valid sets rather than falling back to
543
+ a default. Beyond
544
+ `gaia-voice`, do not design around a skill being on by default.
545
+
546
+ ## 11. The project map — two things it costs you
547
+
548
+ When the agent's working directory is a code repository, every task starts with
549
+ a **project map** in the system prompt: the root, the directory shape, the
550
+ likely entry points, which commands are installed, and the three platform
551
+ differences that change command syntax. It exists so the agent stops burning
552
+ round trips on "no such file" and "command not found".
553
+
554
+ Two consequences an integrator needs to plan for:
555
+
556
+ - **Up to 600 prompt tokens, every turn.** That is the enforced ceiling
557
+ (1.8% of the NPU profile's 32K window), not a typical value — budget it
558
+ alongside `gaia-voice`'s 790.
559
+ - **An embedding pass on the first semantic code search.** If the repo has no
560
+ [code index](https://amd-gaia.ai/docs/guides/code-index), the first
561
+ `search_code_index` builds it, then searches. On a large monorepo that one
562
+ search takes minutes of local embedding; a task that never searches pays
563
+ nothing. `GAIA_PROJECT_MAP_AUTO_INDEX=1` builds it in the background at task
564
+ start instead.
565
+
566
+ The sidecar's CLI accepts only `--host` and `--port`, so pointing the map at a
567
+ specific project means `GAIA_PROJECT_ROOT=/path/to/repo` in its environment, or
568
+ `GaiaAgentConfig(project_root=...)` when embedding. A directory that is neither
569
+ a VCS checkout nor holds a recognised manifest gets **no map** — that is the
570
+ designed answer, not a failure.
571
+
572
+ The same root check also decides whether the shell rides along: when it
573
+ resolves to a repository, `run_shell_command` is offered on every turn instead
574
+ of only when semantic selection guesses the request sounds like a shell
575
+ request. No repository, no change.
576
+
577
+ ## 12. Ports
371
578
 
372
579
  | Service | Port |
373
580
  |---|---|
@@ -379,11 +586,14 @@ Port **4001 is reserved repo-wide**: `spawnSidecar` throws a `RangeError` and
379
586
  speaks for the user's documents and memory and has no business on a LAN
380
587
  interface.
381
588
 
382
- ## 12. Running in a server or long-lived app
589
+ ## 13. Running in a server or long-lived app
383
590
 
384
591
  - **`fetchAll` / `fetchBinary` are a build step**, not per request — network plus
385
- a full SHA-256 hash of a large artifact. Run once; `resolveSidecarPath` /
386
- `resolveTuiPath` at runtime.
592
+ a full SHA-256 hash of a large artifact. Run once at install time.
593
+ - **`resolveSidecarPath` / `resolveTuiPath` are startup, not per request.** They
594
+ re-hash the binary against `binaries.lock.json` before handing back a path that
595
+ gets spawned, so they cost a full read of a large file. Resolve once and keep
596
+ the path. `{ verify: false }` skips the check for a binary you built yourself.
387
597
  - **Spawn once at boot** and hold the `Sidecar` handle for the process lifetime.
388
598
  Never per request.
389
599
  - **Low concurrency.** One local Lemonade model slot, so parallel queries
@@ -408,22 +618,31 @@ There is no silent null.
408
618
  reachable"** means Lemonade isn't running or isn't reachable — not a bug in
409
619
  this package. Start it, or set `LEMONADE_BASE_URL`.
410
620
  - **`needs_confirmation` is followed by a refusal and the run ends.** See §8.
411
- `write_file` / `edit_file` / `run_shell_command` are unreachable over `/query`.
621
+ The nine gated tools are unreachable **over `/query`** — the agent itself can
622
+ run them on a transport that can prompt (SPEC §5.5).
412
623
  - **A placeholder hash in `binaries.lock.json` blocks the fetch before any
413
624
  network call.** Between releases that is the *expected* state — it is not a
414
625
  broken install, and there is no override.
415
626
  - **No `linux-arm64` / `win32-arm64` sidecar.** The TUI has both. A `PlatformError`
416
627
  on those hosts is the design, not a missing artifact.
417
- - **There is no caller-auth token at 0.1.1.** Unlike `@amd-gaia/agent-email`, this
418
- sidecar mints none and this package sends none — don't add an `Authorization`
419
- header looking for one, and don't rely on its absence as a security boundary.
420
- Loopback binding is what protects it.
628
+ - **A `401` from `/v1/gaia/*` is the caller-auth token, not a bug.** The sidecar
629
+ requires `Authorization: Bearer <token>`; `/health`, `/version`, and
630
+ `/v1/gaia/version` are exempt, which is why a green health check sits happily
631
+ in front of a 401 on `/query`. It skips the check only in dev mode — neither
632
+ token env var set — which is what `spawnSidecar` and `gaia serve` produce,
633
+ because this package mints nothing. Do not treat loopback binding as the
634
+ boundary; mint a token and pass it (§6).
421
635
  - **`run_id` must be a UUID**, and unknown fields in the request body are a
422
636
  **422** — the model forbids extras. Typos don't get ignored.
423
637
  - **`gaia run` needs the *Python* `gaia` CLI on `PATH`** — the TUI shells out to
424
- it to start the daemon. The package deliberately strips its own npm bin
425
- directory from the child's `PATH` so the TUI doesn't re-invoke the npm shim; if
426
- the Python CLI isn't installed, the daemon never comes up.
638
+ it to start the daemon. So the TUI doesn't re-invoke our own npm shim, the
639
+ child's `PATH` is rewritten: a directory holding nothing but our shim (an npx
640
+ temp dir) is dropped, and a **shared** bin directory is moved to the end
641
+ instead of removed, so the `python3` / `lemonade-server` / real `gaia` beside
642
+ it stay reachable. If the Python CLI isn't installed anywhere, the daemon never
643
+ comes up. It must also be **0.25.0+**: an older core's daemon starts fine but
644
+ cannot run this agent, which reads as a UI with a dead agent rather than as
645
+ a version problem.
427
646
  - **The TUI is installed as `gaia-tui`, never `gaia`** — the terminal-hub artifact
428
647
  *is* called `gaia-<platform>`, and a file named `gaia` in a cache directory would
429
648
  shadow the npm bin shim. The lock's `filename` and `executable` differ for that
@@ -452,7 +671,7 @@ Then, in another terminal:
452
671
 
453
672
  ```bash
454
673
  curl -s http://127.0.0.1:8141/health # {"status":"ok","service":"gaia-agent-gaia"}
455
- curl -s http://127.0.0.1:8141/version # {"apiVersion":"2.12","agentVersion":"0.1.1"}
674
+ curl -s http://127.0.0.1:8141/version # {"apiVersion":"2.17","agentVersion":"0.1.1"}
456
675
  curl -s http://127.0.0.1:8141/v1/gaia/init # 200 + "ready":true, or 503 + a "hint"
457
676
  curl -N -X POST http://127.0.0.1:8141/v1/gaia/query \
458
677
  -H 'content-type: application/json' \
@@ -463,11 +682,62 @@ A healthy run streams `status` / `token` events and ends with one `final`. If
463
682
  `/v1/gaia/init` is 503, fix what its `hint` names and retry — the rest of your
464
683
  integration is fine.
465
684
 
685
+ That `/query` call carries no `Authorization` header because `gaia serve` starts
686
+ the sidecar in dev mode. Against one started with a token, add
687
+ `-H "authorization: Bearer $TOKEN"` — a **401** here and a green `/health` is
688
+ that and nothing else (§6).
689
+
466
690
  **A 503 from `/query` itself is a different condition**: every retained
467
691
  session slot is busy and none is idle enough to evict (SPEC §5.2). Do NOT
468
692
  loop on `/v1/gaia/init` — it will report ready. Wait for a running turn to
469
693
  finish (or close an idle session) and retry the same `/query`.
470
694
 
695
+ **Three more refusals are yours to avoid**, each naming its fix in `detail`
696
+ (SPEC §5.2 has the reasoning):
697
+
698
+ - **409 — the `run_id` is still in flight.** You mint it, so mint a fresh UUID
699
+ per request; reusing one would leave the earlier run with no way to be
700
+ cancelled.
701
+ - **409 — the model switch itself failed** (a missing Claude credential, an
702
+ unknown local model). The session stays on its previous model, so this is a
703
+ failed request rather than a broken session; `detail` names the reason.
704
+ - **400 — the `Host` header is absent or empty.** The loopback check fails
705
+ closed, so omitting the header is refused rather than served. Send
706
+ `Host: 127.0.0.1:<port>`; every real HTTP client already does.
707
+
708
+ **After cancelling a turn, send the next `/query` on that session straight
709
+ away.** It is held until the stopped turn's model call returns (up to 240 s),
710
+ then runs. A session whose running turn was NOT cancelled still answers `409`.
711
+
471
712
  For the full wire contract, lock schema, exit codes, and timeout table, see
472
713
  [`SPEC.md`](./SPEC.md). For the user-facing overview, see [`README.md`](./README.md)
473
714
  and <https://amd-gaia.ai/docs/guides/gaia>.
715
+
716
+ ## TUI inference providers
717
+
718
+ The stdio TUI supports `/provider` for Local, Fireworks AI, and AMD LLM Gateway.
719
+ Keys are entered in a masked field and sent directly to local Lemonade's runtime
720
+ auth API, never as agent queries. `/model` lists supported discovered cloud and
721
+ downloaded local models, marking the configured default with `(default)`;
722
+ `/model fireworks.gemma-4-31b-it` selects Gemma 4 31B IT
723
+ when available. Cloud chat sends conversation history to the selected provider;
724
+ embeddings remain on Lemonade. The status event names the actual provider and
725
+ marks remote inference. This is a TUI/stdio capability, not an HTTP query command.
726
+
727
+ ## Developer engineering mode
728
+
729
+ Available only when GAIA was started with `--developer-mode` (or
730
+ `GAIA_DEVELOPER_MODE=1`). If the engineering tools are missing, say that developer
731
+ mode is off; don't route around it with shell or file tools. When describing a
732
+ handoff, claim only what happened:
733
+
734
+ - Context was shared only after the user approved that exact snapshot in the prompt.
735
+ Approval can't be remembered, so each share or append asks again.
736
+ - Opening the coding app never creates or submits a task, and for Codex it doesn't
737
+ prefill a prompt either. Never say the context was "posted" or "sent" to Codex;
738
+ give the user the directory and the prompt to paste.
739
+ - Preview and test results come from the coding app. Report them as app-reported,
740
+ not as checks GAIA ran.
741
+ - Revoking stops future reads; it cannot recall data already delivered.
742
+
743
+ See the [usage guide](https://amd-gaia.ai/docs/guides/harness-engineering).