@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/SPEC.md CHANGED
@@ -190,6 +190,44 @@ supervises the sidecar and does its own SHA-256 check on the binary it finds
190
190
  there; installing an already-verified binary at that path turns the daemon's fetch
191
191
  into a cache hit instead of a second multi-hundred-megabyte download.
192
192
 
193
+ ### 4.1 The `.installed` sentinel
194
+
195
+ Installing the sidecar also writes `~/.gaia/agents/gaia/.installed`, the record
196
+ the daemon and the TUI both read to answer "is this agent installed?". It is
197
+ written on a **cache hit as well as a fresh download**, so an install staged by
198
+ an earlier release repairs itself on the next run, and only when the fetch
199
+ produced a runnable local install — that is, for the `sidecar` component (the
200
+ TUI is not a hub agent) **and** for this host's own platform. A `--platform`
201
+ fetch stages a binary for a different host; recording it would hand the daemon a
202
+ wrong-architecture executable that re-hashes correctly and then fails to exec.
203
+
204
+ ```json
205
+ {
206
+ "id": "gaia",
207
+ "version": "<sidecar componentVersion>",
208
+ "language": "python",
209
+ "installed_at": "<ISO-8601 UTC>",
210
+ "artifact_sha256": "<the verified SHA-256>",
211
+ "path": "<install directory>",
212
+ "artifact_kind": "binary",
213
+ "executable": "gaia-agent[.exe]"
214
+ }
215
+ ```
216
+
217
+ The shape is a cross-repo contract with `InstalledAgent.to_dict()` in
218
+ `gaia.hub.installer`. Three fields are load-bearing:
219
+ `gaia.daemon.sidecars.fetch._hub_installed_binary` ignores the install unless
220
+ `artifact_kind` is `binary` and `executable` and `artifact_sha256` are non-empty,
221
+ and it **re-hashes the binary and raises `IntegrityError` if the file no longer
222
+ matches `artifact_sha256`** — so the recorded hash is the one actually verified,
223
+ never the lock's nominal value. `executable` must be a bare filename;
224
+ `installer.read_sentinel` discards a sentinel whose executable contains a path
225
+ separator, which reads as "never installed".
226
+
227
+ Without the sentinel the daemon sees no install and the TUI falls back to running
228
+ `gaia-agent` as its own stdio child — spawning the REST sidecar over stdio and
229
+ filling the chat with uvicorn's startup log.
230
+
193
231
  The TUI directory is keyed by `agentVersion` — this package's version, not the
194
232
  component's — so a bump of either never reuses the previous release's executable.
195
233
 
@@ -207,11 +245,16 @@ Both defaults are overridable (`--sidecar-dir`, `--cache-dir`).
207
245
 
208
246
  Served by `gaia_agent.server`. Bound to `127.0.0.1` only.
209
247
 
248
+ Every request must carry a loopback `Host` header. A `Host` that is non-loopback,
249
+ absent or empty is refused with `400` before routing — that check is what defeats
250
+ DNS rebinding, so it fails closed rather than serving a request that simply omits
251
+ the header.
252
+
210
253
  | Property | Value |
211
254
  | ------------------ | --------------------------------------- |
212
255
  | Default port | `8141` (`DEFAULT_PORT` in `server.py`) |
213
256
  | Reserved port | `4001` — refused with a `RangeError` |
214
- | Contract version | `API_VERSION = "2.12"` |
257
+ | Contract version | `API_VERSION = "2.17"` |
215
258
  | Agent id / prefix | `gaia` → `/v1/gaia/...` |
216
259
 
217
260
  ### 5.1 Endpoints
@@ -222,13 +265,67 @@ Served by `gaia_agent.server`. Bound to `127.0.0.1` only.
222
265
  | `GET` | `/version` | Contract probe. `{ "apiVersion", "agentVersion" }` |
223
266
  | `GET` | `/v1/gaia/version` | The TUI's negotiation probe |
224
267
  | `GET` | `/v1/gaia/init` | Readiness detail (Lemonade, model, connectors) |
268
+ | `GET` | `/v1/gaia/memory` | The `/memory` snapshot (contract ≥ 2.13) |
269
+ | `GET` | `/v1/gaia/skills` | The skills the agent can load (contract ≥ 2.17) |
225
270
  | `POST` | `/v1/gaia/query` | The streaming surface (`text/event-stream`) |
226
271
  | `POST` | `/v1/gaia/query/{run_id}/cancel` | Cancel a run by its host-minted `run_id` |
227
272
  | `POST` | `/v1/gaia/query/{run_id}/respond`| Answer a mid-run question |
273
+ | `POST` | `/v1/gaia/query/{run_id}/tool_decision` | Answer a confirmation-gated tool (≥ 2.14) |
274
+ | `POST` | `/v1/gaia/sessions/{session_id}/bypass` | Run gated tools without asking, for one session (≥ 2.14) |
275
+ | `POST` | `/v1/gaia/query/{run_id}/followup`| Add to a run already in flight (contract ≥ 2.15) |
276
+
277
+ #### `/query` request body
278
+
279
+ The full, machine-readable contract — every field, type, and which are
280
+ required — is the committed
281
+ [`openapi.gaia.json`](https://github.com/amd/gaia/blob/main/hub/agents/gaia/python/openapi.gaia.json),
282
+ regenerated from the live routes by `python -m gaia_agent.export_openapi` and
283
+ checked for drift in CI — the same pattern as the email agent's own
284
+ `openapi.email.json`. Required: **`query`**, **`run_id`**,
285
+ **`context`** — `additionalProperties: false`, so an unrecognized field or a
286
+ missing required one is `422` before the agent runs. `run_id` must parse as a
287
+ UUID (`uuid.UUID(v)`); a non-UUID string is also `422`. Everything else
288
+ (`model`, `provider`, `max_steps`, `session_id`, `can_answer_questions`,
289
+ `documents`) is optional.
290
+
291
+ `documents` (contract ≥ 2.16) carries the absolute paths of the files the user
292
+ attached to the conversation. Send the whole set on every turn. Before the turn
293
+ runs, the agent indexes the ones it has not indexed yet, so a file attached
294
+ mid-conversation is read on the next turn. Each path is granted as that exact
295
+ file, never its folder. A relative path is a `422`. A missing file, a folder, or
296
+ a secret the path policy refuses is reported as not indexed in the turn's
297
+ `index_documents` step, and the turn still runs. Pre-2.16 binaries `422` the
298
+ field, so probe `/version` before sending it.
299
+
300
+ ```json
301
+ {
302
+ "query": "What's in this directory?",
303
+ "run_id": "5b1f6e2a-7c34-4b4d-9b3e-2b6d9a7a1c3e",
304
+ "context": [],
305
+ "session_id": "my-session-1",
306
+ "can_answer_questions": true
307
+ }
308
+ ```
228
309
 
229
310
  `/health` is liveness only. It says nothing about whether Lemonade is up or a
230
311
  model is loaded — `/v1/gaia/init` answers that.
231
312
 
313
+ `GET /v1/gaia/memory` returns the read-only snapshot behind the TUI's
314
+ `/memory` view: `{ "available", "reason", "stats", "contexts", "shown",
315
+ "total", "items" }`. `available: false` means the session has no live memory
316
+ store (Lemonade down, embedding model not pulled, disabled via env) — `reason`
317
+ names why, so an outage never renders as "you have no memories". This is the
318
+ daemon-transport counterpart of the stdio `MEMORY_DUMP_QUERY` sentinel; both
319
+ paths call the same `build_memory_dump()` and return the identical shape.
320
+
321
+ `GET /v1/gaia/skills` (contract ≥ 2.17) lists the skills the agent can load:
322
+ `{ "skills": [{ "name", "description", "version", "tier", "tools", "path" }],
323
+ "invalid": { "<folder>": "<why it failed to parse>" } }`. It reads the same
324
+ roots the agent does — its bundled skills, `~/.gaia/skills`, Claude Code skill
325
+ folders — so a host without the Python package (the bundled skills live inside
326
+ the binary) can still show them. A skill root that cannot be read is a `500`
327
+ naming it.
328
+
232
329
  ### 5.2 `session_id` and agent retention
233
330
 
234
331
  `POST /v1/gaia/query` accepts an optional `session_id` in the request body.
@@ -236,29 +333,83 @@ model is loaded — `/v1/gaia/init` answers that.
236
333
  whole conversation.** Contract ≥ 2.12 resolves `session_id` to a *retained*
237
334
  agent instead of a throwaway built fresh per call — indexed documents and
238
335
  `load_skill` state only survive between turns when the same `session_id`
239
- threads them together.
336
+ threads them together. A `session_id` must be 1–128 characters from
337
+ `A-Z a-z 0-9 . _ -`, not only dots (a UUID works); any other value is a **400**
338
+ naming the allowed characters, on `/query` and on `/sessions/{session_id}/bypass`.
339
+ Omitting it is a valid, explicit one-shot: nothing persists past that single
340
+ turn, and the agent is not told otherwise.
341
+
342
+ Internal explicit deletion follows the same idle-only rule as eviction: it returns
343
+ `False` for an absent or busy session and preserves a running agent. Successful
344
+ deletion claims the turn lock before removal and closes outside the registry lock.
345
+
346
+ A skill **captured** in-conversation (the `capture_skill` tool — itself
347
+ confirmation-gated, so over `/query` it needs a session that can answer) loads
348
+ **instruction-only** until a human runs `gaia skill promote <name>` in a
349
+ terminal: `load_skill` injects its body but defers registering any tools it
350
+ declares, and reports the deferral in its result. Integrators must not expect
351
+ a captured skill's `<skill>/<tool>` names to exist before that promote.
240
352
 
241
353
  A retained skill stays *loaded* but its body is not necessarily in the prompt
242
354
  every turn: the agent selects per turn which loaded bodies match the query and
243
355
  collapses the rest to a one-line menu entry (re-activated by calling
244
356
  `load_skill` again). `GAIA_DYNAMIC_SKILLS=0` disables the selection;
245
357
  `GAIA_DYNAMIC_SKILLS_TAU=<float>` overrides its threshold; an embedder outage
246
- disables it for the session and every body renders. Omitting it is a valid, explicit one-shot: nothing
247
- persists past that single turn, and the agent is not told otherwise.
358
+ disables it for the session and every body renders.
359
+
360
+ A retained session also carries a **project map** — up to 600 prompt tokens of
361
+ directory shape, entry points, installed commands and platform quirks, present
362
+ whenever the agent's working directory resolves to a code repository (a VCS
363
+ directory or a recognised manifest at its root). `GAIA_PROJECT_ROOT=<path>`
364
+ picks the project when the working directory is not it. If that repository has
365
+ no code index, the first `search_code_index` builds one;
366
+ `GAIA_PROJECT_MAP_AUTO_INDEX=1` builds it in a background thread at task start
367
+ instead. The same repository check also
368
+ makes `run_shell_command` always-on for the session instead of semantically
369
+ selected. Neither affects the wire contract — they change what the agent
370
+ knows, what tools it is offered, and what the first turn costs.
248
371
 
249
372
  A second `/query` for a `session_id` that already has a turn in flight gets
250
- `409 Conflict` — cancel the running turn or wait for it, then retry. A
373
+ `409 Conflict` — cancel the running turn or wait for it, then retry. Once that
374
+ turn has been cancelled, the new `/query` is held until it finishes its
375
+ in-flight model call (up to **240 s**) and then runs, instead of getting `409`;
376
+ only a cancelled turn that outlasts the wait still gets `409`. A
251
377
  `/query` that needs a **new** session while every retained slot is busy and
252
378
  none is idle enough to evict gets `503` with the reason in `detail` — a
253
379
  temporary, retryable condition, not a bug: wait for a turn to finish (or
254
380
  close an idle session) and retry. A
255
381
  `session_id` can also be evicted from the retention table under an idle
256
- timeout or an LRU cap on concurrent sessions; a `/query` that lands on an
382
+ timeout (**4 hours** without a turn) or an LRU cap on concurrent sessions
383
+ (**100**) — the two knobs that decide when the `503` above can happen at all
384
+ (`session_registry.py`). Eviction is idle-only: a conversation still in use is
385
+ never timed out mid-flight. A `/query` that lands on an
257
386
  evicted id gets a fresh agent (the conversation is not blocked) but the
258
387
  response stream's first event is a `{"type":"status","status":"warning",...}`
259
388
  telling the caller that per-turn state — most visibly any loaded skill — did
260
389
  not survive and should be reloaded.
261
390
 
391
+ A second `/query` reusing a `run_id` that is still in flight gets `409` —
392
+ `run_id` is caller-minted, so mint a fresh UUID per request; reusing one would
393
+ leave the earlier run with no way to be cancelled. A `/query` supplying a `model`
394
+ that differs from the one its `session_id` was built with **switches the retained
395
+ agent in place** (≥ 2.14), so the conversation and any loaded skills survive the
396
+ change; the same machinery the stdio transport's `/model` uses. A switch that
397
+ fails — a missing Claude credential, an unknown local model — is a `409` naming
398
+ the reason, and leaves the session on its previous model. The session records its
399
+ `provider` too: a turn naming a different `provider` switches it the same way,
400
+ onto that turn's `model` or, if it names none, the provider's default model. A
401
+ `model` from the other provider than the one named (a `claude-*` id with
402
+ `"lemonade"`, or a non-Claude id with `"claude"`) is a `400` on a new session and
403
+ an existing one alike, before anything is built or switched.
404
+
405
+ `provider` is optional, and an omitted one is **not** read as `"lemonade"` — it
406
+ means "whatever the `model` implies", so a `claude-*` id alone reaches Anthropic
407
+ and any other id runs locally. The `400` above therefore fires only when a
408
+ request names a `provider` that disagrees with its `model`; there is no pair to
409
+ disagree when only one is given. On a retained session, omitting both leaves the
410
+ session on the provider it is already using, so an ordinary follow-up turn never
411
+ moves a Claude conversation back to Lemonade.
412
+
262
413
  ### 5.3 Version gate
263
414
 
264
415
  `checkVersion()` reads `/version` and compares the **major** of `apiVersion`
@@ -266,16 +417,163 @@ against this package's `API_VERSION`. A differing major is a breaking contract
266
417
  change and raises `VersionMismatchError`. A higher minor with the same major is a
267
418
  backward-compatible addition and is accepted.
268
419
 
269
- ### 5.4 No caller-auth token
270
-
271
- The email sidecar authenticates callers with a per-session bearer minted into
272
- `GAIA_EMAIL_SIDECAR_TOKEN`. `gaia_agent` has **no equivalent** at 0.1.1, so
273
- this package mints and sends nothing. When the sidecar grows one, it lands here as
274
- a spawn-time env var and a request header — a change to this section, not a new
275
- subsystem.
420
+ ### 5.4 Caller authentication
421
+
422
+ Loopback binding is not access control: this sidecar exposes shell and file
423
+ tools, so an unauthenticated port lets any page the user visits drive it. Three
424
+ controls guard it, all in `gaia_agent.caller_auth`, which binds this sidecar's
425
+ env-var names and exempt paths onto the shared mechanism in
426
+ `gaia.sidecar.caller_auth`:
427
+
428
+ 1. **Per-session bearer token.** Every `/v1/gaia/*` request must carry
429
+ `Authorization: Bearer <token>`. The spawning parent supplies it one of two
430
+ ways: `GAIA_GAIA_SIDECAR_TOKEN_FILE` — the path to a `0600`, owner-only file
431
+ holding the token, which is the preferred channel because the secret never
432
+ sits in the environment — or `GAIA_GAIA_SIDECAR_TOKEN`, the token itself, the
433
+ legacy delivery. A request without it is **401**, with a `detail` naming both
434
+ env vars.
435
+ 2. **Host allowlist.** The `Host` header must be present *and* loopback
436
+ (`127.0.0.1`, `localhost`, `::1`); anything else — including an absent or
437
+ empty header — is **400**. This is what defeats DNS rebinding, where the
438
+ rebound request arrives with `Host: evil.com`, so it fails closed rather
439
+ than letting a caller opt out by omitting the header.
440
+ 3. **Origin rejection.** A request carrying a non-loopback browser `Origin` is
441
+ **403**. Non-browser clients send no `Origin` and are unaffected.
442
+
443
+ `/health`, `/version`, and `/v1/gaia/version` are **token-exempt** — they are
444
+ the probes a host polls during the attach handshake, before any token is in
445
+ play, and none of them exposes user data or accepts work. Host and Origin still
446
+ apply to them, so `waitForHealth` and `checkVersion` work against a
447
+ token-protected sidecar without knowing the token.
448
+
449
+ **Dev mode.** If neither env var is set the token check is skipped and the
450
+ sidecar logs a loud warning saying so; Host and Origin are still enforced. That
451
+ is the state a sidecar this package spawns comes up in, because `spawnSidecar`
452
+ mints nothing — pass your own token through `spawnSidecar`'s `env` option to
453
+ turn the check on. The shipped product does not rely on dev mode: the daemon
454
+ mints a per-session token and passes it as `GAIA_GAIA_SIDECAR_TOKEN_FILE`
455
+ (`gaia.daemon.sidecars.spec` mirrors both names as plain strings so core never
456
+ imports this wheel).
457
+
458
+ **Child processes.** The programs the agent starts — shell commands, MCP
459
+ servers, CLI installs and sign-ins, native hub agents, media tools, the Lemonade
460
+ server — never inherit the sidecar token or GAIA's other internal credentials
461
+ (the model-broker token, custody secret and engineering pairing token): their
462
+ environment comes from `gaia.env.child_env`, which also drops any name listed
463
+ in `GAIA_CHILD_ENV_DENY` (comma or space separated). User credentials such as `GH_TOKEN` pass through unchanged.
464
+ `GAIA_NO_DOTENV=1` in the sidecar's launch environment stops it loading `.env`
465
+ files; a `.env` file cannot set that switch itself.
466
+
467
+ ### 5.5 Other transports
468
+
469
+ The HTTP server above is the surface this package drives, and everything in
470
+ this document describes it. It is not the agent's only transport:
471
+ `gaia_agent.stdio` runs the same agent over newline-delimited JSON on
472
+ stdin/stdout — no port, no token, no discovery file. The terminal UI reaches it
473
+ that way when it spawns the agent as a subprocess; an agent **installed** from
474
+ the hub, which is what this package delivers, is supervised by the daemon over
475
+ the HTTP surface above instead (§6.1).
476
+
477
+ It emits the identical canonical event vocabulary, but its input channel accepts
478
+ a JSON line carrying a `gaia_control` key: a back-channel that answers a
479
+ confirmation prompt *while* a turn is in flight, toggles full access or
480
+ "accept edits" (file edits inside the starting folder run unasked; commands still
481
+ ask), and stops a turn (`cancel`) without ending the process — so loaded skills,
482
+ "always" grants, history and the permission mode survive a cancel.
483
+
484
+ Contract 2.14 gave the HTTP surface the same three capabilities per run and per
485
+ session — `/tool_decision`, `/sessions/{id}/bypass` (the route keeps its
486
+ original spelling), and `provider: "claude"`. What remains stdio-only is the
487
+ *launch* form of those switches: `--full-access` starts a process with gating
488
+ already off, `--accept-edits` starts it in accept-edits mode, and `--use-claude` / `--claude-model` pin the backend for the life
489
+ of the process (embeddings stay on Lemonade either way).
490
+
491
+ The stdio TUI also supports Local, Fireworks AI, and AMD LLM Gateway through
492
+ Lemonade. `/model` lists downloaded local and discovered cloud chat models,
493
+ grouped by provider, with `(default)` beside the configured default model;
494
+ `/model <id>` switches the live client while preserving conversation and skills.
495
+ The switch reply is one line naming the model, where it runs, and, for a cloud
496
+ model, that messages and anything read from files or memory are sent there.
497
+ Cloud IDs (`fireworks.*`, `amd.*`) route through Lemonade without local model
498
+ loading. Model-state status events identify the provider and remote inference.
499
+ The TUI's `/provider` panel configures credentials directly with local Lemonade;
500
+ keys never travel through stdio queries. These controls are not exposed over
501
+ `/v1/gaia/query`. Lemonade may independently be configured to route a model
502
+ remotely, so a local server URL alone does not establish local inference.
503
+
504
+ `--bypass-permissions` turns off more than the prompt. It also lifts the shell
505
+ tool's own guardrails for the session: the shell-only operators — redirection
506
+ (`>`, `>>`, `<`), backgrounding (`&`), substitution (`` ` ``, `$()`) and the
507
+ newline — parse and run, chaining (`&&`, `||`, `;`, `|`) already ran by
508
+ default, the read-only binary allowlist is replaced by a developer set
509
+ (`node`, `npm`, `make`, `cmake`, `go`, `cargo`, `sed`, `awk`, `curl`, `sleep`,
510
+ `timeout`, `export`, `cp`, `mv`, plus `python`/`python3`/`pytest`/`gh`/`git`,
511
+ which already had their own paths), and the shell rate limit is dropped. `git`
512
+ being in that set means its policy's outright refusals — push, reset, rebase —
513
+ also stop applying under bypass. That is
514
+ arbitrary code execution in the working directory, which is why it exists only
515
+ on this transport: one local parent process on a private pipe. It is **not**
516
+ reachable over HTTP, and the request body cannot ask for it —
517
+ `POST /v1/gaia/sessions/{id}/bypass` stops an HTTP session's approval prompts
518
+ but never lifts these shell gates. `rm` is excluded
519
+ from the developer set — not a boundary, since anything in the set can delete a
520
+ file, but a tripwire against an accidental recursive delete. Redirection has one
521
+ exception: a command that is nothing but a skill-granted CLI runs argv-only
522
+ rather than through a shell, so a `>` there is refused with an explanation
523
+ instead of reaching the binary as a literal argument.
524
+
525
+ Every shell command run under bypass is recorded with its full arguments and its
526
+ per-segment breakdown in `~/.gaia/cache/file_audit.log` — skipping the prompt
527
+ does not skip the record. Treat that file as sensitive; arguments are verbatim.
528
+ The host can toggle bypass mid-session over `gaia_control`, and the shell gates
529
+ follow on the very next command.
530
+
531
+ `--use-claude` is the one with a reach beyond the machine, and it cannot be
532
+ turned on for what this package delivers: the terminal UI **refuses** the launch
533
+ flag for a daemon-transport agent, with an error saying so, because it pins the
534
+ backend for the life of the process and the daemon relay has no such lever.
535
+
536
+ That is a limit on the *launch flag*, not on the agent. Since contract 2.14 a
537
+ caller can ask for Anthropic per request with `/query`'s `provider` field (§5.2),
538
+ so local inference is this package's **default**, not a property of the transport
539
+ that nobody can change. Anything reaching Anthropic is named by the request that
540
+ asked for it.
276
541
 
277
542
  ---
278
543
 
544
+ ### 5.6 Adding to a turn already running
545
+
546
+ `POST /v1/gaia/query/{run_id}/followup` with `{ "text": "…" }` hands a live run
547
+ something the user typed after it started. Contract ≥ 2.15.
548
+
549
+ It is not a second turn and not an interrupt. The run keeps going on its
550
+ existing SSE stream; the agent folds the text into that turn's context at its
551
+ next agent-loop step boundary, labelled as arriving mid-task, and answers it
552
+ alongside the work already in progress. A five-minute turn can therefore be
553
+ corrected ("actually, only the unread ones") while it is still running, instead
554
+ of the correction waiting out the turn it was meant to change.
555
+
556
+ Two refusals, both loud, because the caller has already taken the message from
557
+ the user and owes them a truthful answer about where it went:
558
+
559
+ | Status | Meaning |
560
+ | ------ | ------------------------------------------------------------ |
561
+ | `404` | No such run in flight — it finished or was cancelled. Send it as a new `/query`. |
562
+ | `409` | The run's agent is not accepting mid-turn input. |
563
+
564
+ A turn that has already formed its answer takes one more step to address a
565
+ follow-up, so a turn answering in a single step is covered too. The one
566
+ exception is a turn already at its step limit: the message stays queued rather
567
+ than being consumed into a context no model call will read.
568
+
569
+ Because `/query` is stateless (§2.4) the host still owns the transcript: record
570
+ a delivered follow-up in the `context` you push on the **next** turn, between
571
+ that turn's question and its answer, or the conversation loses words the agent
572
+ demonstrably saw.
573
+
574
+ Clients that predate 2.15 get a `404` on the path itself. Probe `/version`
575
+ before sending rather than reading a 404 as "the run ended".
576
+
279
577
  ## 6. Process ownership
280
578
 
281
579
  Two distinct paths, deliberately:
@@ -293,13 +591,23 @@ from `~/.gaia/agents/gaia/`. A sidecar spawned here would be a second process th
293
591
  TUI never talks to, competing for port `8141`.
294
592
 
295
593
  So `run`'s contribution to the sidecar is putting a **verified** binary where the
296
- daemon looks. Daemon and sidecar lifecycle belong to the daemon.
594
+ daemon looks, and recording the install in `.installed` (§4.1) so the daemon and
595
+ the TUI both see it. Daemon and sidecar lifecycle belong to the daemon.
297
596
 
298
597
  ### 6.2 `gaia serve` — the direct path
299
598
 
300
599
  `serve` fetches the sidecar and spawns it itself, for integrators who want the
301
- REST surface without a daemon or a UI. This path owns the process fully: spawn →
302
- `waitForHealth` → `checkVersion` → run until interrupted → tree-kill.
600
+ REST surface without a daemon or a UI. This path owns the process fully:
601
+ port pre-flight → spawn → `waitForHealth` → `checkVersion` → run until
602
+ interrupted → tree-kill.
603
+
604
+ The port is checked **before** the spawn, and a taken port raises
605
+ `PortInUseError` without starting anything. That ordering is the guarantee: the
606
+ frozen sidecar spends seconds unpacking before it attempts its bind, while an
607
+ incumbent answers `/health` in milliseconds — so a post-spawn check alone would
608
+ see a healthy port and a still-alive child, and hand back a handle for a server
609
+ it does not own. `assertOurs` and the re-probe after a failed health wait remain
610
+ as backstops for the narrower window where something binds after the pre-flight.
303
611
 
304
612
  ### 6.3 Tree-kill
305
613
 
@@ -325,8 +633,8 @@ failed start never leaks a process.
325
633
  | Code | Meaning |
326
634
  | ------- | ----------------------------------------------------------------------- |
327
635
  | `0` | Success |
328
- | `1` | A typed failure: `IntegrityError`, `PlatformError`, `HealthTimeoutError`, `VersionMismatchError`, `BinaryNotFoundError`, or an unexpected error |
329
- | `2` | Usage error: unknown command, unknown `--component`, invalid `--port` |
636
+ | `1` | A typed failure: `IntegrityError`, `PlatformError`, `HealthTimeoutError`, `VersionMismatchError`, `BinaryNotFoundError`, `PortInUseError`, `SidecarExitedError`, `MalformedResponseError`, or an unexpected error; also `serve` on Ctrl+C when the sidecar survives the forced kill (the message names its pid and the kill command) |
637
+ | `2` | Usage error: unknown command, invalid `--port`, or a flag the command does not read (`run --port`, `serve --component`, `serve --cache-dir`, `run`/`serve` `--platform`), an unknown `--component`, or a non-https `--base-url` without `--allow-insecure-base-url` |
330
638
  | *other* | From `run`: the TUI's own exit code, propagated verbatim |
331
639
 
332
640
  A TUI killed by a signal propagates as `128 + signum` (the shell convention), so a
@@ -340,12 +648,15 @@ All extend `GaiaError`, so `instanceof GaiaError` catches any of ours.
340
648
 
341
649
  | Class | Raised when |
342
650
  | ---------------------- | -------------------------------------------------------------- |
343
- | `IntegrityError` | A download's SHA-256 does not match the lock |
651
+ | `IntegrityError` | A download's SHA-256 does not match the lock, or a resolved binary's does not |
344
652
  | `PlatformError` | Unsupported platform, missing/incomplete entry, placeholder hash, malformed lock |
345
653
  | `HealthTimeoutError` | The sidecar did not report healthy within the timeout |
346
654
  | `VersionMismatchError` | `apiVersion` major differs from this package's |
347
655
  | `BinaryNotFoundError` | A binary is absent from disk when spawning |
656
+ | `PortInUseError` | The bind port was already taken; nothing was spawned |
657
+ | `SidecarExitedError` | The spawned sidecar died while the port answered — something else owns it |
348
658
  | `HttpError` | A non-2xx from a sidecar probe |
659
+ | `MalformedResponseError` | A 2xx from a sidecar probe whose body is not JSON |
349
660
 
350
661
  Per the repo's no-silent-fallbacks rule, every message names what failed, what to
351
662
  do, and where to look next.
@@ -370,7 +681,8 @@ do, and where to look next.
370
681
  Exported from the package root — see `src/index.ts`.
371
682
 
372
683
  **Fetch:** `fetchAll`, `fetchBinary`, `verifySha256`, `fileSha256`,
373
- `binaryExists`, `defaultCacheDir`, `daemonSidecarCacheDir`.
684
+ `binaryExists`, `defaultCacheDir`, `daemonSidecarCacheDir`,
685
+ `INSTALLED_SENTINEL_NAME`, `SIDECAR_AGENT_ID`.
374
686
 
375
687
  **Lifecycle:** `spawnSidecar`, `startSidecar`, `waitForHealth`, `checkVersion`,
376
688
  `health`, `version`, `shutdown`, `runTui`, `resolveSidecarPath`, `resolveTuiPath`,
@@ -384,6 +696,13 @@ Exported from the package root — see `src/index.ts`.
384
696
  **Constants:** `AGENT_ID`, `API_VERSION`, `DEFAULT_HOST`, `DEFAULT_PORT`,
385
697
  `RESERVED_PORT`.
386
698
 
699
+ `resolveSidecarPath` / `resolveTuiPath` re-verify the file's SHA-256 against
700
+ `binaries.lock.json` before returning a path that is about to be spawned — the
701
+ cache path is predictable, so anything able to write it would otherwise get code
702
+ run. That makes them **O(size of the binary)**, not a cheap path join: resolve
703
+ once at startup, not per request. Pass `{ verify: false }` for a binary you built
704
+ yourself, which no lock describes, and `{ lock }` to reuse an already-loaded lock.
705
+
387
706
  ---
388
707
 
389
708
  ## 11. Logging
@@ -391,3 +710,24 @@ Exported from the package root — see `src/index.ts`.
391
710
  `DEBUG=gaia` (or `DEBUG=*`) enables debug output. **Everything goes to stderr** —
392
711
  stdout belongs to the TUI once it is exec'd, and to machine-readable JSON for
393
712
  `fetch` / `version`.
713
+
714
+ ## Developer engineering mode
715
+
716
+ - **Opt-in.** The agent enables it with `--developer-mode` or `GAIA_DEVELOPER_MODE=1`
717
+ in the host process. `gaia engineering` and the engineering MCP server accept only
718
+ the explicit `--developer-mode` flag. It is separate from diagnostic `--dev`.
719
+ - **Surface.** Only in that mode does the agent load the `gaia-harness-engineering`
720
+ skill and the `share_engineering_context`, `append_engineering_context`,
721
+ `approve_engineering_code`, `engineering_status`, `open_engineering_app` and
722
+ `revoke_engineering_context` tools. Sessions without it have none of them.
723
+ - **Consent.** Sharing, appending and code approval each ask for a fresh approval of
724
+ the exact content or scope shown. None of them can be remembered or auto-approved.
725
+ Snapshots are recipient-bound and expire; revoking stops future reads.
726
+ - **Pairing.** Configure the coding app's MCP connection from a Python GAIA install
727
+ with the `[mcp]` extra. The frozen binary this package ships can reuse that
728
+ configuration but cannot launch the Python MCP server itself.
729
+ - **Code.** Fixes happen in the coding app, in worktrees from GAIA's managed source
730
+ cache. Preview results are reported by that app, not verified by GAIA. Nothing
731
+ self-assesses or updates the installed GAIA.
732
+
733
+ See the [usage guide](https://amd-gaia.ai/docs/guides/harness-engineering).
@@ -1,76 +1,76 @@
1
1
  {
2
2
  "schemaVersion": "3.0",
3
- "agentVersion": "0.1.1",
3
+ "agentVersion": "0.3.0",
4
4
  "components": {
5
5
  "sidecar": {
6
- "componentVersion": "0.1.1",
7
- "baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.1.1",
6
+ "componentVersion": "0.3.0",
7
+ "baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.3.0",
8
8
  "platforms": {
9
9
  "win32-x64": {
10
10
  "filename": "gaia-agent-win32-x64.exe",
11
11
  "executable": "gaia-agent.exe",
12
- "sha256": "416ed0e24cd3457ae66acd1caaf8337d0761f093b86813d8640b7cad6236856a",
13
- "size": 90667208
12
+ "sha256": "12e79e63e1169eafadbdd453e2d5e9114bc8c9c04e7ab95e9bd1c96249f50067",
13
+ "size": 169416076
14
14
  },
15
15
  "darwin-arm64": {
16
16
  "filename": "gaia-agent-darwin-arm64",
17
17
  "executable": "gaia-agent",
18
- "sha256": "f962b8c7ce042938a940a01fcea5c94fc0ff33b4c05c02770804c89c31aaa1c2",
19
- "size": 74152544
18
+ "sha256": "f431642fc5cd9c05110cb8f0ffd05fe36853fe1a579bbd0b5421a3c79adc5ddd",
19
+ "size": 159366272
20
20
  },
21
21
  "darwin-x64": {
22
22
  "filename": "gaia-agent-darwin-x64",
23
23
  "executable": "gaia-agent",
24
- "sha256": "2f001c8c8e75b286c758269d0fc22fe57ef68ef5ee59476d960531e363b73c25",
25
- "size": 78642224
24
+ "sha256": "c793fa760b42c1c1acea7aa411f7a6830ee3bd9f68f56c5b6fac686801883f3b",
25
+ "size": 168077296
26
26
  },
27
27
  "linux-x64": {
28
28
  "filename": "gaia-agent-linux-x64",
29
29
  "executable": "gaia-agent",
30
- "sha256": "b704f83a4beaa4654a37cf8e2fcc7429cd6da91f7431c3381e7005370e760c7a",
31
- "size": 122029424
30
+ "sha256": "b2b115d0ae84e5fbe9aebcf055b264b5b68ae2f1a4b436bdd80de6602ca51437",
31
+ "size": 221414096
32
32
  }
33
33
  }
34
34
  },
35
35
  "tui": {
36
- "componentVersion": "0.23.0",
37
- "baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.23.0",
36
+ "componentVersion": "0.25.0",
37
+ "baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.25.0",
38
38
  "platforms": {
39
39
  "win32-x64": {
40
40
  "filename": "gaia-win-x64.exe",
41
41
  "executable": "gaia-tui.exe",
42
- "sha256": "aad0c38c5c59e8107b608981f34608ee08feb7e5965de1cfcb929a49f21258ca",
43
- "size": 19433984
42
+ "sha256": "9b1c467b04b267f172a6024589f1ba0ddc882729337913d8afa7dcd43a7a7967",
43
+ "size": 20604416
44
44
  },
45
45
  "win32-arm64": {
46
46
  "filename": "gaia-win-arm64.exe",
47
47
  "executable": "gaia-tui.exe",
48
- "sha256": "2f78c8128a8ad6d17d51938de12ed7aa29ceadb642af8d6e929920fe17e2463c",
49
- "size": 17975808
48
+ "sha256": "332068c5588e5639213225d63e4a1827d9258928e4053b7d6602668dc7eda60c",
49
+ "size": 19030016
50
50
  },
51
51
  "darwin-arm64": {
52
52
  "filename": "gaia-darwin-arm64",
53
53
  "executable": "gaia-tui",
54
- "sha256": "766100459d22d9886a8d7839d5eddffb2a51134be76a57360b1584755be3cef1",
55
- "size": 17896354
54
+ "sha256": "bb5ae28ae99ad25c095b3f57c4abf4e2ff67fa560122dc0a99036aedf9184802",
55
+ "size": 18801330
56
56
  },
57
57
  "darwin-x64": {
58
58
  "filename": "gaia-darwin-x64",
59
59
  "executable": "gaia-tui",
60
- "sha256": "494197bd2bf245df94de9361a274723536e73f275bbb92efc1c4db027cc08d9e",
61
- "size": 19003168
60
+ "sha256": "5259c82dc195297130e16377586ed6e0e668c5ec7f8b27a76ee029d67d776b52",
61
+ "size": 20015840
62
62
  },
63
63
  "linux-x64": {
64
64
  "filename": "gaia-linux-x64",
65
65
  "executable": "gaia-tui",
66
- "sha256": "d913c4cc4943f6a5a9be737cf397ad2a05e013e55a7880c27d4ddef973ee16f4",
67
- "size": 18661538
66
+ "sha256": "07f1cc448f8056b5915eff2869f9f3eec3a5d04485d93e448ccad00d186d23c5",
67
+ "size": 19669154
68
68
  },
69
69
  "linux-arm64": {
70
70
  "filename": "gaia-linux-arm64",
71
71
  "executable": "gaia-tui",
72
- "sha256": "c8faee8cc7281819042d9e123d5a68b6ccfe4afa3d92d5fa3b2f42770df1d894",
73
- "size": 17432738
72
+ "sha256": "c324100d2de989823e816a6aae7eb4e1224019096b9a46ef1a6983ca6b98598f",
73
+ "size": 18350242
74
74
  }
75
75
  }
76
76
  }
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAGA;;;;;;;;;;;GAWG;AA8BH,UAAU,UAAU;IAClB,CAAC,EAAE,MAAM,EAAE,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACxC,mEAAmE;IACnE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAaD,4EAA4E;AAC5E,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAuCpD;AA0ED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,MAAM,GAAG,SAA+B,EACjD,KAAK,GAAE,MAAM,GAAG,SAA2B,GAC1C,MAAM,GAAG,SAAS,CAmBpB;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAQnG;AAqJD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiC1D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAGA;;;;;;;;;;;GAWG;AA8BH,UAAU,UAAU;IAClB,CAAC,EAAE,MAAM,EAAE,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACxC,mEAAmE;IACnE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAaD,4EAA4E;AAC5E,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAuCpD;AAsID;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,MAAM,GAAG,SAA+B,EACjD,KAAK,GAAE,MAAM,GAAG,SAA2B,GAC1C,MAAM,GAAG,SAAS,CAsBpB;AA+CD,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAUnG;AAsLD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC1D"}