@amd-gaia/gaia 0.1.1 → 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 +154 -3
- package/README.md +36 -11
- package/SKILL.md +164 -40
- package/SPEC.md +159 -15
- package/binaries.lock.json +25 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +146 -21
- package/dist/cli.js.map +1 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/fetch.d.ts +5 -4
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +174 -22
- package/dist/fetch.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +22 -5
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +257 -41
- package/dist/lifecycle.js.map +1 -1
- package/package.json +1 -1
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,6 +245,11 @@ 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`) |
|
|
@@ -246,6 +289,15 @@ collapses the rest to a one-line menu entry (re-activated by calling
|
|
|
246
289
|
disables it for the session and every body renders. Omitting it is a valid, explicit one-shot: nothing
|
|
247
290
|
persists past that single turn, and the agent is not told otherwise.
|
|
248
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
|
+
|
|
249
301
|
A second `/query` for a `session_id` that already has a turn in flight gets
|
|
250
302
|
`409 Conflict` — cancel the running turn or wait for it, then retry. A
|
|
251
303
|
`/query` that needs a **new** session while every retained slot is busy and
|
|
@@ -253,12 +305,23 @@ none is idle enough to evict gets `503` with the reason in `detail` — a
|
|
|
253
305
|
temporary, retryable condition, not a bug: wait for a turn to finish (or
|
|
254
306
|
close an idle session) and retry. A
|
|
255
307
|
`session_id` can also be evicted from the retention table under an idle
|
|
256
|
-
timeout or an LRU cap on concurrent sessions
|
|
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
|
|
257
312
|
evicted id gets a fresh agent (the conversation is not blocked) but the
|
|
258
313
|
response stream's first event is a `{"type":"status","status":"warning",...}`
|
|
259
314
|
telling the caller that per-turn state — most visibly any loaded skill — did
|
|
260
315
|
not survive and should be reloaded.
|
|
261
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
|
+
|
|
262
325
|
### 5.3 Version gate
|
|
263
326
|
|
|
264
327
|
`checkVersion()` reads `/version` and compares the **major** of `apiVersion`
|
|
@@ -266,13 +329,73 @@ against this package's `API_VERSION`. A differing major is a breaking contract
|
|
|
266
329
|
change and raises `VersionMismatchError`. A higher minor with the same major is a
|
|
267
330
|
backward-compatible addition and is accepted.
|
|
268
331
|
|
|
269
|
-
### 5.4
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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.
|
|
276
399
|
|
|
277
400
|
---
|
|
278
401
|
|
|
@@ -293,13 +416,23 @@ from `~/.gaia/agents/gaia/`. A sidecar spawned here would be a second process th
|
|
|
293
416
|
TUI never talks to, competing for port `8141`.
|
|
294
417
|
|
|
295
418
|
So `run`'s contribution to the sidecar is putting a **verified** binary where the
|
|
296
|
-
daemon looks
|
|
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.
|
|
297
421
|
|
|
298
422
|
### 6.2 `gaia serve` — the direct path
|
|
299
423
|
|
|
300
424
|
`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:
|
|
302
|
-
`waitForHealth` → `checkVersion` → run until
|
|
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.
|
|
303
436
|
|
|
304
437
|
### 6.3 Tree-kill
|
|
305
438
|
|
|
@@ -325,8 +458,8 @@ failed start never leaks a process.
|
|
|
325
458
|
| Code | Meaning |
|
|
326
459
|
| ------- | ----------------------------------------------------------------------- |
|
|
327
460
|
| `0` | Success |
|
|
328
|
-
| `1` | A typed failure: `IntegrityError`, `PlatformError`, `HealthTimeoutError`, `VersionMismatchError`, `BinaryNotFoundError`, or an unexpected error |
|
|
329
|
-
| `2` | Usage error: unknown command, unknown `--component`,
|
|
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` |
|
|
330
463
|
| *other* | From `run`: the TUI's own exit code, propagated verbatim |
|
|
331
464
|
|
|
332
465
|
A TUI killed by a signal propagates as `128 + signum` (the shell convention), so a
|
|
@@ -340,12 +473,15 @@ All extend `GaiaError`, so `instanceof GaiaError` catches any of ours.
|
|
|
340
473
|
|
|
341
474
|
| Class | Raised when |
|
|
342
475
|
| ---------------------- | -------------------------------------------------------------- |
|
|
343
|
-
| `IntegrityError` | A download's SHA-256 does not match the lock
|
|
476
|
+
| `IntegrityError` | A download's SHA-256 does not match the lock, or a resolved binary's does not |
|
|
344
477
|
| `PlatformError` | Unsupported platform, missing/incomplete entry, placeholder hash, malformed lock |
|
|
345
478
|
| `HealthTimeoutError` | The sidecar did not report healthy within the timeout |
|
|
346
479
|
| `VersionMismatchError` | `apiVersion` major differs from this package's |
|
|
347
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 |
|
|
348
483
|
| `HttpError` | A non-2xx from a sidecar probe |
|
|
484
|
+
| `MalformedResponseError` | A 2xx from a sidecar probe whose body is not JSON |
|
|
349
485
|
|
|
350
486
|
Per the repo's no-silent-fallbacks rule, every message names what failed, what to
|
|
351
487
|
do, and where to look next.
|
|
@@ -370,7 +506,8 @@ do, and where to look next.
|
|
|
370
506
|
Exported from the package root — see `src/index.ts`.
|
|
371
507
|
|
|
372
508
|
**Fetch:** `fetchAll`, `fetchBinary`, `verifySha256`, `fileSha256`,
|
|
373
|
-
`binaryExists`, `defaultCacheDir`, `daemonSidecarCacheDir
|
|
509
|
+
`binaryExists`, `defaultCacheDir`, `daemonSidecarCacheDir`,
|
|
510
|
+
`INSTALLED_SENTINEL_NAME`, `SIDECAR_AGENT_ID`.
|
|
374
511
|
|
|
375
512
|
**Lifecycle:** `spawnSidecar`, `startSidecar`, `waitForHealth`, `checkVersion`,
|
|
376
513
|
`health`, `version`, `shutdown`, `runTui`, `resolveSidecarPath`, `resolveTuiPath`,
|
|
@@ -384,6 +521,13 @@ Exported from the package root — see `src/index.ts`.
|
|
|
384
521
|
**Constants:** `AGENT_ID`, `API_VERSION`, `DEFAULT_HOST`, `DEFAULT_PORT`,
|
|
385
522
|
`RESERVED_PORT`.
|
|
386
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
|
+
|
|
387
531
|
---
|
|
388
532
|
|
|
389
533
|
## 11. Logging
|
package/binaries.lock.json
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "3.0",
|
|
3
|
-
"agentVersion": "0.
|
|
3
|
+
"agentVersion": "0.2.0",
|
|
4
4
|
"components": {
|
|
5
5
|
"sidecar": {
|
|
6
|
-
"componentVersion": "0.
|
|
7
|
-
"baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.
|
|
6
|
+
"componentVersion": "0.2.0",
|
|
7
|
+
"baseUrl": "https://hub.amd-gaia.ai/agents/gaia/0.2.0",
|
|
8
8
|
"platforms": {
|
|
9
9
|
"win32-x64": {
|
|
10
10
|
"filename": "gaia-agent-win32-x64.exe",
|
|
11
11
|
"executable": "gaia-agent.exe",
|
|
12
|
-
"sha256": "
|
|
13
|
-
"size":
|
|
12
|
+
"sha256": "5d2ea1a5fd40e2399ba2bf50aec80b44d89c7e7bfdbad846c050f683fc8bf842",
|
|
13
|
+
"size": 103531825
|
|
14
14
|
},
|
|
15
15
|
"darwin-arm64": {
|
|
16
16
|
"filename": "gaia-agent-darwin-arm64",
|
|
17
17
|
"executable": "gaia-agent",
|
|
18
|
-
"sha256": "
|
|
19
|
-
"size":
|
|
18
|
+
"sha256": "fbe526e7eb044a2ce633fbd487a1ae75001683eff190d8359eabf9ccc7196069",
|
|
19
|
+
"size": 88311472
|
|
20
20
|
},
|
|
21
21
|
"darwin-x64": {
|
|
22
22
|
"filename": "gaia-agent-darwin-x64",
|
|
23
23
|
"executable": "gaia-agent",
|
|
24
|
-
"sha256": "
|
|
25
|
-
"size":
|
|
24
|
+
"sha256": "b02731c786d79d7f5e644662238a64d1761eb6a23d421821df46e6a8282b585d",
|
|
25
|
+
"size": 94294544
|
|
26
26
|
},
|
|
27
27
|
"linux-x64": {
|
|
28
28
|
"filename": "gaia-agent-linux-x64",
|
|
29
29
|
"executable": "gaia-agent",
|
|
30
|
-
"sha256": "
|
|
31
|
-
"size":
|
|
30
|
+
"sha256": "6c68a22c25954ea3c8d2a0a541bc04eba2401281d2a2b6448007983acd050a00",
|
|
31
|
+
"size": 140015368
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"tui": {
|
|
36
|
-
"componentVersion": "0.
|
|
37
|
-
"baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.
|
|
36
|
+
"componentVersion": "0.24.1",
|
|
37
|
+
"baseUrl": "https://hub.amd-gaia.ai/agents/terminal-hub/0.24.1",
|
|
38
38
|
"platforms": {
|
|
39
39
|
"win32-x64": {
|
|
40
40
|
"filename": "gaia-win-x64.exe",
|
|
41
41
|
"executable": "gaia-tui.exe",
|
|
42
|
-
"sha256": "
|
|
43
|
-
"size":
|
|
42
|
+
"sha256": "ce07269674de6773fd304d67eef3ff19cd578f447fc90a13f0309a61ae5ef504",
|
|
43
|
+
"size": 19213824
|
|
44
44
|
},
|
|
45
45
|
"win32-arm64": {
|
|
46
46
|
"filename": "gaia-win-arm64.exe",
|
|
47
47
|
"executable": "gaia-tui.exe",
|
|
48
|
-
"sha256": "
|
|
49
|
-
"size":
|
|
48
|
+
"sha256": "41f2a25b2bf15bea6f70626bb519496aeb822cc2629dd3529e325319b7abb3ca",
|
|
49
|
+
"size": 17744384
|
|
50
50
|
},
|
|
51
51
|
"darwin-arm64": {
|
|
52
52
|
"filename": "gaia-darwin-arm64",
|
|
53
53
|
"executable": "gaia-tui",
|
|
54
|
-
"sha256": "
|
|
55
|
-
"size":
|
|
54
|
+
"sha256": "dc334e89cebefaa2cc5117942ccbc4dc391034c3013ebf02d2bebf87f5e99a5a",
|
|
55
|
+
"size": 17499282
|
|
56
56
|
},
|
|
57
57
|
"darwin-x64": {
|
|
58
58
|
"filename": "gaia-darwin-x64",
|
|
59
59
|
"executable": "gaia-tui",
|
|
60
|
-
"sha256": "
|
|
61
|
-
"size":
|
|
60
|
+
"sha256": "9af8c1fefeff4814bae6bbe9698ffaac3b59f248a422525e9f9b50812f5435da",
|
|
61
|
+
"size": 18629664
|
|
62
62
|
},
|
|
63
63
|
"linux-x64": {
|
|
64
64
|
"filename": "gaia-linux-x64",
|
|
65
65
|
"executable": "gaia-tui",
|
|
66
|
-
"sha256": "
|
|
67
|
-
"size":
|
|
66
|
+
"sha256": "ef80198f3c4f6c2a4e92993fe19257c67cf009e29632398642549274ef8c2ffa",
|
|
67
|
+
"size": 18292898
|
|
68
68
|
},
|
|
69
69
|
"linux-arm64": {
|
|
70
70
|
"filename": "gaia-linux-arm64",
|
|
71
71
|
"executable": "gaia-tui",
|
|
72
|
-
"sha256": "
|
|
73
|
-
"size":
|
|
72
|
+
"sha256": "31d9e1fee15e4a21e677ca7abb707fcf08257d5e7f50f1baa71bc7159335c170",
|
|
73
|
+
"size": 17105058
|
|
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;
|
|
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"}
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* written, and a mismatch or a placeholder hash aborts the run. There is no
|
|
14
14
|
* "continue anyway" path.
|
|
15
15
|
*/
|
|
16
|
-
import { realpathSync } from "node:fs";
|
|
16
|
+
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { GaiaError } from "./errors.js";
|
|
@@ -88,7 +88,10 @@ Usage:
|
|
|
88
88
|
|
|
89
89
|
run options:
|
|
90
90
|
--base-url <url> Override the per-component download base URL from
|
|
91
|
-
binaries.lock.json (applies to every component fetched)
|
|
91
|
+
binaries.lock.json (applies to every component fetched).
|
|
92
|
+
Must be https unless --allow-insecure-base-url is given.
|
|
93
|
+
--allow-insecure-base-url
|
|
94
|
+
Permit a non-https --base-url (a trusted local mirror)
|
|
92
95
|
--cache-dir <dir> Where to cache the TUI binary
|
|
93
96
|
--sidecar-dir <dir> Where to install the agent sidecar
|
|
94
97
|
(default ~/.gaia/agents/gaia — the daemon's own cache)
|
|
@@ -102,6 +105,8 @@ fetch options:
|
|
|
102
105
|
|
|
103
106
|
serve options:
|
|
104
107
|
--port <n> Bind port (default ${DEFAULT_PORT}; ${RESERVED_PORT} is reserved and refused)
|
|
108
|
+
--base-url, --sidecar-dir, --force as above. \`serve\` never downloads the TUI,
|
|
109
|
+
so --cache-dir is refused rather than ignored.
|
|
105
110
|
|
|
106
111
|
Notes:
|
|
107
112
|
* No binaries are committed to the repo. Every download is SHA-256 verified
|
|
@@ -116,19 +121,69 @@ Notes:
|
|
|
116
121
|
`;
|
|
117
122
|
const flagStr = (args, k) => typeof args.flags[k] === "string" ? args.flags[k] : undefined;
|
|
118
123
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* to spawn it.
|
|
124
|
+
* Which commands actually READ each value flag, and why the others refuse it.
|
|
125
|
+
* A flag a command ignores is worse than one it rejects: `run --port 9000`
|
|
126
|
+
* used to parse fine and silently come up on the default port.
|
|
123
127
|
*/
|
|
128
|
+
const VALUE_FLAG_SCOPE = {
|
|
129
|
+
"base-url": { commands: ["run", "fetch", "serve"], why: "it only affects downloads" },
|
|
130
|
+
"sidecar-dir": { commands: ["run", "fetch", "serve"], why: "it only affects downloads" },
|
|
131
|
+
platform: {
|
|
132
|
+
commands: ["fetch"],
|
|
133
|
+
why: "`run` and `serve` execute the binary, so it must be built for this host " +
|
|
134
|
+
`(${currentPlatformKey()})`,
|
|
135
|
+
},
|
|
136
|
+
port: {
|
|
137
|
+
commands: ["serve"],
|
|
138
|
+
why: "only `serve` binds a port; `run` reaches the agent through the daemon, which picks its own",
|
|
139
|
+
},
|
|
140
|
+
component: {
|
|
141
|
+
commands: ["fetch"],
|
|
142
|
+
why: "`run` needs both binaries and `serve` needs the sidecar, so neither can fetch a subset",
|
|
143
|
+
},
|
|
144
|
+
"cache-dir": {
|
|
145
|
+
commands: ["run", "fetch"],
|
|
146
|
+
why: "it caches the TUI, which `serve` never downloads (use --sidecar-dir)",
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
/** Refuse a value flag the chosen command would otherwise silently ignore. */
|
|
150
|
+
function assertFlagsInScope(args, cmd) {
|
|
151
|
+
for (const [flag, scope] of Object.entries(VALUE_FLAG_SCOPE)) {
|
|
152
|
+
if (args.flags[flag] === undefined || scope.commands.includes(cmd))
|
|
153
|
+
continue;
|
|
154
|
+
throw new UsageError(`--${flag} is not valid for \`gaia ${cmd}\` — ${scope.why}. ` +
|
|
155
|
+
`It is accepted by: ${scope.commands.map((c) => `\`gaia ${c}\``).join(", ")}.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Require https for a download source. Integrity still holds over plaintext
|
|
160
|
+
* (the SHA is pinned), but a MITM then surfaces as a baffling IntegrityError
|
|
161
|
+
* instead of the transport failure it is.
|
|
162
|
+
*/
|
|
163
|
+
function checkedBaseUrl(args) {
|
|
164
|
+
const raw = flagStr(args, "base-url");
|
|
165
|
+
if (raw === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = new URL(raw);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw new UsageError(`--base-url '${raw}' is not a valid absolute URL (expected e.g. https://host/path).`);
|
|
173
|
+
}
|
|
174
|
+
if (parsed.protocol !== "https:" && !args.flags["allow-insecure-base-url"]) {
|
|
175
|
+
throw new UsageError(`--base-url '${raw}' uses ${parsed.protocol}, not https. Downloads are ` +
|
|
176
|
+
"SHA-256 verified either way, but plaintext turns tampering into a " +
|
|
177
|
+
"confusing integrity failure instead of a transport error. Use an https " +
|
|
178
|
+
"URL, or pass --allow-insecure-base-url for a trusted local mirror.");
|
|
179
|
+
}
|
|
180
|
+
return raw;
|
|
181
|
+
}
|
|
182
|
+
/** Shared download options. Scope and scheme are validated before we get here. */
|
|
124
183
|
function downloadOptions(args, allowPlatform = false) {
|
|
125
184
|
const platformKey = flagStr(args, "platform");
|
|
126
|
-
if (platformKey !== undefined && !allowPlatform) {
|
|
127
|
-
throw new UsageError("--platform is only valid for `gaia fetch`. `run` and `serve` execute the " +
|
|
128
|
-
`binary, so it must be built for this host (${currentPlatformKey()}).`);
|
|
129
|
-
}
|
|
130
185
|
return {
|
|
131
|
-
baseUrl:
|
|
186
|
+
baseUrl: checkedBaseUrl(args),
|
|
132
187
|
...(allowPlatform && platformKey !== undefined ? { platformKey } : {}),
|
|
133
188
|
force: Boolean(args.flags["force"]),
|
|
134
189
|
tuiDir: flagStr(args, "cache-dir"),
|
|
@@ -148,8 +203,10 @@ export function pathWithoutOwnShim(rawPath = process.env["PATH"], argv1 = proces
|
|
|
148
203
|
return rawPath;
|
|
149
204
|
const sep = process.platform === "win32" ? ";" : ":";
|
|
150
205
|
let ownBinDir;
|
|
206
|
+
let ownName;
|
|
151
207
|
try {
|
|
152
208
|
ownBinDir = path.resolve(path.dirname(argv1));
|
|
209
|
+
ownName = path.basename(argv1);
|
|
153
210
|
}
|
|
154
211
|
catch {
|
|
155
212
|
return rawPath;
|
|
@@ -160,14 +217,65 @@ export function pathWithoutOwnShim(rawPath = process.env["PATH"], argv1 = proces
|
|
|
160
217
|
? resolved.toLowerCase() === ownBinDir.toLowerCase()
|
|
161
218
|
: resolved === ownBinDir;
|
|
162
219
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
220
|
+
const entries = rawPath.split(sep).filter((d) => d !== "");
|
|
221
|
+
const ours = entries.filter((d) => same(d) || shimTargetsScript(d, argv1));
|
|
222
|
+
const kept = entries.filter((d) => !ours.includes(d));
|
|
223
|
+
const shared = ours.filter((d) => !isExclusivelyOurs(d, same(d) ? ownName : "gaia"));
|
|
224
|
+
return [...kept, ...shared].join(sep);
|
|
225
|
+
}
|
|
226
|
+
/** Match npm's symlink or generated cmd/PowerShell wrapper to this script. */
|
|
227
|
+
function shimTargetsScript(dir, script) {
|
|
228
|
+
const canonical = (file) => {
|
|
229
|
+
try {
|
|
230
|
+
const resolved = realpathSync(file);
|
|
231
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return undefined; // no resolvable executable at this candidate
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const expected = canonical(script);
|
|
238
|
+
if (!expected)
|
|
239
|
+
return false;
|
|
240
|
+
for (const name of ["gaia", "gaia.cmd", "gaia.ps1"]) {
|
|
241
|
+
const shim = path.join(dir, name);
|
|
242
|
+
if (canonical(shim) === expected)
|
|
243
|
+
return true;
|
|
244
|
+
if (!name.includes("."))
|
|
245
|
+
continue;
|
|
246
|
+
let contents;
|
|
247
|
+
try {
|
|
248
|
+
contents = readFileSync(shim, "utf8");
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
continue; // this PATH directory does not contain a readable wrapper
|
|
252
|
+
}
|
|
253
|
+
for (const match of contents.matchAll(/["'](?:%dp0%|\$basedir)[\\/]([^"']+)["']/gi)) {
|
|
254
|
+
const target = path.resolve(dir, match[1].replace(/[\\/]/g, path.sep));
|
|
255
|
+
if (canonical(target) === expected)
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
/** True when every file in `dir` is a shim for `name` (`gaia`, `gaia.cmd`, …). */
|
|
262
|
+
function isExclusivelyOurs(dir, name) {
|
|
263
|
+
const stem = name.replace(/\.(cmd|bat|ps1|exe)$/i, "");
|
|
264
|
+
let entries;
|
|
265
|
+
try {
|
|
266
|
+
entries = readdirSync(dir);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return false; // unreadable → treat as shared, the non-destructive branch
|
|
270
|
+
}
|
|
271
|
+
return (entries.length > 0 &&
|
|
272
|
+
entries.every((e) => e.replace(/\.(cmd|bat|ps1|exe)$/i, "") === stem));
|
|
167
273
|
}
|
|
168
274
|
/** Validate `--port`. Rejects out-of-range values and the reserved port. */
|
|
169
275
|
export function resolvePort(raw) {
|
|
170
|
-
|
|
276
|
+
// Strict digits, not Number(): that accepts "0x2710", " 80 ", and "1e3",
|
|
277
|
+
// so a typo would quietly bind a port the user never named.
|
|
278
|
+
const port = typeof raw === "string" ? (/^\d+$/.test(raw) ? Number(raw) : NaN) : DEFAULT_PORT;
|
|
171
279
|
if (!Number.isInteger(port) || port <= 0 || port > 65535 || port === RESERVED_PORT) {
|
|
172
280
|
return {
|
|
173
281
|
error: `--port must be a port in 1..65535 and not ${RESERVED_PORT} (got ${String(raw)})`,
|
|
@@ -231,6 +339,7 @@ async function cmdFetch(args) {
|
|
|
231
339
|
}, null, 2) + "\n");
|
|
232
340
|
return 0;
|
|
233
341
|
}
|
|
342
|
+
const SERVE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
234
343
|
async function cmdServe(args) {
|
|
235
344
|
const parsed = resolvePort(args.flags["port"]);
|
|
236
345
|
if ("error" in parsed) {
|
|
@@ -257,11 +366,17 @@ async function cmdServe(args) {
|
|
|
257
366
|
process.stdout.write(`\n ▸ GAIA agent: ${sidecar.baseUrl}/v1/gaia/query\n`);
|
|
258
367
|
process.stdout.write(` Health: ${sidecar.baseUrl}/health\n`);
|
|
259
368
|
process.stdout.write(" Lemonade must be running for live queries. Ctrl+C to stop.\n\n");
|
|
260
|
-
|
|
369
|
+
let stop;
|
|
370
|
+
const stopped = new Promise((resolve) => {
|
|
261
371
|
let stopping = false;
|
|
262
|
-
|
|
263
|
-
if (stopping)
|
|
264
|
-
|
|
372
|
+
stop = () => {
|
|
373
|
+
if (stopping) {
|
|
374
|
+
// Absorbed, so say what is happening — otherwise a repeat Ctrl+C
|
|
375
|
+
// during a slow teardown just looks like a frozen terminal.
|
|
376
|
+
process.stderr.write(`[gaia] already stopping the sidecar (pid ${String(sidecar.child.pid)}); ` +
|
|
377
|
+
"waiting for its process tree to exit ...\n");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
265
380
|
stopping = true;
|
|
266
381
|
process.stderr.write("\n[gaia] stopping the sidecar ...\n");
|
|
267
382
|
void shutdown(sidecar).catch(() => undefined).finally(resolve);
|
|
@@ -270,9 +385,18 @@ async function cmdServe(args) {
|
|
|
270
385
|
// otherwise hit Node's default disposition and kill us mid-teardown,
|
|
271
386
|
// orphaning the detached sidecar tree on port 8141. The `stopping` guard
|
|
272
387
|
// absorbs the repeats.
|
|
273
|
-
for (const sig of
|
|
388
|
+
for (const sig of SERVE_SIGNALS)
|
|
274
389
|
process.on(sig, stop);
|
|
275
390
|
});
|
|
391
|
+
try {
|
|
392
|
+
await stopped;
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
// Left installed, they would swallow every later signal AND suppress
|
|
396
|
+
// Node's default disposition, so Ctrl+C would stop working entirely.
|
|
397
|
+
for (const sig of SERVE_SIGNALS)
|
|
398
|
+
process.removeListener(sig, stop);
|
|
399
|
+
}
|
|
276
400
|
return 0;
|
|
277
401
|
}
|
|
278
402
|
catch (e) {
|
|
@@ -317,6 +441,7 @@ export async function main(argv) {
|
|
|
317
441
|
throw new UsageError(`unexpected argument${extra.length > 1 ? "s" : ""} ${extra.map((a) => `'${a}'`).join(", ")}. ` +
|
|
318
442
|
"Arguments for the terminal UI go after a bare `--`.");
|
|
319
443
|
}
|
|
444
|
+
assertFlagsInScope(args, cmd);
|
|
320
445
|
switch (cmd) {
|
|
321
446
|
case "run":
|
|
322
447
|
return cmdRun(args);
|