From b3eb160f5d1ffa21f684106a015822159347de21 Mon Sep 17 00:00:00 2001 From: echobt <154886644+echobt@users.noreply.github.com> Date: Thu, 20 Aug 2026 05:39:30 +0000 Subject: [PATCH] fix(prism): reuse public Lium template without docker cred --- crates/prism-lium-harness/src/lib.rs | 145 +++++++++++++++++- crates/prism-lium/src/client.rs | 56 +++++-- deploy/env/prism-challenge.env.example | 6 + docs/PRISM.md | 9 +- .../prism-enable-lium-and-emission.md | 8 +- 5 files changed, 197 insertions(+), 27 deletions(-) diff --git a/crates/prism-lium-harness/src/lib.rs b/crates/prism-lium-harness/src/lib.rs index b6216c4f0..1c05934b7 100644 --- a/crates/prism-lium-harness/src/lib.rs +++ b/crates/prism-lium-harness/src/lib.rs @@ -29,6 +29,11 @@ pub const RECIPES_TEMPLATE_NAME: &str = "prism-recipe-v10-digest-fe1197b26e30-ta /// (template identity is name-based / reuse-if-exists on Lium: a new image /// must ship under a new name or pods would keep the old template). pub const RECIPES_TEMPLATE_NAME_V10: &str = "prism-recipe-v10"; +/// Public Lium templates that already boot B200/5090 (daturaai/pytorch). +/// Used when the private DO pin cannot be created (no docker credential). +pub const PUBLIC_TEMPLATE_FALLBACK_NAMES: &[&str] = &["prism-recipe-v10", "prism-recipe-v9"]; +/// Known-good public template id prefix (`prism-recipe-v9` / daturaai). +pub const PUBLIC_TEMPLATE_FALLBACK_ID_PREFIXES: &[&str] = &["f2f5e84c"]; /// Lium replaces `USER_PUBLIC_KEY` before launching this command. The image /// deliberately uses `CMD` so this bootstrap can install the rental key, /// signal readiness, and keep sshd as the container's foreground process. @@ -88,13 +93,8 @@ pub fn lium_template_create_body( .map_or((docker_image, None), |(repository, digest)| { (repository, Some(digest)) }); - if repository.starts_with("registry.digitalocean.com/basecrawl/") - && docker_credential_id.is_none() - { - return Err(LiumError::Integrity( - "PRISM_POD_DOCKER_CREDENTIAL_ID is required to create the private Prism template" - .into(), - )); + if private_registry_needs_credential(docker_image, docker_credential_id) { + return Err(LiumError::Integrity(private_template_create_error())); } let mut body = serde_json::json!({ "name": name, @@ -116,6 +116,105 @@ pub fn lium_template_create_body( Ok(body) } +/// True when `docker_image` is the private DO registry pin and no Lium +/// credential reference is set. Credential is required only to *create* a +/// new private template — existing public templates must still rent. +#[must_use] +pub fn private_registry_needs_credential( + docker_image: &str, + docker_credential_id: Option<&str>, +) -> bool { + let repository = docker_image + .rsplit_once('@') + .map_or(docker_image, |(repository, _)| repository); + repository.starts_with("registry.digitalocean.com/basecrawl/") + && docker_credential_id.is_none_or(str::is_empty) +} + +/// Operator-facing create refusal (miners should never see this if a public +/// `prism-recipe-v9` / `v10` template already exists on Lium). +#[must_use] +pub fn private_template_create_error() -> String { + "operator: PRISM_POD_DOCKER_CREDENTIAL_ID is required to create a private \ + Prism template; unset it and use PRISM_POD_TEMPLATE_ID or an existing \ + public prism-recipe-v9/v10 template instead" + .into() +} + +/// Id of `name` if it already exists on the Lium account. +#[must_use] +pub fn existing_template_id(templates: &[serde_json::Value], name: &str) -> Option { + templates.iter().find_map(|tmpl| { + let listed = tmpl.get("name").and_then(|x| x.as_str())?; + let id = tmpl + .get("id") + .and_then(|x| x.as_str()) + .filter(|s| !s.is_empty())?; + (listed == name).then(|| id.to_owned()) + }) +} + +/// Listed template to rent: exact name, else public fallback if create is blocked. +pub fn listed_template_id( + templates: &[serde_json::Value], + name: &str, + docker_image: &str, + docker_credential_id: Option<&str>, +) -> Result, LiumError> { + if let Some(id) = existing_template_id(templates, name) { + return Ok(Some(id)); + } + reuse_public_template_if_uncreatable(templates, docker_image, docker_credential_id) +} + +/// Reuse a public Lium template when a private pin cannot be created. +/// +/// `Ok(None)` means create may proceed. `Ok(Some(id))` is an existing public +/// template. `Err` is operator-facing (no credential and no public fallback). +pub fn reuse_public_template_if_uncreatable( + templates: &[serde_json::Value], + docker_image: &str, + docker_credential_id: Option<&str>, +) -> Result, LiumError> { + if !private_registry_needs_credential(docker_image, docker_credential_id) { + return Ok(None); + } + match public_template_fallback_id(templates) { + Some((_, id)) => Ok(Some(id)), + None => Err(LiumError::Integrity(private_template_create_error())), + } +} + +/// First allowlisted public template already present on the Lium account. +#[must_use] +pub fn public_template_fallback_id(templates: &[serde_json::Value]) -> Option<(String, String)> { + let named = |want: &str| { + templates.iter().find_map(|tmpl| { + let name = tmpl.get("name").and_then(|x| x.as_str())?; + let id = tmpl + .get("id") + .and_then(|x| x.as_str()) + .filter(|s| !s.is_empty())?; + (name == want).then(|| (want.to_owned(), id.to_owned())) + }) + }; + PUBLIC_TEMPLATE_FALLBACK_NAMES + .iter() + .find_map(|name| named(name)) + .or_else(|| { + templates.iter().find_map(|tmpl| { + let id = tmpl + .get("id") + .and_then(|x| x.as_str()) + .filter(|s| !s.is_empty())?; + PUBLIC_TEMPLATE_FALLBACK_ID_PREFIXES + .iter() + .any(|prefix| id.starts_with(prefix)) + .then(|| (id.to_owned(), id.to_owned())) + }) + }) +} + fn template_name_for_image(image: &str) -> String { let digest = image .rsplit_once("@sha256:") @@ -312,7 +411,8 @@ pub fn random_seed_hex() -> Result { #[cfg(test)] mod tests { use super::{ - credential_scoped_template_name, is_digest_image_ref, template_name_for_image, + credential_scoped_template_name, is_digest_image_ref, lium_template_create_body, + private_registry_needs_credential, public_template_fallback_id, template_name_for_image, RECIPES_TEMPLATE_STARTUP, }; @@ -351,6 +451,35 @@ mod tests { ); } + #[test] + fn public_template_fallback_uses_existing_v9_without_credential() { + assert!(private_registry_needs_credential( + "registry.digitalocean.com/basecrawl/prism-pod@sha256:fe1197b26e30ebd88f200963cc8528533326666873880b62e676adb51663ff88", + None + )); + assert!(!private_registry_needs_credential( + "registry.digitalocean.com/basecrawl/prism-pod@sha256:fe1197b26e30ebd88f200963cc8528533326666873880b62e676adb51663ff88", + Some("cred") + )); + let listed = serde_json::json!([ + {"name": "prism-recipe-v10-digest-fe1197b26e30-tagged", "id": ""}, + {"name": "prism-recipe-v9", "id": "f2f5e84c-public-v9"} + ]); + assert_eq!( + public_template_fallback_id(listed.as_array().unwrap()), + Some(("prism-recipe-v9".into(), "f2f5e84c-public-v9".into())) + ); + let err = lium_template_create_body( + "private", + "registry.digitalocean.com/basecrawl/prism-pod@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + None, + None, + None, + ) + .unwrap_err(); + assert!(err.to_string().contains("operator:")); + } + #[test] fn provider_startup_command_has_no_shell_metacharacters() { assert!(RECIPES_TEMPLATE_STARTUP.contains("USER_PUBLIC_KEY")); diff --git a/crates/prism-lium/src/client.rs b/crates/prism-lium/src/client.rs index 729d44ed4..dee33ec32 100644 --- a/crates/prism-lium/src/client.rs +++ b/crates/prism-lium/src/client.rs @@ -16,14 +16,14 @@ use crate::ssh::{ use crate::{EvalJobBackend, HARNESS_LOG_RETAIN_BYTES, LIUM_API_BASE_URL, MIN_LIFETIME_HOURS}; use prism_lium_harness::{ classify_log, detach_launch_cmd, eval_assets_dir, harness_env_pairs, harness_upload_tar, - lium_template_create_body, parse_harness_probe, parse_metrics_output, random_seed_hex, - resolved_pod_image, HarnessProgress, EVAL_ASSETS_POD_DIR, HARNESS_ABSENT, HARNESS_BOOTSTRAP, - HARNESS_EXTRACT_CMD, HARNESS_HARVEST_CMD, HARNESS_PROBE_CMD, RECIPES_TEMPLATE_STARTUP, - TRAIN_DONE_MARKER, + listed_template_id, lium_template_create_body, parse_harness_probe, parse_metrics_output, + random_seed_hex, resolved_pod_image, HarnessProgress, EVAL_ASSETS_POD_DIR, HARNESS_ABSENT, + HARNESS_BOOTSTRAP, HARNESS_EXTRACT_CMD, HARNESS_HARVEST_CMD, HARNESS_PROBE_CMD, + RECIPES_TEMPLATE_STARTUP, TRAIN_DONE_MARKER, }; -use prism_lium_types::{CostGuardrailError, LiumError}; use prism_lium_types::{ - GpuPreference, Instance, InstanceSpec, LiumSshConfig, Offer, RemoteExecResult, + CostGuardrailError, GpuPreference, Instance, InstanceSpec, LiumError, LiumSshConfig, Offer, + RemoteExecResult, }; const RUNNING_STATUSES: &[&str] = &["RUNNING", "RUNNING_SSH", "READY"]; @@ -261,12 +261,9 @@ impl LiumClient { .as_array() .cloned() .unwrap_or_else(|| get_array(&v, &["templates"])); - for tmpl in templates { - if get_str(&tmpl, &["name"]) == Some(name) { - if let Some(id) = get_str(&tmpl, &["id"]) { - return Ok(id.to_owned()); - } - } + if let Some(id) = listed_template_id(&templates, name, docker_image, docker_credential_id)? + { + return Ok(id); } let body = lium_template_create_body( name, @@ -291,9 +288,7 @@ impl LiumClient { return Ok(id.clone()); } } - // Isolated proofs pin the public v9 template (f2f5e84c). Without this, - // provision tries to create private v10 and fails closed on missing - // PRISM_POD_DOCKER_CREDENTIAL_ID (or walks 8Ɨ5090 on CREATION_FAILED). + // Explicit public id (v9 f2f5e84c) skips private-template create. if let Ok(id) = std::env::var("PRISM_POD_TEMPLATE_ID") { let id = id.trim(); if !id.is_empty() { @@ -1229,6 +1224,37 @@ mod tests { .await .unwrap_err(); assert!(matches!(error, LiumError::Integrity(_))); + assert!(error.to_string().contains("operator:")); + } + + #[tokio::test] + async fn ensure_template_falls_back_to_public_v9_without_credential() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/templates")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([ + {"name": "prism-recipe-v9", "id": "f2f5e84c-public-v9"} + ]))) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/templates")) + .respond_with(ResponseTemplate::new(500).set_body_string("must not create")) + .mount(&server) + .await; + + let client = LiumClient::with_base_url("test-key", server.uri()).unwrap(); + let id = client + .ensure_template( + "prism-recipe-v10-digest-fe1197b26e30-tagged", + "registry.digitalocean.com/basecrawl/prism-pod@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + None, + Some(RECIPES_TEMPLATE_STARTUP), + None, + ) + .await + .unwrap(); + assert_eq!(id, "f2f5e84c-public-v9"); } #[tokio::test] diff --git a/deploy/env/prism-challenge.env.example b/deploy/env/prism-challenge.env.example index 47bfdc35d..aa5b02eee 100644 --- a/deploy/env/prism-challenge.env.example +++ b/deploy/env/prism-challenge.env.example @@ -40,3 +40,9 @@ BASE_NETUID=541 # the token into this env file. Missing/empty → HF publish no-ops. # PRISM_TOPMODEL_HF_TOKEN_FILE=/run/base/huggingface/token # PRISM_TOPMODEL_HF_REPO=BaseIntelligence/top-prism-architecture + +# Optional existing public Lium template (v9 boots B200/5090). If unset, +# provision falls back to public prism-recipe-v9/v10 when present. +# PRISM_POD_TEMPLATE_ID=f2f5e84c-... +# Required only to *create* a new private DO/GHCR template: +# PRISM_POD_DOCKER_CREDENTIAL_ID= diff --git a/docs/PRISM.md b/docs/PRISM.md index 24fe895ca..2014c960a 100644 --- a/docs/PRISM.md +++ b/docs/PRISM.md @@ -640,7 +640,8 @@ into the pod where applicable): | `PRISM_POD_GPU_NAME` | optional comma-separated offer needles (default `NVIDIA B200`/`B200`; fallbacks `RTX 5090` or `RTX PRO 6000`) | | `PRISM_POD_IMAGE_REF` | optional staged pod image, required form `repository@sha256:<64 lowercase hex>` | | `PRISM_POD_IMAGE_TAG` | Lium pull locator (default `v10-cuda13-te`); never accepted without the separate digest pin | -| `PRISM_POD_DOCKER_CREDENTIAL_ID` | non-secret Lium reference required only when creating the private DigitalOcean registry template | +| `PRISM_POD_DOCKER_CREDENTIAL_ID` | non-secret Lium reference required only when **creating** a new private DigitalOcean registry template. Unset is fine when `PRISM_POD_TEMPLATE_ID` is set or public `prism-recipe-v9`/`v10` already exists on Lium | +| `PRISM_POD_TEMPLATE_ID` | optional existing Lium template id (public v9 `f2f5e84c…` is the known-good B200/5090 boot path) | **Anchor set v1 battery keys** (emitted by the harness on every real run; inert under v0 since unknown `org.*` keys are ignored): @@ -931,7 +932,11 @@ the digest and credential ID, preventing stale image or credential reuse. Unset uses the same immutable recipe-v10 pin advertised by `/v1/recipe`. A new private-registry template requires the non-secret `PRISM_POD_DOCKER_CREDENTIAL_ID` reference; registry credentials themselves -remain stored in Lium. Lium's startup bootstrap substitutes +remain stored in Lium. Missing credential must **not** fail miner provision +when a public allowlisted template already exists (`prism-recipe-v9` / +`prism-recipe-v10`, or `PRISM_POD_TEMPLATE_ID`). The integrity error is +operator-facing and only applies to *creating* a new private template. +Lium's startup bootstrap substitutes `USER_PUBLIC_KEY` into a metacharacter-free command; the image script writes `authorized_keys` and touches `/root/container_ready`. The image uses overridable Docker `CMD` so the provider bootstrap can run. diff --git a/docs/runbooks/prism-enable-lium-and-emission.md b/docs/runbooks/prism-enable-lium-and-emission.md index 1088b6206..e6ebfafee 100644 --- a/docs/runbooks/prism-enable-lium-and-emission.md +++ b/docs/runbooks/prism-enable-lium-and-emission.md @@ -89,8 +89,12 @@ PRISM_POD_DOCKER_CREDENTIAL_ID= ``` The credential ID is a non-secret reference; the registry username/password -remain stored in Lium. It is needed to create a new provider template, whose -name is digest- and credential-scoped. Lium needs the tag as a pull locator, +remain stored in Lium. It is needed **only** to create a new private provider +template (digest- and credential-scoped). If it is unset, provision reuses +`PRISM_POD_TEMPLATE_ID` or an existing public `prism-recipe-v9` / `v10` +template instead of failing every miner submit. To pin private images later, +create the Lium docker credential, set `PRISM_POD_DOCKER_CREDENTIAL_ID` to +that id, and restart `prism-challenge`. Lium needs the tag as a pull locator, but records and checks the digest separately; malformed or missing digest refs fail closed. The image must use overridable Docker `CMD`; the Lium bootstrap injects `USER_PUBLIC_KEY` through a metacharacter-free command, then