@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/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.md +225 -6
- package/SKILL.md +597 -0
- package/SPEC.md +537 -0
- package/binaries.lock.json +78 -0
- package/dist/cli.d.ts +41 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +493 -0
- package/dist/cli.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch.d.ts +111 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +369 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +162 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +613 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +30 -0
- package/dist/logger.js.map +1 -0
- package/dist/platform.d.ts +100 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +202 -0
- package/dist/platform.js.map +1 -0
- package/dist/url.d.ts +4 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +22 -0
- package/dist/url.js.map +1 -0
- package/package.json +66 -15
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"}
|