Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 137 additions & 8 deletions crates/prism-lium-harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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<String> {
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<Option<String>, 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<Option<String>, 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:")
Expand Down Expand Up @@ -312,7 +411,8 @@ pub fn random_seed_hex() -> Result<String, LiumError> {
#[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,
};

Expand Down Expand Up @@ -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"));
Expand Down
56 changes: 41 additions & 15 deletions crates/prism-lium/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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,
Expand All @@ -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() {
Expand Down Expand Up @@ -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]
Expand Down
6 changes: 6 additions & 0 deletions deploy/env/prism-challenge.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=<lium-docker-credential-id>
9 changes: 7 additions & 2 deletions docs/PRISM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions docs/runbooks/prism-enable-lium-and-emission.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,12 @@ PRISM_POD_DOCKER_CREDENTIAL_ID=<lium-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
Expand Down
Loading