frutebox-node 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,518 @@
1
+ # 28 §A4 / §A9 — the provider catalog.
2
+ #
3
+ # WHAT THIS FILE IS, AND IS NOT (§A9.4)
4
+ # This is the SCHEMA and the SEED: it defines the shape of a provider row and the defaults a
5
+ # fresh database starts from. It is NOT the live source of truth. Once seeded, the live
6
+ # enable/region/credential state lives in the DATABASE, so adding a provider, turning one on in
7
+ # a region, or rotating a key is a row change — no redeploy, no key copied to N services.
8
+ #
9
+ # PHASE 1 (launch) IS DELIBERATELY TINY (readme LAUNCH PHASING, 2026-09-07):
10
+ # RunPod (US) + ONE EU provider (Verda/DataCrunch). Gateways EU + US only.
11
+ # Everything else in the geo roster is Phase 2 (plan 29) — defined here so the shape is settled,
12
+ # but `enabled: false` so nothing provisions until launch is live and stable.
13
+ #
14
+ # CREDENTIALS ARE NOT IN THIS FILE, EVER. `env` names which variables a cold-start/local-dev
15
+ # fallback would read; the real values live encrypted in the DB under STEVAI_DATA_KEY
16
+ # (readme rule 10 — never a cloud KMS). `providers doctor` reports presence, masked.
17
+
18
+ version: 1
19
+
20
+ # `class` routes work to capacity built for it: an AI job never lands on a CPU-only host, and a
21
+ # plain compute job never overpays on a GPU cloud (§A4).
22
+ # `weights` is not a compute class and never enters the race — it is a model SOURCE
23
+ # (plan 28 §A5 lists Hugging Face in the same credential table as the compute providers),
24
+ # so its token lives in the same encrypted store rather than on a service that never uses it.
25
+ classes: [gpu, compute, tee, weights]
26
+
27
+ # ROSTER TIER 3 (owner ruling 2026-09-24): Alibaba, OVHcloud, Gcore, Cloudflare R2.
28
+ # Z1 was in this tier when it was first recorded and was DROPPED hours later the same day;
29
+ # OBJECT STORAGE STAYS ON CLOUDFLARE R2, which is now the only storage member.
30
+ #
31
+ # RECORDED HERE BECAUSE IT DID NOT EXIST ANYWHERE. A search of the whole repo found no provider
32
+ # roster tiering at all — the geo roster in plan 28 §A6 is organised by continent, and the only
33
+ # "tier 1/2/3" language in the code is `routing.py`'s COMPUTE tiers (tier 1 GPU, tier 2/3 CPU),
34
+ # which is a different axis entirely. So this is a NEW list, not an edit to an existing one, and
35
+ # two things about it are flagged rather than smoothed over:
36
+ #
37
+ # 1. THE NAME COLLIDES. "Tier 3" already means "CPU pool" in `routing.py` and in the planner
38
+ # tests. A reader who meets both will assume they are the same scheme. If this roster tiering
39
+ # is going to persist it wants a name that does not collide — `roster_tier` rather than
40
+ # `tier` — and the owner should say which.
41
+ # 2. IT MIXES CLASSES. Alibaba, OVHcloud and Gcore are compute; Cloudflare R2 is OBJECT
42
+ # STORAGE. This file's `classes` list has no storage class at all, and the merit-order race
43
+ # is class-gated precisely so a job never lands on capacity built for something else. A
44
+ # storage provider in a tier alongside three compute providers cannot be expressed as a row
45
+ # here without either a new class or a separate roster.
46
+ #
47
+ # So it is written down as the owner stated it, and NOT modelled as rows or as a `tier:` field,
48
+ # because modelling it wrong is worse than not modelling it yet. Neither Cloudflare R2 nor
49
+ # OVHcloud has a row, a class or an adapter today.
50
+ roster_tier_3: [alibaba, ovhcloud, gcore, cloudflare_r2]
51
+
52
+ providers:
53
+ # ── PHASE 1 ────────────────────────────────────────────────────────────────────────────
54
+ # ── BEING REPLACED (owner decision 2026-10-16) ───────────────────────────────────────────
55
+ #
56
+ # Verda + Nebius replace RunPod. This row is deliberately UNCHANGED in every field that affects
57
+ # behaviour — `enabled: true`, `phase: 1`, `regions: [us, eu]` — because the cutover is a
58
+ # SEQUENCE and this is step zero of it, not step one. Disabling RunPod before Verda and Nebius
59
+ # have each proven a live boot would remove the only funded capacity there is, which is exactly
60
+ # the mistake the 2026-10-13 EU ruling made and the 10-14 reversal undid.
61
+ #
62
+ # The replacement is PREFERENCE-DRIVEN and needs no field here: `regionmap._DEFAULT` already asks
63
+ # Verda first and Nebius second for EU, and RunPod answers only what they cannot. As they are
64
+ # funded they take the traffic with no code change — the same property that made Verda's
65
+ # preference safe while it was empty.
66
+ #
67
+ # WHAT REMOVES THIS ROW is written down and is not a judgement call:
68
+ # `docs/provider-cutover-verda-nebius.md`, which also names the owner approval that teardown
69
+ # needs. DO NOT disable it here first; the pods currently serving EU media are on this provider.
70
+ - id: runpod
71
+ class: gpu
72
+ status: being_replaced
73
+ enabled: true
74
+ phase: 1
75
+ hq_country: US
76
+ # US ONLY (owner rule, 2026-10-21): "EU GPU work runs ONLY on Verda + Nebius. Never Google
77
+ # GPUs, never RunPod (RunPod pods are terminated and not used in the EU)."
78
+ #
79
+ # THE HISTORY MATTERS, BECAUSE THIS ROW HAS SAID BOTH THINGS. On 2026-10-13 `eu` was removed to
80
+ # stop "EU traffic being served by RunPod"; the owner REVERSED that on 10-14 with a reason —
81
+ # "RunPod must remain a valid EU provider because the other EU systems aren't funded yet" —
82
+ # and at the time it was the only funded EU capacity there was. That reason has now expired
83
+ # rather than been overruled: Verda and Nebius are both funded, both authenticate, and both
84
+ # start instances (an L40S reached RUNNING in 78s on 2026-10-20). So the capacity argument that
85
+ # justified the reversal no longer applies, and the owner has closed it.
86
+ #
87
+ # WHY THE ROW AND NOT ONLY `regionmap`. There are THREE EU candidate sets and they must agree:
88
+ # `regionmap._DEFAULT` (dispatch), `nodewake.warmpool_providers` (pod creation), and
89
+ # `providers.eligible(region="eu")` — which reads THIS field. Leaving `eu` here would have kept
90
+ # RunPod eligible EU capacity while the other two refused it, and a gate has to go where the
91
+ # grant happens. `tests/test_eu_is_verda_and_nebius_only.py` pins all three together.
92
+ #
93
+ # The RESIDENCY lock is unchanged and was never the thing letting RunPod in: it is derived from
94
+ # `hq_country` and has always been Verda (now Verda + Nebius) with no fallback.
95
+ regions: [us]
96
+ api_base: https://rest.runpod.io/v1
97
+ env: [RUNPOD_API_KEY]
98
+ # §A5: account-scoped resource ids are DISCOVERED from the key, never hand-entered.
99
+ derived: [RUNPOD_NETWORK_VOLUME_ID, RUNPOD_VOLUME_DATACENTER]
100
+ capabilities:
101
+ gpu_types: [A40, A100, H100, H200, RTX4090]
102
+ network_volume: true
103
+
104
+ # VERDA (ex-DataCrunch). FIRST CHOICE FOR EU, and as of owner decision 2026-10-16 one of the two
105
+ # providers REPLACING RunPod.
106
+ #
107
+ # THE ID STAYS `datacrunch` AND THAT IS DELIBERATE. The company renamed itself to Verda in
108
+ # Nov 2025, but this id is a primary key: it is the key in `gpuproviders.PROVIDERS`, the key in
109
+ # `gpuprices._FETCHERS`, the `providerId` on every credential row already stored in the database,
110
+ # and the prefix `regionmap.provider_of_node` attributes a running node by. Renaming it would
111
+ # orphan the stored credential and un-attribute every live EU node — a key rename is a migration,
112
+ # not a tidy-up. Operator-facing text says Verda; the id says datacrunch.
113
+ - id: datacrunch
114
+ class: gpu
115
+ status: active
116
+ enabled: true
117
+ phase: 1
118
+ hq_country: FI
119
+ regions: [eu] # the Phase-1 EU-sovereign provider (Verda / DataCrunch)
120
+ # `api.verda.com` since 2026-10-16, following the vendor's OWN SDK default
121
+ # (`verda.VerdaClient.base_url`). The old `api.datacrunch.io` host still answers, so this was
122
+ # never an outage — only a host one vendor decision away from being one.
123
+ api_base: https://api.verda.com/v1
124
+ # BOTH NAMINGS, NEW ONE FIRST — and the adapter reads them in this order.
125
+ #
126
+ # The 2026-09-07 discrepancy (plan 28 §A6 said `DATACRUNCH_API_KEY`; the SUPERVISOR_LOG said
127
+ # client id + secret) is SETTLED: the vendor's own SDK authenticates with an OAuth2
128
+ # client-credentials id and secret, so the log was right and there is no API-key form.
129
+ #
130
+ # The rebrand then added a second question — which name the stored credential is under. That is
131
+ # not knowable from the code, so both are declared and `DataCrunchProvider._creds` resolves
132
+ # each naming WHOLE, taking the first complete pair. Half of a rotated pair and half of a
133
+ # retired one authenticate as neither, and the resulting 401 reads as a revoked key rather
134
+ # than a seeding mistake.
135
+ env: [VERDA_CLIENT_ID, VERDA_CLIENT_SECRET, DATACRUNCH_CLIENT_ID, DATACRUNCH_CLIENT_SECRET]
136
+ # EITHER PAIR IS A COMPLETE CREDENTIAL — see `ProviderRow.env_groups`. Without this the doctor
137
+ # reads `env` as a conjunction and reports a WORKING deployment as unconfigured, missing two
138
+ # names it does not need. New naming first, so that is the one an empty account is told to set.
139
+ env_groups:
140
+ - [VERDA_CLIENT_ID, VERDA_CLIENT_SECRET]
141
+ - [DATACRUNCH_CLIENT_ID, DATACRUNCH_CLIENT_SECRET]
142
+ capabilities:
143
+ gpu_types: [V100, A100, H100]
144
+ network_volume: false
145
+ notes: >-
146
+ Verda (ex-DataCrunch), Finland. EU locations FIN-01/02/03 only — the vendor also offers
147
+ ICE-01, which is in ICELAND (EEA, not EU), and `DataCrunchProvider.create` refuses it.
148
+ `location_code` is REQUIRED on create since 2026-03-23 (absent is HTTP 400, not a default to
149
+ FIN-01). A delete that names no volumes LEAVES THE OS VOLUME BILLING — "by default, no
150
+ storage is selected for deletion" — so `destroy` names the volume ids with
151
+ delete_permanently. A shut-down instance keeps charging, which is why `offline` counts as
152
+ billing-but-not-serving. HDD volumes were decommissioned 2026-09-21: create/resize return
153
+ 400, NVMe only.
154
+ MULTI-REGION (measured live 2026-10-18): the account's EU locations are FIN-01, FIN-02 and
155
+ FIN-03, all Finland, so Verda covers the eu-north geography only - ICE-01 is Iceland and is
156
+ refused by the adapter. An image boot REQUIRES at least one SSH key on the account ("SSH keys
157
+ must be provided if image is not an OS volume"), which the spec derivation reads rather than
158
+ assumes.
159
+
160
+ - id: huggingface
161
+ class: weights
162
+ status: active
163
+ enabled: true
164
+ phase: 1
165
+ hq_country: US
166
+ regions: [eu, us]
167
+ api_base: https://huggingface.co
168
+ env: [HF_TOKEN]
169
+ notes: >-
170
+ Model weight source for plan 28 §B (pinned hf_repo + revision). NOT a compute provider —
171
+ never enters the merit-order race. §B is not built yet, so nothing reads this token on the
172
+ request path today; it is stored here so §B finds it and so it is off the gateways.
173
+
174
+ # OWNER RULING 2026-09-22: vast PARTICIPATES. Four things said so — it is in the launch pool
175
+ # (tier 1 of the roster), it has a credential row, it is live-configured, and it is in the
176
+ # `WARMPOOL_PROVIDERS` default — and one field said otherwise. That field was the lie, so that
177
+ # field moved. `phase: 1` moves WITH `enabled: true`: enabling a phase-2 row on its own would
178
+ # have broken the "no phase-2 provider is enabled" invariant and swapped one inconsistency for
179
+ # a newer, less obvious one. `status: backup` STAYS — it describes vast's ROLE, a marketplace
180
+ # stock-out fallback, which is exactly what it is and is not what enabled/phase are for.
181
+ - id: vast
182
+ class: gpu
183
+ status: backup
184
+ enabled: true
185
+ phase: 1
186
+ hq_country: US
187
+ # US ONLY, and this is the SAME finding as RunPod's above with a sharper edge (2026-10-13).
188
+ #
189
+ # Vast is a MARKETPLACE, and `VastProvider._cheapest_offer` sends no location filter at all —
190
+ # its `/bundles/` query asks for rentable + verified + gpu_ram and nothing about WHERE the
191
+ # machine is. So an `eu` claim on this row could not be honoured even in principle: a job placed
192
+ # on "vast, region eu" lands on whichever stranger's host is cheapest, anywhere in the world.
193
+ # That is weaker than RunPod's claim was, not stronger, because RunPod at least has named
194
+ # datacentres.
195
+ #
196
+ # TO RESTORE `eu` HERE, `_cheapest_offer` must filter offers by geography first — the row is a
197
+ # promise about placement and the adapter is what keeps it. Until then the registry says what is
198
+ # true. (US capacity is unaffected; this is the EU claim only.)
199
+ regions: [us]
200
+ api_base: https://console.vast.ai/api/v0
201
+ env: [VAST_API_KEY]
202
+
203
+ # OWNER RULING 2026-09-21: lambda is IN the launch pool, and the key is already on the
204
+ # directory. It was previously in `on_hold` while `gpuproviders.LambdaProvider` resolved it and
205
+ # `nodewake.warmpool_providers()` defaulted to `vast,runpod,lambda,datacrunch` — the resolver
206
+ # returns {} for a provider the seed does not know, so a Lambda credential could never come
207
+ # from the database however it was stored. With this row the warmpool default is correct as
208
+ # written. `enabled: true` follows "in the launch pool"; until the credential is PUT it
209
+ # resolves as unconfigured and the race skips it, so nothing launches on an absent key.
210
+ - id: lambda
211
+ class: gpu
212
+ status: active
213
+ enabled: true
214
+ phase: 1
215
+ hq_country: US
216
+ regions: [us]
217
+ api_base: https://cloud.lambdalabs.com/api/v1
218
+ env: [LAMBDA_API_KEY]
219
+ # Account-scoped resources, not secrets — §A5: the owner supplies the credential, the code
220
+ # discovers the rest.
221
+ derived: [LAMBDA_REGION, LAMBDA_SSH_KEYS, LAMBDA_INSTANCE_TYPE]
222
+
223
+
224
+ # ── PROMOTED TO PHASE 1 AND ENABLED — OWNER 7 IS DONE, owner ruling 2026-10-20 ───────────
225
+ #
226
+ # THIS REPLACES THE 2026-09-21 RECORD that "Phala = Phase 2 / deferred". That record was true
227
+ # when it was written: the row described an API nobody had called and there was no key, so a
228
+ # tenant asking for the `tee` rung got an empty candidate set. `PHALA_API_KEY` now exists in the
229
+ # estate and is loaded into the encrypted provider-credential store, and the doctor's probe
230
+ # reports `configured: true, auth: accepted, reachable: true` — PROVED with the key ABSENT from
231
+ # the environment, so the only possible source was the store.
232
+ #
233
+ # `phase: 1` MOVES WITH `enabled: true`, following the vast and nebius precedents above:
234
+ # enabling a phase-2 row on its own would break the "no phase-2 provider is enabled" invariant
235
+ # and trade one inconsistency for a less obvious one.
236
+ #
237
+ # ⚠️ ENABLED IS NOT DEFAULT-ON. `class: tee` keeps this provider out of the ordinary merit-order
238
+ # race entirely — the TEE lane is reachable only by a request that asks for it (`require_tee`),
239
+ # which is plan 08's own rule and is asserted in `tests/test_phala_tee_provider.py`. So
240
+ # `enabled: true` here means "available to a tenant who requires attestation", not "available to
241
+ # everyone's next chat". `status: emerging` STAYS: it describes maturity, which this ruling did
242
+ # not change.
243
+ - id: phala
244
+ class: tee
245
+ status: emerging
246
+ enabled: true
247
+ phase: 1
248
+ hq_country: SG
249
+ regions: [eu, us]
250
+ # CORRECTED 2026-10-07 (owner). `https://api.phala.network` was wrong and nothing had ever called it,
251
+ # so nothing failed — the row simply described an API that is not there. `cloud.phala.com` is the
252
+ # DASHBOARD, not the API. The real base is below, and it matches the paths in Phala's own OpenAPI
253
+ # (`/api/v1/cvms/provision`, `/api/v1/cvms`, `/api/v1/cvms/{cvm_id}`); Phala's own SDK calls the
254
+ # override `PHALA_CLOUD_API_PREFIX`.
255
+ api_base: https://cloud-api.phala.com/api/v1
256
+ env: [PHALA_API_KEY]
257
+ notes: >-
258
+ TEE lane, Phase 1 since 2026-10-20 (Owner 7 done, key loaded). DEFAULT-OFF BY CLASS: a `tee`
259
+ provider never enters the merit-order race, so it serves only requests that set `require_tee`
260
+ (plan 08). A tenant requiring the rung now gets attested capacity instead of an empty set.
261
+
262
+ # ── PHASE 2 — defined so the shape is settled; provisioned only after launch (plan 29) ──
263
+ - id: alibaba
264
+ class: gpu
265
+ status: backup
266
+ enabled: false
267
+ phase: 2
268
+ hq_country: CN
269
+ # INTERNATIONAL ONLY, and REGION-LOCKED because the company is PRC-incorporated (2026-10-12).
270
+ #
271
+ # `ap-southeast` is the PRODUCT's region label (the deployment serves eu-west, eu-central,
272
+ # eu-north, uk, us-east, us-west, ap-southeast); Alibaba's own region id is `ap-southeast-1`
273
+ # (Singapore) and lives in `ALIBABA_REGION_ID`. Two vocabularies, deliberately not conflated.
274
+ #
275
+ # `region_locked` is what actually fences it. `regions` alone does NOT: `providers.eligible()`
276
+ # skips region narrowing entirely when no region is given — the warm pool reconciles that way by
277
+ # design — so before this field an enabled row was eligible for every unlabelled launch. It was
278
+ # measured that way before the field existed. Unstated is not permitted.
279
+ regions: [ap-southeast]
280
+ region_locked: true
281
+ api_base: https://ecs.ap-southeast-1.aliyuncs.com
282
+ # The SDK's OWN names. `ALIBABA_ACCESS_KEY_ID`/`_SECRET` were seeded here and are not
283
+ # what any Alibaba Cloud SDK reads — it looks for the `ALIBABA_CLOUD_` prefix. Verified
284
+ # against SDK source 2026-09-05; it had regressed to the short form since. Nothing
285
+ # reads these today (no account, `enabled: false`), which is exactly why a wrong name
286
+ # survives here unnoticed until the day it is needed.
287
+ env: [ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET]
288
+ notes: >-
289
+ Alibaba Cloud INTERNATIONAL (Singapore), ECS gn-series. RAM user credential, never the root
290
+ account. Region-locked: it may serve ap-southeast traffic only and is excluded from every
291
+ EU/UK/US job and from any launch whose region is unstated. `enabled: false` until a key
292
+ exists. Launch spec is the operator's (FRUTEBOX_ALIBABA_ECS_SPEC: ImageId, InstanceType,
293
+ SecurityGroupId, VSwitchId); `create` refuses without it. Terminate never stop —
294
+ DeleteInstance with Force=true, because Force=false requires a Stopped instance first and a
295
+ stopped-but-unreleased instance still bills for its disks. US export controls apply to the
296
+ hardware available (H20-class).
297
+
298
+ # NEBIUS — PROMOTED TO PHASE 1 AND ENABLED (owner decision 2026-10-16: Verda + Nebius replace
299
+ # RunPod). It was `phase: 2, enabled: false` while its API shape was unverified and its
300
+ # credential absent; the shape is now read from the vendor's published OpenAPI document and
301
+ # pinned in `gpuproviders`, so the row no longer has to hold the lane shut.
302
+ #
303
+ # IT STILL CANNOT LAUNCH WITHOUT A CREDENTIAL, and that is the right kind of gate: the authorized
304
+ # key is an owner action (Owner 34), `configured()` is False without it, and the race skips an
305
+ # unconfigured provider. `enabled: true` says "this lane is open"; the credential says "and it
306
+ # can serve". Those are different claims and only the second one is missing.
307
+ - id: nebius
308
+ class: gpu
309
+ status: active
310
+ enabled: true
311
+ phase: 1
312
+ # Nebius Group N.V. is DUTCH (Amsterdam) — verified 2026-10-14 — so `hq_country: NL` makes it
313
+ # EU-incorporated and `regionmap.residency_providers` would derive it as EU-sovereign.
314
+ hq_country: NL
315
+ regions: [eu]
316
+ # SECOND EU PROVIDER, BEHIND VERDA (owner decision 7). Its place in the preference order is in
317
+ # `FRUTEBOX_REGION_PROVIDERS` / `regionmap._DEFAULT`, not here: this row says what it IS, the map
318
+ # says who is asked first.
319
+ #
320
+ # HELD FROM RESIDENCY-LOCKED WORK, and that is a decision recorded rather than taken. Nebius was
321
+ # spun out of Yandex N.V. (Russian assets sold July 2024, international business renamed August
322
+ # 2024). It is Dutch-incorporated, so the hq_country derivation alone would admit it to
323
+ # EU-sovereign work — and whether a Yandex-descended entity may serve legally pinned EU traffic
324
+ # is the same class of question the owner reserved for themselves on `kandinsky-5.0`. The
325
+ # owner's own instruction said strict-EU-residency prefers Verda and Nebius takes non-residency
326
+ # overflow and the frontier/large models.
327
+ #
328
+ # CLEARED BY THE OWNER, 2026-10-20: "Nebius may run EU-locked work; remove the residency hold."
329
+ # The question the hold was holding for has been answered, so the flag goes rather than being
330
+ # left `false` with the prose still arguing for it — a held provider nobody can tell is held is
331
+ # worse than either state. What still gates placement is the thing that is checked rather than
332
+ # declared: `_assert_eu_project` reads the project's own region from IAM before any create, and
333
+ # eu-north2 stays excluded because it is Iceland (EEA, not EU).
334
+ api_base: https://api.nebius.cloud
335
+ # A SERVICE-ACCOUNT AUTHORIZED KEY, as JSON — not an API token. Nebius IAM issues a key file
336
+ # holding a key id, a service-account id and a PRIVATE KEY; the client signs a JWT with it and
337
+ # exchanges that for a short-lived IAM token. So this is ONE env var holding a JSON document,
338
+ # and `AuthorizedKeyError` is what `configured()` reports when it will not parse.
339
+ # TWO NAMES, AND THE SECOND IS NOT A SECRET. The authorized key is the credential; the project
340
+ # id is an account-scoped resource — but it is NOT `derived:`, because an account holds one
341
+ # project PER REGION and choosing one for the operator would be choosing the jurisdiction the
342
+ # data lands in. So it is required configuration, read through `_provider_credential` like
343
+ # everything else, and its region is CHECKED against IAM before any create.
344
+ env: [NEBIUS_SERVICE_ACCOUNT_KEY, NEBIUS_PROJECT_ID, NEBIUS_SERVICE_ACCOUNT_ID,
345
+ NEBIUS_PUBLIC_KEY_ID, NEBIUS_PRIVATE_KEY, NEBIUS_PRIVATE_KEY_FILE,
346
+ NEBIUS_KEY_ID]
347
+ # EITHER FORM IS A COMPLETE CREDENTIAL — see `ProviderRow.env_groups`. IAM hands you a key id,
348
+ # an account id and a PEM in three places and never the assembled document, so requiring the
349
+ # single JSON var meant hand-building JSON around a private key; with the parts present the
350
+ # provider still reported itself unconfigured. The JSON form is listed first because it is one
351
+ # secret instead of three, so it is what an empty account is told to set.
352
+ #
353
+ # NEBIUS_KEY_ID is declared SO IT CAN BE DIAGNOSED, not because it authenticates: it is the
354
+ # object-storage access-key id, which the compute API's IAM rejects outright ("Invalid nid
355
+ # format", measured 2026-10-20). It is in no group, so it never counts towards a complete
356
+ # credential — it is here only so the loader may recognise it and say which kind of key it
357
+ # found and which kind it needs.
358
+ env_groups:
359
+ - [NEBIUS_SERVICE_ACCOUNT_KEY, NEBIUS_PROJECT_ID]
360
+ - [NEBIUS_SERVICE_ACCOUNT_ID, NEBIUS_PUBLIC_KEY_ID, NEBIUS_PRIVATE_KEY, NEBIUS_PROJECT_ID]
361
+ - [NEBIUS_SERVICE_ACCOUNT_ID, NEBIUS_PUBLIC_KEY_ID, NEBIUS_PRIVATE_KEY_FILE, NEBIUS_PROJECT_ID]
362
+ notes: >-
363
+ Second EU GPU provider, behind Verda in the preference order. Credential is a JSON document
364
+ (authorized key id + service account id + RSA private key), signed into a short-lived JWT and
365
+ exchanged at auth.eu.nebius.com for a 12-hour IAM token — never a static API key. The REST
366
+ shape is CONFIRMED: read from the vendor's published OpenAPI document
367
+ (api.nebius.cloud/openapi.json, 2026-10-16), so /compute/v1/instances create/list/get/delete
368
+ are quoted rather than guessed. The REGION IS A PROPERTY OF THE PROJECT, not of the instance —
369
+ a create body carries no region at all — so EU placement is enforced by checking the
370
+ project's spec.region against IAM before creating, and eu-north2 is excluded because it is
371
+ ICELAND (EEA, not EU). The boot disk is always VM-MANAGED so the vendor deletes it with the
372
+ instance. CLEARED FOR EU-RESIDENCY-LOCKED WORK by the owner on 2026-10-20 (the Yandex-lineage
373
+ question is answered); it shares EU work with Verda on price and availability, and still
374
+ carries the frontier/large models needing H200/B200.
375
+ LIVE CALL MADE 2026-10-18 (Owner 34 landed): the authorized key signs, exchanges for an IAM
376
+ token, and the adapter created and terminated a real instance
377
+ (computeinstance-e04fk8xszm6whg8fss) in project default-project-eu-west2. Two shapes only a
378
+ live call could settle: the boot disk is a NAMED resource wrapping its own spec, and the
379
+ public image catalog requires a region parameter.
380
+ WHICH EU PROJECT TO USE, MEASURED 2026-10-20. The account holds one project per region and
381
+ their GPU catalogues are NOT the same, which is what made this provider look broken: the
382
+ configured project was eu-west2, whose catalogue offers gpu-b300-sxm AND NOTHING ELSE, so
383
+ "the cheapest EU single-GPU preset" could only ever be a 1xB300 - and that preset goes
384
+ STARTING -> STOPPED after ~5m20s with no status message, no operation record and no serial
385
+ console (the API exposes none). eu-north1 publishes 17 GPU presets including single-GPU
386
+ L40S, H100 and H200; on the identical code path, image family and discovered subnet an
387
+ L40S-a/1gpu reached RUNNING in 78 seconds. So the B300 churn is vendor-side capacity for
388
+ this account in that region, not configuration - and FRUTEBOX_NEBIUS_PROJECT_ID should name
389
+ the eu-north1 project. Catalogue per region: eu-north1 = L40S/H100/H200/GB300 (17 presets),
390
+ eu-west1 = H200 only, eu-west2 = B300 only, eu-south1 = RTX6000-Ada only.
391
+ MULTI-REGION: the vendor's EU regions are eu-north1 (Finland), eu-west1 and eu-west2
392
+ (France); eu-north2 is Iceland and excluded. The REGION IS A PROPERTY OF THE PROJECT - a
393
+ create body carries none - and this account's project is eu-west2, verified against IAM
394
+ before any create. The project id is discoverable from the service account's own profile
395
+ (/iam/v1/profiles -> serviceAccountProfile.info.metadata.parentId).
396
+ PRICE: the vendor publishes no price LIST but DOES price a configuration, via
397
+ /billing/v1alpha1/calculators/estimate - 9.5/hr for the one qualifying EU single-GPU preset
398
+ (gpu-b300-sxm/1gpu-24vcpu-346gb) on 2026-10-18.
399
+
400
+ - id: hetzner
401
+ class: compute
402
+ status: backup
403
+ enabled: false
404
+ phase: 2
405
+ hq_country: DE
406
+ regions: [eu]
407
+ api_base: https://api.hetzner.cloud/v1
408
+ env: [HETZNER_API_TOKEN]
409
+
410
+ - id: vultr
411
+ class: compute
412
+ status: backup
413
+ enabled: false
414
+ phase: 2
415
+ hq_country: US
416
+ regions: [us]
417
+ api_base: https://api.vultr.com/v2
418
+ env: [VULTR_API_KEY]
419
+
420
+ # OFF on_hold 2026-09-21. The readme records azure "STRUCK FROM THIS LIST (supervisor
421
+ # correction 2026-09-12)", and the owner's 2026-09-08 ruling puts the GCP + Azure TEE lane in
422
+ # PHASE 1 — the readme is authoritative over this file on that point.
423
+ #
424
+ # `enabled: false` is not a hedge: plan 28 §A3's `HyperscalerConfidentialProvider` does not
425
+ # exist yet, so nothing can resolve or launch this. The row exists so the credential has
426
+ # somewhere to live when the service principal is PUT, and so the doctor reports it instead of
427
+ # omitting it — a provider missing from this file does not report as broken, it does not
428
+ # report at all. The lane also stays unreachable unless STEVAI_TEE_ALLOW_HYPERSCALER=true.
429
+ # OWNER RULING 2026-09-24 — AZURE IS ALSO THE AUSTRALIA LANE, AND THAT IS A SECOND LANE.
430
+ # Sharon AI is ELIMINATED from the roster and Hyperstack is DROPPED; Australia moves to Azure.
431
+ # The row below is the TEE lane and does NOT serve that: it is `class: tee`, so the class gate in
432
+ # `nodewake` will never place a general GPU pod on it. The general-purpose Australia lane is a
433
+ # SEPARATE row with its own class and its own `AZURE_POD_SPEC`, and one spec cannot serve both —
434
+ # a TEE instance type and a general GPU instance type are different machines.
435
+ #
436
+ # DELIBERATELY NOT ADDED AS A ROW YET. Owner: "do not build the Azure lane yet." A disabled row
437
+ # is one edit away from being enabled, and a lane with no spec of its own is exactly what the
438
+ # 2026-09-24 ruling on `VAST_POD_SPEC` forbids — a provider that borrows another's spec deploys
439
+ # the wrong machine and bills for it. QUEUED BEHIND `LAMBDA_POD_SPEC` and `DATACRUNCH_POD_SPEC`,
440
+ # in that order.
441
+ #
442
+ # `SHARONAI_API_KEY` has no row here and no code reader, but IS set on all three live gateways.
443
+ # OWNER RULING 2026-09-24: IT STAYS FOR NOW, and the reason is recorded so nobody removes it as
444
+ # tidy-up or leaves it as forgotten drift. Sharon AI is ELIMINATED from the roster but PERMITTED
445
+ # SHORT-TERM while the Azure lane is built, and it is not in `WARMPOOL_PROVIDERS`, so the key is
446
+ # INERT rather than drift: nothing reads it, and the race cannot reach the provider.
447
+ # REMOVE IT WHEN THE AZURE LANE IS LIVE. Until then it is also one of the 13 provider keys that
448
+ # readme rule 9 says belong on the DIRECTORY and not on the gateways (recorded 2026-09-17), so
449
+ # removing it later closes that divergence too.
450
+ - id: azure
451
+ class: tee
452
+ status: emerging
453
+ enabled: false
454
+ phase: 1
455
+ hq_country: US
456
+ regions: [eu, us]
457
+ api_base: https://management.azure.com
458
+ env: [AZURE_TEE_TENANT_ID, AZURE_TEE_CLIENT_ID, AZURE_TEE_CLIENT_SECRET, AZURE_TEE_SUBSCRIPTION_ID]
459
+ notes: >-
460
+ Standard_NCCadsH100v5, typically needing a one-off quota request. Unreachable unless
461
+ STEVAI_TEE_ALLOW_HYPERSCALER=true (default off), so it incurs nothing until an org sets
462
+ require_tee. Provider class still to be built (28 §A3).
463
+
464
+ # GCP CONFIDENTIAL H100 — THE SECOND HALF OF THE TEE PATH THE OWNER NAMED (2026-10-14: "the TEE
465
+ # path (Phala+GCP)").
466
+ #
467
+ # WHY THIS ROW DID NOT EXIST, WHICH IS THE WHOLE FINDING. `gcp` was ALREADY in
468
+ # `tee.HYPERSCALER_TEE_PROVIDERS`, so the policy half was right and the module's own docstring
469
+ # said "Azure and GCP are capable but gated by ALLOW_HYPERSCALER". But
470
+ # `provider_can_host_tee` finishes by asking the REGISTRY for a `class: tee` row, and there was
471
+ # no `gcp` row of any class — so `provider_can_host_tee("gcp")` returned False even with
472
+ # STEVAI_TEE_ALLOW_HYPERSCALER=true. Measured, not read: with the flag ON, phala=True,
473
+ # azure=True, gcp=False. The gate was closed by an absent row rather than by a decision, which
474
+ # is the shape that reads as a policy and is not one.
475
+ #
476
+ # `enabled: false`, and that is NOT a hedge — it is 08-A's own instruction. The plan lists GCP
477
+ # as "4) GCP confidential H100 (*verify GA at build; preview → disabled*)", and nothing here
478
+ # can verify GA. So the row exists (the doctor reports it, the credential has somewhere to
479
+ # live, routing can see it) and it cannot launch anything until an owner both VERIFIES GA and
480
+ # enables it. A provider missing from this file does not report as broken — it does not report
481
+ # at all, which is how this gap survived.
482
+ #
483
+ # Reachable only when STEVAI_TEE_ALLOW_HYPERSCALER=true (default off), exactly like azure:
484
+ # cloud-MEDIATED attestation is a deeper lock-in than renting, so the direct-NRAS lane (phala)
485
+ # sorts first in `allowed_tee_providers` even when this is switched on. Credentials by env name
486
+ # only — never committed, never a cloud KMS or Secret Manager (rule 10).
487
+ - id: gcp
488
+ class: tee
489
+ status: emerging
490
+ enabled: false
491
+ phase: 1
492
+ hq_country: US
493
+ regions: [eu, us]
494
+ api_base: https://compute.googleapis.com/compute/v1
495
+ env: [GCP_TEE_PROJECT, GCP_TEE_ZONE, GCP_TEE_SA_JSON]
496
+ notes: >-
497
+ Confidential H100 (a3-highgpu / CC mode) on Confidential VMs. VERIFY GA BEFORE ENABLING —
498
+ 08-A says preview means disabled. Unreachable unless STEVAI_TEE_ALLOW_HYPERSCALER=true
499
+ (default off), so it incurs nothing until an org sets require_tee. Attestation is
500
+ cloud-MEDIATED, so phala stays the preferred direct-NRAS lane. Provider class still to be
501
+ built (28 §A3) — this row makes the lane VISIBLE to routing, not launchable.
502
+
503
+ # ON HOLD — never a launch dependency (readme provider-key placement note). Deliberately NOT
504
+ # rows above, so nothing can enable them by flipping a flag: Lambda, NAVER, Sakura,
505
+ # Spheron, Azure.
506
+ #
507
+ # MAGALU AND BINARYLANE ARE DROPPED, not on hold (owner ruling 2026-09-24). On-hold means "we may
508
+ # yet use this"; dropped means we will not, so leaving either here would be a standing invitation
509
+ # to re-enable something the owner removed. Neither ever had a row, a code reader or an adapter.
510
+ # `MAGALU_API_KEY` was never set on any service. `BINARYLANE_API_TOKEN` WAS set on all three
511
+ # gateways and has been removed from them.
512
+ #
513
+ # Z1 IS ALSO DROPPED (owner ruling 2026-09-24), and it is the clearest case yet for checking the
514
+ # real variable names before removing anything: the ruling named `Z1_ACCESS_KEY_ID` and
515
+ # `Z1_SECRET_ACCESS_KEY`, but the live names were `Z1_S3_ACCESS_KEY` and `Z1_S3_SECRET_KEY`.
516
+ # Removing exactly what was named would have deleted `Z1_S3_ENDPOINT` and LEFT BOTH CREDENTIALS
517
+ # on all three gateways, while reporting the job done. All three are removed.
518
+ on_hold: [naver, sakura, spheron]