@amd-gaia/gaia 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,537 @@
1
+ # `@amd-gaia/gaia` — technical reference
2
+
3
+ Version **0.1.1**. Companion to [`README.md`](./README.md), which is the
4
+ user-facing doc. This file specifies the wire and file formats the package
5
+ depends on and the guarantees it makes.
6
+
7
+ ---
8
+
9
+ ## 1. Scope
10
+
11
+ `@amd-gaia/gaia` is a **binary delivery and process-lifecycle package**. It ships
12
+ no agent logic. It owns:
13
+
14
+ - resolving the host platform to a lock key,
15
+ - downloading and **SHA-256 verifying** two published binaries — one from this
16
+ package's own hub lane, one from the `terminal-hub` component's,
17
+ - caching them in stable, versioned locations,
18
+ - launching the terminal UI, or the agent sidecar directly.
19
+
20
+ It does **not** own the GAIA daemon's lifecycle, the sidecar's supervision, or
21
+ any agent behaviour. See §6 for where those boundaries sit.
22
+
23
+ ---
24
+
25
+ ## 2. `binaries.lock.json`
26
+
27
+ Ships inside the published package (`files` includes it). It is the single source
28
+ of truth for what is downloaded and what it must hash to.
29
+
30
+ ### 2.1 Schema (`schemaVersion` `3.0`)
31
+
32
+ ```jsonc
33
+ {
34
+ "schemaVersion": "3.0",
35
+ "agentVersion": "0.1.1",
36
+ "components": {
37
+ "sidecar": {
38
+ "componentVersion": "0.1.1",
39
+ "baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.1.1",
40
+ "platforms": { "<platformKey>": { /* entry */ } }
41
+ },
42
+ "tui": {
43
+ "componentVersion": "0.23.0",
44
+ "baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.23.0",
45
+ "platforms": { "<platformKey>": { /* entry */ } }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ A component lane:
52
+
53
+ | Field | Type | Meaning |
54
+ | ------------------ | -------- | ---------------------------------------------------------- |
55
+ | `componentVersion` | `string` | That component's own released version |
56
+ | `baseUrl` | `string` | Where **that component's** artifacts are served from |
57
+ | `platforms` | `object` | Platform key → entry |
58
+
59
+ An entry:
60
+
61
+ | Field | Type | Meaning |
62
+ | ------------ | -------- | ----------------------------------------------------------- |
63
+ | `filename` | `string` | Artifact name as published under **its component's** `baseUrl` |
64
+ | `executable` | `string` | Basename it is written as on disk (with the platform extension) |
65
+ | `sha256` | `string` | Lowercase hex SHA-256 the download must match |
66
+ | `size` | `number` | Informational. **Not** enforced |
67
+
68
+ **Why per-component, and why `3.0`.** `2.0` had one top-level `baseUrl` shared by
69
+ both components. That stopped being true once the TUI became the published
70
+ `terminal-hub` component (§2.2): the two live in different hub lanes at different
71
+ versions. A shared base URL cannot express that, so `2.x` is rejected at load with
72
+ an error naming the schema — never read as `3.x` with a missing field.
73
+
74
+ The email agent's lock is `schemaVersion` `1.0`, a flat `binaries` map, because it
75
+ delivers one binary from one lane. A `1.x`-shaped lock is likewise rejected.
76
+
77
+ ### 2.2 Where each component comes from
78
+
79
+ | Component | Hub lane | Published by |
80
+ | --------- | ------------------------------------- | ----------------------- |
81
+ | `sidecar` | `agents/gaia/<agentVersion>/` | this package's release |
82
+ | `tui` | `agents/terminal-hub/<componentVersion>/` | the core GAIA release |
83
+
84
+ The TUI is **consumed, not built here**. It is the same `tui/cmd/gaia` binary the
85
+ core release publishes as the `terminal-hub` component and a core install runs as
86
+ `gaia tui` — so behaviour is identical by construction rather than by convention.
87
+ Building a second copy under this package's lane would put the same bytes at a
88
+ different version under a third naming convention, and the two would drift.
89
+
90
+ The consequence is a real release dependency: this package cannot ship until
91
+ `terminal-hub` is published at the version its lock pins. The release fails loudly
92
+ naming that version; it never falls back to building its own TUI.
93
+
94
+ The `tui` lane's origin is pinned in the lock rather than derived from the release
95
+ pipeline's hub-origin variable, so the release verifies the exact URL the shipped
96
+ lock will send users to. A release pointed at a non-default origin therefore moves
97
+ the sidecar lane and **not** the TUI lane; re-point `components.tui.baseUrl` too if
98
+ you need both.
99
+
100
+ ### 2.3 Platform keys
101
+
102
+ `` `${process.platform}-${process.arch}` `` — e.g. `win32-x64`, `darwin-arm64`,
103
+ `linux-x64`. This matches `gaia.daemon.sidecars.platform.current_platform_key()`,
104
+ which normalises Python's `sys.platform` / `platform.machine()` into the same
105
+ namespace so the daemon and this package agree on a cache key.
106
+
107
+ **The `win32` ↔ `win` mapping.** The `terminal-hub` lane names its Windows
108
+ artifacts `gaia-win-x64.exe` / `gaia-win-arm64.exe` (Go's `GOOS` vocabulary), while
109
+ our keys come from `process.platform`, which says `win32`. The lock keeps the
110
+ `win32-*` key and carries the hub's spelling in `filename`:
111
+
112
+ | Platform key | `tui` `filename` |
113
+ | -------------- | ------------------------ |
114
+ | `win32-x64` | `gaia-win-x64.exe` |
115
+ | `win32-arm64` | `gaia-win-arm64.exe` |
116
+ | `darwin-x64` | `gaia-darwin-x64` |
117
+ | `darwin-arm64` | `gaia-darwin-arm64` |
118
+ | `linux-x64` | `gaia-linux-x64` |
119
+ | `linux-arm64` | `gaia-linux-arm64` |
120
+
121
+ The mapping therefore lives in **data**, not in a code path — nothing branches on
122
+ the platform to build a name. `TUI_ARTIFACT_NAMES` (in `src/platform.ts` and in
123
+ `packaging/gen_binaries_lock.py`) is the authority both the shipped lock and the
124
+ release pipeline are checked against, because a wrong name here is not a build
125
+ failure anywhere: it is a 404 on a user's first run.
126
+
127
+ ### 2.4 Platform coverage
128
+
129
+ | Platform key | `sidecar` | `tui` |
130
+ | -------------- | :-------: | :---: |
131
+ | `win32-x64` | yes | yes |
132
+ | `darwin-arm64` | yes | yes |
133
+ | `darwin-x64` | yes | yes |
134
+ | `linux-x64` | yes | yes |
135
+ | `linux-arm64` | **no** | yes |
136
+ | `win32-arm64` | **no** | yes |
137
+
138
+ `terminal-hub` publishes all six. The sidecar is a PyInstaller freeze, produced on
139
+ the platform it targets, and there is no arm64 Linux or arm64 Windows freeze.
140
+ Resolving `sidecar` on those two keys raises `PlatformError` naming the platform
141
+ and the supported set — it is not silently skipped, and the TUI is not launched
142
+ without an agent behind it.
143
+
144
+ ### 2.5 Placeholder hashes
145
+
146
+ Between releases every `sha256` is `PENDING-replace-with-real-sha256`. The release
147
+ pipeline regenerates the file with real hashes: for the sidecar, computed from the
148
+ artifacts it just published; for the TUI, computed from the `terminal-hub`
149
+ artifacts it downloaded and cross-checked against that lane's own recorded hashes
150
+ (`agents/terminal-hub/manifest.json`, which the hub computes server-side at publish
151
+ time). Both are then re-fetched from the public origin and re-hashed before the
152
+ release is allowed to ship.
153
+
154
+ `isPlaceholderSha()` treats a value as a placeholder when it is all zeros or
155
+ contains `PENDING` (case-insensitive). A placeholder **blocks the fetch before any
156
+ network call** with a `PlatformError`. There is no flag, env var, or option that
157
+ relaxes this.
158
+
159
+ ---
160
+
161
+ ## 3. Integrity
162
+
163
+ The SHA-256 check is the package's security boundary.
164
+
165
+ - Downloaded bytes are hashed in memory and compared against the lock **before
166
+ anything is written to the cache path**.
167
+ - On mismatch: `IntegrityError`, message naming expected vs actual, nothing left
168
+ on disk.
169
+ - Writes go to `<path>.download.<pid>` and are `rename`d into place, so a crash
170
+ mid-write never leaves a partial file that a later run treats as verified.
171
+ - A cache hit requires re-hashing the on-disk file and matching the lock.
172
+ A cached file whose bytes drifted is re-downloaded, not reused.
173
+ - POSIX installs `chmod 0o755` after the rename.
174
+
175
+ There is no unverified path, no "warn and continue", and no way to disable the
176
+ check.
177
+
178
+ ---
179
+
180
+ ## 4. Filesystem layout
181
+
182
+ | Component | Default directory | Executable |
183
+ | --------- | ------------------------------ | ------------------- |
184
+ | `sidecar` | `~/.gaia/agents/gaia/` | `gaia-agent[.exe]` |
185
+ | `tui` | `~/.gaia/npm-cache/gaia-<agentVersion>/` | `gaia-tui[.exe]` |
186
+
187
+ The sidecar directory is a **cross-repo contract** with
188
+ `gaia.daemon.sidecars.fetch.default_cache_dir("gaia")`. The daemon spawns and
189
+ supervises the sidecar and does its own SHA-256 check on the binary it finds
190
+ there; installing an already-verified binary at that path turns the daemon's fetch
191
+ into a cache hit instead of a second multi-hundred-megabyte download.
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
+
231
+ The TUI directory is keyed by `agentVersion` — this package's version, not the
232
+ component's — so a bump of either never reuses the previous release's executable.
233
+
234
+ The TUI executable is renamed to `gaia-tui` on install, **never** `gaia`: the
235
+ `terminal-hub` artifact is `gaia-<platform>` and npm installs a `gaia` bin shim on
236
+ `PATH`, so keeping the hub's name would shadow the shim. `filename` (what is
237
+ downloaded) and `executable` (what is written) are separate fields for exactly this
238
+ reason, and `test/lock.test.ts` asserts it for every platform.
239
+
240
+ Both defaults are overridable (`--sidecar-dir`, `--cache-dir`).
241
+
242
+ ---
243
+
244
+ ## 5. Sidecar HTTP surface
245
+
246
+ Served by `gaia_agent.server`. Bound to `127.0.0.1` only.
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
+
253
+ | Property | Value |
254
+ | ------------------ | --------------------------------------- |
255
+ | Default port | `8141` (`DEFAULT_PORT` in `server.py`) |
256
+ | Reserved port | `4001` — refused with a `RangeError` |
257
+ | Contract version | `API_VERSION = "2.12"` |
258
+ | Agent id / prefix | `gaia` → `/v1/gaia/...` |
259
+
260
+ ### 5.1 Endpoints
261
+
262
+ | Method | Path | Purpose |
263
+ | ------ | -------------------------------- | ---------------------------------------------- |
264
+ | `GET` | `/health` | Liveness. `{ "status": "ok", "service": "gaia-agent-gaia" }` |
265
+ | `GET` | `/version` | Contract probe. `{ "apiVersion", "agentVersion" }` |
266
+ | `GET` | `/v1/gaia/version` | The TUI's negotiation probe |
267
+ | `GET` | `/v1/gaia/init` | Readiness detail (Lemonade, model, connectors) |
268
+ | `POST` | `/v1/gaia/query` | The streaming surface (`text/event-stream`) |
269
+ | `POST` | `/v1/gaia/query/{run_id}/cancel` | Cancel a run by its host-minted `run_id` |
270
+ | `POST` | `/v1/gaia/query/{run_id}/respond`| Answer a mid-run question |
271
+
272
+ `/health` is liveness only. It says nothing about whether Lemonade is up or a
273
+ model is loaded — `/v1/gaia/init` answers that.
274
+
275
+ ### 5.2 `session_id` and agent retention
276
+
277
+ `POST /v1/gaia/query` accepts an optional `session_id` in the request body.
278
+ **Pass it on every call in a conversation, and reuse the same value for the
279
+ whole conversation.** Contract ≥ 2.12 resolves `session_id` to a *retained*
280
+ agent instead of a throwaway built fresh per call — indexed documents and
281
+ `load_skill` state only survive between turns when the same `session_id`
282
+ threads them together.
283
+
284
+ A retained skill stays *loaded* but its body is not necessarily in the prompt
285
+ every turn: the agent selects per turn which loaded bodies match the query and
286
+ collapses the rest to a one-line menu entry (re-activated by calling
287
+ `load_skill` again). `GAIA_DYNAMIC_SKILLS=0` disables the selection;
288
+ `GAIA_DYNAMIC_SKILLS_TAU=<float>` overrides its threshold; an embedder outage
289
+ disables it for the session and every body renders. Omitting it is a valid, explicit one-shot: nothing
290
+ persists past that single turn, and the agent is not told otherwise.
291
+
292
+ A retained session also carries a **project map** — up to 600 prompt tokens of
293
+ directory shape, entry points, installed commands and platform quirks, present
294
+ whenever the agent's working directory resolves to a code repository (a VCS
295
+ directory or a recognised manifest at its root). `GAIA_PROJECT_ROOT=<path>`
296
+ picks the project when the working directory is not it. If that repository has
297
+ no code index, the first turn starts one in a background thread;
298
+ `GAIA_PROJECT_MAP_AUTO_INDEX=0` disables that. Neither affects the wire
299
+ contract — they change what the agent knows and what the first turn costs.
300
+
301
+ A second `/query` for a `session_id` that already has a turn in flight gets
302
+ `409 Conflict` — cancel the running turn or wait for it, then retry. A
303
+ `/query` that needs a **new** session while every retained slot is busy and
304
+ none is idle enough to evict gets `503` with the reason in `detail` — a
305
+ temporary, retryable condition, not a bug: wait for a turn to finish (or
306
+ close an idle session) and retry. A
307
+ `session_id` can also be evicted from the retention table under an idle
308
+ timeout (**4 hours** without a turn) or an LRU cap on concurrent sessions
309
+ (**100**) — the two knobs that decide when the `503` above can happen at all
310
+ (`session_registry.py`). Eviction is idle-only: a conversation still in use is
311
+ never timed out mid-flight. A `/query` that lands on an
312
+ evicted id gets a fresh agent (the conversation is not blocked) but the
313
+ response stream's first event is a `{"type":"status","status":"warning",...}`
314
+ telling the caller that per-turn state — most visibly any loaded skill — did
315
+ not survive and should be reloaded.
316
+
317
+ A second `/query` reusing a `run_id` that is still in flight gets `409` —
318
+ `run_id` is caller-minted, so mint a fresh UUID per request; reusing one would
319
+ leave the earlier run with no way to be cancelled. A `/query` supplying a `model`
320
+ that differs from the one its `session_id` was built with also gets `409`: only
321
+ agent construction reads a model, so the request cannot be honoured on the
322
+ retained agent. Omit `model` to continue on the session's current one, or start a
323
+ new `session_id` to switch.
324
+
325
+ ### 5.3 Version gate
326
+
327
+ `checkVersion()` reads `/version` and compares the **major** of `apiVersion`
328
+ against this package's `API_VERSION`. A differing major is a breaking contract
329
+ change and raises `VersionMismatchError`. A higher minor with the same major is a
330
+ backward-compatible addition and is accepted.
331
+
332
+ ### 5.4 Caller authentication
333
+
334
+ Loopback binding is not access control: this sidecar exposes shell and file
335
+ tools, so an unauthenticated port lets any page the user visits drive it. Three
336
+ controls guard it, all in `gaia_agent.caller_auth`, which binds this sidecar's
337
+ env-var names and exempt paths onto the shared mechanism in
338
+ `gaia.sidecar.caller_auth`:
339
+
340
+ 1. **Per-session bearer token.** Every `/v1/gaia/*` request must carry
341
+ `Authorization: Bearer <token>`. The spawning parent supplies it one of two
342
+ ways: `GAIA_GAIA_SIDECAR_TOKEN_FILE` — the path to a `0600`, owner-only file
343
+ holding the token, which is the preferred channel because the secret never
344
+ sits in the environment — or `GAIA_GAIA_SIDECAR_TOKEN`, the token itself, the
345
+ legacy delivery. A request without it is **401**, with a `detail` naming both
346
+ env vars.
347
+ 2. **Host allowlist.** The `Host` header must be present *and* loopback
348
+ (`127.0.0.1`, `localhost`, `::1`); anything else — including an absent or
349
+ empty header — is **400**. This is what defeats DNS rebinding, where the
350
+ rebound request arrives with `Host: evil.com`, so it fails closed rather
351
+ than letting a caller opt out by omitting the header.
352
+ 3. **Origin rejection.** A request carrying a non-loopback browser `Origin` is
353
+ **403**. Non-browser clients send no `Origin` and are unaffected.
354
+
355
+ `/health`, `/version`, and `/v1/gaia/version` are **token-exempt** — they are
356
+ the probes a host polls during the attach handshake, before any token is in
357
+ play, and none of them exposes user data or accepts work. Host and Origin still
358
+ apply to them, so `waitForHealth` and `checkVersion` work against a
359
+ token-protected sidecar without knowing the token.
360
+
361
+ **Dev mode.** If neither env var is set the token check is skipped and the
362
+ sidecar logs a loud warning saying so; Host and Origin are still enforced. That
363
+ is the state a sidecar this package spawns comes up in, because `spawnSidecar`
364
+ mints nothing — pass your own token through `spawnSidecar`'s `env` option to
365
+ turn the check on. The shipped product does not rely on dev mode: the daemon
366
+ mints a per-session token and passes it as `GAIA_GAIA_SIDECAR_TOKEN_FILE`
367
+ (`gaia.daemon.sidecars.spec` mirrors both names as plain strings so core never
368
+ imports this wheel).
369
+
370
+ ### 5.5 Other transports
371
+
372
+ The HTTP server above is the surface this package drives, and everything in
373
+ this document describes it. It is not the agent's only transport:
374
+ `gaia_agent.stdio` runs the same agent over newline-delimited JSON on
375
+ stdin/stdout — no port, no token, no discovery file. The terminal UI reaches it
376
+ that way when it spawns the agent as a subprocess; an agent **installed** from
377
+ the hub, which is what this package delivers, is supervised by the daemon over
378
+ the HTTP surface above instead (§6.1).
379
+
380
+ It emits the identical canonical event vocabulary, but its input channel accepts
381
+ a JSON line carrying a `gaia_control` key, which gives it something HTTP does
382
+ not have: a back-channel that can answer a confirmation prompt *while* a turn is
383
+ in flight, and stop that turn (`cancel`) without ending the process — so loaded
384
+ skills, "always" grants, history and the bypass mode survive a cancel. It also
385
+ takes `--bypass-permissions` (start with gating off) and
386
+ `--use-claude` / `--claude-model` (route chat to the Anthropic API instead of
387
+ local Lemonade; embeddings stay on Lemonade either way). None of that is
388
+ reachable over `/v1/gaia/query`.
389
+
390
+ The stdio TUI also supports Local, Fireworks AI, and AMD LLM Gateway through
391
+ Lemonade. `/model` lists downloaded local and discovered cloud chat models;
392
+ `/model <id>` switches the live client while preserving conversation and skills.
393
+ Cloud IDs (`fireworks.*`, `amd.*`) route through Lemonade without local model
394
+ loading. Model-state status events identify the provider and remote inference.
395
+ The TUI's `/provider` panel configures credentials directly with local Lemonade;
396
+ keys never travel through stdio queries. These controls are not exposed over
397
+ `/v1/gaia/query`. Lemonade may independently be configured to route a model
398
+ remotely, so a local server URL alone does not establish local inference.
399
+
400
+ ---
401
+
402
+ ## 6. Process ownership
403
+
404
+ Two distinct paths, deliberately:
405
+
406
+ ### 6.1 `gaia run` — the normal path
407
+
408
+ `run` fetches, verifies, installs, and **execs the TUI**. It does not spawn a
409
+ sidecar.
410
+
411
+ That is not an omission. The TUI reaches agents through the GAIA daemon's relay
412
+ (`/v1/<agent>/*`) and holds only the daemon client token, never a sidecar bearer —
413
+ `tui/internal/daemon` states this as an invariant. The TUI start-or-attaches the
414
+ daemon under an advisory lock, and the daemon spawns and supervises the sidecar
415
+ from `~/.gaia/agents/gaia/`. A sidecar spawned here would be a second process the
416
+ TUI never talks to, competing for port `8141`.
417
+
418
+ So `run`'s contribution to the sidecar is putting a **verified** binary where the
419
+ daemon looks, and recording the install in `.installed` (§4.1) so the daemon and
420
+ the TUI both see it. Daemon and sidecar lifecycle belong to the daemon.
421
+
422
+ ### 6.2 `gaia serve` — the direct path
423
+
424
+ `serve` fetches the sidecar and spawns it itself, for integrators who want the
425
+ REST surface without a daemon or a UI. This path owns the process fully:
426
+ port pre-flight → spawn → `waitForHealth` → `checkVersion` → run until
427
+ interrupted → tree-kill.
428
+
429
+ The port is checked **before** the spawn, and a taken port raises
430
+ `PortInUseError` without starting anything. That ordering is the guarantee: the
431
+ frozen sidecar spends seconds unpacking before it attempts its bind, while an
432
+ incumbent answers `/health` in milliseconds — so a post-spawn check alone would
433
+ see a healthy port and a still-alive child, and hand back a handle for a server
434
+ it does not own. `assertOurs` and the re-probe after a failed health wait remain
435
+ as backstops for the narrower window where something binds after the pre-flight.
436
+
437
+ ### 6.3 Tree-kill
438
+
439
+ The sidecar is a PyInstaller one-file build: it unpacks and spawns a child uvicorn
440
+ process that `child.kill()` on the parent does **not** reap, leaving port `8141`
441
+ bound. Every teardown kills the whole tree:
442
+
443
+ - **POSIX** — spawned `detached` so the child leads its own process group;
444
+ `process.kill(-pid, SIGTERM)`, escalating to `SIGKILL` after the timeout.
445
+ - **Windows** — `taskkill /PID <pid> /T /F`.
446
+
447
+ `autoCleanup` (default `true`) also reaps on `exit`, `SIGINT`, `SIGTERM`,
448
+ `SIGHUP`, `uncaughtException`, and `unhandledRejection`. A `SIGKILL` of the parent
449
+ is the one case no in-process handler can catch.
450
+
451
+ `startSidecar()` shuts the sidecar down before rethrowing on any failure, so a
452
+ failed start never leaks a process.
453
+
454
+ ---
455
+
456
+ ## 7. Exit codes
457
+
458
+ | Code | Meaning |
459
+ | ------- | ----------------------------------------------------------------------- |
460
+ | `0` | Success |
461
+ | `1` | A typed failure: `IntegrityError`, `PlatformError`, `HealthTimeoutError`, `VersionMismatchError`, `BinaryNotFoundError`, `PortInUseError`, `SidecarExitedError`, `MalformedResponseError`, or an unexpected error |
462
+ | `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` |
463
+ | *other* | From `run`: the TUI's own exit code, propagated verbatim |
464
+
465
+ A TUI killed by a signal propagates as `128 + signum` (the shell convention), so a
466
+ Ctrl+C is distinguishable from a clean `0`.
467
+
468
+ ---
469
+
470
+ ## 8. Errors
471
+
472
+ All extend `GaiaError`, so `instanceof GaiaError` catches any of ours.
473
+
474
+ | Class | Raised when |
475
+ | ---------------------- | -------------------------------------------------------------- |
476
+ | `IntegrityError` | A download's SHA-256 does not match the lock, or a resolved binary's does not |
477
+ | `PlatformError` | Unsupported platform, missing/incomplete entry, placeholder hash, malformed lock |
478
+ | `HealthTimeoutError` | The sidecar did not report healthy within the timeout |
479
+ | `VersionMismatchError` | `apiVersion` major differs from this package's |
480
+ | `BinaryNotFoundError` | A binary is absent from disk when spawning |
481
+ | `PortInUseError` | The bind port was already taken; nothing was spawned |
482
+ | `SidecarExitedError` | The spawned sidecar died while the port answered — something else owns it |
483
+ | `HttpError` | A non-2xx from a sidecar probe |
484
+ | `MalformedResponseError` | A 2xx from a sidecar probe whose body is not JSON |
485
+
486
+ Per the repo's no-silent-fallbacks rule, every message names what failed, what to
487
+ do, and where to look next.
488
+
489
+ ---
490
+
491
+ ## 9. Timeouts
492
+
493
+ | Operation | Default | Why |
494
+ | --------------------- | --------- | ------------------------------------------------ |
495
+ | Download (per binary) | `300000`ms | The frozen sidecar is a large artifact |
496
+ | Health wait | `60000`ms | A cold one-file build unpacks before it binds |
497
+ | Health poll interval | `250`ms | |
498
+ | `/health` probe | `1000`ms | |
499
+ | `/version` probe | `5000`ms | |
500
+ | Shutdown grace | `5000`ms | Then `SIGKILL` / forced |
501
+
502
+ ---
503
+
504
+ ## 10. Public API
505
+
506
+ Exported from the package root — see `src/index.ts`.
507
+
508
+ **Fetch:** `fetchAll`, `fetchBinary`, `verifySha256`, `fileSha256`,
509
+ `binaryExists`, `defaultCacheDir`, `daemonSidecarCacheDir`,
510
+ `INSTALLED_SENTINEL_NAME`, `SIDECAR_AGENT_ID`.
511
+
512
+ **Lifecycle:** `spawnSidecar`, `startSidecar`, `waitForHealth`, `checkVersion`,
513
+ `health`, `version`, `shutdown`, `runTui`, `resolveSidecarPath`, `resolveTuiPath`,
514
+ `sidecarExecutableName`, `tuiExecutableName`.
515
+
516
+ **Platform:** `currentPlatformKey`, `loadLock`, `resolveEntry`, `componentLock`,
517
+ `componentBaseUrl`, `platformsFor`, `defaultLockPath`, `isPlaceholderSha`,
518
+ `COMPONENTS`, `SCHEMA_MAJOR`, `TUI_ARTIFACT_NAMES`,
519
+ `SUPPORTED_SIDECAR_PLATFORMS`, `SUPPORTED_TUI_PLATFORMS`, `SUPPORTED_PLATFORMS`.
520
+
521
+ **Constants:** `AGENT_ID`, `API_VERSION`, `DEFAULT_HOST`, `DEFAULT_PORT`,
522
+ `RESERVED_PORT`.
523
+
524
+ `resolveSidecarPath` / `resolveTuiPath` re-verify the file's SHA-256 against
525
+ `binaries.lock.json` before returning a path that is about to be spawned — the
526
+ cache path is predictable, so anything able to write it would otherwise get code
527
+ run. That makes them **O(size of the binary)**, not a cheap path join: resolve
528
+ once at startup, not per request. Pass `{ verify: false }` for a binary you built
529
+ yourself, which no lock describes, and `{ lock }` to reuse an already-loaded lock.
530
+
531
+ ---
532
+
533
+ ## 11. Logging
534
+
535
+ `DEBUG=gaia` (or `DEBUG=*`) enables debug output. **Everything goes to stderr** —
536
+ stdout belongs to the TUI once it is exec'd, and to machine-readable JSON for
537
+ `fetch` / `version`.
@@ -0,0 +1,78 @@
1
+ {
2
+ "schemaVersion": "3.0",
3
+ "agentVersion": "0.2.0",
4
+ "components": {
5
+ "sidecar": {
6
+ "componentVersion": "0.2.0",
7
+ "baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.2.0",
8
+ "platforms": {
9
+ "win32-x64": {
10
+ "filename": "gaia-agent-win32-x64.exe",
11
+ "executable": "gaia-agent.exe",
12
+ "sha256": "5d2ea1a5fd40e2399ba2bf50aec80b44d89c7e7bfdbad846c050f683fc8bf842",
13
+ "size": 103531825
14
+ },
15
+ "darwin-arm64": {
16
+ "filename": "gaia-agent-darwin-arm64",
17
+ "executable": "gaia-agent",
18
+ "sha256": "fbe526e7eb044a2ce633fbd487a1ae75001683eff190d8359eabf9ccc7196069",
19
+ "size": 88311472
20
+ },
21
+ "darwin-x64": {
22
+ "filename": "gaia-agent-darwin-x64",
23
+ "executable": "gaia-agent",
24
+ "sha256": "b02731c786d79d7f5e644662238a64d1761eb6a23d421821df46e6a8282b585d",
25
+ "size": 94294544
26
+ },
27
+ "linux-x64": {
28
+ "filename": "gaia-agent-linux-x64",
29
+ "executable": "gaia-agent",
30
+ "sha256": "6c68a22c25954ea3c8d2a0a541bc04eba2401281d2a2b6448007983acd050a00",
31
+ "size": 140015368
32
+ }
33
+ }
34
+ },
35
+ "tui": {
36
+ "componentVersion": "0.24.1",
37
+ "baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.24.1",
38
+ "platforms": {
39
+ "win32-x64": {
40
+ "filename": "gaia-win-x64.exe",
41
+ "executable": "gaia-tui.exe",
42
+ "sha256": "ce07269674de6773fd304d67eef3ff19cd578f447fc90a13f0309a61ae5ef504",
43
+ "size": 19213824
44
+ },
45
+ "win32-arm64": {
46
+ "filename": "gaia-win-arm64.exe",
47
+ "executable": "gaia-tui.exe",
48
+ "sha256": "41f2a25b2bf15bea6f70626bb519496aeb822cc2629dd3529e325319b7abb3ca",
49
+ "size": 17744384
50
+ },
51
+ "darwin-arm64": {
52
+ "filename": "gaia-darwin-arm64",
53
+ "executable": "gaia-tui",
54
+ "sha256": "dc334e89cebefaa2cc5117942ccbc4dc391034c3013ebf02d2bebf87f5e99a5a",
55
+ "size": 17499282
56
+ },
57
+ "darwin-x64": {
58
+ "filename": "gaia-darwin-x64",
59
+ "executable": "gaia-tui",
60
+ "sha256": "9af8c1fefeff4814bae6bbe9698ffaac3b59f248a422525e9f9b50812f5435da",
61
+ "size": 18629664
62
+ },
63
+ "linux-x64": {
64
+ "filename": "gaia-linux-x64",
65
+ "executable": "gaia-tui",
66
+ "sha256": "ef80198f3c4f6c2a4e92993fe19257c67cf009e29632398642549274ef8c2ffa",
67
+ "size": 18292898
68
+ },
69
+ "linux-arm64": {
70
+ "filename": "gaia-linux-arm64",
71
+ "executable": "gaia-tui",
72
+ "sha256": "31d9e1fee15e4a21e677ca7abb707fcf08257d5e7f50f1baa71bc7159335c170",
73
+ "size": 17105058
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `gaia` — the one command that gets you running.
4
+ *
5
+ * npx @amd-gaia/gaia fetch + verify both binaries, then launch the TUI
6
+ * npx @amd-gaia/gaia fetch fetch + verify only
7
+ * npx @amd-gaia/gaia serve run the agent sidecar alone (REST, no TUI)
8
+ * npx @amd-gaia/gaia version print the lock manifest
9
+ *
10
+ * Every download is SHA-256 verified against `binaries.lock.json` before it is
11
+ * written, and a mismatch or a placeholder hash aborts the run. There is no
12
+ * "continue anyway" path.
13
+ */
14
+ interface ParsedArgs {
15
+ _: string[];
16
+ flags: Record<string, string | boolean>;
17
+ /** Everything after a bare `--`, forwarded verbatim to the TUI. */
18
+ passthrough: string[];
19
+ }
20
+ /** Raised for a malformed command line; the caller turns it into exit 2. */
21
+ export declare class UsageError extends Error {
22
+ }
23
+ export declare function parseArgs(argv: string[]): ParsedArgs;
24
+ /**
25
+ * The child's PATH with this package's own bin directory removed.
26
+ *
27
+ * npm installs our shim as `gaia`, and the TUI starts the daemon by resolving
28
+ * `gaia` on PATH (tui/internal/daemon/client.go) expecting the *Python* CLI.
29
+ * Left alone, the TUI would re-invoke us with `daemon start` and the daemon would
30
+ * never come up. Returns undefined when PATH is unset.
31
+ */
32
+ export declare function pathWithoutOwnShim(rawPath?: string | undefined, argv1?: string | undefined): string | undefined;
33
+ /** Validate `--port`. Rejects out-of-range values and the reserved port. */
34
+ export declare function resolvePort(raw: string | boolean | undefined): {
35
+ port: number;
36
+ } | {
37
+ error: string;
38
+ };
39
+ export declare function main(argv: string[]): Promise<number>;
40
+ export {};
41
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +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;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;AAuKD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkC1D"}