sublera-cli 0.0.0-stage → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +585 -2
  4. package/SECURITY.md +78 -0
  5. package/THIRD_PARTY_NOTICES.md +37 -0
  6. package/dist/agent.d.ts +270 -0
  7. package/dist/agent.js +1867 -0
  8. package/dist/commands.d.ts +76 -0
  9. package/dist/commands.js +841 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +95 -0
  12. package/dist/engine/binaryManager.d.ts +67 -0
  13. package/dist/engine/binaryManager.js +590 -0
  14. package/dist/engine/gguf.d.ts +88 -0
  15. package/dist/engine/gguf.js +458 -0
  16. package/dist/engine/llamaRunner.d.ts +78 -0
  17. package/dist/engine/llamaRunner.js +282 -0
  18. package/dist/engine/modelManager.d.ts +63 -0
  19. package/dist/engine/modelManager.js +510 -0
  20. package/dist/engine/selfCheck.d.ts +34 -0
  21. package/dist/engine/selfCheck.js +108 -0
  22. package/dist/exitCodes.d.ts +20 -0
  23. package/dist/exitCodes.js +20 -0
  24. package/dist/hardware/gpuMonitor.d.ts +32 -0
  25. package/dist/hardware/gpuMonitor.js +378 -0
  26. package/dist/hardware/sleepGuard.d.ts +42 -0
  27. package/dist/hardware/sleepGuard.js +149 -0
  28. package/dist/hardware/types.d.ts +40 -0
  29. package/dist/hardware/types.js +2 -0
  30. package/dist/health.d.ts +17 -0
  31. package/dist/health.js +38 -0
  32. package/dist/index.d.ts +11 -0
  33. package/dist/index.js +268 -0
  34. package/dist/logger.d.ts +53 -0
  35. package/dist/logger.js +201 -0
  36. package/dist/network/proxy.d.ts +58 -0
  37. package/dist/network/proxy.js +328 -0
  38. package/dist/network/streamProxy.d.ts +51 -0
  39. package/dist/network/streamProxy.js +286 -0
  40. package/dist/network/tunnelClient.d.ts +54 -0
  41. package/dist/network/tunnelClient.js +257 -0
  42. package/dist/settings.d.ts +53 -0
  43. package/dist/settings.js +171 -0
  44. package/dist/state.d.ts +52 -0
  45. package/dist/state.js +71 -0
  46. package/dist/storageCommand.d.ts +22 -0
  47. package/dist/storageCommand.js +81 -0
  48. package/dist/types.d.ts +222 -0
  49. package/dist/types.js +43 -0
  50. package/dist/ui/progress.d.ts +29 -0
  51. package/dist/ui/progress.js +82 -0
  52. package/dist/ui/terminalDashboard.d.ts +88 -0
  53. package/dist/ui/terminalDashboard.js +295 -0
  54. package/dist/util/env.d.ts +3 -0
  55. package/dist/util/env.js +6 -0
  56. package/dist/util/exec.d.ts +23 -0
  57. package/dist/util/exec.js +116 -0
  58. package/dist/util/format.d.ts +47 -0
  59. package/dist/util/format.js +125 -0
  60. package/dist/util/fs.d.ts +18 -0
  61. package/dist/util/fs.js +140 -0
  62. package/dist/util/http.d.ts +28 -0
  63. package/dist/util/http.js +128 -0
  64. package/dist/util/instanceLock.d.ts +3 -0
  65. package/dist/util/instanceLock.js +108 -0
  66. package/dist/util/open.d.ts +16 -0
  67. package/dist/util/open.js +65 -0
  68. package/dist/util/storage.d.ts +10 -0
  69. package/dist/util/storage.js +69 -0
  70. package/dist/version.d.ts +15 -0
  71. package/dist/version.js +29 -0
  72. package/package.json +68 -4
package/README.md CHANGED
@@ -1,3 +1,586 @@
1
- # Temporary Holding Version
1
+ # Sublera CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A standalone terminal agent for the Sublera peer-to-peer GPU market. It runs on the
4
+ GPU owner's machine, manages `llama-server` and GGUF files, keeps the machine
5
+ awake during overnight bookings, and holds one outbound WebSocket to the central
6
+ server. Buyers never connect to the host directly.
7
+
8
+ This package is the implementation of `../CLI_PLAN.md`. It is deliberately
9
+ separate from the website/server so it can be published to npm and updated on
10
+ host machines without redeploying the marketplace.
11
+
12
+ ```text
13
+ Browser / buyer
14
+ │ OpenAI-compatible stream
15
+ ▼
16
+ Sublera central server ◄──── outbound WSS tunnel ──── sublera-cli (this package)
17
+ │ │
18
+ │ ├── llama-server on 127.0.0.1
19
+ │ ├── GGUF cache + SHA-256
20
+ │ └── GPU monitor + sleep guard
21
+ ▼
22
+ Postgres/SQLite ledger, wallet, reliability, request board
23
+ ```
24
+
25
+ ## Migration from Sublet
26
+
27
+ - The local package name is `sublera-cli`, with `sublera` and `sublera-cli`
28
+ command names. The old `sublet` and `sublet-cli` command aliases remain supported.
29
+ - Use `SUBLERA_*` environment variables in new examples. Legacy `SUBLET_*`
30
+ variables remain supported. If both names for a setting are set, `SUBLERA_*`
31
+ wins; command-line flags still take precedence over environment settings.
32
+ - The default data directory is intentionally still `~/.sublet`, not a new
33
+ `~/.sublera`: old and new names share credentials, model/engine cache, recovery
34
+ state, and the instance lock. Do not move it merely for the rebrand. Existing
35
+ model `.sublet.json` metadata filenames are unchanged.
36
+ - The repository remains `somemusic69-sudo/Sublet-cli`. The default server is
37
+ `https://sublera.com` (it was `https://sublet.duckdns.org` before 1.3.0).
38
+ - The npm package is `sublera-cli`, published only by the Release workflow
39
+ (`.github/workflows/release.yml`) with npm provenance.
40
+
41
+ ## Quick start
42
+
43
+ You need Node.js 20.18.1 or newer (the LTS version from nodejs.org).
44
+ From a local checkout on the GPU machine, run:
45
+
46
+ ```bash
47
+ cd agent-cli
48
+ npm install
49
+ npm run build
50
+ node dist/index.js start
51
+ ```
52
+
53
+ For subsequent examples, use `node dist/index.js` in place of `sublera` or
54
+ `npx sublera-cli` when running from this local build.
55
+
56
+ On the first run, `start` opens the Sublera website so you can connect this
57
+ computer (click **Connect**), installs the matching `llama-server` build (CUDA
58
+ for NVIDIA, Vulkan for AMD/Intel, Metal for Apple Silicon, CPU fallback), and
59
+ then waits for work. The default server is `https://sublera.com`;
60
+ `--server <url>` uses another Sublera server. Servers must use https, except on
61
+ this machine (`localhost`, `127.0.0.1`); `SUBLERA_ALLOW_HTTP=1` allows plain http
62
+ for a test server on a trusted network.
63
+
64
+ `sublera doctor` checks the machine (`--fix` installs the engine ahead of time).
65
+ `sublera login` connects the computer again; on a machine without a browser,
66
+ open the printed link on any phone or laptop, or use `login --key sh_...`.
67
+
68
+ To serve one buyer request, pick it on the website's market. The board shows
69
+ which requests fit your GPU and which models are already on your disk. Click
70
+ **Take**, so no other host can claim it, then run the command that the website
71
+ shows:
72
+
73
+ ```bash
74
+ npx sublera-cli start r_abc123
75
+ ```
76
+
77
+ The agent downloads and verifies the model, passes the model check, serves the
78
+ booking, and exits when the booking ends. If other bookings still use the GPU at
79
+ that point, it keeps serving them and exits after the last one. If you do not start within 15 minutes,
80
+ the request goes back to the board. With `sublera start` already running, a Take
81
+ on the website starts the download at once.
82
+
83
+ For public hosting from the terminal:
84
+
85
+ ```bash
86
+ npx sublera-cli host \
87
+ https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf \
88
+ --price 0.25 --price-hour 0.5 --ctx 8192 --slots 4
89
+ ```
90
+
91
+ A public host earns three ways, all on the one loaded model. Per-token buyers and
92
+ speed seats share the GPU; a full-GPU rental has it alone:
93
+
94
+ - **Per 1M tokens.** Buyers pay per answer at `--price` (or `--price-in` /
95
+ `--price-out`).
96
+ - **Per hour, full GPU.** One renter gets the whole GPU for a number of hours at
97
+ `--price-hour` (default `0.50` USD; `0` turns hourly rentals off; at most `50`).
98
+ While it runs there are no seats and no per-token selling.
99
+ - **Per hour, speed seat.** A renter reserves part of the measured speed, for
100
+ example 50 of 300 tok/s, for a number of hours. The rest keeps serving other
101
+ seats and per-token buyers. The server sets seat prices from `--price-hour`.
102
+
103
+ The server benchmarks the loaded model, admits only the seats and jobs that fit,
104
+ and paces each seat to its reserved speed. The agent serves every booking side by
105
+ side and never restarts `llama-server` while a booking or public buyers use it.
106
+ The dashboard lists each rental, the one that ends first, and earnings: per-token
107
+ earnings plus the time share of every booking. Per-token earnings made while
108
+ rentals run are not counted there, because the agent cannot tell a seat
109
+ renter's jobs from per-token jobs; the wallet on the website is exact.
110
+
111
+ For an SSH/mini-datacenter host that wants to see the buyer request board:
112
+
113
+ ```bash
114
+ npx sublera-cli jobs
115
+ ```
116
+
117
+ `jobs` claims the selected request and, if no agent is already running, starts
118
+ the tunnel/model/download/serve loop in the same terminal until the booking
119
+ ends and no other booking uses the GPU, or you press Ctrl+C.
120
+
121
+ ## Commands
122
+
123
+ | Command | Purpose |
124
+ | --- | --- |
125
+ | `sublera login` | Link this computer to your account: show a code, open the website, receive a host key when you click Connect. `--key` saves a key directly; `--no-browser` only prints the link. The key is saved with mode 0600. |
126
+ | `sublera doctor` | Agent version, Node, server, key, download speed, GPU, free VRAM, sleep guard, llama-server and cache checks. `--fix` installs the engine. `--json` for machines. |
127
+ | `sublera start [code]` | Main mode: connect, download/verify the model of a request taken on the website, pass the model check, serve. With a code, serve that request; exit after it ends, once no other booking is active. |
128
+ | `sublera host <url\|path>` | Public mode with a chosen GGUF: a price per 1M tokens, plus hourly rentals of the full GPU or a speed seat (`--price-hour`). |
129
+ | `sublera jobs` | Table of open requests with VRAM fit and payout. Pick one and it claims + serves in this terminal; `--claim <id>` is non-interactive. |
130
+ | `sublera status` | Read-only host, reliability, wallet and a line for every active booking. |
131
+ | `sublera config [show\|path]` | The settings in use and where each one came from (flag, env, `agent.json`, login, default). Keys are masked. |
132
+ | `sublera logout` | Remove the saved host key. |
133
+
134
+ Global flags: `--server`, `--key`, `--ws-path`, `--simulation`, `--gpu`,
135
+ `--vram`, `--ctx`, `--slots`, `--price`, `--price-in`, `--price-out`,
136
+ `--price-hour`, `--tok-s`, `--no-install`, `--no-browser`, `--json`, `--verbose`, `--log-level`,
137
+ `--log-file`, `--no-color`, `--once`. Each can also be set with a `SUBLERA_*`
138
+ environment variable or in `~/.sublet/agent.json` (see [Configuration](#configuration)).
139
+ `--gpu` and `--vram` also correct a wrongly detected GPU name or VRAM size on a real host.
140
+
141
+ ### Test mode
142
+
143
+ Everything can be exercised without a GPU or a multi-GB download:
144
+
145
+ ```bash
146
+ sublera start --simulation --gpu "NVIDIA GeForce RTX 4060" --vram 8188 --tok-s 24
147
+ ```
148
+
149
+ Simulation mode still uses the real tunnel, metering, billing, model checks, booking
150
+ timer, reconnect logic and dashboard. Only the generated words are a stand-in.
151
+ The `simulate` flag on the `hello` frame tells the server this is a test node.
152
+
153
+ ## What is in the package
154
+
155
+ ```text
156
+ agent-cli/
157
+ ├── src/
158
+ │ ├── index.ts CLI entry, parser, exit codes, shebang
159
+ │ ├── commands.ts login / doctor / start / host / jobs / status
160
+ │ ├── agent.ts orchestration of tunnel, engine and dashboard
161
+ │ ├── config.ts ~/.sublet, URL normalization, key masking
162
+ │ ├── logger.ts human and JSON logs (stderr, never dashboard stdout)
163
+ │ ├── hardware/
164
+ │ │ ├── gpuMonitor.ts nvidia-smi / rocm-smi / macOS / Windows fallback
165
+ │ │ ├── sleepGuard.ts SetThreadExecutionState / caffeinate / systemd-inhibit
166
+ │ │ └── types.ts
167
+ │ ├── engine/
168
+ │ │ ├── binaryManager.ts GitHub release picker, checksum, extraction, manifest
169
+ │ │ ├── modelManager.ts resumable download, streaming SHA-256, GGUF header
170
+ │ │ ├── llamaRunner.ts hidden 127.0.0.1 port, locked flags, crash restart
171
+ │ │ └── gguf.ts header parser + VRAM estimator
172
+ │ ├── network/
173
+ │ │ ├── tunnelClient.ts WSS, 1/2/5/10/15/30s backoff, heartbeat, dead-link detection, auth fail, version gate
174
+ │ │ └── streamProxy.ts SSE parser, stream=true piping, token probabilities
175
+ │ └── ui/
176
+ │ ├── terminalDashboard.ts the overnight status box
177
+ │ └── progress.ts download bar with speed and ETA
178
+ ├── tests/ unit tests, no network or GPU required
179
+ ├── scripts/e2e.mjs 9-check end-to-end proof against a live Sublera server
180
+ └── README.md
181
+ ```
182
+
183
+ ## What a host sees
184
+
185
+ ```
186
+ ┌──────────────────────────────────────────────────────────────────────┐
187
+ │ Sublera GPU TERMINAL AGENT v1.0.0 STATUS: ● WORKING │
188
+ ├──────────────────────────────────────────────────────────────────────┤
189
+ │ Host Endpoint : https://host.example.com/api/v1 │
190
+ │ Reliability : 98.5% (OK - held only after repeated failures) │
191
+ │ GPU Detected : NVIDIA GeForce RTX 3090 (21.2 GB free / 24.0 GB) │
192
+ │ GPU Temp/Power: 61°C · 185W · Wake-Lock: ACTIVE - sleep blocked │
193
+ ├──────────────────────────────────────────────────────────────────────┤
194
+ │ CURRENT JOB : Dedicated Buyer Request (#804) │
195
+ │ Model Loaded : Llama-3.3-70B-Abliterated-Q4_K_M ✓ verified │
196
+ │ Time Remaining: 06:14:32 / 08:00:00 │
197
+ │ Live Speed : 38.4 tok/s (min required: 25.0 tok/s ✓) │
198
+ ├──────────────────────────────────────────────────────────────────────┤
199
+ │ Session Earn. : $4.68 / $6.00 Guaranteed │
200
+ │ Total Wallet : $42.15 │
201
+ │ Server : https://sublera.example.com │
202
+ └──────────────────────────────────────────────────────────────────────┘
203
+ ```
204
+
205
+ The dashboard redraws in place. Logs go to stderr, so terminal logs never
206
+ destroy the box. `--json` switches logs to JSON lines and is suitable for
207
+ systemd/journald.
208
+
209
+ ## Data directory
210
+
211
+ Everything lives under `~/.sublet` (override with `SUBLERA_HOME`):
212
+
213
+ ```text
214
+ ~/.sublet/config.json host key + server, written by sublera login (0600)
215
+ ~/.sublet/agent.json optional settings file you write (see Configuration)
216
+ ~/.sublet/state.json recovery snapshot of every active booking (0600)
217
+ ~/.sublet/bin/ llama-server builds + manifest.json
218
+ ~/.sublet/models/ verified .gguf files + .sublet.json sidecars
219
+ ~/.sublet/cache/ GitHub release metadata, temporary archives
220
+ ~/.sublet/logs/agent.log agent log, JSON lines, rotated (0600)
221
+ ~/.sublet/logs/llama-server.log engine log (prompt logging disabled)
222
+ ```
223
+
224
+ A model that is already on disk is not downloaded again, but it is hashed again
225
+ before it is served (once per agent run), so the SHA-256 sent to the server is
226
+ of the bytes on disk and not of the sidecar note beside them. A file that fails
227
+ is deleted and downloaded again. A resume sends `Range: bytes=N-` and appends
228
+ only the missing bytes. If a server ignores Range, the partial file is discarded
229
+ and the download restarts from zero rather than corrupting weights.
230
+
231
+ ## Configuration
232
+
233
+ ### Production operations
234
+
235
+ `start`, `host`, and a serving `jobs` agent exclusively own `SUBLERA_HOME` via
236
+ `agent.lock`. A second process refuses to start. Storage maintenance acquires
237
+ the same lock, as does engine installation by `doctor --fix`. After an abrupt crash
238
+ the next agent takes the lock over only when the process that wrote it is no
239
+ longer running, so a service manager can restart the agent and it restores its
240
+ booking. A lock held by a running process, or one that cannot be read, is
241
+ refused: check every agent using that directory, then remove only `agent.lock`.
242
+ Never remove `state.json` just to unlock the directory.
243
+
244
+ - `sublera health live --json`: local process liveness.
245
+ - `sublera health ready --json`: local readiness (default health mode). Requires
246
+ a fresh snapshot, connected tunnel, loaded ready engine (or simulation), and
247
+ no drain/model load. Exit 0 means the requested check passed; exit 1 means
248
+ failed/unknown. Snapshots update every 5 seconds and expire after 15 seconds.
249
+ Simulation is explicitly labeled; these checks are not model attestation,
250
+ server-side billing approval, or proof of GPU performance/capacity.
251
+ - SIGTERM enters drain: refuse new inference/model loads, allow active inference
252
+ up to 30 seconds, then cancel leftovers, stop the engine, and preserve booking
253
+ state. Configure `--drain-timeout <ms>` / `SUBLERA_DRAIN_TIMEOUT_MS` /
254
+ `agent.json` `drainTimeoutMs` (0–300000). Ctrl+C remains immediate stop.
255
+ Drain is local admission control; it does not implement a server scheduling
256
+ handshake. Use server-side maintenance controls for long booked windows.
257
+ - `sublera storage status --json`: reports model, engine, cache and log sizes.
258
+ - `sublera storage prune --json`: **preview only**, for regular model `.part`
259
+ and `.part.json` files older than seven days. Add `--force` to apply. Stop
260
+ serving first. Complete models, engine versions, keys and booking state are
261
+ never pruned by this command; recent partial downloads remain resumable.
262
+
263
+ Use fixed CLI/engine versions, a dedicated non-admin service account and a
264
+ dedicated local `SUBLERA_HOME` (not a shared network filesystem). Configure your
265
+ service supervisor with bounded restart/backoff, log rotation and enough stop
266
+ grace for drain plus engine teardown. Health snapshots include no prompts/keys,
267
+ but are local diagnostics, not tamper-proof attestation. Service installation,
268
+ fleet metrics/alerts, server billing/idempotency validation, real GPU soak and
269
+ rollback acceptance remain operator/release gates; see `SOAK_TEST.md`.
270
+
271
+ Engine replacement validates the staged binary's presence and hash before
272
+ promotion and attempts rollback on ordinary errors. Promotion and manifest
273
+ updates are not one crash-atomic transaction: abrupt termination can leave
274
+ uniquely named `.tmp`/`.bak` directories requiring operator inspection. Disk
275
+ headroom checks cannot reserve space against other applications. During shutdown,
276
+ ownership remains held until pending message handlers settle; a stuck handler
277
+ may therefore require supervisor intervention rather than unsafe lock takeover.
278
+
279
+ ### Storage safety
280
+
281
+ The CLI does not create screenshot browser profiles. Its application data lives
282
+ under `~/.sublet`, or `SUBLERA_HOME` on a drive you choose.
283
+
284
+ - Model downloads share a **20 GiB cache budget**, including completed models,
285
+ resumable `.part` files, sidecars and nested files. A full cache refuses new
286
+ downloads; it never automatically deletes a model or booking recovery data.
287
+ - Downloads retain **5 GiB free disk headroom**. Unknown-length model streams
288
+ are bounded too; free space is checked periodically during large transfers.
289
+ - Set `SUBLERA_MODEL_CACHE_GB=40` for a larger model cache, and
290
+ `SUBLERA_MIN_FREE_DISK_GB=10` for more headroom. Values are positive GiB and
291
+ these are environment-only controls, not `agent.json` keys.
292
+ - New engine installs refuse a `bin` directory above **10 GiB** and archive
293
+ downloads above **2 GiB each**. Extraction is bounded; the installer requires
294
+ additional temporary disk space and removes its work/staging on normal exit.
295
+ - Default agent logs rotate at **10 MiB**, with **5 previous files** (about
296
+ **60 MiB** total). External stdout/service-manager logs need their own rotation.
297
+
298
+ Existing oversized caches are not erased or shrunk automatically. Stop the
299
+ agent before manually removing unused cached models/partials or old engine
300
+ versions. Interrupted downloads remain resumable and count toward the budget;
301
+ forced termination can leave installer work directories. Limits apply to new
302
+ writes, not an absolute filesystem quota: other processes, simultaneous CLI
303
+ instances, npm caches and external engine writes can still consume disk space.
304
+
305
+ The settings listed below work three ways. The first one found wins:
306
+
307
+ 1. a command-line flag (`--ctx 16384`)
308
+ 2. a `SUBLERA_*` environment variable (`SUBLERA_CTX=16384`)
309
+ 3. the corresponding legacy `SUBLET_*` environment variable (`SUBLET_CTX=16384`)
310
+ 4. `~/.sublet/agent.json` (path override: `SUBLERA_CONFIG`, legacy `SUBLET_CONFIG`)
311
+
312
+ When both environment names are set, `SUBLERA_*` wins. This also applies to
313
+ `SUBLERA_HOME` / `SUBLET_HOME`; without an override the home remains `~/.sublet`.
314
+
315
+ ```json
316
+ {
317
+ "server": "https://sublera.com",
318
+ "context": 8192,
319
+ "slots": 4,
320
+ "price": 0.25,
321
+ "priceHour": 0.5,
322
+ "noBrowser": true,
323
+ "logLevel": "info"
324
+ }
325
+ ```
326
+
327
+ | `agent.json` key | Environment variable | Flag |
328
+ | --- | --- | --- |
329
+ | `server` | `SUBLERA_SERVER` | `--server` |
330
+ | `token` | `SUBLERA_KEY` | `--key` |
331
+ | `wsPath` | `SUBLERA_WS_PATH` | `--ws-path` |
332
+ | `context` | `SUBLERA_CTX` | `--ctx` |
333
+ | `slots` | `SUBLERA_SLOTS` | `--slots` |
334
+ | `price` / `priceIn` / `priceOut` | `SUBLERA_PRICE` / `SUBLERA_PRICE_IN` / `SUBLERA_PRICE_OUT` | `--price` / `--price-in` / `--price-out` |
335
+ | `priceHour` | `SUBLERA_PRICE_HOUR` | `--price-hour` |
336
+ | `gpu` / `vram` | `SUBLERA_GPU` / `SUBLERA_VRAM` | `--gpu` / `--vram` |
337
+ | `tokPerSec` | `SUBLERA_TOK_S` | `--tok-s` |
338
+ | `noInstall` | `SUBLERA_NO_INSTALL` | `--no-install` |
339
+ | `noBrowser` | `SUBLERA_NO_BROWSER` | `--no-browser` |
340
+ | `requireChecksum` | `SUBLERA_REQUIRE_CHECKSUM` | `--require-checksum` |
341
+ | `logLevel` | `SUBLERA_LOG_LEVEL` | `--log-level` |
342
+ | `logFile` | `SUBLERA_LOG_FILE` | `--log-file` |
343
+
344
+ An unknown key or a bad value in `agent.json` stops the agent with exit code 2
345
+ and names the problem; a typo is never ignored silently. `sublera config` prints
346
+ the value in use for every setting and where it came from (keys masked);
347
+ `sublera config --json` gives the same for tooling.
348
+
349
+ ## Corporate networks
350
+
351
+ The agent only connects outward: the Sublera server (HTTPS + one WebSocket),
352
+ HuggingFace for models, GitHub for llama.cpp releases, and npm for the version
353
+ check. Nothing listens on the network; `llama-server` binds to `127.0.0.1`.
354
+
355
+ ```bash
356
+ export HTTPS_PROXY=http://user:password@proxy.corp.example:3128
357
+ export NO_PROXY=.corp.example,10.0.0.5
358
+ export NODE_EXTRA_CA_CERTS=/etc/ssl/corp-root.pem # TLS-inspecting proxies
359
+ sublera doctor # "Network: via proxy http://***@proxy.corp.example:3128"
360
+ ```
361
+
362
+ - `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` are read in upper or lower case.
363
+ `NO_PROXY` takes host names, `.domain` suffixes, exact IP addresses,
364
+ `host:port`, or `*` (CIDR ranges are not supported).
365
+ - The tunnel and downloads use HTTP CONNECT, so TLS runs end to end and the
366
+ host key is never visible to the proxy. Proxy passwords are never printed.
367
+ - Only `http://` and `https://` proxies are supported (no SOCKS); anything else
368
+ stops the agent with a clear message and exit code 2.
369
+ - Loopback (`localhost`, `127.*`) is never proxied.
370
+
371
+ ## Logs
372
+
373
+ `start`, `host`, and `jobs` write every record, debug included, to
374
+ `~/.sublet/logs/agent.log` as JSON lines (`ts`, `level`, `message`, fields).
375
+ The file rotates at 10 MB and keeps 5 old files (`agent.log.1` ... `.5`). Use
376
+ `--log-file <path>` to move it or `--log-file off` to disable it. The
377
+ terminal shows `info` and above (`--log-level`, `--verbose`). Host keys, API
378
+ keys, bearer tokens, and `token=` URL parameters are replaced with `***`
379
+ everywhere. Prompts and model output are never logged.
380
+
381
+ ## Exit codes
382
+
383
+ | Code | Meaning |
384
+ | --- | --- |
385
+ | `0` | Success, or a booking that ended normally |
386
+ | `1` | Runtime error: server unreachable, host key rejected, request refused, crash loop |
387
+ | `2` | Usage or settings error: unknown command or flag, invalid value, bad `agent.json`, bad proxy |
388
+ | `130` | Stopped with Ctrl+C; saved booking state is cleared |
389
+ | `143` | Stopped by SIGTERM (shutdown, `systemctl stop`); saved booking state is kept so the next start reattaches |
390
+
391
+ ## Running unattended
392
+
393
+ A service manager should restart the agent and send SIGTERM to stop it. A
394
+ systemd example (Linux):
395
+
396
+ ```ini
397
+ # /etc/systemd/system/sublera.service
398
+ [Unit]
399
+ Description=Sublera GPU host agent
400
+ Wants=network-online.target
401
+ After=network-online.target
402
+
403
+ [Service]
404
+ User=sublera
405
+ Environment=SUBLERA_NO_BROWSER=true
406
+ # Run "sublera login --key sh_..." once as this user, or provide SUBLERA_KEY from a secret store.
407
+ ExecStart=/usr/bin/env npx --yes sublera-cli@1 start --no-color
408
+ Restart=always
409
+ RestartSec=10
410
+ # Exit code 2 is a configuration mistake: do not restart in a loop.
411
+ RestartPreventExitStatus=2
412
+ KillSignal=SIGTERM
413
+ TimeoutStopSec=30
414
+
415
+ [Install]
416
+ WantedBy=multi-user.target
417
+ ```
418
+
419
+ Pin a major version (`sublera-cli@1`) so an unattended machine never jumps to
420
+ a release with breaking changes. On Windows, Task Scheduler ("At startup",
421
+ "Restart on failure") or a service wrapper such as NSSM running
422
+ `npx --yes sublera-cli@1 start` does the same.
423
+
424
+ ## Verification and safety
425
+
426
+ 1. **Only GGUF.** Pickle/PyTorch/safetensors files are refused before download.
427
+ 2. **Streaming SHA-256.** The hash is computed while bytes hit disk. A mismatch
428
+ deletes both the partial and final file. The server's `sha256` is mandatory
429
+ for paid request loads.
430
+ 3. **GGUF header.** Magic, architecture, layer count, embedding length, context
431
+ and quantization are read from the first few MB. A 28-layer file cannot call
432
+ itself a 70B model.
433
+ 4. **VRAM fit.** The agent checks free VRAM with a 2% safety margin before it
434
+ claims/loads, so a host does not fail a booking on an OOM.
435
+ 5. **Locked engine flags.** `--n-gpu-layers 999 --ctx-size <n> --parallel <n>
436
+ --cont-batching --log-disable`, bound to `127.0.0.1`. llama.cpp's own
437
+ environment settings (`LLAMA_ARG_*`, `LLAMA_LOG_*`, `LLAMA_API_KEY`) are
438
+ removed from the engine's environment, so a host's private llama.cpp setup
439
+ cannot change the engine. A `llama-server` from `LLAMA_SERVER` or `PATH` is
440
+ used as it is, with a warning that another build can fail model checks.
441
+ 6. **Model checks.** Every job asks `llama-server` for token probabilities
442
+ (`logprobs`, top 5), and the first 32 tokens of every job carry them to the
443
+ server. The server sends hidden check prompts as normal `infer` jobs (a host
444
+ cannot tell them apart) and compares the probabilities with other hosts that
445
+ run the same file, or with a trusted reference host. The booking timer starts
446
+ only after the first check, and checks continue at random times. Asking for
447
+ probabilities costs `llama-server` a little extra work on every token.
448
+ **The server is what catches a fake model.** This program runs on the host's
449
+ machine and is open source, so nothing it reports about itself is proof.
450
+ What it does locally is for the honest host: after every model load it asks
451
+ the engine one fixed question twice at temperature 0 and compares the token
452
+ probabilities, and after a crash restart it compares against the load-time
453
+ answer. A measured difference is a diagnostic warning, not a diagnosis of
454
+ faulty hardware or proof of model identity. Missing, malformed, incomplete,
455
+ or ambiguous evidence is inconclusive, never a pass. The self-check never
456
+ stops a booking, and a buyer's job cancels it. Server-side authenticity and
457
+ billing behavior require separate end-to-end validation.
458
+ 7. **Sleep guard.** Windows uses a native `SetThreadExecutionState` loop;
459
+ macOS uses `caffeinate -dimsu`; Linux uses `systemd-inhibit`. The lock is
460
+ released when the job ends and on process exit.
461
+ 8. **Crash recovery.** `llamaRunner` restarts a crashed server after 500ms, up
462
+ to five times in two minutes. A crash loop becomes a fatal state instead of
463
+ spinning forever.
464
+ 9. **Reconnect grace + crash recovery.** A 10-second Wi-Fi blip reconnects with
465
+ backoff. A tunnel that stops answering pings for 45 seconds (a half-open TCP
466
+ connection after a router reset) is torn down and reconnected. The agent
467
+ keeps `llama-server` and the loaded model in VRAM. The server preserves
468
+ claims and the booking clock for 60 seconds. If the agent process itself is
469
+ killed, `~/.sublet/state.json` lets the next `start` reload the cached model
470
+ and reattach to the same booking instead of starting over. `SIGTERM`
471
+ (shutdown, `systemctl stop`) keeps that file; Ctrl+C clears it.
472
+ 10. **Host key transport.** The tunnel authenticates with an
473
+ `Authorization: Bearer` header so the key is not placed in the WebSocket
474
+ URL or proxy access logs. If an older server rejects header auth, the CLI
475
+ retries once with the legacy query-token form.
476
+ 11. **HuggingFace-only model downloads.** A model URL must be an `https://`
477
+ link to a `.gguf` file on `huggingface.co` or `hf.co`. The agent follows
478
+ redirects itself, and every redirect must also stay on HuggingFace over
479
+ `https://`, so a model link can never reach the host's own machine or local
480
+ network. A server `load` must be such a URL, never a local path. A download
481
+ stops at its announced size (at most 64 GB) and does not start without
482
+ enough free disk space. Only the unit tests can opt into localhost.
483
+ 12. **Dashboard controls.** In an interactive terminal: `W` opens your host
484
+ page on the website, `C` copies the host endpoint, `L` toggles the llama log tail,
485
+ and `?` shows the key help.
486
+
487
+ ## Server contract
488
+
489
+ This CLI works with the Sublera server (separate, not public yet) and with any
490
+ server that implements the same small contract:
491
+
492
+ | Endpoint / frame | Purpose |
493
+ | --- | --- |
494
+ | `GET /api/health` | doctor reachability check |
495
+ | `GET /api/agent/me` | host key verification, wallet, host, `activeRequest` (the newest) and `activeRequests` (every live request, newest first; the CLI falls back to `activeRequest` when it is missing) |
496
+ | `POST /api/cli/login`, `POST /api/cli/login/poll` | device login: a user code for the website, then the host key once it is approved |
497
+ | `GET /api/agent/requests` | open buyer requests + VRAM fit + `cached` (the model is already on this host's disk) |
498
+ | `POST /api/agent/requests/:id/claim` | claim from the terminal |
499
+ | `GET /tunnel?token=...` or `/ws/agent` | persistent outbound WebSocket |
500
+ | `hello`, `heartbeat`, `progress`, `loaded`, `load_failed`, `models`, `chunk`, `done`, `error` | agent → server. `hello` carries `requestId` (from `sublera start <code>`), `cachedModels` (SHA-256 of each verified GGUF on disk) and `priceHourUsd` (the full-GPU hourly price; `0` from `start` and `jobs`, and when hourly rentals are off); `models` updates that list after a download. The first 32 tokens of every job send `chunk.lp`: `[{ t, l, top: [[token, logprob], ...] }]`. |
501
+ | HTTP `426` (JSON `minVersion` optional) or WebSocket close `4426` (reason `<min version>` optional) | server refuses an agent that is too old; the CLI stops and tells the host to run `npx sublera-cli@latest` |
502
+ | `welcome`, `infer`, `load`, `cancel`, `verified`, `booking`, `reject` | server → agent. Model checks arrive as normal `infer` jobs. Several `booking` frames for different requests can be active at once; `shared` and `tokS` mark a speed seat (no `tokS` means the full GPU). A booking on the model that is already loaded needs no `load` frame. `reject` ends only its own request. |
503
+
504
+ The agent sends its version as `user-agent: sublera-cli/<version>` on every
505
+ request and as `agentVersion` in `hello`, so the server can refuse old agents
506
+ before a protocol change. A handshake refused with HTTP `401`/`403` is treated
507
+ like a rejected host key.
508
+
509
+ `loaded` is followed by the server's first model check. The server starts the
510
+ booking timer only after that check, then sends `verified`; the CLI displays the
511
+ timer it receives in the `booking` frame. The agent never trusts the host for billing: the central
512
+ proxy counts the SSE chunks.
513
+
514
+ ## Development
515
+
516
+ ```bash
517
+ npm install
518
+ npm run typecheck
519
+ npm test
520
+ npm run build
521
+ node dist/index.js --help
522
+
523
+ # end-to-end against a running Sublera server
524
+ $env:SUBLERA_HOME="$env:TEMP\sublera-cli-e2e"
525
+ npm run e2e
526
+ ```
527
+
528
+ The e2e script signs up a fresh host, issues a key, drives the real tunnel, does
529
+ a billed pay-per-token stream, creates and claims a 30-minute booking, verifies
530
+ the timer starts after the model check, verifies booked tokens are not charged,
531
+ drops the tunnel, reconnects inside the grace window, verifies the same timer
532
+ survives, and cancels the request. It uses simulation mode so it does not need
533
+ a real 4GB+ model.
534
+
535
+ ## Packaging and publication status
536
+
537
+ The local package name is `sublera-cli`. Its primary binaries are `sublera` and
538
+ `sublera-cli`; `sublet` and `sublet-cli` remain compatibility aliases.
539
+ For local package inspection only:
540
+
541
+ ```bash
542
+ npm pack --dry-run
543
+ ```
544
+
545
+ Releases are published to npm as `sublera-cli` by the Release workflow, never
546
+ from a laptop. Check a release's provenance on its npm page.
547
+
548
+ ## Verification status
549
+
550
+ This build was verified on a Windows machine with an RTX 4060:
551
+
552
+ - `npm run typecheck`, `npm test`, and `npm run build` pass.
553
+ - `npm run e2e` passes against a live Sublera server: tunnel, billed public
554
+ stream, model check, booking timer, unlimited booked stream, jobs board,
555
+ process-kill crash recovery with the same timer, standalone `jobs` serving,
556
+ escrow cleanup, device login, and take + `start <code>`.
557
+ - On Linux x64 (CPU), `sublera doctor --fix` installed the official llama.cpp
558
+ b12202 build, including its library link chains, and served SmolLM2 GGUFs.
559
+ Those runs measured the model-check thresholds.
560
+ - `sublera doctor --fix` downloaded the official CUDA `llama-server` build
561
+ (b12176), verified its published SHA-256, extracted it, and recorded the
562
+ binary hash in `~/.sublet/bin/manifest.json`.
563
+ - A real 637MB TinyLlama 1.1B Q4_K_M GGUF was downloaded, header-checked,
564
+ loaded into `llama-server`, verified with the HF SHA-256, and served a real
565
+ 40-token chat through the central tunnel with billing. The same real booking
566
+ was then SIGKILLed and restored from `state.json`; it reloaded the cached
567
+ model and served another chat at 85 tok/s with the original timer.
568
+
569
+ ## Limits
570
+
571
+ - Real inference requires a `llama-server` build and a machine that can fit the
572
+ requested GGUF. `--simulation` is explicitly a test mode and is labeled as
573
+ such in the dashboard and on the wire.
574
+ - The agent downloads official llama.cpp release assets from GitHub. If a
575
+ release has no published digest/sidecar, the agent records the local SHA-256
576
+ and marks the install `verified: false` instead of pretending it was signed.
577
+ Pass `--require-checksum` on `doctor` to make a missing published checksum a
578
+ failure.
579
+ - Withdrawals and real card processing live on the server, not in this package.
580
+
581
+ ## License
582
+
583
+ MIT. See [LICENSE](LICENSE) and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
584
+ The CLI does not bundle model weights; each HuggingFace model keeps its own
585
+ license. Security reports: [SECURITY.md](SECURITY.md). Contributions:
586
+ [CONTRIBUTING.md](CONTRIBUTING.md).
package/SECURITY.md ADDED
@@ -0,0 +1,78 @@
1
+ # Security
2
+
3
+ ## Reporting
4
+
5
+ Do not open a public issue for a vulnerability. Email the maintainer with:
6
+
7
+ - affected version or commit
8
+ - operating system and Node version
9
+ - reproduction steps or proof of concept
10
+ - impact assessment
11
+
12
+ We will acknowledge the report and coordinate a fix and disclosure date.
13
+
14
+ ## Host key handling
15
+
16
+ - Host keys are saved in `~/.sublet/config.json` with mode 0600.
17
+ - The tunnel prefers `Authorization: Bearer`, keeping the key out of URLs.
18
+ The query-token form exists only as a compatibility fallback for older
19
+ servers.
20
+ - Do not paste host keys into screenshots, issues, or logs. The CLI masks keys
21
+ in its own output, but the terminal scrollback is yours.
22
+ - Every log line, on screen and in `~/.sublet/logs/agent.log`, is filtered:
23
+ `sh_`/`sk-` keys, `Bearer` values, and `token=` parameters are replaced with
24
+ `***`. The agent log file is created with mode 0600. Prompts and model
25
+ output are never logged.
26
+ - `SUBLERA_KEY` is supported for fleets and containers; legacy `SUBLET_KEY`
27
+ remains supported, with `SUBLERA_KEY` winning when both are set. The default
28
+ `~/.sublet` intentionally shares credentials, cache, and the instance lock
29
+ across old and new command aliases. Prefer a secret store
30
+ (systemd `LoadCredential`, Docker secrets, a CI secret) over a plain shell
31
+ profile. `sublera config` always shows the key masked.
32
+
33
+ ## Network
34
+
35
+ - The agent only makes outbound connections: the Sublera server (HTTPS and one
36
+ WebSocket), `huggingface.co`/`hf.co` and its CDN for models,
37
+ `api.github.com`/`github.com` for llama.cpp releases, and
38
+ `registry.npmjs.org` for the version check. The engine opens a loopback-only
39
+ HTTP port; the agent does not expose a public listening port by default.
40
+ - With `HTTPS_PROXY` set, the tunnel uses HTTP CONNECT, so TLS runs end to end
41
+ between the agent and the Sublera server for HTTPS/WSS destinations. A normal
42
+ forwarding proxy sees destination and traffic metadata, not encrypted content.
43
+ An authorized TLS-inspecting proxy with a trusted root CA can read host keys
44
+ and content. HTTP/WS destinations are not end-to-end encrypted. Corporate roots
45
+ can be configured with `NODE_EXTRA_CA_CERTS`; certificate checks stay enabled.
46
+ - Proxy credentials in `HTTPS_PROXY` are hidden in all output.
47
+
48
+ ## Model and binary trust
49
+
50
+ - Model downloads must use HTTPS (localhost is allowed for tests).
51
+ - GGUF files are checked for magic bytes, architecture, layers, and SHA-256
52
+ when HuggingFace publishes an LFS hash.
53
+ - A cached model is hashed again before it is served (once per agent run); the
54
+ sidecar file next to it is not accepted as the hash of a model being served.
55
+ The list of cached models that marks a request "ready now" on the market
56
+ still comes from sidecars; it is a hint, and the file is hashed when taken.
57
+ - The agent's reports about itself (model hash, engine, self-check) are not a
58
+ security boundary: the host controls the machine and can change this
59
+ program. Authenticity requires independent server-side verification. This
60
+ repository cannot establish the coverage or effectiveness of the server's
61
+ hidden model checks. Local repeatability is diagnostic, not attestation.
62
+ - llama.cpp environment settings (`LLAMA_ARG_*`, `LLAMA_LOG_*`,
63
+ `LLAMA_API_KEY`) are not passed to the engine.
64
+ - llama.cpp release archives are checked against the GitHub asset digest when
65
+ available; `--require-checksum` refuses engines without verified archive
66
+ checksums, including custom PATH/environment engines. This is integrity
67
+ checking, not an independently signed authenticity guarantee.
68
+ - The downloaded llama.cpp binary is hashed and recorded in
69
+ `~/.sublet/bin/manifest.json`, then re-checked on use.
70
+ - Only `.gguf` weights are accepted. Pickle, PyTorch, ONNX, and safetensors
71
+ files are refused.
72
+ - `llama-server` is bound to `127.0.0.1`, runs with `--log-disable`, and is not
73
+ exposed to the LAN.
74
+
75
+ ## Reporting scope
76
+
77
+ The marketplace server, wallet, and payout infrastructure are a separate
78
+ codebase. This repository covers the terminal agent only.