diff --git a/Cargo.lock b/Cargo.lock index 185fe345..3d735ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,7 +486,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c0mpute-api" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "c0mpute-proto", @@ -501,7 +501,7 @@ dependencies = [ [[package]] name = "c0mpute-bench" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "blake3", @@ -512,7 +512,7 @@ dependencies = [ [[package]] name = "c0mpute-cli" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "axum", @@ -546,7 +546,7 @@ dependencies = [ [[package]] name = "c0mpute-core" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes-gcm", "anyhow", @@ -596,7 +596,7 @@ dependencies = [ [[package]] name = "c0mpute-doctor" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "serde", @@ -608,7 +608,7 @@ dependencies = [ [[package]] name = "c0mpute-envelope" -version = "0.3.4" +version = "0.3.5" dependencies = [ "blake3", "bs58", @@ -621,7 +621,7 @@ dependencies = [ [[package]] name = "c0mpute-gateway" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "axum", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "c0mpute-market" -version = "0.3.4" +version = "0.3.5" dependencies = [ "c0mpute-envelope", "serde", @@ -653,7 +653,7 @@ dependencies = [ [[package]] name = "c0mpute-net" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "async-trait", @@ -671,7 +671,7 @@ dependencies = [ [[package]] name = "c0mpute-placement" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "async-trait", @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "c0mpute-proto" -version = "0.3.4" +version = "0.3.5" dependencies = [ "blake3", "hex", @@ -704,7 +704,7 @@ dependencies = [ [[package]] name = "c0mpute-sealed" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes", "aes-gcm", @@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "c0mpute-secure-chat" -version = "0.3.4" +version = "0.3.5" dependencies = [ "aes-gcm", "anyhow", @@ -749,7 +749,7 @@ dependencies = [ [[package]] name = "c0mpute-store" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "blake3", @@ -767,7 +767,7 @@ dependencies = [ [[package]] name = "c0mpute-transcode" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "c0mpute-proto", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "c0mpute-update" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "hex", @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "c0mpute-verify" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 09d9c622..939e857d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.3.4" +version = "0.3.5" edition = "2024" license = "MIT" repository = "https://github.com/profullstack/c0mpute" diff --git a/apps/tui/package.json b/apps/tui/package.json index 05c215d1..9a1de6eb 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/tui", - "version": "0.3.4", + "version": "0.3.5", "private": true, "type": "module", "bin": { diff --git a/apps/web/package.json b/apps/web/package.json index 4dddd73d..635dec47 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute", - "version": "0.3.4", + "version": "0.3.5", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/app/releases/latest.json/route.ts b/apps/web/src/app/releases/latest.json/route.ts index 054b949e..86ef0ecf 100644 --- a/apps/web/src/app/releases/latest.json/route.ts +++ b/apps/web/src/app/releases/latest.json/route.ts @@ -28,7 +28,7 @@ export const dynamic = "force-dynamic"; * this in sync with the workspace version in `Cargo.toml` on each release so a * freshly built CLI reports "already latest" rather than a phantom upgrade. */ -const FALLBACK_VERSION = "0.3.4"; +const FALLBACK_VERSION = "0.3.5"; const FALLBACK_MIN_REQUIRED = "0.0.1"; type Channel = "stable" | "beta" | "nightly"; diff --git a/docs/apps.md b/docs/apps.md index 80344775..6b335a42 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -97,6 +97,12 @@ Every gateway routes to apps whether or not it runs any. snapshot are lost. This is a replicated single-node database, not a distributed one (no Raft yet; rqlite/dqlite as an app needs node-to-node links, which come next). +- **Pin your hosts for anything private.** By default any `apps` node may + hold an app, and every holder receives its secrets and app key. `--host + ` (repeatable; `hosts = [...]` in `c0mpute.toml`) signs an allowlist + into the record: holders seal secrets only to, and take snapshots only from, + those peers, and nodes off the list never take a copy. Operators can + likewise limit whose apps they run with `[apps] owners`. - **Operators can read a running app.** The node running the container sees its memory and its state volume, as with any VPS host. Snapshots at rest on standbys are encrypted. diff --git a/node/crates/c0mpute-cli/src/apps_mcp.rs b/node/crates/c0mpute-cli/src/apps_mcp.rs index 9abe2ca4..fd6fa1a4 100644 --- a/node/crates/c0mpute-cli/src/apps_mcp.rs +++ b/node/crates/c0mpute-cli/src/apps_mcp.rs @@ -34,6 +34,7 @@ fn tools() -> Value { "memory_mb": {"type": "integer"}, "replicas": {"type": "integer"}, "domains": {"type": "array", "items": {"type": "string"}}, + "hosts": {"type": "array", "items": {"type": "string"}, "description": "Peer ids allowed to hold the app; empty = any apps node (each holder gets the app's secrets)"}, "wait_secs": {"type": "integer"} }, "required": ["name"] @@ -113,6 +114,10 @@ async fn call(gateway: &str, name: &str, a: &Value) -> Result { } } } + if let Some(h) = a.get("hosts").and_then(Value::as_array) { + opts.hosts + .extend(h.iter().filter_map(Value::as_str).map(str::to_string)); + } if let Some(d) = a.get("domains").and_then(Value::as_array) { opts.domains .extend(d.iter().filter_map(Value::as_str).map(str::to_string)); @@ -168,6 +173,7 @@ fn base(name: &str, source: deploy::Source, wait: Duration) -> deploy::DeployOpt millicpus: 500, replicas: c0mpute_core::apps::record::DEFAULT_REPLICAS, domains: vec![], + hosts: vec![], wait, } } diff --git a/node/crates/c0mpute-cli/src/main.rs b/node/crates/c0mpute-cli/src/main.rs index 7111dc49..9721d2ee 100644 --- a/node/crates/c0mpute-cli/src/main.rs +++ b/node/crates/c0mpute-cli/src/main.rs @@ -749,6 +749,10 @@ enum AppsCmd { /// `_c0mpute-app.` = `app=`. #[arg(long = "domain")] domains: Vec, + /// Peer id allowed to hold and run the app (repeatable). Without it + /// any apps node may, and every holder receives the app's secrets. + #[arg(long = "host")] + hosts: Vec, /// Give up waiting for a healthy primary after this many seconds. #[arg(long, default_value_t = 600)] wait_secs: u64, @@ -3859,6 +3863,7 @@ async fn run_apps(cmd: AppsCmd) -> Result<()> { cpus, replicas, domains, + hosts, wait_secs, } => { let wait = std::time::Duration::from_secs(wait_secs); @@ -3883,6 +3888,7 @@ async fn run_apps(cmd: AppsCmd) -> Result<()> { millicpus: 500, replicas: c0mpute_core::apps::record::DEFAULT_REPLICAS, domains: vec![], + hosts: vec![], wait, }, }; @@ -3924,6 +3930,7 @@ async fn run_apps(cmd: AppsCmd) -> Result<()> { opts.replicas = r; } opts.domains.extend(domains); + opts.hosts.extend(hosts); opts.wait = wait; let rec = deploy::deploy(opts).await?; println!("{}", rec.app_id); diff --git a/node/crates/c0mpute-core/src/apps/deploy.rs b/node/crates/c0mpute-core/src/apps/deploy.rs index f30eef0f..7340e490 100644 --- a/node/crates/c0mpute-core/src/apps/deploy.rs +++ b/node/crates/c0mpute-core/src/apps/deploy.rs @@ -48,6 +48,8 @@ pub struct DeployOptions { pub millicpus: u32, pub replicas: u8, pub domains: Vec, + /// Peer ids allowed to hold the app (empty = any apps node). + pub hosts: Vec, pub wait: Duration, } @@ -256,6 +258,7 @@ pub async fn deploy(opts: DeployOptions) -> Result { millicpus: opts.millicpus, replicas: opts.replicas, domains: opts.domains.clone(), + hosts: opts.hosts.clone(), }; let rec = AppRecord::sign(&keypair, spec, seq)?; if let Some(s) = &secrets { @@ -396,6 +399,9 @@ pub struct AppSection { pub replicas: u8, #[serde(default)] pub domains: Vec, + /// Peer ids allowed to hold the app (empty = any apps node). + #[serde(default)] + pub hosts: Vec, /// A pinned registry image instead of building the repository. pub image: Option, #[serde(default)] @@ -438,6 +444,7 @@ impl AppSection { millicpus: (self.cpus * 1000.0).round() as u32, replicas: self.replicas, domains: self.domains.clone(), + hosts: self.hosts.clone(), wait, } } diff --git a/node/crates/c0mpute-core/src/apps/mod.rs b/node/crates/c0mpute-core/src/apps/mod.rs index ab610c22..c72a45aa 100644 --- a/node/crates/c0mpute-core/src/apps/mod.rs +++ b/node/crates/c0mpute-core/src/apps/mod.rs @@ -300,6 +300,18 @@ fn load_x25519(dir: Option<&PathBuf>) -> crypto::KeyPair { kp } +/// The view as this app sees it: nodes off its host allowlist do not exist +/// for its placement, primary election, snapshots or secrets. +fn scoped(rec: &AppRecord, view: &HashMap) -> HashMap { + if rec.hosts.is_empty() { + return view.clone(); + } + view.iter() + .filter(|(p, _)| rec.allows(p)) + .map(|(p, n)| (p.clone(), n.clone())) + .collect() +} + /// Write a file only its owner can read. pub fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> { let tmp = path.with_extension("tmp"); @@ -567,6 +579,9 @@ impl Apps { } // Only the owner or a node that holds the app may hand out secrets. let from_owner = rec.owner_peer().as_deref() == Some(src); + if !from_owner && !rec.allows(src) { + return; + } let from_holder = self .0 .nodes @@ -775,8 +790,9 @@ impl Apps { let view = self.live_view().await; let mut changed = false; for rec in records { - changed |= self.step(&rec, &view).await; - self.seal_to_holders(&rec, &view).await; + let v = scoped(&rec, &view); + changed |= self.step(&rec, &v).await; + self.seal_to_holders(&rec, &v).await; } // Apps this node holds whose record vanished: nothing to do but wait. if changed { @@ -790,6 +806,11 @@ impl Apps { async fn step(&self, rec: &AppRecord, view: &HashMap) -> bool { let id = rec.app_id.clone(); let holding = self.0.local.read().await.contains_key(&id); + if holding && !rec.deleted && !rec.allows(&self.0.me) { + info!(app = %id, "apps: no longer on the app's host list; dropping"); + self.drop_app(&id).await; + return true; + } if rec.deleted { if holding { info!(app = %id, "apps: deleted by owner; dropping"); @@ -1469,6 +1490,7 @@ impl Apps { } pub async fn info(&self, rec: &AppRecord, view: &HashMap) -> AppInfo { + let view = &scoped(rec, view); let holders = placement::holders(&rec.app_id, view); let p = placement::primary(&rec.app_id, rec.needs_secrets(), view); let (primary, epoch, healthy) = match &p { @@ -1520,7 +1542,7 @@ impl Apps { /// Owner side: the last `tail` log lines from the primary. pub async fn logs(&self, app_id: &str, tail: u32) -> Result { let rec = self.lookup_record(app_id).await.context("unknown app")?; - let view = self.live_view().await; + let view = scoped(&rec, &self.live_view().await); let peer = placement::primary(app_id, rec.needs_secrets(), &view) .peer() .map(str::to_string) @@ -1538,7 +1560,7 @@ impl Apps { let Some(rec) = self.lookup_record(&req.app_id).await.filter(|r| !r.deleted) else { return (StatusCode::NOT_FOUND, "no such app").into_response(); }; - let view = self.live_view().await; + let view = scoped(&rec, &self.live_view().await); let Some(peer) = placement::primary(&rec.app_id, rec.needs_secrets(), &view) .peer() .map(str::to_string) diff --git a/node/crates/c0mpute-core/src/apps/record.rs b/node/crates/c0mpute-core/src/apps/record.rs index e3e9e9cd..798fd0cb 100644 --- a/node/crates/c0mpute-core/src/apps/record.rs +++ b/node/crates/c0mpute-core/src/apps/record.rs @@ -23,6 +23,8 @@ pub const MAX_REPLICAS: u8 = 5; pub const MAX_ENV: usize = 64; pub const MAX_ENV_VALUE: usize = 4096; pub const MAX_DOMAINS: usize = 8; +/// Most peers an app's host allowlist may name. +pub const MAX_HOSTS: usize = 32; /// Memory an app may ask for, MiB. pub const MIN_MEMORY_MB: u32 = 64; pub const MAX_MEMORY_MB: u32 = 8192; @@ -90,6 +92,11 @@ pub struct AppRecord { /// `app=`). #[serde(default)] pub domains: Vec, + /// Peer ids (base58) allowed to hold and run this app. Empty = any apps + /// node. Holders seal secrets only to, and take snapshots only from, + /// peers on this list, so a private app never reaches a stranger's box. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub hosts: Vec, /// A tombstone: holders stop the app and drop its copies. #[serde(default)] pub deleted: bool, @@ -172,6 +179,7 @@ pub struct AppSpec { pub millicpus: u32, pub replicas: u8, pub domains: Vec, + pub hosts: Vec, } impl AppRecord { @@ -210,6 +218,7 @@ impl AppRecord { millicpus: spec.millicpus, replicas: spec.replicas, domains: spec.domains, + hosts: spec.hosts, deleted: false, sig: String::new(), }; @@ -316,6 +325,20 @@ impl AppRecord { if self.domains.len() > MAX_DOMAINS || !self.domains.iter().all(|d| valid_domain(d)) { bail!("invalid domain list"); } + if self.hosts.len() > MAX_HOSTS + || !self + .hosts + .iter() + .all(|h| crate::hosting::site::valid_peer_id(h)) + || self + .hosts + .iter() + .collect::>() + .len() + != self.hosts.len() + { + bail!("hosts must be up to {MAX_HOSTS} distinct peer ids"); + } Ok(()) } @@ -343,6 +366,11 @@ impl AppRecord { Some(pk.to_peer_id().to_base58()) } + /// May `peer` hold and run this app? + pub fn allows(&self, peer: &str) -> bool { + self.hosts.is_empty() || self.hosts.iter().any(|h| h == peer) + } + /// The docker image reference a holder runs. pub fn image_ref(&self) -> &str { match &self.image { @@ -378,9 +406,35 @@ pub(crate) mod tests { millicpus: 500, replicas: 3, domains: vec!["api.example.org".into()], + hosts: vec![], } } + #[test] + fn host_allowlist_is_signed_and_old_records_still_verify() { + let kp = c0mpute_net::libp2p::identity::Keypair::generate_ed25519(); + let open = AppRecord::sign(&kp, spec("demo"), 1).unwrap(); + assert!( + !serde_json::to_string(&open).unwrap().contains("hosts"), + "empty list stays off the wire" + ); + assert!(open.allows("anyone")); + let peer = c0mpute_net::libp2p::identity::Keypair::generate_ed25519() + .public() + .to_peer_id() + .to_base58(); + let mut s = spec("demo"); + s.hosts = vec![peer.clone()]; + let pinned = AppRecord::sign(&kp, s, 2).unwrap(); + assert!(pinned.verify() && pinned.allows(&peer) && !pinned.allows("someone-else")); + let mut bad = pinned.clone(); + bad.hosts.push(peer.clone()); + assert!(!bad.verify(), "the list is covered by the signature"); + let mut s = spec("demo"); + s.hosts = vec!["not a peer".into()]; + assert!(AppRecord::sign(&kp, s, 3).is_err()); + } + #[test] fn sign_verify_and_tamper() { let kp = c0mpute_net::libp2p::identity::Keypair::generate_ed25519(); diff --git a/node/crates/c0mpute-core/src/hosting/site.rs b/node/crates/c0mpute-core/src/hosting/site.rs index 81b85b0e..e7015520 100644 --- a/node/crates/c0mpute-core/src/hosting/site.rs +++ b/node/crates/c0mpute-core/src/hosting/site.rs @@ -308,7 +308,7 @@ pub struct HostingTerms { pub payer: Option, } -fn valid_peer_id(p: &str) -> bool { +pub fn valid_peer_id(p: &str) -> bool { p.len() <= 64 && p.parse::().is_ok() } diff --git a/node/crates/c0mpute-core/tests/apps_e2e.rs b/node/crates/c0mpute-core/tests/apps_e2e.rs index 6808ef1c..c21a9806 100644 --- a/node/crates/c0mpute-core/tests/apps_e2e.rs +++ b/node/crates/c0mpute-core/tests/apps_e2e.rs @@ -144,6 +144,9 @@ async fn apps_place_three_copies_serve_snapshot_and_fail_over() { std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap(); } unsafe { + // Never discover (and join) a live network on the machine running + // the tests: loopback peers only. + std::env::set_var("C0MPUTE_DISABLE_MDNS", "1"); std::env::set_var(c0mpute_core::container::ENV, &bin); std::env::set_var("FAKE_DIR", &fake); } @@ -210,6 +213,7 @@ async fn apps_place_three_copies_serve_snapshot_and_fail_over() { millicpus: 500, replicas: 3, domains: vec![], + hosts: vec![], }, 1, ) diff --git a/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs b/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs index 019259ef..80029ffb 100644 --- a/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs +++ b/node/crates/c0mpute-core/tests/hosting_parts_e2e.rs @@ -24,6 +24,8 @@ impl ChunkSource for StoreSource { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn a_nine_thousand_file_site_publishes_and_serves() { + // Loopback only: never publish test sites to a live network via mDNS. + unsafe { std::env::set_var("C0MPUTE_DISABLE_MDNS", "1") }; let tmp = tempdir().unwrap(); let site = tmp.path().join("site"); for i in 0..9_000 { @@ -38,8 +40,14 @@ async fn a_nine_thousand_file_site_publishes_and_serves() { let built = build_from_dir(&site, "big").unwrap(); assert!(built.manifest.files.len() > INLINE_FILES); let stored = built.stored(); - assert!(!stored.parts.is_empty(), "a large site gets a parted manifest"); - assert!(matches!(Manifest::from_bytes(&stored.root).unwrap(), Manifest::Parted(_))); + assert!( + !stored.parts.is_empty(), + "a large site gets a parted manifest" + ); + assert!(matches!( + Manifest::from_bytes(&stored.root).unwrap(), + Manifest::Parted(_) + )); let store = ChunkStore::open(tmp.path().join("store")).await.unwrap(); let net = Libp2pNetwork::spawn( @@ -49,14 +57,32 @@ async fn a_nine_thousand_file_site_publishes_and_serves() { ) .await .unwrap(); - let h = Hosting::start(Arc::new(net), store, Options::default()).await.unwrap(); + let h = Hosting::start(Arc::new(net), store, Options::default()) + .await + .unwrap(); let kp = libp2p::identity::Keypair::generate_ed25519(); - let rec = SiteRecord::sign(&kp, "big", &built.manifest_hash(), 1, built.manifest.total_bytes(), vec![]).unwrap(); + let rec = SiteRecord::sign( + &kp, + "big", + &built.manifest_hash(), + 1, + built.manifest.total_bytes(), + vec![], + ) + .unwrap(); h.publish_site(&built, rec.clone()).await.unwrap(); - let (_, body) = h.read_file(&rec, "d7/p8197.html").await.unwrap().expect("file in a late part"); + let (_, body) = h + .read_file(&rec, "d7/p8197.html") + .await + .unwrap() + .expect("file in a late part"); assert_eq!(body, b"page 8197"); - let (_, body) = h.read_file(&rec, "d0/p0.html").await.unwrap().expect("file in the first part"); + let (_, body) = h + .read_file(&rec, "d0/p0.html") + .await + .unwrap() + .expect("file in the first part"); assert_eq!(body, b"page 0"); assert!(h.read_file(&rec, "d0/nope.html").await.unwrap().is_none()); @@ -65,8 +91,18 @@ async fn a_nine_thousand_file_site_publishes_and_serves() { let b = axum::body::to_bytes(r.into_body(), 1 << 20).await.unwrap(); (status, String::from_utf8_lossy(&b).into_owned()) }; - assert_eq!(body_of(h.serve(&rec, "/d3/p4503").await).await, (200, "page 4503".into()), "extensionless path finds .html"); - assert_eq!(body_of(h.serve(&rec, "/docs/").await).await, (200, "docs home".into())); - assert_eq!(body_of(h.serve(&rec, "/missing/thing").await).await, (404, "custom 404".into())); + assert_eq!( + body_of(h.serve(&rec, "/d3/p4503").await).await, + (200, "page 4503".into()), + "extensionless path finds .html" + ); + assert_eq!( + body_of(h.serve(&rec, "/docs/").await).await, + (200, "docs home".into()) + ); + assert_eq!( + body_of(h.serve(&rec, "/missing/thing").await).await, + (404, "custom 404".into()) + ); let _ = Duration::from_secs(0); } diff --git a/package.json b/package.json index 16f37dc3..02f04d02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute-monorepo", - "version": "0.3.4", + "version": "0.3.5", "private": true, "workspaces": [ "apps/*", diff --git a/packages/captcha/package.json b/packages/captcha/package.json index d5b07131..55ce5b3d 100644 --- a/packages/captcha/package.json +++ b/packages/captcha/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/c0mpute-captcha", - "version": "0.3.4", + "version": "0.3.5", "description": "Solve image, slider, gap, click-text, audio and math captchas on the c0mpute network: library (fetch().then(solveCaptcha)), CLI and stdio MCP server.", "type": "module", "license": "MIT", diff --git a/packages/shared/package.json b/packages/shared/package.json index 3d563d9a..82c724c5 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/shared", - "version": "0.3.4", + "version": "0.3.5", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/plugins/ad-generator-agent/module.toml b/plugins/ad-generator-agent/module.toml index 77c33a1e..a92a6b54 100644 --- a/plugins/ad-generator-agent/module.toml +++ b/plugins/ad-generator-agent/module.toml @@ -21,7 +21,7 @@ [module] id = "ad-generator-agent" name = "Ad Generator Agent — every ad type from one URL" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "available" diff --git a/plugins/ads-manager/module.toml b/plugins/ads-manager/module.toml index 77bc8667..d3cf9b02 100644 --- a/plugins/ads-manager/module.toml +++ b/plugins/ads-manager/module.toml @@ -38,7 +38,7 @@ [module] id = "ads-manager" name = "Ads Manager — shared ad operations layer" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "partial" diff --git a/plugins/apps/module.toml b/plugins/apps/module.toml index 8b03b8a6..6c698ba1 100644 --- a/plugins/apps/module.toml +++ b/plugins/apps/module.toml @@ -18,7 +18,7 @@ [module] id = "apps" name = "Apps — backends with 3x redundancy" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/captcha/module.toml b/plugins/captcha/module.toml index 5c7af001..9a4813f3 100644 --- a/plugins/captcha/module.toml +++ b/plugins/captcha/module.toml @@ -22,7 +22,7 @@ [module] id = "captcha" name = "Captcha — image, slider, audio and math solving" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/coinpay/module.toml b/plugins/coinpay/module.toml index 4971456f..aa7256c7 100644 --- a/plugins/coinpay/module.toml +++ b/plugins/coinpay/module.toml @@ -7,7 +7,7 @@ [module] id = "coinpay" name = "CoinPay — DID, payments, escrow, reputation" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "available" diff --git a/plugins/crawl/module.toml b/plugins/crawl/module.toml index aabf9a13..a4d046e9 100644 --- a/plugins/crawl/module.toml +++ b/plugins/crawl/module.toml @@ -8,7 +8,7 @@ [module] id = "crawl" name = "Crawl — distributed web fetching" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "data" status = "available" diff --git a/plugins/crowdgpt/module.toml b/plugins/crowdgpt/module.toml index cb27cb38..45a9e7ed 100644 --- a/plugins/crowdgpt/module.toml +++ b/plugins/crowdgpt/module.toml @@ -43,7 +43,7 @@ [module] id = "crowdgpt" name = "CrowdGPT: donate idle compute" -version = "0.3.4" +version = "0.3.5" kind = "donation" category = "ai" status = "available" diff --git a/plugins/ctv-ads/module.toml b/plugins/ctv-ads/module.toml index f3728056..6c579978 100644 --- a/plugins/ctv-ads/module.toml +++ b/plugins/ctv-ads/module.toml @@ -28,7 +28,7 @@ [module] id = "ctv-ads" name = "CTV Ads — VAST and VMAP from every gateway" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "partial" diff --git a/plugins/diffusion/module.toml b/plugins/diffusion/module.toml index d8b439df..60b3c189 100644 --- a/plugins/diffusion/module.toml +++ b/plugins/diffusion/module.toml @@ -11,7 +11,7 @@ [module] id = "diffusion" name = "Diffusion — image generation" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/embed/module.toml b/plugins/embed/module.toml index 4a340fb9..e5069de0 100644 --- a/plugins/embed/module.toml +++ b/plugins/embed/module.toml @@ -19,7 +19,7 @@ [module] id = "embed" name = "Embeddings — vector generation" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/gameserver/module.toml b/plugins/gameserver/module.toml index 364cac7d..b61b3acc 100644 --- a/plugins/gameserver/module.toml +++ b/plugins/gameserver/module.toml @@ -55,7 +55,7 @@ [module] id = "gameserver" name = "Game server — Minecraft / Factorio / Valheim" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/hosting/module.toml b/plugins/hosting/module.toml index de916b81..701a34a0 100644 --- a/plugins/hosting/module.toml +++ b/plugins/hosting/module.toml @@ -30,7 +30,7 @@ [module] id = "hosting" name = "Hosting — p2p static sites" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "data" status = "available" diff --git a/plugins/infernet/module.toml b/plugins/infernet/module.toml index 1b05edcd..e7e4de8b 100644 --- a/plugins/infernet/module.toml +++ b/plugins/infernet/module.toml @@ -22,7 +22,7 @@ [module] id = "infernet" name = "Infernet Protocol — AI inference" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/live-stream/module.toml b/plugins/live-stream/module.toml index a9f402bd..5c148d36 100644 --- a/plugins/live-stream/module.toml +++ b/plugins/live-stream/module.toml @@ -34,7 +34,7 @@ [module] id = "live-stream" name = "Live Stream — p2p HLS live streaming" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "media" status = "available" diff --git a/plugins/music/module.toml b/plugins/music/module.toml index d3f85188..d205bbf2 100644 --- a/plugins/music/module.toml +++ b/plugins/music/module.toml @@ -15,7 +15,7 @@ [module] id = "music" name = "Music — song generation (ACE-Step 1.5)" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/ocr/module.toml b/plugins/ocr/module.toml index 23530f34..f532c213 100644 --- a/plugins/ocr/module.toml +++ b/plugins/ocr/module.toml @@ -19,7 +19,7 @@ [module] id = "ocr" name = "OCR — document text extraction" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/podcast-ads/module.toml b/plugins/podcast-ads/module.toml index 327b8097..2095ecbf 100644 --- a/plugins/podcast-ads/module.toml +++ b/plugins/podcast-ads/module.toml @@ -31,7 +31,7 @@ [module] id = "podcast-ads" name = "Podcast Ads — dynamic ad insertion" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "partial" diff --git a/plugins/podcasting/module.toml b/plugins/podcasting/module.toml index a2fdd7ba..8e356548 100644 --- a/plugins/podcasting/module.toml +++ b/plugins/podcasting/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasting" name = "Podcasting — p2p podcast publishing" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "media" status = "available" diff --git a/plugins/podcasts/module.toml b/plugins/podcasts/module.toml index cb0baf64..f837427d 100644 --- a/plugins/podcasts/module.toml +++ b/plugins/podcasts/module.toml @@ -24,7 +24,7 @@ [module] id = "podcasts" name = "Podcasts — p2p podcast listener" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "media" status = "available" diff --git a/plugins/render/module.toml b/plugins/render/module.toml index 7c6f555a..c753dc53 100644 --- a/plugins/render/module.toml +++ b/plugins/render/module.toml @@ -33,7 +33,7 @@ [module] id = "render" name = "Render — Blender 3D rendering" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "media" status = "available" diff --git a/plugins/secure-chat/module.toml b/plugins/secure-chat/module.toml index 64550ad7..9bd35597 100644 --- a/plugins/secure-chat/module.toml +++ b/plugins/secure-chat/module.toml @@ -21,7 +21,7 @@ [module] id = "secure-chat" name = "Secure Chat — E2E encrypted p2p messaging" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "applications" status = "available" diff --git a/plugins/storage/module.toml b/plugins/storage/module.toml index 1a56631c..ddb1e165 100644 --- a/plugins/storage/module.toml +++ b/plugins/storage/module.toml @@ -20,7 +20,7 @@ [module] id = "storage" name = "Storage — Reed-Solomon shard service" -version = "0.3.4" +version = "0.3.5" kind = "service" category = "data" status = "available" diff --git a/plugins/train/module.toml b/plugins/train/module.toml index 307127ef..7034f1b4 100644 --- a/plugins/train/module.toml +++ b/plugins/train/module.toml @@ -55,7 +55,7 @@ [module] id = "train" name = "Fine-tune — LoRA training" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/transcode/module.toml b/plugins/transcode/module.toml index d2c5b1ec..453a305c 100644 --- a/plugins/transcode/module.toml +++ b/plugins/transcode/module.toml @@ -7,7 +7,7 @@ [module] id = "transcode" name = "FFmpeg Transcoding" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "media" status = "available" diff --git a/plugins/tts/module.toml b/plugins/tts/module.toml index 45428e0f..6a7f7813 100644 --- a/plugins/tts/module.toml +++ b/plugins/tts/module.toml @@ -15,7 +15,7 @@ [module] id = "tts" name = "TTS — text-to-speech" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/video/module.toml b/plugins/video/module.toml index 89ea3bf8..3af1496e 100644 --- a/plugins/video/module.toml +++ b/plugins/video/module.toml @@ -65,7 +65,7 @@ [module] id = "video" name = "Video — generation (Wan 2.2, Wan 2.1 1.3B on 8 GiB or CPU, LTX-2.3 with audio)" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/webhook/module.toml b/plugins/webhook/module.toml index 1695348b..056eb973 100644 --- a/plugins/webhook/module.toml +++ b/plugins/webhook/module.toml @@ -7,7 +7,7 @@ [module] id = "webhook" name = "Webhooks — paid, end-to-end encrypted delivery" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available" diff --git a/plugins/whisper/module.toml b/plugins/whisper/module.toml index ad351413..5a382e74 100644 --- a/plugins/whisper/module.toml +++ b/plugins/whisper/module.toml @@ -7,7 +7,7 @@ [module] id = "whisper" name = "Whisper — speech-to-text" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "ai" status = "available" diff --git a/plugins/workflow-step/module.toml b/plugins/workflow-step/module.toml index 8092fd6f..a906a3f1 100644 --- a/plugins/workflow-step/module.toml +++ b/plugins/workflow-step/module.toml @@ -14,7 +14,7 @@ [module] id = "workflow-step" name = "Workflow step — webhook workflow steps (meshhook)" -version = "0.3.4" +version = "0.3.5" kind = "workload" category = "infrastructure" status = "available"