diff --git a/Cargo.lock b/Cargo.lock index 3b60dc4579..39d14d0d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,6 +769,7 @@ dependencies = [ "buzz-core", "buzz-persona", "buzz-sdk", + "buzz-world-view-resolver", "chrono", "clap", "evalexpr", @@ -791,6 +792,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "zeroize", ] [[package]] @@ -888,6 +890,7 @@ dependencies = [ "buzz-core", "buzz-persona", "buzz-sdk", + "buzz-world-view-resolver", "buzz-ws-client", "bytes", "chrono", @@ -908,6 +911,7 @@ dependencies = [ "tokio", "url", "uuid", + "zeroize", ] [[package]] @@ -1285,6 +1289,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "buzz-world-view-resolver" +version = "0.1.0" +dependencies = [ + "buzz-core", + "chrono", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "uuid", +] + [[package]] name = "buzz-ws-client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3ac7ee4cce..8dd93f1a5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/buzz-search", "crates/buzz-audit", "crates/buzz-acp", + "crates/buzz-world-view-resolver", "crates/buzz-agent", "crates/sprig", "crates/buzz-test-client", @@ -134,6 +135,7 @@ buzz-media = { path = "crates/buzz-media" } buzz-sdk = { path = "crates/buzz-sdk" } buzz-ws-client = { path = "crates/buzz-ws-client" } buzz-relay-mesh = { path = "crates/buzz-relay-mesh" } +buzz-world-view-resolver = { path = "crates/buzz-world-view-resolver" } # CI profile — builds the relay for desktop e2e. Dependencies keep full # release optimization (warm from main's cache; they carry the runtime hot diff --git a/crates/buzz-acp/Cargo.toml b/crates/buzz-acp/Cargo.toml index d047849806..9b326f81a5 100644 --- a/crates/buzz-acp/Cargo.toml +++ b/crates/buzz-acp/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" buzz-core = { workspace = true } buzz-sdk = { workspace = true } buzz-persona = { path = "../buzz-persona" } +buzz-world-view-resolver = { workspace = true } # Nostr nostr = { workspace = true } @@ -53,6 +54,7 @@ url = { workspace = true } sha2 = { workspace = true } base64 = "0.22" hex = { workspace = true } +zeroize = { workspace = true } # Logging tracing = { workspace = true } diff --git a/crates/buzz-acp/src/acp.rs b/crates/buzz-acp/src/acp.rs index c0147baf1b..885d8d063b 100644 --- a/crates/buzz-acp/src/acp.rs +++ b/crates/buzz-acp/src/acp.rs @@ -227,119 +227,188 @@ fn deep_merge( } } -/// Build the merged `CODEX_CONFIG` environment-variable value for a Codex agent spawn. +/// Internal spawn metadata carrying absolute filesystem permissions for Codex. /// -/// Returns `Some(json_string)` when `has_generated_codex_config` is true (Buzz injected a -/// `CODEX_CONFIG` entry via `codex_network_env()`), `None` otherwise. -/// -/// # Merge contract (when `has_generated_codex_config` is true) -/// -/// 1. **Persona base** — the first `CODEX_CONFIG` value in `extra_env` is taken as -/// the base object (all keys preserved, recursively). When there is no persona entry, -/// the generated entry serves as the base. -/// 2. **Generated overlay** — all subsequent `CODEX_CONFIG` entries are deep-merged into -/// the base so unrelated nested persona keys survive. -/// 3. **Parent-env precedence** — if `parent_codex_config` is `Some`, its keys are -/// deep-merged into the result (parent wins on colliding keys at every nesting level; -/// unrelated keys from either side survive). -/// 4. **Forced overlay** — `sandbox_workspace_write.network_access = true` is applied -/// last so relay access is guaranteed regardless of operator / persona config. -/// -/// When `has_generated_codex_config` is false, the function returns `None` and the -/// caller handles any persona-supplied `CODEX_CONFIG` with ordinary operator-wins -/// semantics (no merging, no sandbox widening). +/// The harness consumes this value while constructing `CODEX_CONFIG`; it is +/// never forwarded to the agent process. +pub(crate) const CODEX_FILESYSTEM_PERMISSIONS_ENV: &str = "BUZZ_ACP_CODEX_FILESYSTEM_PERMISSIONS"; +const BUZZ_CODEX_PERMISSION_PROFILE: &str = "buzz-agent"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum CodexFilesystemPathAccess { + Deny, + Write, +} + +impl CodexFilesystemPathAccess { + const fn as_codex_value(self) -> &'static str { + match self { + Self::Deny => "deny", + Self::Write => "write", + } + } +} + +fn codex_filesystem_config( + path_permissions: std::collections::BTreeMap, + workspace_root: Option<&std::path::Path>, +) -> serde_json::Map { + use std::path::Path; + + let mut workspace_rules = serde_json::Map::from_iter([( + ".".to_string(), + serde_json::Value::String("write".to_string()), + )]); + let mut absolute_permissions = std::collections::BTreeMap::new(); + for (path, access) in path_permissions { + let relative = workspace_root + .and_then(|root| Path::new(&path).strip_prefix(root).ok()) + .filter(|relative| !relative.as_os_str().is_empty()); + if let Some(relative) = relative { + workspace_rules.insert( + relative.to_string_lossy().into_owned(), + serde_json::Value::String(access.as_codex_value().to_string()), + ); + } else { + absolute_permissions.insert(path, access); + } + } + + let mut filesystem = serde_json::Map::from_iter([ + ( + ":minimal".to_string(), + serde_json::Value::String("read".to_string()), + ), + ( + ":workspace_roots".to_string(), + serde_json::Value::Object(workspace_rules), + ), + ]); + for (path, access) in absolute_permissions { + filesystem.insert( + path, + serde_json::Value::String(access.as_codex_value().to_string()), + ); + } + filesystem +} + +/// Build the merged `CODEX_CONFIG` value for a Codex-backed Buzz agent. /// -/// # Errors +/// Persona, generated, and parent config objects are recursively merged in +/// that order. Buzz then installs and selects one canonical permission profile +/// that preserves workspace writes, denies private registry discovery, and +/// applies explicitly supplied World source paths. The generated network grant +/// is represented on that same profile rather than as a second sandbox +/// authority. /// -/// Returns `Err(AcpError::Protocol)` when `has_generated_codex_config` is true and any -/// `CODEX_CONFIG` value is not valid JSON or is not a JSON object, or when -/// `sandbox_workspace_write` is present but not an object after all merges. +/// The function returns `None` only when neither a generated Codex config nor +/// Buzz filesystem metadata is present. Invalid or empty metadata fails closed +/// with [`AcpError::Protocol`]. pub(crate) fn build_codex_config_env( extra_env: &[(String, String)], parent_codex_config: Option<&str>, has_generated_codex_config: bool, + workspace_root: Option<&std::path::Path>, ) -> Result, AcpError> { - // Without an explicit Buzz-generated overlay signal, skip the merge entirely. - // Any persona CODEX_CONFIG is handled by the caller with operator-wins semantics. - if !has_generated_codex_config { - return Ok(None); - } - - // Collect all CODEX_CONFIG entries from extra_env in order. - let codex_entries: Vec<&str> = extra_env + let filesystem_permission_entries: Vec<&str> = extra_env .iter() - .filter(|(k, _)| k == "CODEX_CONFIG") - .map(|(_, v)| v.as_str()) + .filter(|(key, _)| key == CODEX_FILESYSTEM_PERMISSIONS_ENV) + .map(|(_, value)| value.as_str()) .collect(); - - if codex_entries.is_empty() { - // has_generated_codex_config is true but no entry in extra_env — shouldn't - // happen in practice, but treat as no-op rather than panic. + let permissions_required = !filesystem_permission_entries.is_empty(); + if !has_generated_codex_config && !permissions_required { return Ok(None); } - // Parse all entries; first one is the persona base (or the generated entry if no - // persona CODEX_CONFIG was set), rest are additional generated entries. - let mut parsed_entries: Vec> = Vec::new(); - for (i, raw) in codex_entries.iter().enumerate() { - match serde_json::from_str::(raw) { - Ok(serde_json::Value::Object(obj)) => parsed_entries.push(obj), - Ok(_) => { - let source = if i == 0 { "persona" } else { "generated" }; - return Err(AcpError::Protocol(format!( - "CODEX_CONFIG {source} value is valid JSON but not an object" - ))); - } - Err(e) => { - let source = if i == 0 { "persona" } else { "generated" }; - return Err(AcpError::Protocol(format!( - "CODEX_CONFIG {source} value is not valid JSON: {e}" - ))); - } - } + let mut path_permissions = std::collections::BTreeMap::new(); + for raw in filesystem_permission_entries { + let permissions = serde_json::from_str::< + std::collections::BTreeMap, + >(raw) + .map_err(|error| { + AcpError::Protocol(format!( + "{CODEX_FILESYSTEM_PERMISSIONS_ENV} is not a JSON path-access object: {error}" + )) + })?; + path_permissions.extend(permissions); + } + path_permissions.retain(|path, _| !path.trim().is_empty()); + if path_permissions.is_empty() { + return Err(AcpError::Protocol(format!( + "{CODEX_FILESYSTEM_PERMISSIONS_ENV} must contain at least one path" + ))); } - // Start from first entry, deep-merge remaining entries. - let mut base = parsed_entries.remove(0); - for overlay in parsed_entries { - deep_merge(&mut base, overlay); + let codex_entries: Vec<&str> = extra_env + .iter() + .filter(|(key, _)| key == "CODEX_CONFIG") + .map(|(_, value)| value.as_str()) + .collect(); + let mut base = serde_json::Map::new(); + for (index, raw) in codex_entries.iter().enumerate() { + let parsed = serde_json::from_str::(raw).map_err(|error| { + let source = if index == 0 { "persona" } else { "generated" }; + AcpError::Protocol(format!( + "CODEX_CONFIG {source} value is not valid JSON: {error}" + )) + })?; + let serde_json::Value::Object(object) = parsed else { + let source = if index == 0 { "persona" } else { "generated" }; + return Err(AcpError::Protocol(format!( + "CODEX_CONFIG {source} value is valid JSON but not an object" + ))); + }; + deep_merge(&mut base, object); } - // Deep-merge parent env (parent wins on colliding keys at every nesting level). if let Some(parent_raw) = parent_codex_config { - match serde_json::from_str::(parent_raw) { - Ok(serde_json::Value::Object(parent_obj)) => { - deep_merge(&mut base, parent_obj); - } - Ok(_) => { - return Err(AcpError::Protocol( - "CODEX_CONFIG in parent environment is valid JSON but not an object".into(), - )); - } - Err(e) => { - return Err(AcpError::Protocol(format!( - "CODEX_CONFIG in parent environment is not valid JSON: {e}" - ))); - } - } + let parent = serde_json::from_str::(parent_raw).map_err(|error| { + AcpError::Protocol(format!( + "CODEX_CONFIG in parent environment is not valid JSON: {error}" + )) + })?; + let serde_json::Value::Object(parent_object) = parent else { + return Err(AcpError::Protocol( + "CODEX_CONFIG in parent environment is valid JSON but not an object".into(), + )); + }; + deep_merge(&mut base, parent_object); } - // Force sandbox_workspace_write.network_access = true (our invariant, always wins). - let sws_entry = base - .entry("sandbox_workspace_write") + // Permission profiles and the legacy sandbox settings do not compose. + // Network access and filesystem authority are represented below, so remove + // every older sandbox selector before selecting the Buzz profile. + base.remove("sandbox_mode"); + base.remove("sandbox_workspace_write"); + + let filesystem = codex_filesystem_config(path_permissions, workspace_root); + let permission_profile = serde_json::json!({ + "workspace_roots": { + ".": true, + }, + "filesystem": serde_json::Value::Object(filesystem), + "network": { + "enabled": has_generated_codex_config, + }, + }); + let permissions = base + .entry("permissions") .or_insert_with(|| serde_json::json!({})); - match sws_entry { - serde_json::Value::Object(sws_obj) => { - sws_obj.insert("network_access".to_string(), serde_json::Value::Bool(true)); - } - other => { - return Err(AcpError::Protocol(format!( - "CODEX_CONFIG sandbox_workspace_write is not an object (got {}); \ - cannot set network_access=true", - other - ))); - } - } + let serde_json::Value::Object(permissions) = permissions else { + return Err(AcpError::Protocol( + "CODEX_CONFIG permissions is not an object; cannot install Buzz permissions".into(), + )); + }; + permissions.insert( + BUZZ_CODEX_PERMISSION_PROFILE.to_string(), + permission_profile, + ); + base.insert( + "default_permissions".to_string(), + serde_json::Value::String(BUZZ_CODEX_PERMISSION_PROFILE.to_string()), + ); Ok(Some(serde_json::Value::Object(base).to_string())) } @@ -400,9 +469,8 @@ impl AcpClient { /// Spawn the agent binary as a subprocess and connect to its stdio pipes. /// /// `has_generated_codex_config` must be true when `codex_network_env()` successfully - /// injected a `CODEX_CONFIG` entry into `extra_env`. The spawn path uses it to - /// trigger the recursive merge + forced `network_access=true` in - /// `build_codex_config_env`. Pass `false` for test spawns and non-Codex agents. + /// injected a `CODEX_CONFIG` entry into `extra_env`. Buzz isolation metadata + /// independently activates the secure config merge for Codex agents. /// /// After spawning, call [`initialize`](Self::initialize) before any other method. pub async fn spawn( @@ -427,29 +495,31 @@ impl AcpClient { // For most keys, operator precedence wins: skip injection if already set // in the parent environment. // - // CODEX_CONFIG is handled specially via build_codex_config_env: - // • has_generated_codex_config=true: merge all CODEX_CONFIG entries + parent - // recursively and force network_access=true. - // • has_generated_codex_config=false: return None; any persona-supplied - // CODEX_CONFIG falls through to the normal operator-wins loop below. - let has_codex_config = extra_env.iter().any(|(k, _)| k == "CODEX_CONFIG"); - let parent_codex_config = if has_generated_codex_config && has_codex_config { + // CODEX_CONFIG and the private filesystem metadata are handled together. + // The metadata is consumed here and is never exposed to the child. + let codex_config_build_requested = has_generated_codex_config + || extra_env + .iter() + .any(|(key, _)| key == CODEX_FILESYSTEM_PERMISSIONS_ENV); + let parent_codex_config = if codex_config_build_requested { std::env::var("CODEX_CONFIG").ok() } else { None }; + let workspace_root = std::env::current_dir().ok(); let codex_config_value = build_codex_config_env( extra_env, parent_codex_config.as_deref(), has_generated_codex_config, + workspace_root.as_deref(), )?; - // When the merge path was not taken (None returned), any persona CODEX_CONFIG - // entry falls through to the standard operator-wins treatment below. let codex_merge_active = codex_config_value.is_some(); for (key, value) in extra_env { + if key == CODEX_FILESYSTEM_PERMISSIONS_ENV { + continue; + } if key == "CODEX_CONFIG" && codex_merge_active { - // Handled by build_codex_config_env; skip here to avoid double-setting. continue; } if std::env::var(key).is_err() { @@ -3589,230 +3659,209 @@ mod tests { fn env(pairs: &[(&str, &str)]) -> Vec<(String, String)> { pairs .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(key, value)| (key.to_string(), value.to_string())) .collect() } + fn permissioned_env(pairs: &[(&str, &str)]) -> Vec<(String, String)> { + let mut environment = env(pairs); + environment.push(( + CODEX_FILESYSTEM_PERMISSIONS_ENV.to_string(), + r#"{ + "/private/world-authorities.json":"deny", + "/private/world-authority-secrets":"deny", + "/worlds/delivery.world":"write" + }"# + .to_string(), + )); + environment + } + const GENERATED: &str = r#"{"sandbox_workspace_write":{"network_access":true}}"#; #[test] - fn build_codex_config_env_returns_none_when_no_codex_config_in_extra_env() { - // Non-Codex agents: extra_env has no CODEX_CONFIG → None regardless of signal. + fn build_codex_config_env_returns_none_without_codex_or_filesystem_config() { let extra = env(&[("GOOSE_PROVIDER", "openai")]); - let result = build_codex_config_env(&extra, None, false).unwrap(); assert_eq!( - result, None, - "no CODEX_CONFIG in extra_env must return None" + build_codex_config_env(&extra, None, false, None).unwrap(), + None ); } #[test] - fn build_codex_config_env_generated_only_single_entry_with_signal_true_merges_with_parent() { - // No persona: Buzz injects one CODEX_CONFIG; signal=true. - // Parent may have its own CODEX_CONFIG — deep_merge applies, network_access forced. + fn build_codex_config_env_requires_filesystem_metadata_for_generated_config() { let extra = env(&[("CODEX_CONFIG", GENERATED)]); - let parent = - r#"{"some_operator_key":"val","sandbox_workspace_write":{"operator_key":"keep"}}"#; - let merged = build_codex_config_env(&extra, Some(parent), true) - .unwrap() - .unwrap(); - let v: serde_json::Value = serde_json::from_str(&merged).unwrap(); - // network_access forced true even though only one entry in extra_env. + let error = build_codex_config_env(&extra, None, true, None).unwrap_err(); + assert!(error.to_string().contains(CODEX_FILESYSTEM_PERMISSIONS_ENV)); + } + + #[test] + fn build_codex_config_env_installs_world_source_paths_and_network_grant() { + let persona = r#"{"persona":{"enabled":true},"shared":"persona"}"#; + let extra = permissioned_env(&[("CODEX_CONFIG", persona), ("CODEX_CONFIG", GENERATED)]); + let parent = r#"{"operator":"keep","shared":"parent"}"#; + let merged = build_codex_config_env( + &extra, + Some(parent), + true, + Some(std::path::Path::new("/private")), + ) + .unwrap() + .unwrap(); + let value: serde_json::Value = serde_json::from_str(&merged).unwrap(); + + assert_eq!(value["persona"]["enabled"], true); + assert_eq!(value["operator"], "keep"); + assert_eq!(value["shared"], "parent"); + assert_eq!(value["default_permissions"], "buzz-agent"); assert_eq!( - v["sandbox_workspace_write"]["network_access"], true, - "network_access must be forced true with signal=true" + value["permissions"]["buzz-agent"]["workspace_roots"]["."], + true ); - // Operator key preserved via deep_merge. assert_eq!( - v["sandbox_workspace_write"]["operator_key"], "keep", - "operator nested key must survive" + value["permissions"]["buzz-agent"]["filesystem"][":minimal"], + "read" ); assert_eq!( - v["some_operator_key"], "val", - "operator top-level key must survive" + value["permissions"]["buzz-agent"]["filesystem"][":workspace_roots"]["."], + "write" ); - } - - #[test] - fn build_codex_config_env_persona_only_signal_false_returns_none() { - // Persona set CODEX_CONFIG; Buzz did not inject a generated overlay (signal=false). - // Must return None — no merging, no sandbox widening. - let persona = r#"{"some_feature":"on"}"#; - let extra = env(&[("CODEX_CONFIG", persona)]); - let result = build_codex_config_env(&extra, None, false).unwrap(); assert_eq!( - result, None, - "persona-only CODEX_CONFIG with signal=false must return None" + value["permissions"]["buzz-agent"]["filesystem"][":workspace_roots"] + ["world-authorities.json"], + "deny" ); - } - - #[test] - fn build_codex_config_env_returns_none_for_persona_only_no_generated_overlay() { - // Alias: same scenario as above, confirms the old count-based path no longer exists. - let persona = r#"{"some_feature":"on"}"#; - let extra = env(&[("CODEX_CONFIG", persona)]); - let result = build_codex_config_env(&extra, None, false).unwrap(); assert_eq!( - result, None, - "persona-only CODEX_CONFIG with signal=false must return None" + value["permissions"]["buzz-agent"]["filesystem"][":workspace_roots"] + ["world-authority-secrets"], + "deny" ); - } - - #[test] - fn build_codex_config_env_sets_network_access_from_scratch() { - // Persona + generated overlay, signal=true: network_access is forced true. - let persona = r#"{}"#; - let extra = env(&[("CODEX_CONFIG", persona), ("CODEX_CONFIG", GENERATED)]); - let merged = build_codex_config_env(&extra, None, true).unwrap().unwrap(); - let v: serde_json::Value = serde_json::from_str(&merged).unwrap(); - assert_eq!(v["sandbox_workspace_write"]["network_access"], true); - } - - #[test] - fn build_codex_config_env_persona_keys_survive_merge() { - // Persona has CODEX_CONFIG with unrelated keys; generated overlay must - // force network_access=true without erasing persona keys. - let persona_cfg = r#"{"some_feature":{"enabled":true}}"#; - // Config::from_args appends generated AFTER persona env vars. - let extra = env(&[("CODEX_CONFIG", persona_cfg), ("CODEX_CONFIG", GENERATED)]); - let merged = build_codex_config_env(&extra, None, true).unwrap().unwrap(); - let v: serde_json::Value = serde_json::from_str(&merged).unwrap(); assert_eq!( - v["some_feature"]["enabled"], true, - "persona key must survive merge" + value["permissions"]["buzz-agent"]["filesystem"]["/worlds/delivery.world"], + "write" ); assert_eq!( - v["sandbox_workspace_write"]["network_access"], true, - "network_access must be forced true" + value["permissions"]["buzz-agent"]["network"]["enabled"], + true ); + assert!(value.get("sandbox_workspace_write").is_none()); } #[test] - fn build_codex_config_env_nested_persona_keys_survive_when_parent_has_same_top_level_key() { - // Persona has sandbox_workspace_write.persona_only; parent has - // sandbox_workspace_write.parent_only. A flat top-level spread would drop - // persona_only. deep_merge must preserve both nested keys, and - // network_access must be forced true last. - let persona_cfg = r#"{"sandbox_workspace_write":{"persona_only":"keep_me"}}"#; - let extra = env(&[("CODEX_CONFIG", persona_cfg), ("CODEX_CONFIG", GENERATED)]); - let parent = r#"{"sandbox_workspace_write":{"parent_only":"also_here"}}"#; - let merged = build_codex_config_env(&extra, Some(parent), true) + fn build_codex_config_env_permissions_apply_without_generated_network_grant() { + let extra = permissioned_env(&[("CODEX_CONFIG", r#"{"persona":"keep"}"#)]); + let merged = build_codex_config_env(&extra, None, false, None) .unwrap() .unwrap(); - let v: serde_json::Value = serde_json::from_str(&merged).unwrap(); - // Both nested keys survive — no flat-spread drop. - assert_eq!( - v["sandbox_workspace_write"]["persona_only"], "keep_me", - "nested persona key must survive when parent has the same top-level key" - ); - assert_eq!( - v["sandbox_workspace_write"]["parent_only"], "also_here", - "nested parent key must be present" - ); - // Forced last. + let value: serde_json::Value = serde_json::from_str(&merged).unwrap(); + + assert_eq!(value["persona"], "keep"); + assert_eq!(value["default_permissions"], "buzz-agent"); assert_eq!( - v["sandbox_workspace_write"]["network_access"], true, - "network_access must be forced true" + value["permissions"]["buzz-agent"]["network"]["enabled"], + false ); } #[test] - fn build_codex_config_env_parent_env_wins_on_collisions_persona_keys_survive() { - // Parent env has CODEX_CONFIG with some keys; persona has different keys. - // Parent wins on collision; unrelated persona keys survive. - // network_access is always forced true. - let persona_cfg = r#"{"persona_key":"persona_val","shared_key":"persona_version"}"#; - // Config::from_args appends generated AFTER persona env vars. - let extra = env(&[("CODEX_CONFIG", persona_cfg), ("CODEX_CONFIG", GENERATED)]); - let parent = r#"{"parent_key":"parent_val","shared_key":"parent_version"}"#; - let merged = build_codex_config_env(&extra, Some(parent), true) + fn build_codex_config_env_forces_buzz_profile_over_parent_permissions() { + let extra = permissioned_env(&[("CODEX_CONFIG", GENERATED)]); + let parent = r#"{ + "sandbox_mode":"danger-full-access", + "sandbox_workspace_write":{"writable_roots":["/unsafe"]}, + "default_permissions":"operator-profile", + "permissions":{ + "buzz-agent":{ + "filesystem":{"/private/world-authorities.json":"write"}, + "network":{"enabled":false} + }, + "operator-profile":{"filesystem":{":root":"write"}} + } + }"#; + let merged = build_codex_config_env(&extra, Some(parent), true, None) .unwrap() .unwrap(); - let v: serde_json::Value = serde_json::from_str(&merged).unwrap(); - // Parent-only key present + let value: serde_json::Value = serde_json::from_str(&merged).unwrap(); + + assert_eq!(value["default_permissions"], "buzz-agent"); assert_eq!( - v["parent_key"], "parent_val", - "parent-only key must be present" + value["permissions"]["buzz-agent"]["workspace_roots"]["."], + true ); - // Unrelated persona key survives (no collision with parent) assert_eq!( - v["persona_key"], "persona_val", - "unrelated persona key must survive" + value["permissions"]["buzz-agent"]["filesystem"]["/private/world-authorities.json"], + "deny" ); - // Collision: parent wins assert_eq!( - v["shared_key"], "parent_version", - "parent must win on colliding key" + value["permissions"]["operator-profile"]["filesystem"][":root"], + "write" ); - // network_access always true (forced last) - assert_eq!(v["sandbox_workspace_write"]["network_access"], true); + assert!(value.get("sandbox_mode").is_none()); + assert!(value.get("sandbox_workspace_write").is_none()); } #[test] - fn build_codex_config_env_parent_has_existing_sandbox_other_keys_survive() { - // Parent env has sandbox_workspace_write with extra keys; after merge - // those extra keys survive alongside network_access=true. - let persona = r#"{}"#; - let extra = env(&[("CODEX_CONFIG", persona), ("CODEX_CONFIG", GENERATED)]); - let parent = - r#"{"sandbox_workspace_write":{"network_access":false,"other_sandbox_key":"val"}}"#; - let merged = build_codex_config_env(&extra, Some(parent), true) + fn build_codex_config_env_does_not_promote_legacy_world_root_metadata() { + let extra = permissioned_env(&[ + ("CODEX_CONFIG", GENERATED), + ( + "BUZZ_ACP_LOCAL_WORLD_WRITABLE_ROOTS", + r#"["/worlds/legacy.world"]"#, + ), + ]); + let merged = build_codex_config_env(&extra, None, true, None) .unwrap() .unwrap(); - let v: serde_json::Value = serde_json::from_str(&merged).unwrap(); - // network_access forced true even though parent set false - assert_eq!(v["sandbox_workspace_write"]["network_access"], true); - // other_sandbox_key survives (parent's sws merged, then network_access forced) - assert_eq!(v["sandbox_workspace_write"]["other_sandbox_key"], "val"); - } - #[test] - fn build_codex_config_env_errors_on_invalid_persona_json() { - // Bad persona JSON + generated overlay, signal=true → parse error before merging. - let extra = env(&[("CODEX_CONFIG", "not-json"), ("CODEX_CONFIG", GENERATED)]); - let result = build_codex_config_env(&extra, None, true); - assert!(result.is_err(), "invalid persona JSON must return Err"); - let msg = format!("{}", result.unwrap_err()); - assert!( - msg.contains("CODEX_CONFIG"), - "error must mention CODEX_CONFIG" - ); + assert!(!merged.contains("/worlds/legacy.world")); } #[test] - fn build_codex_config_env_errors_on_non_object_persona_json() { - // Non-object persona JSON + generated overlay, signal=true → parse error. - let extra = env(&[("CODEX_CONFIG", "[1,2,3]"), ("CODEX_CONFIG", GENERATED)]); - let result = build_codex_config_env(&extra, None, true); - assert!(result.is_err(), "non-object persona JSON must return Err"); + fn build_codex_config_env_rejects_invalid_filesystem_metadata() { + let extra = env(&[ + ("CODEX_CONFIG", GENERATED), + (CODEX_FILESYSTEM_PERMISSIONS_ENV, "not-json"), + ]); + let error = build_codex_config_env(&extra, None, true, None).unwrap_err(); + + assert!(error.to_string().contains(CODEX_FILESYSTEM_PERMISSIONS_ENV)); } #[test] - fn build_codex_config_env_errors_on_invalid_parent_json() { - let persona = r#"{}"#; - let extra = env(&[("CODEX_CONFIG", persona), ("CODEX_CONFIG", GENERATED)]); - let result = build_codex_config_env(&extra, Some("bad-json"), true); - assert!(result.is_err(), "invalid parent env JSON must return Err"); + fn build_codex_config_env_rejects_invalid_config_objects() { + let invalid_persona = + permissioned_env(&[("CODEX_CONFIG", "not-json"), ("CODEX_CONFIG", GENERATED)]); + assert!(build_codex_config_env(&invalid_persona, None, true, None).is_err()); + + let non_object_persona = + permissioned_env(&[("CODEX_CONFIG", "[1,2,3]"), ("CODEX_CONFIG", GENERATED)]); + assert!(build_codex_config_env(&non_object_persona, None, true, None).is_err()); + + let valid = permissioned_env(&[("CODEX_CONFIG", GENERATED)]); + assert!(build_codex_config_env(&valid, Some("bad-json"), true, None).is_err()); + let error = + build_codex_config_env(&valid, Some(r#"{"permissions":42}"#), true, None).unwrap_err(); + assert!(error.to_string().contains("permissions")); } - #[test] - fn build_codex_config_env_errors_on_non_object_sandbox_workspace_write() { - // sandbox_workspace_write must be an object for network_access forcing. - // If the parent env sets it to a non-object scalar, deep_merge replaces - // our object with the scalar, and the force step must fail clearly. - let persona = r#"{}"#; - let extra = env(&[("CODEX_CONFIG", persona), ("CODEX_CONFIG", GENERATED)]); - // Parent replaces the object with a scalar — deep_merge: scalar overlay wins. - let parent = r#"{"sandbox_workspace_write": 42}"#; - let result = build_codex_config_env(&extra, Some(parent), true); - assert!( - result.is_err(), - "non-object sandbox_workspace_write must return Err" - ); - let msg = format!("{}", result.unwrap_err()); - assert!( - msg.contains("sandbox_workspace_write"), - "error must mention sandbox_workspace_write" - ); + #[cfg(unix)] + #[tokio::test] + async fn spawn_consumes_filesystem_metadata_instead_of_forwarding_it() { + let extra = permissioned_env(&[]); + let mut client = AcpClient::spawn( + "sh", + &[ + "-c".to_string(), + format!(r#"printf '%s\n' "${{{CODEX_FILESYSTEM_PERMISSIONS_ENV}-not-forwarded}}""#), + ], + &extra, + false, + ) + .await + .unwrap(); + + let line = client.reader.next().await.unwrap().unwrap(); + assert_eq!(line, "not-forwarded"); + client.shutdown().await; } } diff --git a/crates/buzz-acp/src/base_prompt.md b/crates/buzz-acp/src/base_prompt.md index c42e65cb83..16355be53e 100644 --- a/crates/buzz-acp/src/base_prompt.md +++ b/crates/buzz-acp/src/base_prompt.md @@ -10,6 +10,7 @@ The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ | `buzz messages` | `send`, `get`, `thread`, `search` | | `buzz channels` | `list`, `get`, `create`, `join`, `members` | | `buzz canvas` | `get`, `set` | +| `buzz world-views` | `sources`, `catalog`, `get`, `bind`, `resolve`, `script`, `set` | | `buzz reactions` | `add`, `remove` | | `buzz dms` | `list`, `open` | | `buzz users` | `get`, `set-profile`, `presence` | @@ -22,6 +23,9 @@ The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ Run `buzz --help` or `buzz --help` for full usage. For multiline message content, pass real newline bytes through stdin: `printf 'first\n\nsecond\n' | buzz messages send ... --content -`. Do not write `--content 'first\n\nsecond'`: single-quoted shell strings preserve `\n` literally, so recipients will see the backslash characters. `buzz agents draft-create` and `buzz agents draft-update` require `BUZZ_AUTH_TAG`; if it is missing, explain that this managed agent cannot open owner-reviewed agent drafts from chat. +For a new Shivai binding, use `buzz world-views sources`, inspect one source with `buzz world-views catalog`, read the scope revision with `buzz world-views get`, then use `buzz world-views bind` with that exact revision. Binding a connected hosted world mints or reuses a stable public live-view capability; its private edit authority stays machine-local. +For a mutable Shivai binding, use only the exact scoped `buzz world-views script` command in `[Shivai World Views]`. If no mutation command is present, the binding is read-only for this agent. Buzz resolves machine-local authority behind the scoped command. Never ask for, inspect, print, or pass a local world path, edit-share token, credential file, or secret directory, and never invoke `world` mutation commands directly. + When opening a pull request in response to channel work, always pass `--channel ` using the UUID from `[Context]`. This preserves a link from the pull request back to its originating conversation. ## Conversational Agent Creation diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index b11d96d8f7..35e2d22bf0 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -1760,7 +1760,7 @@ async fn tokio_main() -> Result<()> { tracing::info!(agent = idx, "slot refill: spawning background respawn"); let cmd = config.agent_command.clone(); let args = config.agent_args.clone(); - let env = config.persona_env_vars.clone(); + let env = agent_spawn_environment(&config); let has_codex = config.has_generated_codex_config; let observer = observer.clone(); let guard = RespawnGuard::new(idx, respawn_tx.clone()); @@ -3486,7 +3486,7 @@ fn recover_panicked_agent( slot.respawn_in_flight = true; let cmd = config.agent_command.clone(); let args = config.agent_args.clone(); - let env = config.persona_env_vars.clone(); + let env = agent_spawn_environment(config); let has_codex = config.has_generated_codex_config; let guard = RespawnGuard::new(i, respawn_tx.clone()); respawn_tasks.spawn(async move { @@ -3664,7 +3664,7 @@ fn spawn_respawn_task( // Spawn the actual work (shutdown + sleep + spawn + init) off the main loop. let cmd = config.agent_command.clone(); let args = config.agent_args.clone(); - let env = config.persona_env_vars.clone(); + let env = agent_spawn_environment(config); let has_codex = config.has_generated_codex_config; let guard = RespawnGuard::new(index, respawn_tx.clone()); respawn_tasks.spawn(async move { @@ -3715,6 +3715,26 @@ async fn shutdown_agent_pool(pool: &mut AgentPool) { } } +fn agent_spawn_environment(config: &Config) -> Vec<(String, String)> { + let mut environment = config.persona_env_vars.clone(); + environment.retain(|(key, _)| key != acp::CODEX_FILESYSTEM_PERMISSIONS_ENV); + if matches!( + config::normalize_agent_command_identity(&config.agent_command).as_str(), + "codex" | "codex-acp" + ) { + let filesystem_permissions = std::env::current_dir() + .ok() + .and_then(|cwd| cwd.to_str().map(pool::world_agent_filesystem_permissions)) + .unwrap_or_default(); + environment.push(( + acp::CODEX_FILESYSTEM_PERMISSIONS_ENV.to_string(), + serde_json::to_string(&filesystem_permissions) + .expect("World filesystem permission metadata is serializable"), + )); + } + environment +} + struct PoolStartup { agents: u32, command: String, @@ -3731,7 +3751,7 @@ impl PoolStartup { agents: config.agents, command: config.agent_command.clone(), args: config.agent_args.clone(), - extra_env: config.persona_env_vars.clone(), + extra_env: agent_spawn_environment(config), has_generated_codex_config: config.has_generated_codex_config, model: config.model.clone(), observer, diff --git a/crates/buzz-acp/src/pool.rs b/crates/buzz-acp/src/pool.rs index 0c51fe954f..2afa1befc9 100644 --- a/crates/buzz-acp/src/pool.rs +++ b/crates/buzz-acp/src/pool.rs @@ -1467,33 +1467,36 @@ pub async fn run_prompt_task( } } - // Canvas metadata fetch — same lifecycle as core: once per new channel session, - // never for heartbeats, cached until session invalidation. - // - // DM check: use startup channel_info first; lazy-fetch only when missing. - // A confirmed DM never receives a canvas section. If the channel type cannot - // be determined (metadata absent and lazy fetch fails/unknown), skip the canvas - // rather than assuming non-DM — failing closed on DM ambiguity is safer. - // - // I3 lifecycle: hold the fetched section in a local `pending_canvas` and - // commit it to `canvas_sections` only after session creation succeeds. This - // prevents a stale revision A surviving a failed create and being re-used by - // the next attempt after the canvas was cleared. + // Canvas is stable session narrative, but world bindings are live operational + // context. Cache only Canvas; fetch and render world bindings on every channel + // prompt so agents see newly bound views and current local edit authority. let mut pending_canvas: Option<(Uuid, String)> = None; - // Channel name for the session title, from the same single resolve the - // canvas DM check uses — see `resolve_new_session_channel_context`. + let mut current_world_views: Option = None; + // Channel name for the session title, from the same single resolve used by + // the canvas and live world-view DM gates. let mut title_channel: Option = None; if let PromptSource::Channel(cid) = &source { let is_new_channel_session = !agent.state.sessions.contains_key(cid); let needs_canvas = is_new_channel_session && !agent.state.canvas_sections.contains_key(cid); let needs_title = is_new_channel_session && ctx.session_title.is_some(); - if needs_canvas || needs_title { - let (is_dm, resolved_channel) = - resolve_new_session_channel_context(&ctx.channel_info, *cid).await; + let (is_dm, resolved_channel) = + resolve_new_session_channel_context(&ctx.channel_info, *cid).await; + if needs_title { title_channel = resolved_channel; - // A confirmed DM never receives a canvas section; an undeterminable - // channel type fails closed as a DM for the same reason. - if needs_canvas && !is_dm { + } + if !is_dm { + let authorities = load_world_authority_registry(&ctx.cwd); + let agent_pubkey = ctx.agent_keys.public_key().to_hex(); + let thread_root_event_id = batch.as_ref().and_then(world_view_thread_root); + current_world_views = fetch_world_view_bindings_section( + *cid, + thread_root_event_id.as_deref(), + &ctx.rest_client, + &authorities, + &agent_pubkey, + ) + .await; + if needs_canvas { if let Some(section) = fetch_canvas_section(*cid, &ctx.rest_client).await { pending_canvas = Some((*cid, section)); } @@ -1508,15 +1511,15 @@ pub async fn run_prompt_task( PromptSource::Heartbeat => None, }; - // The canvas metadata section — channel-scoped, absent for heartbeats/DMs. - // Prefer the committed cache; fall back to pending (for new sessions being created now). + // Canvas is session narrative. Keep live world-view state separate so it + // rides in every user turn rather than freezing in session/new. let agent_canvas: Option = match &source { PromptSource::Channel(cid) => agent .state .canvas_sections .get(cid) .cloned() - .or_else(|| pending_canvas.as_ref().map(|(_, s)| s.clone())), + .or_else(|| pending_canvas.as_ref().map(|(_, section)| section.clone())), PromptSource::Heartbeat => None, }; @@ -1839,6 +1842,7 @@ pub async fn run_prompt_task( system_prompt: ctx.system_prompt.as_deref(), team_instructions: ctx.team_instructions.as_deref(), agent_canvas: agent_canvas.as_deref(), + agent_world_views: current_world_views.as_deref(), }, ) } else { @@ -2363,55 +2367,414 @@ pub(crate) async fn fetch_channel_info( /// /// Called at most once per new channel session; the result is cached in /// `SessionState::canvas_sections` and cleared on session invalidation. -async fn fetch_canvas_section(channel_id: Uuid, rest: &RestClient) -> Option { +async fn fetch_channel_context_events( + channel_id: Uuid, + rest: &RestClient, + kind: u32, + surface: &'static str, + d_tag: Option<&str>, +) -> Option> { use nostr::{Alphabet, SingleLetterTag}; let h_tag = SingleLetterTag::lowercase(Alphabet::H); - let filter = nostr::Filter::new() - .kind(nostr::Kind::Custom(buzz_core::kind::KIND_CANVAS as u16)) + let mut filter = nostr::Filter::new() + .kind(nostr::Kind::Custom(kind as u16)) .custom_tags(h_tag, [channel_id.to_string()]) .limit(1); - - const CANVAS_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); + if let Some(d_tag) = d_tag { + let d_tag_key = SingleLetterTag::lowercase(Alphabet::D); + filter = filter.custom_tags(d_tag_key, [d_tag]); + } + const CHANNEL_CONTEXT_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); let json = match tokio::time::timeout( - CANVAS_FETCH_TIMEOUT, + CHANNEL_CONTEXT_FETCH_TIMEOUT, rest.query(std::slice::from_ref(&filter)), ) .await { - Ok(Ok(v)) => v, - Ok(Err(e)) => { + Ok(Ok(value)) => value, + Ok(Err(error)) => { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_id, - "canvas query failed: {e} — emitting no section" + surface, + "channel context query failed: {error}", ); return None; } Err(_) => { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_id, - timeout_ms = CANVAS_FETCH_TIMEOUT.as_millis() as u64, - "canvas fetch timed out — emitting no section" + surface, + timeout_ms = CHANNEL_CONTEXT_FETCH_TIMEOUT.as_millis() as u64, + "channel context fetch timed out", ); return None; } }; - - let events = match json.as_array() { - Some(arr) => arr, + match json.as_array() { + Some(events) => Some(events.clone()), None => { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_id, - "canvas query response is not a JSON array — emitting no section" + surface, + "channel context query response is not a JSON array", ); - return None; + None + } + } +} + +async fn fetch_canvas_section(channel_id: Uuid, rest: &RestClient) -> Option { + let events = fetch_channel_context_events( + channel_id, + rest, + buzz_core::kind::KIND_CANVAS, + "canvas", + None, + ) + .await?; + canvas_section_from_query_response(&events, &channel_id.to_string()) +} + +fn load_world_authority_registry(cwd: &str) -> buzz_core::world_view::WorldAuthorityRegistry { + use buzz_core::world_view::{ + decode_world_authority_registry, WorldAuthorityRegistry, WORLD_AUTHORITY_REGISTRY_FILE_NAME, + }; + + let path = std::path::Path::new(cwd).join(WORLD_AUTHORITY_REGISTRY_FILE_NAME); + let text = match std::fs::read_to_string(&path) { + Ok(text) => text, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return WorldAuthorityRegistry::default(); + } + Err(error) => { + tracing::warn!( + target: "channel_context::world_authority", + path = %path.display(), + %error, + "failed to read world authority registry", + ); + return WorldAuthorityRegistry::default(); + } + }; + let value = match serde_json::from_str(&text) { + Ok(value) => value, + Err(error) => { + tracing::warn!( + target: "channel_context::world_authority", + path = %path.display(), + %error, + "world authority registry contains invalid JSON", + ); + return WorldAuthorityRegistry::default(); + } + }; + let decoded = match decode_world_authority_registry(value) { + Ok(decoded) => decoded, + Err(error) => { + tracing::warn!( + target: "channel_context::world_authority", + path = %path.display(), + %error, + "world authority registry failed validation", + ); + return WorldAuthorityRegistry::default(); + } + }; + if decoded.migrated { + let mut bytes = match serde_json::to_vec_pretty(&decoded.registry) { + Ok(bytes) => bytes, + Err(error) => { + tracing::warn!( + target: "channel_context::world_authority", + path = %path.display(), + %error, + "failed to encode migrated world authority registry", + ); + return WorldAuthorityRegistry::default(); + } + }; + bytes.push(b'\n'); + if let Err(error) = std::fs::write(&path, bytes) { + tracing::warn!( + target: "channel_context::world_authority", + path = %path.display(), + %error, + "failed to persist migrated world authority registry", + ); + return WorldAuthorityRegistry::default(); + } + } + decoded.registry +} + +/// Filesystem authority exposed to a Codex-backed Buzz agent's local-exec +/// sandbox. +/// +/// Private registry, capability secrets, and mutable local world roots are +/// never exposed. All world mutations cross the same scoped Buzz command +/// broker, regardless of whether the underlying authority is local or hosted. +pub(crate) fn world_agent_filesystem_permissions( + cwd: &str, +) -> std::collections::BTreeMap { + use buzz_core::world_view::{ + WORLD_AUTHORITY_REGISTRY_FILE_NAME, WORLD_AUTHORITY_SECRET_DIRECTORY, + }; + use std::path::Path; + + let root = Path::new(cwd); + if !root.is_absolute() { + tracing::warn!( + target: "channel_context::world_authority", + %cwd, + "cannot scope world authority paths from a non-absolute agent working directory", + ); + return std::collections::BTreeMap::new(); + } + + let mut permissions = std::collections::BTreeMap::new(); + + // Insert exact private discovery paths last so a malformed registry cannot + // promote either path through an identical authority entry. + permissions.insert( + root.join(WORLD_AUTHORITY_REGISTRY_FILE_NAME) + .to_string_lossy() + .into_owned(), + crate::acp::CodexFilesystemPathAccess::Deny, + ); + permissions.insert( + root.join(WORLD_AUTHORITY_SECRET_DIRECTORY) + .to_string_lossy() + .into_owned(), + crate::acp::CodexFilesystemPathAccess::Deny, + ); + permissions +} + +fn world_view_thread_root(batch: &FlushBatch) -> Option { + let event = &batch.events.last()?.event; + let thread_tags = crate::queue::parse_thread_tags(event); + thread_tags.root_event_id.or_else(|| { + (event.kind.as_u16() as u32 == buzz_core::kind::KIND_FORUM_POST).then(|| event.id.to_hex()) + }) +} + +async fn fetch_world_view_bindings_state( + channel_id: Uuid, + rest: &RestClient, + scope: &buzz_core::world_view::WorldViewBindingScope, +) -> Result, ()> { + let d_tag = scope.d_tag(); + let events = fetch_channel_context_events( + channel_id, + rest, + buzz_core::kind::KIND_WORLD_VIEW_BINDINGS, + "world-views", + Some(&d_tag), + ) + .await + .ok_or(())?; + if events.is_empty() { + return Ok(None); + } + world_view_bindings_state_from_query_response(&events, &channel_id.to_string(), scope) + .map(Some) + .ok_or(()) +} + +async fn fetch_world_view_bindings_section( + channel_id: Uuid, + thread_root_event_id: Option<&str>, + rest: &RestClient, + authorities: &buzz_core::world_view::WorldAuthorityRegistry, + agent_pubkey: &str, +) -> Option { + use buzz_core::world_view::{ + effective_world_view_bindings, WorldViewBindingScope, WorldViewBindingsSnapshot, + }; + + let channel_scope = WorldViewBindingScope::Channel; + let channel_state = fetch_world_view_bindings_state(channel_id, rest, &channel_scope) + .await + .ok()?; + let effective_scope = match thread_root_event_id { + Some(event_id) => WorldViewBindingScope::thread(event_id).ok()?, + None => WorldViewBindingScope::Channel, + }; + let thread_state = match &effective_scope { + WorldViewBindingScope::Channel => None, + WorldViewBindingScope::Thread { .. } => { + fetch_world_view_bindings_state(channel_id, rest, &effective_scope) + .await + .ok()? } }; + let channel_snapshot = channel_state + .as_ref() + .map(|state| state.snapshot.clone()) + .unwrap_or_else(|| WorldViewBindingsSnapshot::empty(channel_scope)); + let thread_snapshot = match &effective_scope { + WorldViewBindingScope::Channel => None, + WorldViewBindingScope::Thread { .. } => Some( + thread_state + .as_ref() + .map(|state| state.snapshot.clone()) + .unwrap_or_else(|| WorldViewBindingsSnapshot::empty(effective_scope.clone())), + ), + }; + let effective = + effective_world_view_bindings(&channel_snapshot, thread_snapshot.as_ref()).ok()?; + if effective.bindings.is_empty() { + return None; + } + let timestamp = [channel_state.as_ref(), thread_state.as_ref()] + .into_iter() + .flatten() + .max_by_key(|state| state.snapshot.updated_at) + .map(|state| state.timestamp.clone()) + .expect("effective bindings have at least one source event"); + let state = EffectiveWorldViewBindingsPromptState { + effective, + timestamp, + channel_uuid: channel_id.to_string(), + }; + let resolutions = + futures_util::future::join_all(state.effective.bindings.iter().map(|entry| { + let request = buzz_world_view_resolver::WorldViewResolutionRequest { + channel_id, + binding: entry.binding.clone(), + declared_scope: entry.declared_scope.clone(), + effective_scope: state.effective.effective_scope.clone(), + binding_revision_event_id: entry.binding_revision_event_id.clone(), + }; + async move { + let binding_id = request.binding.id; + let resolution = buzz_world_view_resolver::resolve_world_view(request, authorities) + .await + .map_err(|error| error.to_string()); + (binding_id, resolution) + } + })) + .await; + let authority_grants = + issue_world_authority_grants(channel_id, agent_pubkey, &state, authorities, &resolutions); + Some(render_world_view_bindings_section( + &state, + authorities, + &resolutions, + &authority_grants, + )) +} - canvas_section_from_query_response(events, &channel_id.to_string()) +fn issue_world_authority_grants( + channel_id: Uuid, + agent_pubkey: &str, + state: &EffectiveWorldViewBindingsPromptState, + authorities: &buzz_core::world_view::WorldAuthorityRegistry, + resolutions: &[( + Uuid, + Result, + )], +) -> std::collections::HashMap { + use buzz_core::world_view::{ + issue_world_authority_grant, WorldAuthorityGrantScope, WorldMutationAuthority, + WORLD_AUTHORITY_GRANT_TTL_SECONDS, + }; + use buzz_world_view_resolver::WorldViewResolutionAuthority; + use zeroize::Zeroizing; + + let mut grants = std::collections::HashMap::with_capacity(resolutions.len()); + let expires_at_unix_seconds = + chrono::Utc::now().timestamp() + WORLD_AUTHORITY_GRANT_TTL_SECONDS; + for entry in &state.effective.bindings { + let Some(Ok(resolved)) = resolutions + .iter() + .find(|(binding_id, _)| binding_id == &entry.binding.id) + .map(|(_, resolution)| resolution) + else { + continue; + }; + let mutation_authority = match &resolved.authority { + WorldViewResolutionAuthority::LocalWorldMirrorLatest { origin, mirror_id } => { + WorldMutationAuthority::LocalWorldMirrorLatest { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + } + } + WorldViewResolutionAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } + | WorldViewResolutionAuthority::HostedWorldLiveViewShare { + origin, + hosted_world_id, + } => WorldMutationAuthority::HostedWorldLatest { + origin: origin.clone(), + hosted_world_id: hosted_world_id.clone(), + }, + WorldViewResolutionAuthority::HostedWorldViewExport { .. } => continue, + }; + let Some(delegation) = authorities.resolve_mutation_delegation( + channel_id, + &entry.declared_scope, + entry.binding.id, + &entry.binding_revision_event_id, + ) else { + continue; + }; + if delegation.authority != mutation_authority { + continue; + } + let secret_file = match &mutation_authority { + WorldMutationAuthority::LocalWorldMirrorLatest { origin, mirror_id } => authorities + .resolve_local(origin, mirror_id) + .map(|authority| authority.capability_secret_file.as_str()), + WorldMutationAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } => authorities + .resolve_hosted(origin, hosted_world_id) + .map(|authority| authority.credential_file.as_str()), + }; + let Some(secret_file) = secret_file else { + continue; + }; + let authority_secret = match std::fs::read(secret_file) { + Ok(secret) => Zeroizing::new(secret), + Err(error) => { + tracing::warn!( + binding_id = %entry.binding.id, + error = %error, + "could not read private world authority while minting scoped grant" + ); + continue; + } + }; + let scope = WorldAuthorityGrantScope { + agent_pubkey: agent_pubkey.to_owned(), + channel_id, + effective_scope: state.effective.effective_scope.clone(), + binding_id: entry.binding.id, + binding_revision_event_id: entry.binding_revision_event_id.clone(), + source_revision: resolved.source_revision.clone(), + }; + match issue_world_authority_grant(&scope, &authority_secret, expires_at_unix_seconds) { + Ok(grant) => { + grants.insert(entry.binding.id, grant); + } + Err(error) => { + tracing::warn!( + binding_id = %entry.binding.id, + error = %error, + "could not mint scoped world authority grant" + ); + } + } + } + grants } /// Parse a canvas query response array and render a `[Channel Canvas]` section. @@ -2422,105 +2785,119 @@ async fn fetch_canvas_section(channel_id: Uuid, rest: &RestClient) -> Option Option { + expected_kind: u32, + surface: &'static str, +) -> Option { let raw = events.first()?; - - // Deserialise as a complete Nostr Event. Partial objects (missing pubkey, - // sig, kind, or tags) are rejected here rather than trusted implicitly. let event = match serde_json::from_value::(raw.clone()) { - Ok(ev) => ev, - Err(err) => { + Ok(event) => event, + Err(error) => { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_uuid, - %err, - "canvas query returned a malformed event — emitting no section", + surface, + %error, + "channel context query returned a malformed event", ); return None; } }; - - // Verify the event's id and signature agree with its content. - // A structurally complete but tampered event must not supply trusted metadata. - if let Err(err) = event.verify() { + if let Err(error) = event.verify() { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_uuid, - %err, - "canvas event failed signature verification — emitting no section", + surface, + %error, + "channel context event failed signature verification", ); return None; } - - // Validate kind: must be KIND_CANVAS (40100). - if event.kind != nostr::Kind::Custom(buzz_core::kind::KIND_CANVAS as u16) { + if event.kind != nostr::Kind::Custom(expected_kind as u16) { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_uuid, + surface, kind = %event.kind.as_u16(), - "canvas event has unexpected kind — emitting no section", + expected_kind, + "channel context event has unexpected kind", ); return None; } - - // Validate h-tag: must carry the channel UUID we queried. - // The REST boundary filters by #h, but we verify here to prevent a - // misbehaving relay from injecting a different channel's canvas. let h_tag_matches = event.tags.iter().any(|tag| { - let v = tag.as_slice(); - v.len() >= 2 && v[0] == "h" && v[1] == channel_uuid + let value = tag.as_slice(); + value.len() >= 2 && value[0] == "h" && value[1] == channel_uuid }); if !h_tag_matches { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_uuid, - "canvas event is missing expected h-tag — emitting no section", + surface, + "channel context event is missing expected h-tag", ); return None; } + Some(event) +} - // Blank content means the canvas was cleared; do not fall back to older events. - if event.content.trim().is_empty() { - tracing::debug!( - target: "canvas::fetch", - channel = %channel_uuid, - "latest canvas event has blank content — emitting no section" - ); - return None; - } - - let id = event.id.to_hex(); - - // Convert the Nostr timestamp to a UTC RFC3339 string with Z suffix. - // Use checked conversion: a u64 that exceeds i64::MAX (e.g. Timestamp::max()) - // wraps silently with `as i64`, producing a negative value that chrono would - // accept as a date in 1969. Reject out-of-range values explicitly instead. - let ts_secs = match i64::try_from(event.created_at.as_secs()) { - Ok(s) => s, +fn channel_event_timestamp( + event: &nostr::Event, + channel_uuid: &str, + surface: &'static str, +) -> Option { + let seconds = match i64::try_from(event.created_at.as_secs()) { + Ok(seconds) => seconds, Err(_) => { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_uuid, - "canvas event created_at overflows i64 — emitting no section", + surface, + "channel context event created_at overflows i64", ); return None; } }; - let timestamp = match chrono::DateTime::from_timestamp(ts_secs, 0) { - Some(dt) => dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), + match chrono::DateTime::from_timestamp(seconds, 0) { + Some(timestamp) => Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)), None => { tracing::warn!( - target: "canvas::fetch", + target: "channel_context::fetch", channel = %channel_uuid, - ts_secs, - "canvas event has out-of-range created_at — emitting no section", + surface, + seconds, + "channel context event has out-of-range created_at", ); - return None; + None } - }; + } +} + +pub(crate) fn canvas_section_from_query_response( + events: &[serde_json::Value], + channel_uuid: &str, +) -> Option { + let event = verified_channel_event_from_query_response( + events, + channel_uuid, + buzz_core::kind::KIND_CANVAS, + "canvas", + )?; + + // Blank content means the canvas was cleared; do not fall back to older events. + if event.content.trim().is_empty() { + tracing::debug!( + target: "canvas::fetch", + channel = %channel_uuid, + "latest canvas event has blank content — emitting no section" + ); + return None; + } + + let id = event.id.to_hex(); + + let timestamp = channel_event_timestamp(&event, channel_uuid, "canvas")?; tracing::info!( target: "canvas::fetch", @@ -2544,6 +2921,383 @@ pub(crate) fn render_canvas_section(event_id: &str, timestamp: &str, channel_uui ) } +#[derive(Debug, Clone)] +struct ExactWorldViewBindingsPromptState { + snapshot: buzz_core::world_view::WorldViewBindingsSnapshot, + timestamp: String, +} + +#[derive(Debug, Clone)] +struct EffectiveWorldViewBindingsPromptState { + effective: buzz_core::world_view::EffectiveWorldViewBindings, + timestamp: String, + channel_uuid: String, +} + +fn world_view_bindings_state_from_query_response( + events: &[serde_json::Value], + channel_uuid: &str, + expected_scope: &buzz_core::world_view::WorldViewBindingScope, +) -> Option { + let event = verified_channel_event_from_query_response( + events, + channel_uuid, + buzz_core::kind::KIND_WORLD_VIEW_BINDINGS, + "world-views", + )?; + let expected_channel_id = match Uuid::parse_str(channel_uuid) { + Ok(channel_id) => channel_id, + Err(error) => { + tracing::warn!( + target: "channel_context::fetch", + channel = %channel_uuid, + %error, + "world-view query used an invalid channel coordinate", + ); + return None; + } + }; + let snapshot = match buzz_core::world_view::world_view_bindings_snapshot_from_verified_event( + &event, + expected_channel_id, + expected_scope, + ) { + Ok(snapshot) => snapshot, + Err(error) => { + tracing::warn!( + target: "channel_context::fetch", + channel = %channel_uuid, + %error, + "world view bindings event failed envelope validation", + ); + return None; + } + }; + let timestamp = channel_event_timestamp(&event, channel_uuid, "world-views")?; + Some(ExactWorldViewBindingsPromptState { + snapshot, + timestamp, + }) +} + +fn render_world_view_bindings_section( + state: &EffectiveWorldViewBindingsPromptState, + authorities: &buzz_core::world_view::WorldAuthorityRegistry, + resolutions: &[( + Uuid, + Result, + )], + authority_grants: &std::collections::HashMap, +) -> String { + use buzz_core::world_view::{WorldViewBindingScope, WorldViewDisplayMode, WorldViewReference}; + use buzz_world_view_resolver::{WorldViewResolutionAuthority, WorldViewResolutionFreshness}; + + let mut lines = vec![ + "[Shivai World Views]".to_string(), + "Command surface: run supplied commands through the Buzz workspace shell tool; do not substitute a sandboxed local-exec tool.".to_string(), + format!( + "Effective scope: {}", + world_view_scope_label(&state.effective.effective_scope) + ), + format!( + "Channel bindings revision (event ID): {}", + state + .effective + .channel_revision_event_id + .as_deref() + .unwrap_or("none") + ), + format!("Last modified: {}", state.timestamp), + ]; + if matches!( + state.effective.effective_scope, + WorldViewBindingScope::Thread { .. } + ) { + lines.insert( + 3, + format!( + "Thread bindings revision (event ID): {}", + state + .effective + .thread_revision_event_id + .as_deref() + .unwrap_or("none") + ), + ); + } + for entry in &state.effective.bindings { + let binding = &entry.binding; + let resolution = resolutions + .iter() + .find(|(binding_id, _)| binding_id == &binding.id) + .map(|(_, resolution)| resolution); + let resolved_source_revision = resolution + .and_then(|resolution| resolution.as_ref().ok()) + .map(|resolved| resolved.source_revision.as_str()); + let local_authority = match &binding.reference { + WorldViewReference::LocalWorldMirrorLatest { origin, mirror_id } => { + authorities.resolve_local(origin, mirror_id) + } + WorldViewReference::HostedWorldLatest { .. } + | WorldViewReference::HostedWorldViewExport { .. } + | WorldViewReference::HostedWorldLiveViewShare { .. } => None, + }; + let hosted_authority = match &binding.reference { + WorldViewReference::HostedWorldLatest { + origin, + hosted_world_id, + } => authorities.resolve_hosted(origin, hosted_world_id), + WorldViewReference::HostedWorldLiveViewShare { .. } => resolution + .and_then(|resolution| resolution.as_ref().ok()) + .and_then(|resolved| match &resolved.authority { + WorldViewResolutionAuthority::HostedWorldLiveViewShare { + origin, + hosted_world_id, + } => authorities.resolve_hosted(origin, hosted_world_id), + _ => None, + }), + WorldViewReference::HostedWorldViewExport { .. } + | WorldViewReference::LocalWorldMirrorLatest { .. } => None, + }; + let source = match &binding.reference { + WorldViewReference::LocalWorldMirrorLatest { .. } => "local-world-mirror-latest", + WorldViewReference::HostedWorldViewExport { .. } => "hosted-world-view-export", + WorldViewReference::HostedWorldLiveViewShare { .. } => "hosted-world-live-view-share", + WorldViewReference::HostedWorldLatest { .. } => "hosted-world-latest", + }; + let display = match binding.display_mode { + WorldViewDisplayMode::Graph => "graph", + WorldViewDisplayMode::Tasks => "tasks", + }; + lines.push(format!( + "- {} | {} | {} | realm={} | view={} | display={}", + binding.id, + binding.label.as_deref().unwrap_or("Untitled view"), + source, + binding.realm_qualified_name, + binding.view_qualified_name, + display, + )); + lines.push(format!( + " Declaration: scope={} binding-revision={}", + world_view_scope_label(&entry.declared_scope), + entry.binding_revision_event_id + )); + + match resolution { + Some(Ok(resolved)) => { + let freshness = match resolved.freshness { + WorldViewResolutionFreshness::Pinned => "pinned", + WorldViewResolutionFreshness::LatestAtResolution => "latest-at-resolution", + }; + lines.push(format!( + " Resolved source revision: {} ({freshness})", + resolved.source_revision + )); + lines.push(format!( + " Effective scope: {}", + world_view_scope_label(&resolved.effective_scope) + )); + lines.push(format!( + " Counts: nodes={} ready={} actionable-ready={} satisfied={} blocked={}", + resolved.view_dump.counts.nodes, + resolved.view_dump.counts.ready, + resolved.view_dump.counts.actionable_ready, + resolved.view_dump.counts.satisfied, + resolved.view_dump.counts.blocked, + )); + lines.push(format!( + " Ready leaves: {}", + world_view_node_names(&resolved.view_dump.ready_leaves) + )); + lines.push(format!( + " Blocked nodes: {}", + world_view_blocked_node_names(&resolved.view_dump.blocked_nodes) + )); + lines.push(format!( + " Satisfied nodes: {}", + world_view_node_names(&resolved.view_dump.satisfied_nodes) + )); + lines.push(format!(" Refresh: {}", resolved.next_command)); + } + Some(Err(error)) => { + lines.push(format!( + " Resolution unavailable: {}", + compact_world_view_diagnostic(error) + )); + let mut retry = + format!("buzz world-views resolve --channel {}", state.channel_uuid); + if let Some(thread_root_event_id) = + state.effective.effective_scope.thread_root_event_id() + { + retry.push_str(" --thread-root "); + retry.push_str(thread_root_event_id); + } + retry.push_str(" --binding "); + retry.push_str(&binding.id.to_string()); + lines.push(format!(" Retry: {retry}")); + } + None => { + lines.push(" Resolution unavailable: resolver produced no readback".into()); + } + } + + match (&binding.reference, local_authority, hosted_authority) { + ( + WorldViewReference::LocalWorldMirrorLatest { .. }, + Some(_), + _, + ) => match ( + resolved_source_revision, + authority_grants.get(&binding.id), + ) { + (Some(source_revision), Some(authority_grant)) => { + let script_command = scoped_world_view_script_command( + &state.channel_uuid, + &state.effective.effective_scope, + binding.id, + source_revision, + authority_grant, + ); + lines.push( + " Authority: mutable local world available through a host-owned scoped command." + .into(), + ); + lines.push(format!(" Mutation command: {script_command}")); + lines.push( + " Command access: pipe ordinary `world ...` lines to this command. Buzz resolves private machine-local authority from the agent/channel/thread/revision grant; never request paths or credentials. On a revision conflict, reread the binding and make one deliberate retry with the refreshed command." + .into(), + ); + } + _ => lines.push( + " Authority: read-only for this agent; the local source is connected, but a user has not enabled agent edits for this binding." + .into(), + ), + }, + (WorldViewReference::LocalWorldMirrorLatest { .. }, None, _) => lines.push( + " Authority: read-only public mirror; no mutable source is registered on this host." + .into(), + ), + (WorldViewReference::HostedWorldViewExport { .. }, _, _) => { + lines.push(" Authority: read-only hosted view export".into()); + } + ( + WorldViewReference::HostedWorldLatest { .. } + | WorldViewReference::HostedWorldLiveViewShare { .. }, + _, + Some(_), + ) => match ( + resolved_source_revision, + authority_grants.get(&binding.id), + ) { + (Some(source_revision), Some(authority_grant)) => { + let script_command = scoped_world_view_script_command( + &state.channel_uuid, + &state.effective.effective_scope, + binding.id, + source_revision, + authority_grant, + ); + lines.push( + " Authority: mutable hosted world available through a host-owned scoped command." + .into(), + ); + lines.push(format!(" Mutation command: {script_command}")); + lines.push( + " Command access: pipe ordinary `world ...` lines to this command. Buzz resolves private machine-local authority from the agent/channel/thread/revision grant; never request or print credentials. On a revision conflict, reread the binding and make one deliberate retry with the refreshed command." + .into(), + ); + } + _ => lines.push( + " Authority: read-only for this agent; hosted edit authority is connected, but a user has not enabled agent edits for this binding." + .into(), + ), + }, + ( + WorldViewReference::HostedWorldLatest { .. } + | WorldViewReference::HostedWorldLiveViewShare { .. }, + _, + None, + ) => lines.push( + " Authority: read-only on this client; register the hosted edit-share URL here before a user can enable agent edits." + .into(), + ), + } + } + lines.join("\n") +} + +fn scoped_world_view_script_command( + channel_id: &str, + scope: &buzz_core::world_view::WorldViewBindingScope, + binding_id: Uuid, + source_revision: &str, + authority_grant: &str, +) -> String { + let mut command = format!("buzz world-views script --channel {channel_id}"); + if let Some(thread_root_event_id) = scope.thread_root_event_id() { + command.push_str(" --thread-root "); + command.push_str(thread_root_event_id); + } + command.push_str(" --binding "); + command.push_str(&binding_id.to_string()); + command.push_str(" --expected-revision "); + command.push_str(source_revision); + command.push_str(" --script - --grant "); + command.push_str(authority_grant); + command +} + +fn world_view_scope_label(scope: &buzz_core::world_view::WorldViewBindingScope) -> String { + match scope { + buzz_core::world_view::WorldViewBindingScope::Channel => "channel".into(), + buzz_core::world_view::WorldViewBindingScope::Thread { + thread_root_event_id, + } => format!("thread:{thread_root_event_id}"), + } +} + +fn world_view_node_names(nodes: &[buzz_world_view_resolver::ResolvedWorldViewNode]) -> String { + if nodes.is_empty() { + return "none".into(); + } + nodes + .iter() + .map(|node| node.qualified_name.as_str()) + .collect::>() + .join(", ") +} + +fn world_view_blocked_node_names( + nodes: &[buzz_world_view_resolver::ResolvedWorldViewNode], +) -> String { + if nodes.is_empty() { + return "none".into(); + } + nodes + .iter() + .map(|node| { + let blockers = if node.blockers.is_empty() { + "unspecified".into() + } else { + node.blockers.join(", ") + }; + format!("{} <- {blockers}", node.qualified_name) + }) + .collect::>() + .join("; ") +} + +fn compact_world_view_diagnostic(diagnostic: &str) -> String { + diagnostic + .split_whitespace() + .collect::>() + .join(" ") + .chars() + .take(500) + .collect() +} + /// Fetch conversation context (thread or DM) for a batch before prompting. /// /// Returns `None` if: @@ -5681,6 +6435,360 @@ mod tests { ); } + fn empty_resolved_world_view( + binding_id: Uuid, + authority: serde_json::Value, + ) -> buzz_world_view_resolver::ResolvedWorldView { + let source_revision = "c".repeat(64); + let presentation_model = serde_json::json!({ + "graph": { "kind": "empty", "reason": "no-preferences" }, + "revision": source_revision.clone(), + "selection": { + "realmQualifiedName": "delivery::main", + "viewQualifiedName": "delivery::main::@Remaining" + } + }); + serde_json::from_value(serde_json::json!({ + "formatVersion": 1, + "bindingId": binding_id, + "channelId": CHANNEL_UUID, + "declaredScope": { "kind": "channel" }, + "effectiveScope": { "kind": "channel" }, + "bindingRevisionEventId": "a".repeat(64), + "sourceRevision": source_revision, + "freshness": "latest-at-resolution", + "authority": authority, + "realm": { + "name": "main", + "qualifiedName": "delivery::main" + }, + "view": { + "name": "Remaining", + "qualifiedName": "delivery::main::@Remaining" + }, + "viewDump": { + "counts": { + "nodes": 0, + "edges": 0, + "ready": 0, + "actionableReady": 0, + "satisfied": 0, + "blocked": 0 + }, + "nodes": [], + "readyLeaves": [], + "satisfiedNodes": [], + "blockedNodes": [], + "edges": [] + }, + "presentation": { + "formatVersion": 1, + "dark": presentation_model, + "light": presentation_model + }, + "resolvedAt": "2026-07-24T12:00:00Z", + "nextCommand": format!( + "buzz world-views resolve --channel {CHANNEL_UUID} --binding {binding_id}" + ) + })) + .unwrap() + } + + #[test] + fn world_view_prompt_only_exposes_local_scoped_command_after_consent() { + use buzz_core::world_view::{ + LocalWorldAuthority, WorldAuthorityRegistry, WorldMutationAuthority, WorldViewBinding, + WorldViewBindingScope, WorldViewBindingsDocument, WorldViewDisplayMode, + WorldViewMutationDelegation, WorldViewReference, WORLD_AUTHORITY_REGISTRY_VERSION, + WORLD_VIEW_BINDINGS_VERSION, + }; + + let binding_id = Uuid::nil(); + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![WorldViewBinding { + id: binding_id, + label: Some("Delivery".into()), + reference: WorldViewReference::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }, + realm_qualified_name: "delivery::main".into(), + view_qualified_name: "delivery::main::@Remaining".into(), + display_mode: WorldViewDisplayMode::Tasks, + }], + }; + let event = EventBuilder::new( + Kind::Custom(buzz_core::kind::KIND_WORLD_VIEW_BINDINGS as u16), + serde_json::to_string(&document).unwrap(), + ) + .tags([ + Tag::parse(["h", CHANNEL_UUID]).unwrap(), + Tag::parse([ + "d", + buzz_core::world_view::CHANNEL_WORLD_VIEW_BINDINGS_D_TAG, + ]) + .unwrap(), + Tag::parse(["prev", ""]).unwrap(), + ]) + .sign_with_keys(&Keys::generate()) + .unwrap(); + let mut registry = WorldAuthorityRegistry { + version: WORLD_AUTHORITY_REGISTRY_VERSION, + trusted_origins: vec!["https://manifest.shivai.space".into()], + local_authorities: vec![LocalWorldAuthority { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + source_root: "/worlds/delivery.world".into(), + capability_secret_file: "/credentials/local-delivery.txt".into(), + }], + hosted_authorities: Vec::new(), + mutation_delegations: Vec::new(), + }; + + let exact_state = world_view_bindings_state_from_query_response( + &[serde_json::to_value(event).unwrap()], + CHANNEL_UUID, + &WorldViewBindingScope::Channel, + ) + .expect("valid world binding event"); + let state = EffectiveWorldViewBindingsPromptState { + effective: buzz_core::world_view::effective_world_view_bindings( + &exact_state.snapshot, + None, + ) + .expect("effective channel bindings"), + timestamp: exact_state.timestamp, + channel_uuid: CHANNEL_UUID.into(), + }; + let resolved = empty_resolved_world_view( + binding_id, + serde_json::json!({ + "kind": "local-world-mirror-latest", + "origin": "https://manifest.shivai.space", + "mirrorId": "mirror-1" + }), + ); + let section = render_world_view_bindings_section( + &state, + ®istry, + &[(binding_id, Ok(resolved))], + &std::collections::HashMap::new(), + ); + + assert!(section.contains(&format!("--binding {binding_id}"))); + assert!(section.contains("Effective scope: channel")); + assert!(section.contains("Declaration: scope=channel binding-revision=")); + assert!(section.contains( + "Authority: read-only for this agent; the local source is connected, but a user has not enabled agent edits for this binding." + )); + assert!(!section.contains("/worlds/delivery.world")); + assert!(!section.contains("Mutation command:")); + assert!(!section.contains("world hosted sync-local")); + + let authority_root = + std::env::temp_dir().join(format!("buzz-acp-local-grant-{}", Uuid::new_v4())); + std::fs::create_dir(&authority_root).expect("create local authority fixture root"); + let capability_secret_file = authority_root.join("local.capability"); + std::fs::write(&capability_secret_file, "private-local-capability") + .expect("write local capability fixture"); + registry.local_authorities[0].capability_secret_file = + capability_secret_file.to_string_lossy().into_owned(); + registry + .mutation_delegations + .push(WorldViewMutationDelegation { + channel_id: Uuid::parse_str(CHANNEL_UUID).unwrap(), + declared_scope: WorldViewBindingScope::Channel, + binding_id, + binding_revision_event_id: state.effective.bindings[0] + .binding_revision_event_id + .clone(), + authority: WorldMutationAuthority::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }, + }); + let resolved = empty_resolved_world_view( + binding_id, + serde_json::json!({ + "kind": "local-world-mirror-latest", + "origin": "https://manifest.shivai.space", + "mirrorId": "mirror-1" + }), + ); + let resolutions = vec![(binding_id, Ok(resolved))]; + let authority_grants = issue_world_authority_grants( + Uuid::parse_str(CHANNEL_UUID).unwrap(), + &"d".repeat(64), + &state, + ®istry, + &resolutions, + ); + let section = + render_world_view_bindings_section(&state, ®istry, &resolutions, &authority_grants); + + assert!(section.contains( + "Authority: mutable local world available through a host-owned scoped command." + )); + assert!(section.contains("Mutation command: buzz world-views script")); + assert!(section.contains("never request paths or credentials")); + assert!(!section.contains("/worlds/delivery.world")); + assert!(!section.contains(&capability_secret_file.to_string_lossy().to_string())); + assert!(!section.contains("private-local-capability")); + std::fs::remove_dir_all(authority_root).expect("remove local authority fixture root"); + } + + #[test] + fn world_view_prompt_renders_scoped_hosted_world_command() { + use buzz_core::world_view::{ + verify_world_authority_grant, EffectiveWorldViewBinding, EffectiveWorldViewBindings, + HostedWorldAuthority, WorldAuthorityGrantScope, WorldAuthorityRegistry, + WorldMutationAuthority, WorldViewBinding, WorldViewBindingScope, WorldViewDisplayMode, + WorldViewMutationDelegation, WorldViewReference, WORLD_AUTHORITY_REGISTRY_VERSION, + }; + + let binding_id = Uuid::nil(); + let binding = WorldViewBinding { + id: binding_id, + label: Some("Hosted delivery".into()), + reference: WorldViewReference::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }, + realm_qualified_name: "delivery::main".into(), + view_qualified_name: "delivery::main::@Remaining".into(), + display_mode: WorldViewDisplayMode::Tasks, + }; + let state = EffectiveWorldViewBindingsPromptState { + effective: EffectiveWorldViewBindings { + effective_scope: WorldViewBindingScope::Channel, + bindings: vec![EffectiveWorldViewBinding { + binding, + declared_scope: WorldViewBindingScope::Channel, + binding_revision_event_id: "a".repeat(64), + }], + channel_revision_event_id: Some("a".repeat(64)), + thread_revision_event_id: None, + }, + timestamp: "2026-07-24T12:00:00Z".into(), + channel_uuid: CHANNEL_UUID.into(), + }; + let authority_root = + std::env::temp_dir().join(format!("buzz-acp-world-grant-{}", Uuid::new_v4())); + std::fs::create_dir(&authority_root).expect("create authority fixture root"); + let credential_file = authority_root.join("hosted-1.edit-share"); + std::fs::write(&credential_file, "private-edit-share").expect("write authority fixture"); + let registry = WorldAuthorityRegistry { + version: WORLD_AUTHORITY_REGISTRY_VERSION, + trusted_origins: vec!["https://manifest.shivai.space".into()], + local_authorities: Vec::new(), + hosted_authorities: vec![HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: credential_file.to_string_lossy().into_owned(), + }], + mutation_delegations: vec![WorldViewMutationDelegation { + channel_id: Uuid::parse_str(CHANNEL_UUID).unwrap(), + declared_scope: WorldViewBindingScope::Channel, + binding_id, + binding_revision_event_id: "a".repeat(64), + authority: WorldMutationAuthority::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }, + }], + }; + let resolved = empty_resolved_world_view( + binding_id, + serde_json::json!({ + "kind": "hosted-world-latest", + "origin": "https://manifest.shivai.space", + "hostedWorldId": "hosted-1" + }), + ); + + let resolutions = vec![(binding_id, Ok(resolved))]; + let agent_pubkey = "d".repeat(64); + let authority_grants = issue_world_authority_grants( + Uuid::parse_str(CHANNEL_UUID).unwrap(), + &agent_pubkey, + &state, + ®istry, + &resolutions, + ); + let authority_grant = authority_grants + .get(&binding_id) + .expect("host issued scoped grant"); + verify_world_authority_grant( + authority_grant, + b"private-edit-share", + &WorldAuthorityGrantScope { + agent_pubkey, + channel_id: Uuid::parse_str(CHANNEL_UUID).unwrap(), + effective_scope: WorldViewBindingScope::Channel, + binding_id, + binding_revision_event_id: "a".repeat(64), + source_revision: "c".repeat(64), + }, + chrono::Utc::now().timestamp(), + ) + .expect("grant verifies against exact prompt scope"); + let section = + render_world_view_bindings_section(&state, ®istry, &resolutions, &authority_grants); + + assert!(section.contains( + "Authority: mutable hosted world available through a host-owned scoped command." + )); + assert!(section.contains( + "Command surface: run supplied commands through the Buzz workspace shell tool" + )); + assert!(section.contains(&format!( + "Mutation command: buzz world-views script --channel {CHANNEL_UUID} --binding {binding_id} --expected-revision {} --script - --grant {authority_grant}", + "c".repeat(64), + ))); + assert!(section.contains("Buzz resolves private machine-local authority")); + assert!(section.contains("never request or print credentials")); + assert!(!section.contains(&credential_file.to_string_lossy().to_string())); + assert!(!section.contains("--edit-share")); + assert!(!section.contains("--base-url")); + assert!(!section.contains("world hosted")); + assert!(!section.contains("private-edit-share")); + std::fs::remove_dir_all(authority_root).expect("remove authority fixture root"); + } + + #[test] + fn world_agent_filesystem_permissions_deny_discovery_without_exposing_local_sources() { + use buzz_core::world_view::{ + WORLD_AUTHORITY_REGISTRY_FILE_NAME, WORLD_AUTHORITY_SECRET_DIRECTORY, + }; + + let root = std::env::temp_dir().join(format!("buzz-acp-permissions-{}", Uuid::new_v4())); + let local_source = root.join("private.world"); + let permissions = world_agent_filesystem_permissions(root.to_str().unwrap()); + assert_eq!( + permissions.get( + &root + .join(WORLD_AUTHORITY_REGISTRY_FILE_NAME) + .to_string_lossy() + .into_owned() + ), + Some(&crate::acp::CodexFilesystemPathAccess::Deny) + ); + assert_eq!( + permissions.get( + &root + .join(WORLD_AUTHORITY_SECRET_DIRECTORY) + .to_string_lossy() + .into_owned() + ), + Some(&crate::acp::CodexFilesystemPathAccess::Deny) + ); + assert!(!permissions.contains_key(&local_source.to_string_lossy().into_owned())); + assert_eq!(permissions.len(), 2); + assert!(world_agent_filesystem_permissions("relative/nest").is_empty()); + } + // ── new-session channel context (one resolve, two consumers) ───────────── /// A [`ChannelInfoResolver`] whose lazy REST fallback is served by a local diff --git a/crates/buzz-acp/src/queue.rs b/crates/buzz-acp/src/queue.rs index 029bf86dbf..6f634d29ce 100644 --- a/crates/buzz-acp/src/queue.rs +++ b/crates/buzz-acp/src/queue.rs @@ -1372,6 +1372,11 @@ pub struct FormatPromptArgs<'a> { /// For legacy agents it rides in the user message on every turn of the /// session, alongside `[Base]`/`[System]`/`[Agent Memory — core]`. pub agent_canvas: Option<&'a str>, + /// Freshly resolved operational state for the effective channel/thread scope. + /// + /// Unlike Canvas and agent core, this section is never session-cached: + /// every agent protocol receives it in the current user turn. + pub agent_world_views: Option<&'a str>, } /// Format the `[Base]` section for the base prompt. @@ -1389,9 +1394,10 @@ pub(crate) fn base_section(base_prompt: &str) -> String { /// 0. `[Base]` — base prompt (only for legacy agents without systemPrompt support) /// 1. `[System]` — system prompt (only for legacy agents without systemPrompt support) /// 2. `[Agent Memory — core]` — if agent core memory is set -/// 3. `[Context]` — scope, channel name, and contextual hints for the agent -/// 4. `[Thread Context]` or `[Conversation Context]` — if fetched -/// 5. `[Event]` / `[Buzz events]` — the triggering event(s) +/// 3. `[Shivai World Views]` — freshly resolved effective scope, if bound +/// 4. `[Context]` — scope, channel name, and contextual hints for the agent +/// 5. `[Thread Context]` or `[Conversation Context]` — if fetched +/// 6. `[Event]` / `[Buzz events]` — the triggering event(s) /// /// Each section is returned as its own block rather than one joined string so /// the observer frame's size trimmer (`fit_observer_event_to_budget`) elides @@ -1421,7 +1427,7 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec = Vec::with_capacity(7); + let mut sections: Vec = Vec::with_capacity(8); // For legacy agents (protocol_version < 2), inject base_prompt and // system_prompt as user-message sections. Modern agents receive these @@ -1457,6 +1463,12 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec) -> Result { + thread_root.map_or(Ok(WorldViewBindingScope::Channel), |event_id| { + WorldViewBindingScope::thread(event_id) + .map_err(|error| CliError::Usage(format!("invalid thread scope: {error}"))) + }) +} + +async fn fetch_snapshot( + client: &BuzzClient, + channel_id: &str, + scope: &WorldViewBindingScope, +) -> Result { + let expected_channel_id = parse_uuid(channel_id)?; + let d_tag = scope.d_tag(); + let filter = serde_json::json!({ + "kinds": [KIND_WORLD_VIEW_BINDINGS], + "#h": [channel_id], + "#d": [d_tag], + "limit": 1 + }); + let response = client.query(&filter).await?; + let events: Vec = serde_json::from_str(&response) + .map_err(|error| CliError::Other(format!("decode world view bindings query: {error}")))?; + let Some(event_value) = events.into_iter().next() else { + return Ok(WorldViewBindingsSnapshot::empty(scope.clone())); + }; + let event: nostr::Event = serde_json::from_value(event_value) + .map_err(|error| CliError::Other(format!("decode world view bindings event: {error}")))?; + let expected_scope = scope.clone(); + tokio::task::spawn_blocking(move || { + verify_event(&event) + .map_err(|error| format!("verify world view bindings event: {error}"))?; + world_view_bindings_snapshot_from_verified_event( + &event, + expected_channel_id, + &expected_scope, + ) + .map_err(|error| format!("decode world view bindings event: {error}")) + }) + .await + .map_err(|error| { + CliError::Other(format!( + "world view bindings verification task failed: {error}" + )) + })? + .map_err(CliError::Other) +} + +fn read_command(channel_id: &str, scope: &WorldViewBindingScope) -> String { + let mut command = format!("buzz world-views get --channel {channel_id}"); + if let Some(thread_root_event_id) = scope.thread_root_event_id() { + command.push_str(" --thread-root "); + command.push_str(thread_root_event_id); + } + command +} + +async fn cmd_get( + client: &BuzzClient, + channel_id: &str, + thread_root: Option<&str>, +) -> Result<(), CliError> { + let scope = exact_scope(thread_root)?; + let snapshot = fetch_snapshot(client, channel_id, &scope).await?; + let expected_revision = snapshot.revision_event_id.as_deref().unwrap_or("none"); + let next_set_command = format!( + "buzz world-views set --channel {channel_id} \ + --expected-revision {expected_revision} --document -" + ); + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "document": snapshot.document, + "revisionEventId": snapshot.revision_event_id, + "updatedAt": snapshot.updated_at, + "author": snapshot.author, + "nextReadCommand": read_command(channel_id, &scope), + "nextSetCommand": next_set_command, + })) + .map_err(|error| CliError::Other(format!("encode world view bindings: {error}")))? + ); + Ok(()) +} + +fn parse_expected_revision(value: &str) -> Result, CliError> { + if value == "none" { + return Ok(None); + } + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(CliError::Usage( + "--expected-revision must be `none` or 64 lowercase hex characters".into(), + )); + } + Ok(Some(value.into())) +} + +async fn cmd_set( + client: &BuzzClient, + channel_id: &str, + document_json: &str, + expected_revision: &str, +) -> Result<(), CliError> { + let channel_uuid = parse_uuid(channel_id)?; + let document_text = read_or_stdin(document_json)?; + let document: WorldViewBindingsDocument = serde_json::from_str(&document_text) + .map_err(|error| CliError::Usage(format!("invalid world view bindings JSON: {error}")))?; + document + .validate() + .map_err(|error| CliError::Usage(format!("invalid world view bindings: {error}")))?; + let expected_revision = parse_expected_revision(expected_revision)?; + let current = fetch_snapshot(client, channel_id, &document.scope).await?; + if current.revision_event_id != expected_revision { + return Err(CliError::Other(format!( + "world-view bindings revision conflict: expected {}, current {}; refresh with `{}`", + expected_revision.as_deref().unwrap_or("none"), + current.revision_event_id.as_deref().unwrap_or("none"), + read_command(channel_id, &document.scope) + ))); + } + let builder = buzz_sdk::build_set_world_view_bindings( + channel_uuid, + expected_revision.as_deref(), + &document, + ) + .map_err(|error| CliError::Other(format!("build world view bindings event: {error}")))?; + let event = client.sign_event(builder)?; + let response = client.submit_event(event).await?; + println!("{response}"); + Ok(()) +} + +fn world_authority_registry_path() -> Result { + let cwd = std::env::current_dir() + .map_err(|error| CliError::Other(format!("resolve current directory: {error}")))?; + Ok(cwd.join(WORLD_AUTHORITY_REGISTRY_FILE_NAME)) +} + +fn load_world_authority_registry() -> Result { + load_world_authority_registry_at(&world_authority_registry_path()?) +} + +fn load_world_authority_registry_at(path: &Path) -> Result { + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(WorldAuthorityRegistry::default()); + } + Err(error) => { + return Err(CliError::Other(format!( + "read world authority registry {}: {error}", + path.display() + ))); + } + }; + let value = serde_json::from_str(&text).map_err(|error| { + CliError::Other(format!( + "decode world authority registry {}: {error}", + path.display() + )) + })?; + let decoded = decode_world_authority_registry(value) + .map_err(|error| CliError::Other(format!("invalid world authority registry: {error}")))?; + if decoded.migrated { + write_world_authority_registry(path, &decoded.registry)?; + } + Ok(decoded.registry) +} + +fn write_world_authority_registry( + path: &Path, + registry: &WorldAuthorityRegistry, +) -> Result<(), CliError> { + let temp_path = path.with_extension(format!("json.{}.tmp", Uuid::new_v4())); + let write_result = (|| -> Result<(), CliError> { + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(&temp_path).map_err(|error| { + CliError::Other(format!( + "create world authority registry {}: {error}", + temp_path.display() + )) + })?; + let mut bytes = serde_json::to_vec_pretty(registry).map_err(|error| { + CliError::Other(format!("encode world authority registry: {error}")) + })?; + bytes.push(b'\n'); + file.write_all(&bytes) + .map_err(|error| CliError::Other(format!("write world authority registry: {error}")))?; + file.sync_all() + .map_err(|error| CliError::Other(format!("sync world authority registry: {error}")))?; + fs::rename(&temp_path, path).map_err(|error| { + CliError::Other(format!("replace world authority registry: {error}")) + })?; + Ok(()) + })(); + if write_result.is_err() { + let _ = fs::remove_file(temp_path); + } + write_result +} + +fn cmd_set_origin_trust(origin: &str, trusted: bool) -> Result<(), CliError> { + let path = world_authority_registry_path()?; + let mut registry = load_world_authority_registry_at(&path)?; + let changed = if trusted { + registry + .trust_origin(origin.to_owned()) + .map_err(CliError::Usage)? + } else { + registry + .revoke_origin_trust(origin) + .map_err(CliError::Usage)? + }; + if changed { + write_world_authority_registry(&path, ®istry)?; + } + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "origin": origin, + "trusted": trusted, + "changed": changed, + })) + .map_err(|error| CliError::Other(format!("encode world origin trust result: {error}")))? + ); + Ok(()) +} + +#[derive(Debug, Clone)] +enum ConnectedWorldSource { + Local { + reference: WorldViewReference, + }, + Hosted { + credential_file: String, + reference: WorldViewReference, + }, +} + +impl ConnectedWorldSource { + fn reference(&self) -> &WorldViewReference { + match self { + Self::Local { reference } | Self::Hosted { reference, .. } => reference, + } + } +} + +fn connected_world_source( + registry: &WorldAuthorityRegistry, + source: &str, + origin: &str, +) -> Result { + if let Some(hosted_world_id) = source.strip_prefix("hosted:") { + let authority = registry + .resolve_hosted(origin, hosted_world_id) + .ok_or_else(|| { + CliError::Usage(format!( + "unknown connected world source `{source}` at `{origin}`; refresh with `buzz world-views sources`" + )) + })?; + return Ok(ConnectedWorldSource::Hosted { + credential_file: authority.credential_file.clone(), + reference: WorldViewReference::HostedWorldLatest { + origin: authority.origin.clone(), + hosted_world_id: authority.hosted_world_id.clone(), + }, + }); + } + if let Some(mirror_id) = source.strip_prefix("local:") { + let authority = registry.resolve_local(origin, mirror_id).ok_or_else(|| { + CliError::Usage(format!( + "unknown connected world source `{source}` at `{origin}`; refresh with `buzz world-views sources`" + )) + })?; + return Ok(ConnectedWorldSource::Local { + reference: WorldViewReference::LocalWorldMirrorLatest { + origin: authority.origin.clone(), + mirror_id: authority.mirror_id.clone(), + }, + }); + } + Err(CliError::Usage( + "--source must be an exact `hosted:` or `local:` from `buzz world-views sources`" + .into(), + )) +} + +async fn catalog_connected_world_source( + source: &ConnectedWorldSource, + registry: &WorldAuthorityRegistry, +) -> Result { + catalog_world_views(source.reference().clone(), registry) + .await + .map_err(|error| CliError::Other(error.to_string())) +} + +fn cmd_sources() -> Result<(), CliError> { + let registry = load_world_authority_registry()?; + let local = registry.local_authorities.iter().map(|authority| { + let source = format!("local:{}", authority.mirror_id); + serde_json::json!({ + "source": source, + "kind": "local-world-mirror-latest", + "origin": authority.origin, + "mirrorId": authority.mirror_id, + "nextCatalogCommandArgs": [ + "world-views", "catalog", "--source", source, "--origin", authority.origin + ], + }) + }); + let hosted = registry.hosted_authorities.iter().map(|authority| { + let source = format!("hosted:{}", authority.hosted_world_id); + serde_json::json!({ + "source": source, + "kind": "hosted-world-latest", + "origin": authority.origin, + "hostedWorldId": authority.hosted_world_id, + "nextCatalogCommandArgs": [ + "world-views", "catalog", "--source", source, "--origin", authority.origin + ], + }) + }); + let sources = local.chain(hosted).collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "sources": sources, + "trustedOrigins": registry.trusted_origins, + "nextCommand": sources.first().map(|source| source["nextCatalogCommandArgs"].clone()), + })) + .map_err(|error| CliError::Other(format!("encode connected world sources: {error}")))? + ); + Ok(()) +} + +async fn cmd_catalog(source: &str, origin: &str) -> Result<(), CliError> { + let registry = load_world_authority_registry()?; + let source = connected_world_source(®istry, source, origin)?; + let catalog = catalog_connected_world_source(&source, ®istry).await?; + println!( + "{}", + serde_json::to_string_pretty(&catalog) + .map_err(|error| CliError::Other(format!("encode world view catalog: {error}")))? + ); + Ok(()) +} + +fn parse_display_mode(value: &str) -> Result { + match value { + "graph" => Ok(WorldViewDisplayMode::Graph), + "tasks" => Ok(WorldViewDisplayMode::Tasks), + _ => Err(CliError::Usage( + "--display must be either `graph` or `tasks`".into(), + )), + } +} + +async fn publish_connected_world_reference( + source: &ConnectedWorldSource, + view_qualified_name: &str, +) -> Result<(WorldViewReference, Option), CliError> { + match source { + ConnectedWorldSource::Local { reference } => Ok((reference.clone(), None)), + ConnectedWorldSource::Hosted { + credential_file, + reference: + WorldViewReference::HostedWorldLatest { + origin, + hosted_world_id, + }, + } => { + let share = + publish_hosted_live_view_share(origin, credential_file, view_qualified_name) + .await + .map_err(|error| CliError::Other(error.to_string()))?; + if &share.hosted_world_id != hosted_world_id { + return Err(CliError::Other(format!( + "live-view share resolved hosted world `{}` instead of `{hosted_world_id}`", + share.hosted_world_id + ))); + } + Ok(( + WorldViewReference::HostedWorldLiveViewShare { + origin: origin.clone(), + share_token: share.share_token.clone(), + }, + Some(share), + )) + } + ConnectedWorldSource::Hosted { .. } => Err(CliError::Other( + "connected hosted source carried an invalid reference".into(), + )), + } +} + +async fn cmd_bind( + client: &BuzzClient, + channel_id: &str, + thread_root: Option<&str>, + source_id: &str, + origin: &str, + view_qualified_name: &str, + label: Option<&str>, + display: &str, + binding_id: Option<&str>, + expected_revision: &str, +) -> Result<(), CliError> { + let channel_uuid = parse_uuid(channel_id)?; + let scope = exact_scope(thread_root)?; + let expected_revision = parse_expected_revision(expected_revision)?; + let current = fetch_snapshot(client, channel_id, &scope).await?; + if current.revision_event_id != expected_revision { + return Err(CliError::Other(format!( + "world-view bindings revision conflict: expected {}, current {}; refresh with `{}`", + expected_revision.as_deref().unwrap_or("none"), + current.revision_event_id.as_deref().unwrap_or("none"), + read_command(channel_id, &scope) + ))); + } + + let registry = load_world_authority_registry()?; + let source = connected_world_source(®istry, source_id, origin)?; + let catalog = catalog_connected_world_source(&source, ®istry).await?; + let selected_view = catalog + .views + .iter() + .find(|view| view.qualified_name == view_qualified_name) + .ok_or_else(|| { + CliError::Usage(format!( + "unknown view `{view_qualified_name}` for `{source_id}`; refresh with `buzz world-views catalog --source {source_id} --origin {origin}`" + )) + })? + .clone(); + let (reference, live_share) = + publish_connected_world_reference(&source, view_qualified_name).await?; + if let Some(share) = &live_share { + if share.realm_qualified_name != selected_view.realm.qualified_name { + return Err(CliError::Other(format!( + "live-view share resolved realm `{}` instead of catalog realm `{}`", + share.realm_qualified_name, selected_view.realm.qualified_name + ))); + } + } + + let explicit_binding_id = binding_id + .map(|value| { + Uuid::parse_str(value) + .map_err(|_| CliError::Usage(format!("invalid binding UUID: {value}"))) + }) + .transpose()?; + if let Some(id) = explicit_binding_id { + if !current + .document + .bindings + .iter() + .any(|binding| binding.id == id) + { + return Err(CliError::Usage(format!( + "unknown world view binding `{id}`; refresh with `{}`", + read_command(channel_id, &scope) + ))); + } + } + let binding_id = explicit_binding_id + .or_else(|| { + current + .document + .bindings + .iter() + .find(|binding| { + binding.reference == reference + && binding.view_qualified_name == selected_view.qualified_name + }) + .map(|binding| binding.id) + }) + .unwrap_or_else(Uuid::new_v4); + let label = label + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_owned); + let binding = WorldViewBinding { + id: binding_id, + label, + reference, + realm_qualified_name: selected_view.realm.qualified_name, + view_qualified_name: selected_view.qualified_name, + display_mode: parse_display_mode(display)?, + }; + let replaces_binding = current + .document + .bindings + .iter() + .any(|candidate| candidate.id == binding_id); + let bindings = if replaces_binding { + current + .document + .bindings + .into_iter() + .map(|candidate| { + if candidate.id == binding_id { + binding.clone() + } else { + candidate + } + }) + .collect() + } else { + let mut bindings = current.document.bindings; + bindings.push(binding.clone()); + bindings + }; + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: scope.clone(), + bindings, + }; + document + .validate() + .map_err(|error| CliError::Usage(format!("invalid world view binding: {error}")))?; + let builder = buzz_sdk::build_set_world_view_bindings( + channel_uuid, + expected_revision.as_deref(), + &document, + ) + .map_err(|error| CliError::Other(format!("build world view bindings event: {error}")))?; + let event = client.sign_event(builder)?; + let revision_event_id = event.id.to_hex(); + let relay_response = client.submit_event(event).await?; + let mut next_resolve_command = format!("buzz world-views resolve --channel {channel_id}"); + if let Some(thread_root_event_id) = scope.thread_root_event_id() { + next_resolve_command.push_str(" --thread-root "); + next_resolve_command.push_str(thread_root_event_id); + } + next_resolve_command.push_str(" --binding "); + next_resolve_command.push_str(&binding_id.to_string()); + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "binding": binding, + "revisionEventId": revision_event_id, + "sourceRevision": live_share + .as_ref() + .map(|share| share.source_revision.as_str()) + .unwrap_or(catalog.revision.as_str()), + "nextReadCommand": read_command(channel_id, &scope), + "nextResolveCommand": next_resolve_command, + "relayResponse": serde_json::from_str::(&relay_response) + .unwrap_or_else(|_| serde_json::Value::String(relay_response)), + })) + .map_err(|error| CliError::Other(format!("encode bound world view: {error}")))? + ); + Ok(()) +} + +struct ScopedWorldViewResolution { + selected: EffectiveWorldViewBinding, + registry: WorldAuthorityRegistry, + resolved: ResolvedWorldView, +} + +async fn resolve_scoped_world_view( + client: &BuzzClient, + channel_id: &str, + thread_root: Option<&str>, + binding_id: Option<&str>, +) -> Result { + let effective_scope = exact_scope(thread_root)?; + let channel = fetch_snapshot(client, channel_id, &WorldViewBindingScope::Channel).await?; + let thread = match &effective_scope { + WorldViewBindingScope::Channel => None, + WorldViewBindingScope::Thread { .. } => { + Some(fetch_snapshot(client, channel_id, &effective_scope).await?) + } + }; + let effective = effective_world_view_bindings(&channel, thread.as_ref()) + .map_err(|error| CliError::Other(format!("merge effective world views: {error}")))?; + let selected = select_binding(&effective.bindings, binding_id)?.clone(); + let registry = load_world_authority_registry()?; + let resolved = resolve_world_view( + WorldViewResolutionRequest { + channel_id: parse_uuid(channel_id)?, + binding: selected.binding.clone(), + declared_scope: selected.declared_scope.clone(), + effective_scope, + binding_revision_event_id: selected.binding_revision_event_id.clone(), + }, + ®istry, + ) + .await + .map_err(|error| CliError::Other(error.to_string()))?; + Ok(ScopedWorldViewResolution { + selected, + registry, + resolved, + }) +} + +fn resolve_command(channel_id: &str, scope: &WorldViewBindingScope, binding_id: Uuid) -> String { + let mut command = format!("buzz world-views resolve --channel {channel_id}"); + if let Some(thread_root_event_id) = scope.thread_root_event_id() { + command.push_str(" --thread-root "); + command.push_str(thread_root_event_id); + } + command.push_str(" --binding "); + command.push_str(&binding_id.to_string()); + command +} + +async fn cmd_resolve( + client: &BuzzClient, + channel_id: &str, + thread_root: Option<&str>, + binding_id: Option<&str>, +) -> Result<(), CliError> { + let resolution = resolve_scoped_world_view(client, channel_id, thread_root, binding_id).await?; + println!( + "{}", + serde_json::to_string_pretty(&resolution.resolved).map_err(|error| { + CliError::Other(format!("encode resolved Shivai world view: {error}")) + })? + ); + Ok(()) +} + +async fn execute_scoped_world_script( + client: &BuzzClient, + channel_id: &str, + expected_revision: &str, + authority_grant: &str, + script: &str, + resolution: ScopedWorldViewResolution, + world_binary: Option<&std::path::Path>, +) -> Result { + let next_resolve_command = resolve_command( + channel_id, + &resolution.resolved.effective_scope, + resolution.selected.binding.id, + ); + if resolution.resolved.source_revision != expected_revision { + return Err(CliError::Conflict(format!( + "world revision changed: expected {expected_revision}, current {}; no mutation was attempted; refresh with `{next_resolve_command}`", + resolution.resolved.source_revision + ))); + } + let mutation_authority = match &resolution.resolved.authority { + WorldViewResolutionAuthority::LocalWorldMirrorLatest { origin, mirror_id } => { + WorldMutationAuthority::LocalWorldMirrorLatest { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + } + } + WorldViewResolutionAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } + | WorldViewResolutionAuthority::HostedWorldLiveViewShare { + origin, + hosted_world_id, + } => WorldMutationAuthority::HostedWorldLatest { + origin: origin.clone(), + hosted_world_id: hosted_world_id.clone(), + }, + WorldViewResolutionAuthority::HostedWorldViewExport { .. } => { + return Err(CliError::Usage( + "selected world view is read-only on this client".into(), + )); + } + }; + let channel_uuid = parse_uuid(channel_id)?; + let delegation = resolution + .registry + .resolve_mutation_delegation( + channel_uuid, + &resolution.selected.declared_scope, + resolution.selected.binding.id, + &resolution.selected.binding_revision_event_id, + ) + .ok_or_else(|| { + CliError::Auth("agent edits are not enabled for the selected world view binding".into()) + })?; + if delegation.authority != mutation_authority { + return Err(CliError::Auth( + "the selected world view binding no longer matches its agent mutation consent".into(), + )); + } + let authority_secret_file = match &mutation_authority { + WorldMutationAuthority::LocalWorldMirrorLatest { origin, mirror_id } => resolution + .registry + .resolve_local(origin, mirror_id) + .map(|authority| authority.capability_secret_file.as_str()), + WorldMutationAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } => resolution + .registry + .resolve_hosted(origin, hosted_world_id) + .map(|authority| authority.credential_file.as_str()), + } + .ok_or_else(|| { + CliError::Auth( + "no machine-local mutation authority is registered for the selected world view".into(), + ) + })?; + let actor_pubkey = client.keys().public_key().to_hex(); + let authority_secret = + zeroize::Zeroizing::new(std::fs::read(authority_secret_file).map_err(|_| { + CliError::Auth( + "registered machine-local mutation authority is unavailable; reconnect the world" + .into(), + ) + })?); + let grant_scope = WorldAuthorityGrantScope { + agent_pubkey: actor_pubkey.clone(), + channel_id: channel_uuid, + effective_scope: resolution.resolved.effective_scope.clone(), + binding_id: resolution.selected.binding.id, + binding_revision_event_id: resolution.selected.binding_revision_event_id.clone(), + source_revision: expected_revision.to_owned(), + }; + verify_world_authority_grant( + authority_grant, + &authority_secret, + &grant_scope, + chrono::Utc::now().timestamp(), + ) + .map_err(|error| { + CliError::Auth(format!( + "{error}; refresh with `{next_resolve_command}` to obtain the current scoped command" + )) + })?; + let apply_result = match &mutation_authority { + WorldMutationAuthority::LocalWorldMirrorLatest { origin, mirror_id } => { + let authority = resolution + .registry + .resolve_local(origin, mirror_id) + .expect("validated local mutation authority"); + match world_binary { + Some(binary) => { + apply_local_world_script_with_binary( + origin, + mirror_id, + &authority.source_root, + expected_revision, + script, + binary, + ) + .await + } + None => { + apply_local_world_script( + origin, + mirror_id, + &authority.source_root, + expected_revision, + script, + ) + .await + } + } + } + WorldMutationAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } => { + let authority = resolution + .registry + .resolve_hosted(origin, hosted_world_id) + .expect("validated hosted mutation authority"); + match world_binary { + Some(binary) => { + apply_hosted_world_script_with_binary( + origin, + hosted_world_id, + &authority.credential_file, + expected_revision, + script, + binary, + ) + .await + } + None => { + apply_hosted_world_script( + origin, + hosted_world_id, + &authority.credential_file, + expected_revision, + script, + ) + .await + } + } + } + }; + let world_result = apply_result.map_err(|error| { + if matches!(&error, WorldViewResolutionError::RevisionConflict(_)) { + CliError::Conflict(error.to_string()) + } else { + CliError::Other(error.to_string()) + } + })?; + let source_revision = world_result + .pointer("/result/revision") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| CliError::Other("world script result omitted its revision".into()))? + .to_owned(); + Ok(serde_json::json!({ + "command": "world-views script", + "actorPubkey": actor_pubkey, + "channelId": channel_id, + "effectiveScope": resolution.resolved.effective_scope, + "bindingId": resolution.selected.binding.id, + "bindingRevisionEventId": resolution.selected.binding_revision_event_id, + "sourceRevision": source_revision, + "nextResolveCommand": next_resolve_command, + "worldResult": world_result, + })) +} + +async fn cmd_script( + client: &BuzzClient, + channel_id: &str, + thread_root: Option<&str>, + binding_id: &str, + expected_revision: &str, + authority_grant: &str, + script_arg: &str, +) -> Result<(), CliError> { + let expected_revision = parse_expected_revision(expected_revision)?.ok_or_else(|| { + CliError::Usage( + "--expected-revision for `world-views script` must be 64 lowercase hex characters" + .into(), + ) + })?; + let script = read_or_stdin(script_arg)?; + if script.trim().is_empty() { + return Err(CliError::Usage("--script must not be blank".into())); + } + let resolution = + resolve_scoped_world_view(client, channel_id, thread_root, Some(binding_id)).await?; + let result = execute_scoped_world_script( + client, + channel_id, + &expected_revision, + authority_grant, + &script, + resolution, + None, + ) + .await?; + println!( + "{}", + serde_json::to_string_pretty(&result).map_err(|error| { + CliError::Other(format!("encode hosted world script result: {error}")) + })? + ); + Ok(()) +} + +fn select_binding<'a>( + bindings: &'a [EffectiveWorldViewBinding], + binding_id: Option<&str>, +) -> Result<&'a EffectiveWorldViewBinding, CliError> { + if let Some(binding_id) = binding_id { + let id = Uuid::parse_str(binding_id) + .map_err(|_| CliError::Usage(format!("invalid binding UUID: {binding_id}")))?; + return bindings + .iter() + .find(|entry| entry.binding.id == id) + .ok_or_else(|| CliError::Usage(format!("unknown world view binding: {binding_id}"))); + } + match bindings { + [binding] => Ok(binding), + [] => Err(CliError::Usage( + "channel has no world view bindings to resolve".into(), + )), + _ => Err(CliError::Usage( + "channel has multiple world views; pass --binding ".into(), + )), + } +} + +pub async fn dispatch(cmd: WorldViewsCmd, client: &BuzzClient) -> Result<(), CliError> { + match cmd { + WorldViewsCmd::Get { + channel, + thread_root, + } => cmd_get(client, &channel, thread_root.as_deref()).await, + WorldViewsCmd::Set { + channel, + document, + expected_revision, + } => cmd_set(client, &channel, &document, &expected_revision).await, + WorldViewsCmd::Sources => cmd_sources(), + WorldViewsCmd::TrustOrigin { origin } => cmd_set_origin_trust(&origin, true), + WorldViewsCmd::RevokeOriginTrust { origin } => cmd_set_origin_trust(&origin, false), + WorldViewsCmd::Catalog { source, origin } => cmd_catalog(&source, &origin).await, + WorldViewsCmd::Bind { + channel, + thread_root, + source, + origin, + view, + label, + display, + binding, + expected_revision, + } => { + cmd_bind( + client, + &channel, + thread_root.as_deref(), + &source, + &origin, + &view, + label.as_deref(), + &display, + binding.as_deref(), + &expected_revision, + ) + .await + } + WorldViewsCmd::Resolve { + channel, + thread_root, + binding, + } => cmd_resolve(client, &channel, thread_root.as_deref(), binding.as_deref()).await, + WorldViewsCmd::Script { + channel, + thread_root, + binding, + expected_revision, + script, + grant, + } => { + cmd_script( + client, + &channel, + thread_root.as_deref(), + &binding, + &expected_revision, + &grant, + &script, + ) + .await + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use buzz_core::world_view::{ + issue_world_authority_grant, HostedWorldAuthority, LocalWorldAuthority, + WorldAuthorityGrantScope, WorldMutationAuthority, WorldViewDisplayMode, + WorldViewMutationDelegation, WorldViewReference, + }; + + fn binding(id: Uuid) -> WorldViewBinding { + WorldViewBinding { + id, + label: None, + reference: WorldViewReference::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }, + realm_qualified_name: "world::main".into(), + view_qualified_name: "world::main::@Board".into(), + display_mode: WorldViewDisplayMode::Graph, + } + } + + fn effective(binding: WorldViewBinding) -> EffectiveWorldViewBinding { + EffectiveWorldViewBinding { + binding, + declared_scope: WorldViewBindingScope::Channel, + binding_revision_event_id: "a".repeat(64), + } + } + + #[test] + fn requires_an_id_when_multiple_bindings_exist() { + let bindings = [ + effective(binding(Uuid::nil())), + effective(binding(Uuid::new_v4())), + ]; + let error = select_binding(&bindings, None).expect_err("ambiguous"); + assert!(error.to_string().contains("--binding")); + } + + #[test] + fn selects_one_binding_without_extra_choreography() { + let expected = effective(binding(Uuid::nil())); + assert_eq!( + select_binding(std::slice::from_ref(&expected), None).unwrap(), + &expected + ); + } + + fn hosted_resolution( + credential_file: &std::path::Path, + source_revision: &str, + ) -> ScopedWorldViewResolution { + let channel_id = Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").unwrap(); + let binding_id = Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap(); + let binding_revision = "a".repeat(64); + let binding = WorldViewBinding { + id: binding_id, + label: Some("Hosted board".into()), + reference: WorldViewReference::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }, + realm_qualified_name: "world::main".into(), + view_qualified_name: "world::main::@Board".into(), + display_mode: WorldViewDisplayMode::Graph, + }; + let presentation_model = serde_json::json!({ + "graph": { "kind": "empty", "reason": "no-preferences" }, + "revision": source_revision, + "selection": { + "realmQualifiedName": "world::main", + "viewQualifiedName": "world::main::@Board" + } + }); + let resolved = serde_json::from_value(serde_json::json!({ + "formatVersion": 1, + "bindingId": binding_id, + "channelId": channel_id, + "declaredScope": { "kind": "channel" }, + "effectiveScope": { "kind": "channel" }, + "bindingRevisionEventId": binding_revision, + "sourceRevision": source_revision, + "freshness": "latest-at-resolution", + "authority": { + "kind": "hosted-world-latest", + "origin": "https://manifest.shivai.space", + "hostedWorldId": "hosted-1" + }, + "realm": { "name": "main", "qualifiedName": "world::main" }, + "view": { "name": "Board", "qualifiedName": "world::main::@Board" }, + "viewDump": { + "counts": { + "nodes": 0, + "edges": 0, + "ready": 0, + "actionableReady": 0, + "satisfied": 0, + "blocked": 0 + }, + "nodes": [], + "readyLeaves": [], + "satisfiedNodes": [], + "blockedNodes": [], + "edges": [] + }, + "presentation": { + "formatVersion": 1, + "dark": presentation_model, + "light": presentation_model + }, + "resolvedAt": "2026-07-26T00:00:00Z", + "nextCommand": format!( + "buzz world-views resolve --channel {channel_id} --binding {binding_id}" + ) + })) + .expect("decode hosted resolution fixture"); + ScopedWorldViewResolution { + selected: EffectiveWorldViewBinding { + binding, + declared_scope: WorldViewBindingScope::Channel, + binding_revision_event_id: binding_revision.clone(), + }, + registry: WorldAuthorityRegistry { + version: buzz_core::world_view::WORLD_AUTHORITY_REGISTRY_VERSION, + trusted_origins: vec!["https://manifest.shivai.space".into()], + local_authorities: Vec::new(), + hosted_authorities: vec![HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: credential_file.to_string_lossy().into_owned(), + }], + mutation_delegations: vec![WorldViewMutationDelegation { + channel_id, + declared_scope: WorldViewBindingScope::Channel, + binding_id, + binding_revision_event_id: binding_revision, + authority: WorldMutationAuthority::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }, + }], + }, + resolved, + } + } + + fn local_resolution( + source_root: &std::path::Path, + capability_secret_file: &std::path::Path, + source_revision: &str, + ) -> ScopedWorldViewResolution { + let mut resolution = hosted_resolution(capability_secret_file, source_revision); + let origin = "https://manifest.shivai.space".to_owned(); + let mirror_id = "mirror-1".to_owned(); + resolution.selected.binding.reference = WorldViewReference::LocalWorldMirrorLatest { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + }; + resolution.resolved.authority = WorldViewResolutionAuthority::LocalWorldMirrorLatest { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + }; + resolution.registry.hosted_authorities.clear(); + resolution.registry.local_authorities = vec![LocalWorldAuthority { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + source_root: source_root.to_string_lossy().into_owned(), + capability_secret_file: capability_secret_file.to_string_lossy().into_owned(), + }]; + resolution.registry.mutation_delegations[0].authority = + WorldMutationAuthority::LocalWorldMirrorLatest { origin, mirror_id }; + resolution + } + + fn scoped_grant(keys: &nostr::Keys, source_revision: &str) -> String { + scoped_grant_with_secret(keys, source_revision, b"private-edit-share") + } + + fn scoped_grant_with_secret( + keys: &nostr::Keys, + source_revision: &str, + authority_secret: &[u8], + ) -> String { + issue_world_authority_grant( + &WorldAuthorityGrantScope { + agent_pubkey: keys.public_key().to_hex(), + channel_id: Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").unwrap(), + effective_scope: WorldViewBindingScope::Channel, + binding_id: Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap(), + binding_revision_event_id: "a".repeat(64), + source_revision: source_revision.into(), + }, + authority_secret, + chrono::Utc::now().timestamp() + 60, + ) + .expect("issue scoped grant") + } + + #[cfg(unix)] + fn fake_world_binary( + root: &std::path::Path, + current_revision: &str, + next_revision: &str, + credential_file: &std::path::Path, + ) -> (std::path::PathBuf, std::path::PathBuf) { + use std::os::unix::fs::PermissionsExt; + + let latest_output = root.join("latest.json"); + std::fs::write( + &latest_output, + serde_json::to_vec(&serde_json::json!({ + "ok": true, + "result": { + "projection": { "hostedWorldId": "hosted-1" }, + "revision": current_revision, + } + })) + .unwrap(), + ) + .unwrap(); + let script_output = root.join("script.json"); + std::fs::write( + &script_output, + serde_json::to_vec(&serde_json::json!({ + "ok": true, + "result": { + "command": "hosted script", + "credentialPathDiagnostic": credential_file, + "revision": next_revision, + } + })) + .unwrap(), + ) + .unwrap(); + let mutation_marker = root.join("mutation-ran"); + let binary = root.join("world"); + std::fs::write( + &binary, + format!( + "#!/bin/sh\n\ + if [ \"$1\" = hosted ] && [ \"$2\" = latest ]; then\n\ + cat '{}'\n\ + exit 0\n\ + fi\n\ + if [ \"$1\" = hosted ] && [ \"$2\" = script ]; then\n\ + cat >/dev/null\n\ + touch '{}'\n\ + cat '{}'\n\ + exit 0\n\ + fi\n\ + exit 42\n", + latest_output.display(), + mutation_marker.display(), + script_output.display(), + ), + ) + .unwrap(); + let mut permissions = std::fs::metadata(&binary).unwrap().permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary, permissions).unwrap(); + (binary, mutation_marker) + } + + #[cfg(unix)] + fn fake_local_world_binary( + root: &std::path::Path, + next_revision: &str, + source_root: &std::path::Path, + ) -> (std::path::PathBuf, std::path::PathBuf) { + use std::os::unix::fs::PermissionsExt; + + let script_output = root.join("local-script.json"); + std::fs::write( + &script_output, + serde_json::to_vec(&serde_json::json!({ + "ok": true, + "result": { + "command": "script", + "sourceRootDiagnostic": source_root, + "revision": next_revision, + } + })) + .unwrap(), + ) + .unwrap(); + let mutation_marker = root.join("local-mutation-ran"); + let binary = root.join("local-world"); + std::fs::write( + &binary, + format!( + "#!/bin/sh\n\ + if [ \"$1\" = script ]; then\n\ + cat >/dev/null\n\ + touch '{}'\n\ + cat '{}'\n\ + exit 0\n\ + fi\n\ + exit 42\n", + mutation_marker.display(), + script_output.display(), + ), + ) + .unwrap(); + let mut permissions = std::fs::metadata(&binary).unwrap().permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary, permissions).unwrap(); + (binary, mutation_marker) + } + #[cfg(unix)] + #[tokio::test] + async fn scoped_hosted_script_applies_without_exposing_private_authority() { + let root = tempfile::tempdir().unwrap(); + let credential_file = root.path().join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share").unwrap(); + let current_revision = "b".repeat(64); + let next_revision = "c".repeat(64); + let keys = nostr::Keys::generate(); + let client = + BuzzClient::new("http://127.0.0.1:1".into(), keys.clone(), None, None).unwrap(); + let grant = scoped_grant(&keys, ¤t_revision); + let (binary, mutation_marker) = fake_world_binary( + root.path(), + ¤t_revision, + &next_revision, + &credential_file, + ); + + let result = execute_scoped_world_script( + &client, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + ¤t_revision, + &grant, + "world add --disconnected \"Broker triage\"", + hosted_resolution(&credential_file, ¤t_revision), + Some(&binary), + ) + .await + .expect("apply through scoped broker"); + + assert!(mutation_marker.exists()); + assert_eq!( + result + .get("sourceRevision") + .and_then(serde_json::Value::as_str), + Some(next_revision.as_str()) + ); + let encoded = serde_json::to_string(&result).unwrap(); + assert!(!encoded.contains(&grant)); + assert!(!encoded.contains("private-edit-share")); + assert!(!encoded.contains(&credential_file.to_string_lossy().to_string())); + assert!(encoded.contains("")); + } + + #[cfg(unix)] + #[tokio::test] + async fn scoped_world_script_requires_current_device_consent() { + let root = tempfile::tempdir().unwrap(); + let credential_file = root.path().join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share").unwrap(); + let current_revision = "b".repeat(64); + let keys = nostr::Keys::generate(); + let client = + BuzzClient::new("http://127.0.0.1:1".into(), keys.clone(), None, None).unwrap(); + let grant = scoped_grant(&keys, ¤t_revision); + let (binary, mutation_marker) = fake_world_binary( + root.path(), + ¤t_revision, + &"c".repeat(64), + &credential_file, + ); + let mut resolution = hosted_resolution(&credential_file, ¤t_revision); + resolution.registry.mutation_delegations.clear(); + + let error = execute_scoped_world_script( + &client, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + ¤t_revision, + &grant, + "world add --disconnected \"No consent\"", + resolution, + Some(&binary), + ) + .await + .expect_err("missing device consent must fail"); + + assert!(matches!(error, CliError::Auth(_))); + assert!(error.to_string().contains("agent edits are not enabled")); + assert!(!mutation_marker.exists()); + } + + #[cfg(unix)] + #[tokio::test] + async fn scoped_local_script_applies_without_exposing_private_authority() { + let root = tempfile::tempdir().unwrap(); + let source_root = root.path().join("private.world"); + let capability_secret_file = root.path().join("local.capability"); + let capability_secret = b"private-local-capability"; + std::fs::write(&capability_secret_file, capability_secret).unwrap(); + let current_revision = "b".repeat(64); + let next_revision = "c".repeat(64); + let keys = nostr::Keys::generate(); + let client = + BuzzClient::new("http://127.0.0.1:1".into(), keys.clone(), None, None).unwrap(); + let grant = scoped_grant_with_secret(&keys, ¤t_revision, capability_secret); + let (binary, mutation_marker) = + fake_local_world_binary(root.path(), &next_revision, &source_root); + + let result = execute_scoped_world_script( + &client, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + ¤t_revision, + &grant, + "world add --disconnected \"Local broker triage\"", + local_resolution(&source_root, &capability_secret_file, ¤t_revision), + Some(&binary), + ) + .await + .expect("apply local world through scoped broker"); + + assert!(mutation_marker.exists()); + assert_eq!( + result + .get("sourceRevision") + .and_then(serde_json::Value::as_str), + Some(next_revision.as_str()) + ); + let encoded = serde_json::to_string(&result).unwrap(); + assert!(!encoded.contains(&grant)); + assert!(!encoded.contains("private-local-capability")); + assert!(!encoded.contains(&source_root.to_string_lossy().to_string())); + assert!(!encoded.contains(&capability_secret_file.to_string_lossy().to_string())); + assert!(encoded.contains("")); + } + + #[cfg(unix)] + #[tokio::test] + async fn scoped_hosted_script_rejects_stale_revision_before_mutation() { + let root = tempfile::tempdir().unwrap(); + let credential_file = root.path().join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share").unwrap(); + let stale_revision = "b".repeat(64); + let current_revision = "c".repeat(64); + let keys = nostr::Keys::generate(); + let client = + BuzzClient::new("http://127.0.0.1:1".into(), keys.clone(), None, None).unwrap(); + let grant = scoped_grant(&keys, &stale_revision); + let (binary, mutation_marker) = fake_world_binary( + root.path(), + ¤t_revision, + &"d".repeat(64), + &credential_file, + ); + + let error = execute_scoped_world_script( + &client, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + &stale_revision, + &grant, + "world add --disconnected \"Stale broker triage\"", + hosted_resolution(&credential_file, ¤t_revision), + Some(&binary), + ) + .await + .expect_err("stale broker request must fail"); + + assert!(matches!(error, CliError::Conflict(_))); + assert!(error.to_string().contains(¤t_revision)); + assert!(!mutation_marker.exists()); + } + + #[cfg(unix)] + #[tokio::test] + async fn scoped_hosted_script_rejects_another_agent_grant() { + let root = tempfile::tempdir().unwrap(); + let credential_file = root.path().join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share").unwrap(); + let current_revision = "b".repeat(64); + let grant_owner = nostr::Keys::generate(); + let caller = nostr::Keys::generate(); + let client = BuzzClient::new("http://127.0.0.1:1".into(), caller, None, None).unwrap(); + let grant = scoped_grant(&grant_owner, ¤t_revision); + let (binary, mutation_marker) = fake_world_binary( + root.path(), + ¤t_revision, + &"c".repeat(64), + &credential_file, + ); + + let error = execute_scoped_world_script( + &client, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + ¤t_revision, + &grant, + "world add --disconnected \"Wrong agent\"", + hosted_resolution(&credential_file, ¤t_revision), + Some(&binary), + ) + .await + .expect_err("another agent must not reuse the grant"); + + assert!(matches!(error, CliError::Auth(_))); + assert!(error.to_string().contains("does not match this request")); + assert!(!mutation_marker.exists()); + } +} diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs index 6ab81a082d..8070f18fb5 100644 --- a/crates/buzz-cli/src/lib.rs +++ b/crates/buzz-cli/src/lib.rs @@ -185,6 +185,9 @@ enum Cmd { /// Get and set channel canvas documents #[command(subcommand)] Canvas(CanvasCmd), + /// Bind, inspect, and resolve Shivai world views for channels + #[command(subcommand, name = "world-views")] + WorldViews(WorldViewsCmd), /// Add, remove, and list emoji reactions #[command(subcommand)] Reactions(ReactionsCmd), @@ -694,6 +697,117 @@ pub enum CanvasCmd { }, } +#[derive(Subcommand)] +pub enum WorldViewsCmd { + /// Get the ordered world view bindings document for a channel + Get { + /// Channel UUID + #[arg(long)] + channel: String, + /// Exact thread-root event id; omit for channel declarations + #[arg(long)] + thread_root: Option, + }, + /// Replace the ordered world view bindings document for a channel + Set { + /// Channel UUID + #[arg(long)] + channel: String, + /// Versioned bindings JSON (use '-' to read from stdin) + #[arg(long)] + document: String, + /// Current event revision, or the literal `none` for first creation + #[arg(long)] + expected_revision: String, + }, + /// List connected local and hosted world sources without exposing credentials + Sources, + /// Trust one canonical Shivai origin for public world-view resolution + TrustOrigin { + /// Exact HTTPS origin to trust + #[arg(long)] + origin: String, + }, + /// Revoke public world-view resolution trust for one origin + RevokeOriginTrust { + /// Exact HTTPS origin whose trust should be revoked + #[arg(long)] + origin: String, + }, + /// List authored views for one connected source + Catalog { + /// Source id from `buzz world-views sources` (`hosted:` or `local:`) + #[arg(long)] + source: String, + /// Exact Shivai origin from `buzz world-views sources` + #[arg(long)] + origin: String, + }, + /// Bind one connected source and authored view with revision protection + Bind { + /// Channel UUID + #[arg(long)] + channel: String, + /// Exact thread-root event id; omit for a channel binding + #[arg(long)] + thread_root: Option, + /// Source id from `buzz world-views sources` (`hosted:` or `local:`) + #[arg(long)] + source: String, + /// Exact Shivai origin from `buzz world-views sources` + #[arg(long)] + origin: String, + /// Qualified authored view name from `buzz world-views catalog` + #[arg(long)] + view: String, + /// Optional channel-authored label + #[arg(long)] + label: Option, + /// Initial presentation: graph or tasks + #[arg(long, default_value = "graph")] + display: String, + /// Existing binding UUID to replace; omit to append or reuse an exact match + #[arg(long)] + binding: Option, + /// Current event revision, or the literal `none` for first creation + #[arg(long)] + expected_revision: String, + }, + /// Resolve one binding into current normalized Shivai presentation JSON + Resolve { + /// Channel UUID + #[arg(long)] + channel: String, + /// Exact thread-root event id; omit to resolve channel declarations + #[arg(long)] + thread_root: Option, + /// Binding UUID; optional only when the channel has exactly one binding + #[arg(long)] + binding: Option, + }, + /// Apply one revision-checked script through host-owned authority + Script { + /// Channel UUID whose effective binding authorizes the target + #[arg(long)] + channel: String, + /// Exact thread-root event id; omit for a channel-scoped operation + #[arg(long)] + thread_root: Option, + /// Exact binding UUID; resolved by the host instead of the agent + #[arg(long)] + binding: String, + /// Current hosted package revision from `world-views resolve` + #[arg(long)] + expected_revision: String, + /// WorldLang command document (use '-' to read from stdin) + #[arg(long)] + script: String, + /// Opaque agent/channel/binding/revision grant from the host prompt + #[arg(long)] + grant: String, + }, +} + #[derive(Subcommand)] pub enum ReactionsCmd { /// Add an emoji reaction to a message @@ -1772,6 +1886,7 @@ async fn run(cli: Cli) -> Result<(), CliError> { Cmd::Messages(sub) => commands::messages::dispatch(sub, &client, &cli.format).await, Cmd::Channels(sub) => commands::channels::dispatch(sub, &client, &cli.format).await, Cmd::Canvas(sub) => commands::channels::dispatch_canvas(sub, &client).await, + Cmd::WorldViews(sub) => commands::world_views::dispatch(sub, &client).await, Cmd::Reactions(sub) => commands::reactions::dispatch(sub, &client).await, Cmd::Emoji(sub) => commands::emoji::dispatch(sub, &client).await, Cmd::Dms(sub) => commands::dms::dispatch(sub, &client).await, @@ -1827,6 +1942,7 @@ mod tests { "upload", "users", "workflows", + "world-views", ]; let cmd = Cli::command(); diff --git a/crates/buzz-core/src/kind.rs b/crates/buzz-core/src/kind.rs index afec52305a..498d2f2aa9 100644 --- a/crates/buzz-core/src/kind.rs +++ b/crates/buzz-core/src/kind.rs @@ -433,6 +433,8 @@ pub const KIND_STREAM_REMINDER: u32 = 40007; pub const KIND_STREAM_MESSAGE_DIFF: u32 = 40008; /// Canvas (shared document) for a channel. pub const KIND_CANVAS: u32 = 40100; +/// Ordered Shivai world view bindings for a channel. +pub const KIND_WORLD_VIEW_BINDINGS: u32 = 40101; /// System message for channel state changes (join, leave, rename, etc.). pub const KIND_SYSTEM_MESSAGE: u32 = 40099; @@ -637,6 +639,7 @@ pub const ALL_KINDS: &[u32] = &[ KIND_STREAM_REMINDER, KIND_STREAM_MESSAGE_DIFF, KIND_CANVAS, + KIND_WORLD_VIEW_BINDINGS, KIND_SYSTEM_MESSAGE, KIND_CHANNEL_SUMMARY, KIND_PRESENCE_SNAPSHOT, @@ -803,6 +806,7 @@ const _: () = assert!( // Compile-time: all Buzz kind constants fit in nostr's u16-backed Kind. const _: () = assert!(KIND_AUTH <= u16::MAX as u32); const _: () = assert!(KIND_CANVAS <= u16::MAX as u32); +const _: () = assert!(KIND_WORLD_VIEW_BINDINGS <= u16::MAX as u32); const _: () = assert!(KIND_HUDDLE_GUIDELINES <= u16::MAX as u32); const _: () = assert!(EPHEMERAL_KIND_MIN < EPHEMERAL_KIND_MAX); // Compile-time: KIND_AGENT_TURN_METRIC is a regular stored kind (not ephemeral, not replaceable). diff --git a/crates/buzz-core/src/lib.rs b/crates/buzz-core/src/lib.rs index 66b7708f1d..52e47d14dc 100644 --- a/crates/buzz-core/src/lib.rs +++ b/crates/buzz-core/src/lib.rs @@ -38,6 +38,8 @@ pub mod relay; pub mod tenant; /// Schnorr signature and event ID verification. pub mod verification; +/// Channel-scoped Shivai world view binding contracts. +pub mod world_view; pub use error::VerificationError; pub use event::StoredEvent; diff --git a/crates/buzz-core/src/world_view.rs b/crates/buzz-core/src/world_view.rs new file mode 100644 index 0000000000..9918c8be5a --- /dev/null +++ b/crates/buzz-core/src/world_view.rs @@ -0,0 +1,1659 @@ +//! Channel-scoped Shivai world view binding contracts. + +use std::collections::{HashMap, HashSet}; + +use hmac::{Hmac, KeyInit, Mac}; +use nostr::{Event, EventId}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use uuid::Uuid; + +/// Current serialized scoped world-view binding document version. +pub const WORLD_VIEW_BINDINGS_VERSION: u8 = 4; +/// Maximum number of world views that one channel or thread scope may bind. +pub const MAX_WORLD_VIEW_BINDINGS_PER_SCOPE: usize = 8; +/// Canonical parameterized-replaceable coordinate for channel bindings. +pub const CHANNEL_WORLD_VIEW_BINDINGS_D_TAG: &str = "world-view-bindings:channel"; +/// Current private world-authority registry version. +pub const WORLD_AUTHORITY_REGISTRY_VERSION: u8 = 4; +/// Registry file shared by the desktop host and locally running ACP agents. +pub const WORLD_AUTHORITY_REGISTRY_FILE_NAME: &str = "world-authorities.json"; +/// Private credential directory stored beside the authority registry. +pub const WORLD_AUTHORITY_SECRET_DIRECTORY: &str = "world-authority-secrets"; +/// Version prefix for host-minted, revision-scoped world authority grants. +pub const WORLD_AUTHORITY_GRANT_VERSION: u8 = 1; +/// Short lifetime for prompt-carried grants before a fresh turn must remint. +pub const WORLD_AUTHORITY_GRANT_TTL_SECONDS: i64 = 15 * 60; +/// Shivai's canonical hosted origin, trusted by a fresh Buzz profile. +pub const DEFAULT_SHIVAI_WORLD_ORIGIN: &str = "https://manifest.shivai.space"; + +/// Private machine-local mappings from public world identities to mutation authority. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldAuthorityRegistry { + /// Contract version for explicit forward evolution. + pub version: u8, + /// Hosted origins this device may contact while resolving public bindings. + pub trusted_origins: Vec, + /// Mutable local packages behind public mirror identities. + pub local_authorities: Vec, + /// Private hosted edit-share credentials behind public hosted-world identities. + pub hosted_authorities: Vec, + /// Explicit device-local consent for agents to mutate one bound world. + pub mutation_delegations: Vec, +} + +/// One private local authority mapping. This shape must never be published to Nostr. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct LocalWorldAuthority { + /// Hosted Shivai origin that owns the mirror identity. + pub origin: String, + /// Stable public mirror identity. + pub mirror_id: String, + /// Canonical absolute path of the mutable local `.world` package. + pub source_root: String, + /// Canonical absolute path of the owner-only local mutation grant secret. + pub capability_secret_file: String, +} + +/// One private hosted authority mapping. This shape must never be published to Nostr. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct HostedWorldAuthority { + /// Hosted Shivai origin that owns the world. + pub origin: String, + /// Stable public hosted-world identity. + pub hosted_world_id: String, + /// Canonical absolute path of the owner-only edit-share credential file. + pub credential_file: String, +} + +/// Credential-free identity of one mutable world source. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub enum WorldMutationAuthority { + /// Mutable local package behind a public mirror. + LocalWorldMirrorLatest { + /// Hosted Shivai origin that owns the mirror identity. + origin: String, + /// Stable public mirror identity. + #[serde(rename = "mirrorId")] + mirror_id: String, + }, + /// Mutable hosted world behind a private edit-share capability. + HostedWorldLatest { + /// Hosted Shivai origin that owns the world. + origin: String, + /// Stable public hosted-world identity. + #[serde(rename = "hostedWorldId")] + hosted_world_id: String, + }, +} + +/// Explicit device-local consent for agents in one binding scope to mutate its world. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewMutationDelegation { + /// Channel containing the effective binding. + pub channel_id: Uuid, + /// Exact declaration scope that owns the binding. + pub declared_scope: WorldViewBindingScope, + /// Stable public binding identity. + pub binding_id: Uuid, + /// Exact Nostr event revision that published this binding declaration. + pub binding_revision_event_id: String, + /// Mutable world authority agents may exercise through a scoped host grant. + pub authority: WorldMutationAuthority, +} + +impl Default for WorldAuthorityRegistry { + fn default() -> Self { + Self { + version: WORLD_AUTHORITY_REGISTRY_VERSION, + trusted_origins: vec![DEFAULT_SHIVAI_WORLD_ORIGIN.into()], + local_authorities: Vec::new(), + hosted_authorities: Vec::new(), + mutation_delegations: Vec::new(), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct WorldAuthorityRegistryV3 { + version: u8, + local_authorities: Vec, + hosted_authorities: Vec, + mutation_delegations: Vec, +} + +/// Decoded registry plus whether the caller must persist an explicit migration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodedWorldAuthorityRegistry { + /// Validated current registry. + pub registry: WorldAuthorityRegistry, + /// Whether the decoded predecessor must be written back at the current version. + pub migrated: bool, +} + +/// Decode the current private registry or explicitly migrate its immediate predecessor. +pub fn decode_world_authority_registry( + value: serde_json::Value, +) -> Result { + let version = value + .get("version") + .and_then(serde_json::Value::as_u64) + .ok_or_else(|| "world authority registry is missing a numeric version".to_string())?; + let (registry, migrated) = match version { + 3 => { + let legacy: WorldAuthorityRegistryV3 = serde_json::from_value(value) + .map_err(|error| format!("invalid v3 world authority registry: {error}"))?; + if legacy.version != 3 { + return Err(format!( + "unsupported world authority registry version: {}", + legacy.version + )); + } + let trusted_origins = + derive_trusted_world_origins(&legacy.local_authorities, &legacy.hosted_authorities); + ( + WorldAuthorityRegistry { + version: WORLD_AUTHORITY_REGISTRY_VERSION, + trusted_origins, + local_authorities: legacy.local_authorities, + hosted_authorities: legacy.hosted_authorities, + mutation_delegations: legacy.mutation_delegations, + }, + true, + ) + } + current if current == u64::from(WORLD_AUTHORITY_REGISTRY_VERSION) => ( + serde_json::from_value(value) + .map_err(|error| format!("invalid world authority registry: {error}"))?, + false, + ), + _ => { + return Err(format!( + "unsupported world authority registry version: {version}" + )); + } + }; + registry.validate()?; + Ok(DecodedWorldAuthorityRegistry { registry, migrated }) +} + +/// Derive explicit trust while migrating registries whose authorities implied admission. +pub fn derive_trusted_world_origins( + local_authorities: &[LocalWorldAuthority], + hosted_authorities: &[HostedWorldAuthority], +) -> Vec { + let mut origins = vec![DEFAULT_SHIVAI_WORLD_ORIGIN.to_owned()]; + origins.extend( + local_authorities + .iter() + .map(|authority| authority.origin.clone()), + ); + origins.extend( + hosted_authorities + .iter() + .map(|authority| authority.origin.clone()), + ); + origins.sort(); + origins.dedup(); + origins +} + +impl WorldAuthorityRegistry { + /// Validate registry identities, paths, and one-to-one mapping invariants. + pub fn validate(&self) -> Result<(), String> { + if self.version != WORLD_AUTHORITY_REGISTRY_VERSION { + return Err(format!( + "unsupported world authority registry version: {}", + self.version + )); + } + let mut trusted_origins = HashSet::with_capacity(self.trusted_origins.len()); + for origin in &self.trusted_origins { + validate_hosted_origin(origin)?; + if !trusted_origins.insert(origin) { + return Err(format!("duplicate trusted world origin: {origin}")); + } + } + + let mut local_references = HashSet::with_capacity(self.local_authorities.len()); + let mut roots = HashSet::with_capacity(self.local_authorities.len()); + let mut secret_files = + HashSet::with_capacity(self.local_authorities.len() + self.hosted_authorities.len()); + for authority in &self.local_authorities { + validate_hosted_origin(&authority.origin)?; + if !trusted_origins.contains(&authority.origin) { + return Err(format!( + "local world authority origin is not trusted: {}", + authority.origin + )); + } + validate_required_text("mirrorId", &authority.mirror_id, 1024)?; + validate_required_text("sourceRoot", &authority.source_root, 4096)?; + validate_required_text( + "capabilitySecretFile", + &authority.capability_secret_file, + 4096, + )?; + if !std::path::Path::new(&authority.source_root).is_absolute() { + return Err("sourceRoot must be an absolute path".into()); + } + if !std::path::Path::new(&authority.capability_secret_file).is_absolute() { + return Err("capabilitySecretFile must be an absolute path".into()); + } + if !local_references.insert((&authority.origin, &authority.mirror_id)) { + return Err(format!( + "duplicate local world authority: {} {}", + authority.origin, authority.mirror_id + )); + } + if !roots.insert(&authority.source_root) { + return Err(format!( + "duplicate local world source root: {}", + authority.source_root + )); + } + if !secret_files.insert(&authority.capability_secret_file) { + return Err(format!( + "duplicate world authority secret file: {}", + authority.capability_secret_file + )); + } + } + + let mut hosted_references = HashSet::with_capacity(self.hosted_authorities.len()); + for authority in &self.hosted_authorities { + validate_hosted_origin(&authority.origin)?; + if !trusted_origins.contains(&authority.origin) { + return Err(format!( + "hosted world authority origin is not trusted: {}", + authority.origin + )); + } + validate_required_text("hostedWorldId", &authority.hosted_world_id, 1024)?; + validate_required_text("credentialFile", &authority.credential_file, 4096)?; + if !std::path::Path::new(&authority.credential_file).is_absolute() { + return Err("credentialFile must be an absolute path".into()); + } + if !hosted_references.insert((&authority.origin, &authority.hosted_world_id)) { + return Err(format!( + "duplicate hosted world authority: {} {}", + authority.origin, authority.hosted_world_id + )); + } + if !secret_files.insert(&authority.credential_file) { + return Err(format!( + "duplicate world authority secret file: {}", + authority.credential_file + )); + } + } + + let mut delegation_coordinates = HashSet::with_capacity(self.mutation_delegations.len()); + for delegation in &self.mutation_delegations { + delegation.declared_scope.validate()?; + validate_nostr_event_id( + "mutationDelegation.bindingRevisionEventId", + &delegation.binding_revision_event_id, + )?; + if !delegation_coordinates.insert(( + delegation.channel_id, + delegation.declared_scope.d_tag(), + delegation.binding_id, + )) { + return Err(format!( + "duplicate world mutation delegation: {} {} {}", + delegation.channel_id, + delegation.declared_scope.d_tag(), + delegation.binding_id, + )); + } + if !self.mutation_authority_is_registered(&delegation.authority) { + return Err(format!( + "world mutation delegation references an unregistered authority: {:?}", + delegation.authority + )); + } + } + Ok(()) + } + + /// Whether this device has explicitly admitted one canonical hosted origin. + pub fn is_trusted_origin(&self, origin: &str) -> bool { + self.trusted_origins.iter().any(|trusted| trusted == origin) + } + + /// Admit one canonical hosted origin for public world-view resolution. + pub fn trust_origin(&mut self, origin: String) -> Result { + validate_hosted_origin(&origin)?; + if self.is_trusted_origin(&origin) { + return Ok(false); + } + self.trusted_origins.push(origin); + self.trusted_origins.sort(); + self.validate()?; + Ok(true) + } + + /// Remove device-local trust when no connected authority still depends on it. + pub fn revoke_origin_trust(&mut self, origin: &str) -> Result { + if self + .local_authorities + .iter() + .any(|authority| authority.origin == origin) + || self + .hosted_authorities + .iter() + .any(|authority| authority.origin == origin) + { + return Err(format!( + "cannot revoke world origin trust while `{origin}` has connected authority" + )); + } + let previous_len = self.trusted_origins.len(); + self.trusted_origins.retain(|trusted| trusted != origin); + Ok(self.trusted_origins.len() != previous_len) + } + + /// Resolve mutable local authority for one public mirror reference. + pub fn resolve_local(&self, origin: &str, mirror_id: &str) -> Option<&LocalWorldAuthority> { + self.local_authorities + .iter() + .find(|authority| authority.origin == origin && authority.mirror_id == mirror_id) + } + + /// Resolve mutable hosted authority for one public hosted-world reference. + pub fn resolve_hosted( + &self, + origin: &str, + hosted_world_id: &str, + ) -> Option<&HostedWorldAuthority> { + self.hosted_authorities.iter().find(|authority| { + authority.origin == origin && authority.hosted_world_id == hosted_world_id + }) + } + + /// Resolve explicit mutation consent for one exact binding coordinate. + pub fn resolve_mutation_delegation( + &self, + channel_id: Uuid, + declared_scope: &WorldViewBindingScope, + binding_id: Uuid, + binding_revision_event_id: &str, + ) -> Option<&WorldViewMutationDelegation> { + self.mutation_delegations.iter().find(|delegation| { + delegation.channel_id == channel_id + && delegation.declared_scope == *declared_scope + && delegation.binding_id == binding_id + && delegation.binding_revision_event_id == binding_revision_event_id + }) + } + + /// Whether one credential-free mutation identity is connected on this device. + pub fn mutation_authority_is_registered(&self, authority: &WorldMutationAuthority) -> bool { + match authority { + WorldMutationAuthority::LocalWorldMirrorLatest { origin, mirror_id } => { + self.resolve_local(origin, mirror_id).is_some() + } + WorldMutationAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } => self.resolve_hosted(origin, hosted_world_id).is_some(), + } + } + + /// Replace consent for one exact binding coordinate, then validate. + pub fn upsert_mutation_delegation( + &mut self, + delegation: WorldViewMutationDelegation, + ) -> Result<(), String> { + self.mutation_delegations.retain(|candidate| { + candidate.channel_id != delegation.channel_id + || candidate.declared_scope != delegation.declared_scope + || candidate.binding_id != delegation.binding_id + }); + self.mutation_delegations.push(delegation); + self.mutation_delegations.sort_by(|left, right| { + ( + left.channel_id, + left.declared_scope.d_tag(), + left.binding_id, + ) + .cmp(&( + right.channel_id, + right.declared_scope.d_tag(), + right.binding_id, + )) + }); + self.validate() + } + + /// Revoke mutation consent for one exact binding coordinate. + pub fn revoke_mutation_delegation( + &mut self, + channel_id: Uuid, + declared_scope: &WorldViewBindingScope, + binding_id: Uuid, + ) -> bool { + let previous_len = self.mutation_delegations.len(); + self.mutation_delegations.retain(|delegation| { + delegation.channel_id != channel_id + || delegation.declared_scope != *declared_scope + || delegation.binding_id != binding_id + }); + self.mutation_delegations.len() != previous_len + } + + /// Replace local mappings that share either identity or source, then validate. + pub fn upsert_local(&mut self, authority: LocalWorldAuthority) -> Result<(), String> { + self.trust_origin(authority.origin.clone())?; + self.local_authorities.retain(|candidate| { + (candidate.origin != authority.origin || candidate.mirror_id != authority.mirror_id) + && candidate.source_root != authority.source_root + && candidate.capability_secret_file != authority.capability_secret_file + }); + self.local_authorities.push(authority); + self.local_authorities.sort_by(|left, right| { + (&left.origin, &left.mirror_id).cmp(&(&right.origin, &right.mirror_id)) + }); + self.retain_registered_mutation_delegations(); + self.validate() + } + + /// Replace hosted mappings that share either identity or credential, then validate. + pub fn upsert_hosted(&mut self, authority: HostedWorldAuthority) -> Result<(), String> { + self.trust_origin(authority.origin.clone())?; + self.hosted_authorities.retain(|candidate| { + (candidate.origin != authority.origin + || candidate.hosted_world_id != authority.hosted_world_id) + && candidate.credential_file != authority.credential_file + }); + self.hosted_authorities.push(authority); + self.hosted_authorities.sort_by(|left, right| { + (&left.origin, &left.hosted_world_id).cmp(&(&right.origin, &right.hosted_world_id)) + }); + self.retain_registered_mutation_delegations(); + self.validate() + } + + fn retain_registered_mutation_delegations(&mut self) { + let local_authorities = &self.local_authorities; + let hosted_authorities = &self.hosted_authorities; + self.mutation_delegations + .retain(|delegation| match &delegation.authority { + WorldMutationAuthority::LocalWorldMirrorLatest { origin, mirror_id } => { + local_authorities.iter().any(|authority| { + authority.origin == *origin && authority.mirror_id == *mirror_id + }) + } + WorldMutationAuthority::HostedWorldLatest { + origin, + hosted_world_id, + } => hosted_authorities.iter().any(|authority| { + authority.origin == *origin && authority.hosted_world_id == *hosted_world_id + }), + }); + } +} + +/// Exact channel or thread-root scope owned by one binding document. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub enum WorldViewBindingScope { + /// Bindings declared directly on the channel. + Channel, + /// Bindings declared on one canonical thread root. + Thread { + /// Lowercase Nostr event id of the canonical thread root. + #[serde(rename = "threadRootEventId")] + thread_root_event_id: String, + }, +} + +impl WorldViewBindingScope { + /// Construct a validated thread scope. + pub fn thread(thread_root_event_id: impl Into) -> Result { + let scope = Self::Thread { + thread_root_event_id: thread_root_event_id.into(), + }; + scope.validate()?; + Ok(scope) + } + + /// Stable `d` tag used by relay replacement and exact-scope reads. + pub fn d_tag(&self) -> String { + match self { + Self::Channel => CHANNEL_WORLD_VIEW_BINDINGS_D_TAG.into(), + Self::Thread { + thread_root_event_id, + } => format!("world-view-bindings:thread:{thread_root_event_id}"), + } + } + + /// Canonical thread root when this is a thread scope. + pub fn thread_root_event_id(&self) -> Option<&str> { + match self { + Self::Channel => None, + Self::Thread { + thread_root_event_id, + } => Some(thread_root_event_id), + } + } + + /// Validate the serialized scope identity. + pub fn validate(&self) -> Result<(), String> { + if let Self::Thread { + thread_root_event_id, + } = self + { + validate_nostr_event_id("scope.threadRootEventId", thread_root_event_id)?; + } + Ok(()) + } +} + +impl Default for WorldViewBindingScope { + fn default() -> Self { + Self::Channel + } +} + +/// Opaque grant scope signed by the host from private world authority. +/// +/// Every field is immutable input to the signature. A grant therefore cannot +/// be retargeted to another agent, conversation scope, binding revision, or +/// world source revision. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldAuthorityGrantScope { + /// Managed Buzz agent allowed to exercise the grant. + pub agent_pubkey: String, + /// Channel containing the effective binding. + pub channel_id: Uuid, + /// Exact channel or thread scope active for this turn. + pub effective_scope: WorldViewBindingScope, + /// Opaque public binding id resolved by the host. + pub binding_id: Uuid, + /// Nostr event revision that defined the effective binding. + pub binding_revision_event_id: String, + /// Hosted package revision accepted by the mutation. + pub source_revision: String, +} + +impl WorldAuthorityGrantScope { + /// Validate every identity included in a host grant. + pub fn validate(&self) -> Result<(), String> { + validate_nostr_event_id("grant.agentPubkey", &self.agent_pubkey)?; + self.effective_scope.validate()?; + validate_nostr_event_id( + "grant.bindingRevisionEventId", + &self.binding_revision_event_id, + )?; + validate_nostr_event_id("grant.sourceRevision", &self.source_revision) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct WorldAuthorityGrantClaims { + expires_at_unix_seconds: i64, + scope: WorldAuthorityGrantScope, +} + +/// Mint one opaque, expiring grant from private authority material. +/// +/// The token contains only signed public scope data. It never contains the +/// private edit-share value used as the HMAC key. +pub fn issue_world_authority_grant( + scope: &WorldAuthorityGrantScope, + authority_secret: &[u8], + expires_at_unix_seconds: i64, +) -> Result { + scope.validate()?; + if authority_secret.is_empty() { + return Err("world authority secret must not be empty".into()); + } + if expires_at_unix_seconds <= 0 { + return Err("world authority grant expiry must be positive".into()); + } + let payload = serde_json::to_vec(&WorldAuthorityGrantClaims { + expires_at_unix_seconds, + scope: scope.clone(), + }) + .map_err(|error| format!("encode world authority grant: {error}"))?; + let mut mac = Hmac::::new_from_slice(authority_secret) + .map_err(|_| "world authority secret is invalid".to_owned())?; + mac.update(&payload); + let signature = mac.finalize().into_bytes(); + Ok(format!( + "wvg{}.{}.{}", + WORLD_AUTHORITY_GRANT_VERSION, + hex::encode(payload), + hex::encode(signature) + )) +} + +/// Verify that an unexpired opaque host grant authorizes one exact scope. +pub fn verify_world_authority_grant( + token: &str, + authority_secret: &[u8], + expected_scope: &WorldAuthorityGrantScope, + now_unix_seconds: i64, +) -> Result<(), String> { + expected_scope.validate()?; + if authority_secret.is_empty() || token.len() > 16_384 { + return Err("invalid world authority grant".into()); + } + let mut parts = token.split('.'); + let expected_prefix = format!("wvg{}", WORLD_AUTHORITY_GRANT_VERSION); + if parts.next() != Some(expected_prefix.as_str()) { + return Err("invalid world authority grant".into()); + } + let payload = parts + .next() + .ok_or_else(|| "invalid world authority grant".to_owned()) + .and_then(|value| { + hex::decode(value).map_err(|_| "invalid world authority grant".to_owned()) + })?; + let signature = parts + .next() + .ok_or_else(|| "invalid world authority grant".to_owned()) + .and_then(|value| { + hex::decode(value).map_err(|_| "invalid world authority grant".to_owned()) + })?; + if parts.next().is_some() { + return Err("invalid world authority grant".into()); + } + let mut mac = Hmac::::new_from_slice(authority_secret) + .map_err(|_| "invalid world authority grant".to_owned())?; + mac.update(&payload); + mac.verify_slice(&signature) + .map_err(|_| "invalid world authority grant".to_owned())?; + let claims: WorldAuthorityGrantClaims = + serde_json::from_slice(&payload).map_err(|_| "invalid world authority grant".to_owned())?; + claims.scope.validate()?; + if &claims.scope != expected_scope { + return Err("world authority grant does not match this request".into()); + } + if now_unix_seconds >= claims.expires_at_unix_seconds { + return Err("world authority grant expired".into()); + } + Ok(()) +} + +/// One exact-scope document containing every bound Shivai world view. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewBindingsDocument { + /// Contract version for explicit forward evolution. + pub version: u8, + /// Exact channel or thread-root scope represented by this document. + pub scope: WorldViewBindingScope, + /// Ordered views rendered by clients and supplied to agents. + pub bindings: Vec, +} + +/// Exact-scope binding state plus the relay revision needed for the next write. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewBindingsSnapshot { + /// Current document, or an empty document for an absent coordinate. + pub document: WorldViewBindingsDocument, + /// Current event id; `None` means the next write must explicitly expect creation. + pub revision_event_id: Option, + /// Relay event timestamp for the current revision. + pub updated_at: Option, + /// Public key that authored the current revision. + pub author: Option, +} + +impl WorldViewBindingsSnapshot { + /// Construct an absent exact-scope snapshot. + pub fn empty(scope: WorldViewBindingScope) -> Self { + Self { + document: WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope, + bindings: Vec::new(), + }, + revision_event_id: None, + updated_at: None, + author: None, + } + } +} + +/// Structurally decoded state from one already signature-verified bindings event. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodedWorldViewBindingsEvent { + /// Exact channel coordinate carried by the event's sole `h` tag. + pub channel_id: Uuid, + /// Exact-scope state represented by this event. + pub snapshot: WorldViewBindingsSnapshot, + /// Revision that this event expected to replace; `None` means creation. + pub previous_revision_event_id: Option, +} + +/// Decode the canonical signed-event envelope after signature verification. +/// +/// This validates kind, channel/scope coordinates, optimistic revision, thread +/// root tags, and strict JSON content in one source-shaped boundary. Callers at +/// untrusted relay boundaries must verify the event signature before invoking +/// this CPU-cheap structural decoder. +pub fn decode_verified_world_view_bindings_event( + event: &Event, +) -> Result { + if event.kind.as_u16() as u32 != crate::kind::KIND_WORLD_VIEW_BINDINGS { + return Err("world-view bindings event has the wrong kind".into()); + } + + let document: WorldViewBindingsDocument = serde_json::from_str(&event.content) + .map_err(|error| format!("world-view bindings content is invalid: {error}"))?; + document.validate()?; + + let h_tags = exact_event_tags(event, "h"); + if h_tags.len() != 1 || h_tags[0].len() != 2 { + return Err("world-view bindings require one exact channel h tag".into()); + } + let channel_id = Uuid::parse_str(&h_tags[0][1]) + .map_err(|_| "world-view bindings require one exact channel h tag".to_string())?; + + let d_tag = document.scope.d_tag(); + let d_tags = exact_event_tags(event, "d"); + if d_tags.len() != 1 || d_tags[0].len() != 2 || d_tags[0][1] != d_tag { + return Err("world-view bindings d tag does not match document scope".into()); + } + + let previous_tags = exact_event_tags(event, "prev"); + if previous_tags.len() != 1 || previous_tags[0].len() != 2 { + return Err("world-view bindings require one exact prev tag".into()); + } + let previous = &previous_tags[0][1]; + let previous_revision_event_id = if previous.is_empty() { + None + } else { + validate_nostr_event_id("world-view bindings prev tag", previous)?; + Some( + EventId::from_hex(previous) + .map_err(|_| "world-view bindings prev tag is not valid hex".to_string())?, + ) + }; + + let root_event_id = document.scope.thread_root_event_id(); + let e_tags = exact_event_tags(event, "e"); + match root_event_id { + Some(root) + if e_tags.len() != 1 + || e_tags[0].len() != 4 + || e_tags[0][1] != root + || !e_tags[0][2].is_empty() + || e_tags[0][3] != "root" => + { + return Err("thread world-view bindings require one canonical root e tag".into()); + } + None if !e_tags.is_empty() => { + return Err("channel world-view bindings must not carry e tags".into()); + } + Some(_) | None => {} + } + + Ok(DecodedWorldViewBindingsEvent { + channel_id, + snapshot: WorldViewBindingsSnapshot { + document, + revision_event_id: Some(event.id.to_hex()), + updated_at: Some(event.created_at.as_secs()), + author: Some(event.pubkey.to_hex()), + }, + previous_revision_event_id, + }) +} + +/// Decode one already verified event and require an exact requested coordinate. +pub fn world_view_bindings_snapshot_from_verified_event( + event: &Event, + expected_channel_id: Uuid, + expected_scope: &WorldViewBindingScope, +) -> Result { + let decoded = decode_verified_world_view_bindings_event(event)?; + if decoded.channel_id != expected_channel_id { + return Err("world-view bindings event channel did not match its relay coordinate".into()); + } + if &decoded.snapshot.document.scope != expected_scope { + return Err("world-view bindings event scope did not match its relay coordinate".into()); + } + Ok(decoded.snapshot) +} + +fn exact_event_tags<'a>(event: &'a Event, name: &str) -> Vec<&'a [String]> { + event + .tags + .iter() + .map(|tag| tag.as_slice()) + .filter(|parts| parts.first().is_some_and(|part| part == name)) + .collect() +} +/// One effective binding with the exact declaration that currently owns it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EffectiveWorldViewBinding { + /// Bound view selected after applying thread shadowing. + pub binding: WorldViewBinding, + /// Exact channel or thread-root scope that declared this binding. + pub declared_scope: WorldViewBindingScope, + /// Relay revision event that supplied this binding. + pub binding_revision_event_id: String, +} + +/// Effective bindings for one channel turn, with optional thread inheritance. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EffectiveWorldViewBindings { + /// Scope in which the views are being consumed. + pub effective_scope: WorldViewBindingScope, + /// Channel order with same-id thread overrides and new thread views appended. + pub bindings: Vec, + /// Current exact channel document revision, when present. + pub channel_revision_event_id: Option, + /// Current exact thread document revision, when present. + pub thread_revision_event_id: Option, +} + +/// Merge exact channel and thread-root snapshots into one effective declaration. +/// +/// A thread binding with the same stable id replaces its channel declaration +/// in place. New thread bindings append in authored order. +pub fn effective_world_view_bindings( + channel: &WorldViewBindingsSnapshot, + thread: Option<&WorldViewBindingsSnapshot>, +) -> Result { + if channel.document.scope != WorldViewBindingScope::Channel { + return Err("channel inheritance source must declare channel scope".into()); + } + + let effective_scope = thread + .map(|snapshot| snapshot.document.scope.clone()) + .unwrap_or(WorldViewBindingScope::Channel); + let mut bindings = Vec::with_capacity( + channel.document.bindings.len() + + thread + .map(|snapshot| snapshot.document.bindings.len()) + .unwrap_or_default(), + ); + let mut positions = HashMap::with_capacity(channel.document.bindings.len()); + + if let Some(revision_event_id) = channel.revision_event_id.as_ref() { + for binding in &channel.document.bindings { + positions.insert(binding.id, bindings.len()); + bindings.push(EffectiveWorldViewBinding { + binding: binding.clone(), + declared_scope: WorldViewBindingScope::Channel, + binding_revision_event_id: revision_event_id.clone(), + }); + } + } else if !channel.document.bindings.is_empty() { + return Err("channel bindings require a source revision event id".into()); + } + + if let Some(thread) = thread { + if !matches!(thread.document.scope, WorldViewBindingScope::Thread { .. }) { + return Err("thread override source must declare thread scope".into()); + } + if let Some(revision_event_id) = thread.revision_event_id.as_ref() { + for binding in &thread.document.bindings { + let effective = EffectiveWorldViewBinding { + binding: binding.clone(), + declared_scope: thread.document.scope.clone(), + binding_revision_event_id: revision_event_id.clone(), + }; + if let Some(position) = positions.get(&binding.id).copied() { + bindings[position] = effective; + } else { + positions.insert(binding.id, bindings.len()); + bindings.push(effective); + } + } + } else if !thread.document.bindings.is_empty() { + return Err("thread bindings require a source revision event id".into()); + } + } + + Ok(EffectiveWorldViewBindings { + effective_scope, + bindings, + channel_revision_event_id: channel.revision_event_id.clone(), + thread_revision_event_id: thread.and_then(|snapshot| snapshot.revision_event_id.clone()), + }) +} + +/// A single channel-bound Shivai world view. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewBinding { + /// Stable binding identity used by clients when views are reordered or replaced. + pub id: Uuid, + /// Optional channel-authored label shown above the rendered view. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub label: Option, + /// Source authority for resolving the world snapshot. + pub reference: WorldViewReference, + /// Qualified realm name selected inside the world. + pub realm_qualified_name: String, + /// Qualified view name selected inside the realm. + pub view_qualified_name: String, + /// Initial presentation selected by the channel author. + pub display_mode: WorldViewDisplayMode, +} + +/// A supported authority for resolving a bound world view. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub enum WorldViewReference { + /// The latest read-only projection of a stable published local world mirror. + LocalWorldMirrorLatest { + /// Hosted Shivai origin serving the public mirror projection. + origin: String, + /// Stable mirror identity; revisions advance without replacing this binding. + #[serde(rename = "mirrorId")] + mirror_id: String, + }, + /// A read-only hosted world view export shared by bearer token. + HostedWorldViewExport { + /// Hosted Shivai origin serving the public export. + origin: String, + /// Public read-only export token. + #[serde(rename = "shareToken")] + share_token: String, + }, + /// A stable public view capability that follows the hosted world's latest revision. + HostedWorldLiveViewShare { + /// Hosted Shivai origin serving the public live view. + origin: String, + /// Stable public read-only live-view share token. + #[serde(rename = "shareToken")] + share_token: String, + }, + /// The latest projection of a hosted world, authorized privately on each client. + HostedWorldLatest { + /// Hosted Shivai origin serving and mutating the world. + origin: String, + /// Stable public hosted-world identity. + #[serde(rename = "hostedWorldId")] + hosted_world_id: String, + }, +} + +impl WorldViewReference { + /// Canonical hosted origin named by this public reference. + pub fn origin(&self) -> &str { + match self { + Self::LocalWorldMirrorLatest { origin, .. } + | Self::HostedWorldViewExport { origin, .. } + | Self::HostedWorldLiveViewShare { origin, .. } + | Self::HostedWorldLatest { origin, .. } => origin, + } + } + /// Validate one public source identity without requiring a bound realm/view. + pub fn validate(&self) -> Result<(), String> { + match self { + Self::LocalWorldMirrorLatest { origin, mirror_id } => { + validate_hosted_origin(origin)?; + validate_required_text("reference.mirrorId", mirror_id, 1024) + } + Self::HostedWorldViewExport { + origin, + share_token, + } => { + validate_hosted_origin(origin)?; + validate_required_text("reference.shareToken", share_token, 1024) + } + Self::HostedWorldLiveViewShare { + origin, + share_token, + } => { + validate_hosted_origin(origin)?; + validate_required_text("reference.shareToken", share_token, 1024) + } + Self::HostedWorldLatest { + origin, + hosted_world_id, + } => { + validate_hosted_origin(origin)?; + validate_required_text("reference.hostedWorldId", hosted_world_id, 1024) + } + } + } +} + +/// Initial channel presentation for a bound world view. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewDisplayMode { + /// Interactive Shivai dependency graph. + Graph, + /// Plain ordered task list. + Tasks, +} + +impl WorldViewBindingsDocument { + /// Validate scope, limits, and identity invariants before publication. + pub fn validate(&self) -> Result<(), String> { + if self.version != WORLD_VIEW_BINDINGS_VERSION { + return Err(format!( + "unsupported world view bindings version: {}", + self.version + )); + } + self.scope.validate()?; + if self.bindings.len() > MAX_WORLD_VIEW_BINDINGS_PER_SCOPE { + return Err(format!( + "a scope may bind at most {MAX_WORLD_VIEW_BINDINGS_PER_SCOPE} world views" + )); + } + + let mut ids = HashSet::with_capacity(self.bindings.len()); + for binding in &self.bindings { + if !ids.insert(binding.id) { + return Err(format!("duplicate world view binding id: {}", binding.id)); + } + validate_required_text("realmQualifiedName", &binding.realm_qualified_name, 512)?; + validate_required_text("viewQualifiedName", &binding.view_qualified_name, 512)?; + if let Some(label) = &binding.label { + validate_required_text("label", label, 160)?; + } + binding.reference.validate()?; + } + Ok(()) + } +} + +fn validate_hosted_origin(value: &str) -> Result<(), String> { + validate_required_text("reference.origin", value, 2048)?; + let parsed = url::Url::parse(value) + .map_err(|error| format!("reference.origin must be an absolute URL: {error}"))?; + let is_loopback_http = parsed.scheme() == "http" + && parsed.host().is_some_and(|host| match host { + url::Host::Domain(domain) => domain == "localhost", + url::Host::Ipv4(address) => address.is_loopback(), + url::Host::Ipv6(address) => address.is_loopback(), + }); + if parsed.scheme() != "https" && !is_loopback_http { + return Err( + "reference.origin must use https (http is allowed only for loopback development)" + .into(), + ); + } + if parsed.origin().ascii_serialization() != value { + return Err("reference.origin must contain only scheme, host, and optional port".into()); + } + Ok(()) +} + +fn validate_nostr_event_id(field: &str, value: &str) -> Result<(), String> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(format!("{field} must be 64 lowercase hex characters")); + } + Ok(()) +} + +fn validate_required_text(field: &str, value: &str, max_len: usize) -> Result<(), String> { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(format!("{field} must not be blank")); + } + if trimmed.len() > max_len { + return Err(format!("{field} exceeds {max_len} bytes")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn binding(id: Uuid) -> WorldViewBinding { + WorldViewBinding { + id, + label: Some("Launch board".into()), + reference: WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "view-token".into(), + }, + realm_qualified_name: "world::main".into(), + view_qualified_name: "world::main::@Board".into(), + display_mode: WorldViewDisplayMode::Graph, + } + } + + fn signed_bindings_event( + channel_id: Uuid, + document: &WorldViewBindingsDocument, + previous_revision_event_id: &str, + root_override: Option<&str>, + ) -> Event { + use nostr::{EventBuilder, Kind, Tag}; + + let mut tags = vec![ + Tag::parse(["h", channel_id.to_string().as_str()]).expect("h tag"), + Tag::parse(["d", document.scope.d_tag().as_str()]).expect("d tag"), + Tag::parse(["prev", previous_revision_event_id]).expect("prev tag"), + ]; + if let Some(root) = root_override.or(document.scope.thread_root_event_id()) { + tags.push(Tag::parse(["e", root, "", "root"]).expect("e tag")); + } + EventBuilder::new( + Kind::Custom(crate::kind::KIND_WORLD_VIEW_BINDINGS as u16), + serde_json::to_string(document).expect("serialize"), + ) + .tags(tags) + .sign_with_keys(&nostr::Keys::generate()) + .expect("sign") + } + + #[test] + fn round_trips_the_versioned_binding_document() { + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![binding(Uuid::nil())], + }; + + let encoded = serde_json::to_value(&document).expect("serialize"); + assert_eq!( + encoded["bindings"][0]["reference"]["kind"], + "hosted-world-view-export" + ); + assert_eq!( + encoded["bindings"][0]["reference"]["origin"], + "https://manifest.shivai.space" + ); + assert_eq!(encoded["scope"]["kind"], "channel"); + assert_eq!(encoded["bindings"][0]["displayMode"], "graph"); + let decoded: WorldViewBindingsDocument = + serde_json::from_value(encoded).expect("deserialize"); + assert_eq!(decoded, document); + assert_eq!(decoded.validate(), Ok(())); + } + + #[test] + fn decodes_verified_binding_event_into_typed_revision_state() { + let channel_id = Uuid::new_v4(); + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![binding(Uuid::nil())], + }; + let event = signed_bindings_event(channel_id, &document, &"a".repeat(64), None); + + let decoded = + decode_verified_world_view_bindings_event(&event).expect("decode canonical event"); + + assert_eq!(decoded.channel_id, channel_id); + assert_eq!(decoded.snapshot.document, document); + assert_eq!( + decoded.previous_revision_event_id.map(|id| id.to_hex()), + Some("a".repeat(64)) + ); + assert_eq!(decoded.snapshot.revision_event_id, Some(event.id.to_hex())); + } + + #[test] + fn rejects_mismatched_thread_root_at_the_shared_event_boundary() { + let channel_id = Uuid::new_v4(); + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::thread("a".repeat(64)).expect("thread scope"), + bindings: Vec::new(), + }; + let event = signed_bindings_event( + channel_id, + &document, + &"b".repeat(64), + Some(&"c".repeat(64)), + ); + + assert_eq!( + decode_verified_world_view_bindings_event(&event).map(|_| ()), + Err("thread world-view bindings require one canonical root e tag".into()) + ); + } + + #[test] + fn rejects_unknown_nested_binding_and_reference_fields() { + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![binding(Uuid::nil())], + }; + let mut binding_value = serde_json::to_value(&document).expect("serialize"); + binding_value["bindings"][0]["unexpected"] = serde_json::json!(true); + assert!( + serde_json::from_value::(binding_value) + .expect_err("binding extras must fail") + .to_string() + .contains("unknown field") + ); + + let mut reference_value = serde_json::to_value(&document).expect("serialize"); + reference_value["bindings"][0]["reference"]["accessToken"] = + serde_json::json!("must-not-cross-boundary"); + assert!( + serde_json::from_value::(reference_value) + .expect_err("reference extras must fail") + .to_string() + .contains("unknown field") + ); + } + + #[test] + fn thread_bindings_shadow_channel_ids_without_reordering_inherited_views() { + let inherited_id = Uuid::nil(); + let shadowed_id = Uuid::from_u128(1); + let appended_id = Uuid::from_u128(2); + let channel = WorldViewBindingsSnapshot { + document: WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![binding(inherited_id), binding(shadowed_id)], + }, + revision_event_id: Some("a".repeat(64)), + updated_at: Some(1), + author: Some("channel-author".into()), + }; + let thread_scope = WorldViewBindingScope::Thread { + thread_root_event_id: "b".repeat(64), + }; + let mut shadow = binding(shadowed_id); + shadow.label = Some("Thread override".into()); + let mut appended = binding(appended_id); + appended.label = Some("Thread only".into()); + let thread = WorldViewBindingsSnapshot { + document: WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: thread_scope.clone(), + bindings: vec![shadow, appended], + }, + revision_event_id: Some("c".repeat(64)), + updated_at: Some(2), + author: Some("thread-author".into()), + }; + + let effective = + effective_world_view_bindings(&channel, Some(&thread)).expect("merge effective views"); + + assert_eq!(effective.effective_scope, thread_scope); + assert_eq!( + effective + .bindings + .iter() + .map(|entry| entry.binding.id) + .collect::>(), + vec![inherited_id, shadowed_id, appended_id] + ); + assert_eq!( + effective.bindings[0].declared_scope, + WorldViewBindingScope::Channel + ); + assert_eq!( + effective.bindings[0].binding_revision_event_id, + "a".repeat(64) + ); + assert_eq!( + effective.bindings[1].declared_scope, + effective.effective_scope + ); + assert_eq!( + effective.bindings[1].binding.label.as_deref(), + Some("Thread override") + ); + assert_eq!( + effective.bindings[2].binding.label.as_deref(), + Some("Thread only") + ); + assert_eq!(effective.thread_revision_event_id, Some("c".repeat(64))); + } + + #[test] + fn rejects_non_origin_and_insecure_remote_urls() { + let mut document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![binding(Uuid::nil())], + }; + if let WorldViewReference::HostedWorldViewExport { origin, .. } = + &mut document.bindings[0].reference + { + *origin = "https://manifest.shivai.space/world/export".into(); + } + assert_eq!( + document.validate(), + Err("reference.origin must contain only scheme, host, and optional port".into()) + ); + + if let WorldViewReference::HostedWorldViewExport { origin, .. } = + &mut document.bindings[0].reference + { + *origin = "https://manifest.shivai.space/".into(); + } + assert_eq!( + document.validate(), + Err("reference.origin must contain only scheme, host, and optional port".into()) + ); + + if let WorldViewReference::HostedWorldViewExport { origin, .. } = + &mut document.bindings[0].reference + { + *origin = "http://manifest.shivai.space".into(); + } + assert_eq!( + document.validate(), + Err( + "reference.origin must use https (http is allowed only for loopback development)" + .into() + ) + ); + } + + #[test] + fn world_authority_registry_preserves_one_to_one_mappings() { + let mut registry = WorldAuthorityRegistry::default(); + registry + .upsert_local(LocalWorldAuthority { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + source_root: "/worlds/one.world".into(), + capability_secret_file: "/credentials/local-one.txt".into(), + }) + .unwrap(); + registry + .upsert_local(LocalWorldAuthority { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-2".into(), + source_root: "/worlds/one.world".into(), + capability_secret_file: "/credentials/local-two.txt".into(), + }) + .unwrap(); + registry + .upsert_hosted(HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: "/credentials/one.txt".into(), + }) + .unwrap(); + registry + .upsert_hosted(HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-2".into(), + credential_file: "/credentials/one.txt".into(), + }) + .unwrap(); + + assert_eq!(registry.local_authorities.len(), 1); + assert!(registry + .resolve_local("https://manifest.shivai.space", "mirror-1") + .is_none()); + assert_eq!( + registry + .resolve_local("https://manifest.shivai.space", "mirror-2") + .map(|authority| authority.source_root.as_str()), + Some("/worlds/one.world") + ); + assert_eq!(registry.hosted_authorities.len(), 1); + assert!(registry + .resolve_hosted("https://manifest.shivai.space", "hosted-1") + .is_none()); + assert_eq!( + registry + .resolve_hosted("https://manifest.shivai.space", "hosted-2") + .map(|authority| authority.credential_file.as_str()), + Some("/credentials/one.txt") + ); + } + + #[test] + fn world_origin_trust_is_explicit_canonical_and_authority_aware() { + let mut registry = WorldAuthorityRegistry::default(); + + assert!(registry.is_trusted_origin(DEFAULT_SHIVAI_WORLD_ORIGIN)); + assert!(!registry.is_trusted_origin("https://untrusted.example")); + assert_eq!( + registry + .trust_origin("https://trusted.example".into()) + .unwrap(), + true + ); + assert_eq!( + registry + .trust_origin("https://trusted.example".into()) + .unwrap(), + false + ); + + registry + .upsert_local(LocalWorldAuthority { + origin: "https://connected.example".into(), + mirror_id: "mirror-1".into(), + source_root: "/worlds/connected.world".into(), + capability_secret_file: "/credentials/connected.txt".into(), + }) + .unwrap(); + assert!(registry.is_trusted_origin("https://connected.example")); + assert_eq!( + registry + .revoke_origin_trust("https://connected.example") + .unwrap_err(), + "cannot revoke world origin trust while `https://connected.example` has connected authority" + ); + assert!(registry + .revoke_origin_trust("https://trusted.example") + .unwrap()); + assert!(!registry.is_trusted_origin("https://trusted.example")); + assert_eq!(registry.validate(), Ok(())); + } + + #[test] + fn world_mutation_delegation_is_exact_replaceable_and_revocable() { + let channel_id = Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").unwrap(); + let binding_id = Uuid::parse_str("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb").unwrap(); + let mut registry = WorldAuthorityRegistry::default(); + let binding_revision_event_id = "d".repeat(64); + registry + .upsert_local(LocalWorldAuthority { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + source_root: "/worlds/one.world".into(), + capability_secret_file: "/credentials/local-one.txt".into(), + }) + .unwrap(); + registry + .upsert_hosted(HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: "/credentials/hosted-one.txt".into(), + }) + .unwrap(); + registry + .upsert_mutation_delegation(WorldViewMutationDelegation { + channel_id, + declared_scope: WorldViewBindingScope::Channel, + binding_id, + binding_revision_event_id: binding_revision_event_id.clone(), + authority: WorldMutationAuthority::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }, + }) + .unwrap(); + + assert_eq!( + registry + .resolve_mutation_delegation( + channel_id, + &WorldViewBindingScope::Channel, + binding_id, + &binding_revision_event_id, + ) + .map(|delegation| &delegation.authority), + Some(&WorldMutationAuthority::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }) + ); + assert!(registry + .resolve_mutation_delegation( + channel_id, + &WorldViewBindingScope::thread("c".repeat(64)).unwrap(), + binding_id, + &binding_revision_event_id, + ) + .is_none()); + + registry + .upsert_mutation_delegation(WorldViewMutationDelegation { + channel_id, + declared_scope: WorldViewBindingScope::Channel, + binding_id, + binding_revision_event_id: "e".repeat(64), + authority: WorldMutationAuthority::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }, + }) + .unwrap(); + assert_eq!(registry.mutation_delegations.len(), 1); + assert!(registry + .resolve_mutation_delegation( + channel_id, + &WorldViewBindingScope::Channel, + binding_id, + &binding_revision_event_id, + ) + .is_none()); + assert!(registry + .resolve_mutation_delegation( + channel_id, + &WorldViewBindingScope::Channel, + binding_id, + &"e".repeat(64), + ) + .is_some()); + assert!(registry.revoke_mutation_delegation( + channel_id, + &WorldViewBindingScope::Channel, + binding_id, + )); + assert!(registry.mutation_delegations.is_empty()); + assert!(!registry.revoke_mutation_delegation( + channel_id, + &WorldViewBindingScope::Channel, + binding_id, + )); + } + + #[test] + fn derives_stable_thread_scope_coordinate() { + let root = "a".repeat(64); + let scope = WorldViewBindingScope::thread(root.clone()).expect("valid thread scope"); + + assert_eq!(scope.d_tag(), format!("world-view-bindings:thread:{root}")); + assert_eq!(scope.thread_root_event_id(), Some(root.as_str())); + } + + #[test] + fn rejects_noncanonical_thread_event_ids() { + assert_eq!( + WorldViewBindingScope::thread("A".repeat(64)), + Err("scope.threadRootEventId must be 64 lowercase hex characters".into()) + ); + } + + #[test] + fn rejects_duplicate_binding_ids() { + let id = Uuid::nil(); + let document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![binding(id), binding(id)], + }; + + assert_eq!( + document.validate(), + Err(format!("duplicate world view binding id: {id}")) + ); + } + + #[test] + fn world_authority_grant_is_exactly_agent_scope_binding_and_revision_bound() { + let scope = WorldAuthorityGrantScope { + agent_pubkey: "a".repeat(64), + channel_id: Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").unwrap(), + effective_scope: WorldViewBindingScope::thread("b".repeat(64)).unwrap(), + binding_id: Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap(), + binding_revision_event_id: "c".repeat(64), + source_revision: "d".repeat(64), + }; + let token = issue_world_authority_grant(&scope, b"private-edit-share", 200).unwrap(); + + assert!(verify_world_authority_grant(&token, b"private-edit-share", &scope, 100,).is_ok()); + assert!(!token.contains("private-edit-share")); + + let mut another_agent = scope.clone(); + another_agent.agent_pubkey = "e".repeat(64); + assert_eq!( + verify_world_authority_grant(&token, b"private-edit-share", &another_agent, 100,), + Err("world authority grant does not match this request".into()) + ); + + let mut another_scope = scope.clone(); + another_scope.effective_scope = WorldViewBindingScope::Channel; + assert!( + verify_world_authority_grant(&token, b"private-edit-share", &another_scope, 100,) + .is_err() + ); + + let mut another_revision = scope.clone(); + another_revision.source_revision = "f".repeat(64); + assert!(verify_world_authority_grant( + &token, + b"private-edit-share", + &another_revision, + 100, + ) + .is_err()); + assert_eq!( + verify_world_authority_grant(&token, b"private-edit-share", &scope, 200,), + Err("world authority grant expired".into()) + ); + } + + #[test] + fn world_authority_grant_rejects_tampering() { + let scope = WorldAuthorityGrantScope { + agent_pubkey: "a".repeat(64), + channel_id: Uuid::nil(), + effective_scope: WorldViewBindingScope::Channel, + binding_id: Uuid::nil(), + binding_revision_event_id: "b".repeat(64), + source_revision: "c".repeat(64), + }; + let token = issue_world_authority_grant(&scope, b"private-edit-share", 200).unwrap(); + let mut tampered = token.into_bytes(); + let last = tampered.last_mut().unwrap(); + *last = if *last == b'0' { b'1' } else { b'0' }; + let tampered = String::from_utf8(tampered).unwrap(); + + assert_eq!( + verify_world_authority_grant(&tampered, b"private-edit-share", &scope, 100,), + Err("invalid world authority grant".into()) + ); + } +} diff --git a/crates/buzz-db/src/error.rs b/crates/buzz-db/src/error.rs index f8b8a2eb56..955fe508f9 100644 --- a/crates/buzz-db/src/error.rs +++ b/crates/buzz-db/src/error.rs @@ -37,6 +37,19 @@ pub enum DbError { #[error("access denied: {0}")] AccessDenied(String), + /// An optimistic channel document write did not name the current revision. + #[error( + "revision conflict: expected {expected}, current {current}", + expected = expected.as_deref().unwrap_or("none"), + current = current.as_deref().unwrap_or("none") + )] + RevisionConflict { + /// Revision supplied by the writer, or `None` for an expected create. + expected: Option, + /// Current live revision, or `None` when the coordinate is absent. + current: Option, + }, + /// JSON serialization or deserialization failed. #[error("serialization error: {0}")] Serde(#[from] serde_json::Error), diff --git a/crates/buzz-db/src/lib.rs b/crates/buzz-db/src/lib.rs index 2a3ba9a63e..fc2a238b16 100644 --- a/crates/buzz-db/src/lib.rs +++ b/crates/buzz-db/src/lib.rs @@ -3903,6 +3903,123 @@ impl Db { true, )) } + + /// Atomically replace one channel-owned world-view binding scope. + /// + /// Unlike ordinary NIP-33 coordinates, this state belongs to the channel + /// scope rather than an individual author. The exact key is + /// `(community, channel, kind, d_tag)`, and every write must name the + /// current event id (or explicitly expect no current event). + pub async fn replace_scoped_world_view_bindings_event( + &self, + community_id: CommunityId, + event: &nostr::Event, + channel_id: Uuid, + d_tag: &str, + expected_revision_event_id: Option<&[u8]>, + ) -> Result<(StoredEvent, bool)> { + let kind_i32 = buzz_core::kind::event_kind_i32(event); + if kind_i32 != buzz_core::kind::KIND_WORLD_VIEW_BINDINGS as i32 { + return Err(DbError::InvalidData( + "scoped world-view replacement requires kind 40101".into(), + )); + } + + let lock_key = event_replacement_lock_key( + community_id, + kind_i32, + channel_id.as_bytes(), + Some(d_tag.as_bytes()), + ); + let mut tx = self.pool.begin().await?; + sqlx::query("SELECT pg_advisory_xact_lock($1)") + .bind(lock_key) + .execute(&mut *tx) + .await?; + + let current_event_id: Option> = sqlx::query_scalar( + "SELECT id FROM events \ + WHERE community_id = $1 AND channel_id = $2 AND kind = $3 \ + AND d_tag = $4 AND deleted_at IS NULL \ + LIMIT 1", + ) + .bind(community_id.as_uuid()) + .bind(channel_id) + .bind(kind_i32) + .bind(d_tag) + .fetch_optional(&mut *tx) + .await?; + + if current_event_id.as_deref() != expected_revision_event_id { + tx.rollback().await?; + return Err(DbError::RevisionConflict { + expected: expected_revision_event_id.map(hex::encode), + current: current_event_id.as_deref().map(hex::encode), + }); + } + + if current_event_id.is_some() { + sqlx::query( + "UPDATE events SET deleted_at = NOW() \ + WHERE community_id = $1 AND channel_id = $2 AND kind = $3 \ + AND d_tag = $4 AND deleted_at IS NULL", + ) + .bind(community_id.as_uuid()) + .bind(channel_id) + .bind(kind_i32) + .bind(d_tag) + .execute(&mut *tx) + .await?; + } + + let created_at_secs = event.created_at.as_secs() as i64; + let created_at = chrono::DateTime::from_timestamp(created_at_secs, 0) + .ok_or(DbError::InvalidTimestamp(created_at_secs))?; + let tags_json = serde_json::to_value(&event.tags)?; + let sig_bytes = event.sig.serialize(); + let received_at = chrono::Utc::now(); + let insert_result = sqlx::query( + "INSERT INTO events \ + (community_id, id, pubkey, created_at, kind, tags, content, sig, \ + received_at, channel_id, d_tag, not_before) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) \ + ON CONFLICT DO NOTHING", + ) + .bind(community_id.as_uuid()) + .bind(event.id.as_bytes().as_slice()) + .bind(event.pubkey.to_bytes().as_slice()) + .bind(created_at) + .bind(kind_i32) + .bind(&tags_json) + .bind(&event.content) + .bind(sig_bytes.as_slice()) + .bind(received_at) + .bind(channel_id) + .bind(d_tag) + .bind(event::extract_not_before(event)) + .execute(&mut *tx) + .await?; + + if insert_result.rows_affected() == 0 { + tx.rollback().await?; + return Ok(( + StoredEvent::with_received_at(event.clone(), received_at, Some(channel_id), false), + false, + )); + } + + tx.commit().await?; + if let Err(error) = + crate::insert_mentions(&self.pool, community_id, event, Some(channel_id)).await + { + tracing::warn!(event_id = %event.id, "Failed to insert mentions: {error}"); + } + + Ok(( + StoredEvent::with_received_at(event.clone(), received_at, Some(channel_id), true), + true, + )) + } } /// A full API token record. @@ -4016,6 +4133,90 @@ mod tests { id } + #[tokio::test] + #[ignore = "requires Postgres"] + async fn scoped_world_view_replacement_enforces_channel_revision_across_authors() { + use nostr::{EventBuilder, Keys, Kind, Tag}; + + let db = setup_db().await; + let community_uuid = make_community(&db.pool).await; + let community = CommunityId::from_uuid(community_uuid); + let channel_id = Uuid::new_v4(); + let first_keys = Keys::generate(); + let second_keys = Keys::generate(); + sqlx::query( + "INSERT INTO channels \ + (id, community_id, name, channel_type, visibility, created_by) \ + VALUES ($1, $2, $3, 'stream'::channel_type, \ + 'open'::channel_visibility, $4)", + ) + .bind(channel_id) + .bind(community_uuid) + .bind(format!("world-view-revision-{}", channel_id.simple())) + .bind(first_keys.public_key().to_bytes().as_slice()) + .execute(&db.pool) + .await + .expect("insert channel"); + + let d_tag = buzz_core::world_view::CHANNEL_WORLD_VIEW_BINDINGS_D_TAG; + let tags = vec![ + Tag::parse(["h", channel_id.to_string().as_str()]).expect("h tag"), + Tag::parse(["d", d_tag]).expect("d tag"), + Tag::parse(["prev", ""]).expect("prev tag"), + ]; + let first = EventBuilder::new( + Kind::Custom(buzz_core::kind::KIND_WORLD_VIEW_BINDINGS as u16), + "{\"version\":2,\"scope\":{\"kind\":\"channel\"},\"bindings\":[]}", + ) + .tags(tags.clone()) + .sign_with_keys(&first_keys) + .expect("sign first revision"); + assert!( + db.replace_scoped_world_view_bindings_event( + community, + &first, + channel_id, + d_tag, + None, + ) + .await + .expect("create first revision") + .1 + ); + + let second = EventBuilder::new( + Kind::Custom(buzz_core::kind::KIND_WORLD_VIEW_BINDINGS as u16), + "{\"version\":2,\"scope\":{\"kind\":\"channel\"},\"bindings\":[]}", + ) + .tags(tags) + .sign_with_keys(&second_keys) + .expect("sign second revision"); + let stale_error = db + .replace_scoped_world_view_bindings_event(community, &second, channel_id, d_tag, None) + .await + .expect_err("stale create must conflict"); + assert!(matches!( + &stale_error, + DbError::RevisionConflict { + expected: None, + current: Some(current), + } if current == &first.id.to_hex() + )); + + assert!( + db.replace_scoped_world_view_bindings_event( + community, + &second, + channel_id, + d_tag, + Some(first.id.as_bytes().as_slice()), + ) + .await + .expect("replace current revision") + .1 + ); + } + #[tokio::test] #[ignore = "requires Postgres"] async fn nip_rs_replacement_hard_deletes_payload_and_watermark_rejects_replay() { diff --git a/crates/buzz-dev-mcp/src/shim.rs b/crates/buzz-dev-mcp/src/shim.rs index cccf0e6eca..c13fd3b5fc 100644 --- a/crates/buzz-dev-mcp/src/shim.rs +++ b/crates/buzz-dev-mcp/src/shim.rs @@ -39,6 +39,12 @@ impl Shim { symlink(&self_exe, &dir.path().join(name))?; } + if let Some(world_binary) = + shivai_world_binary_for_shim(&self_exe, std::env::var_os("SHIVAI_WORLD_BIN").as_deref()) + { + symlink(&world_binary, &dir.path().join("world"))?; + } + let original = std::env::var_os("PATH").unwrap_or_default(); let mut entries = vec![PathBuf::from(dir.path())]; entries.extend(std::env::split_paths(&original)); @@ -75,6 +81,20 @@ impl Shim { } } +fn shivai_world_binary_for_shim( + self_exe: &Path, + configured: Option<&std::ffi::OsStr>, +) -> Option { + let configured = configured + .map(PathBuf::from) + .filter(|path| path.is_absolute() && path.is_file()); + configured.or_else(|| { + let sibling = + self_exe.with_file_name(format!("shivai-world{}", std::env::consts::EXE_SUFFIX)); + sibling.is_file().then_some(sibling) + }) +} + struct KeyInfo { keyfile_path: String, pubkey_hex: String, @@ -359,6 +379,28 @@ pub fn artifact_dir(session_root: &Path) -> PathBuf { } #[cfg(test)] +mod tests { + use super::*; + + #[test] + fn world_shim_prefers_configured_binary_then_bundled_sibling() { + let dir = tempfile::tempdir().unwrap(); + let self_exe = dir.path().join("buzz-dev-mcp"); + let sibling = + self_exe.with_file_name(format!("shivai-world{}", std::env::consts::EXE_SUFFIX)); + let configured = dir.path().join("configured-world"); + std::fs::write(&self_exe, b"helper").unwrap(); + std::fs::write(&sibling, b"bundled").unwrap(); + std::fs::write(&configured, b"configured").unwrap(); + + assert_eq!( + shivai_world_binary_for_shim(&self_exe, Some(configured.as_os_str())), + Some(configured) + ); + assert_eq!(shivai_world_binary_for_shim(&self_exe, None), Some(sibling)); + } +} + mod git_user_name_tests { use super::{ build_git_env, is_git_crud, is_unicode_format, sanitize_git_user_name, KeyInfo, diff --git a/crates/buzz-relay/src/handlers/event.rs b/crates/buzz-relay/src/handlers/event.rs index 88dd5f5180..d730e08acd 100644 --- a/crates/buzz-relay/src/handlers/event.rs +++ b/crates/buzz-relay/src/handlers/event.rs @@ -1163,6 +1163,7 @@ mod tests { use buzz_core::kind::{ KIND_AGENT_OBSERVER_FRAME, KIND_CANVAS, KIND_FORUM_COMMENT, KIND_FORUM_POST, KIND_FORUM_VOTE, KIND_PRESENCE_UPDATE, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_DIFF, + KIND_WORLD_VIEW_BINDINGS, }; use buzz_core::observer::{ encrypt_observer_payload, OBSERVER_AGENT_TAG, OBSERVER_FRAME_CONTROL, OBSERVER_FRAME_TAG, @@ -1215,6 +1216,7 @@ mod tests { KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_DIFF, KIND_CANVAS, + KIND_WORLD_VIEW_BINDINGS, KIND_FORUM_POST, KIND_FORUM_VOTE, KIND_FORUM_COMMENT, diff --git a/crates/buzz-relay/src/handlers/ingest.rs b/crates/buzz-relay/src/handlers/ingest.rs index a30b0e714d..700c55df59 100644 --- a/crates/buzz-relay/src/handlers/ingest.rs +++ b/crates/buzz-relay/src/handlers/ingest.rs @@ -32,11 +32,12 @@ use buzz_core::kind::{ KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED, KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED, KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEAM, KIND_TEXT_NOTE, KIND_USER_STATUS, - KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, RELAY_ADMIN_ADD_MEMBER, RELAY_ADMIN_CHANGE_ROLE, - RELAY_ADMIN_REMOVE_MEMBER, RELAY_ADMIN_SET_WORKSPACE_PROFILE, + KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, KIND_WORLD_VIEW_BINDINGS, RELAY_ADMIN_ADD_MEMBER, + RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER, RELAY_ADMIN_SET_WORKSPACE_PROFILE, }; use buzz_core::tenant::TenantContext; use buzz_core::verification::verify_event; +use buzz_core::world_view::decode_verified_world_view_bindings_event; use buzz_core::CommunityId; use nostr::Event; @@ -289,7 +290,9 @@ fn required_scope_for_kind(kind: u32, event: &Event) -> Result Ok(Scope::ChannelsWrite), + KIND_NIP29_CREATE_GROUP | KIND_CANVAS | KIND_WORLD_VIEW_BINDINGS => { + Ok(Scope::ChannelsWrite) + } KIND_NIP29_JOIN_REQUEST | KIND_NIP29_LEAVE_REQUEST | KIND_NIP43_LEAVE_REQUEST => { Ok(Scope::ChannelsRead) } @@ -477,6 +480,7 @@ pub(crate) fn requires_h_channel_scope(kind: u32) -> bool { | KIND_STREAM_REMINDER | KIND_STREAM_MESSAGE_DIFF | KIND_CANVAS + | KIND_WORLD_VIEW_BINDINGS | KIND_FORUM_POST | KIND_FORUM_VOTE | KIND_FORUM_COMMENT @@ -1758,6 +1762,21 @@ async fn ingest_event_inner( )); } + let world_view_admission = if kind_u32 == KIND_WORLD_VIEW_BINDINGS { + let channel_id = channel_id.expect("world-view bindings require channel scope"); + let admission = decode_verified_world_view_bindings_event(&event) + .map_err(|error| IngestError::Rejected(format!("invalid: {error}")))?; + if admission.channel_id != channel_id { + return Err(IngestError::Rejected( + "invalid: world-view bindings event channel did not match its relay coordinate" + .into(), + )); + } + Some(admission) + } else { + None + }; + if let Some(ch_id) = channel_id { check_token_channel_access(&auth, ch_id).map_err(IngestError::AuthFailed)?; } else if auth.channel_ids().is_some() { @@ -2409,7 +2428,42 @@ async fn ingest_event_inner( }); } - let (stored_event, was_inserted) = if buzz_core::kind::is_replaceable(kind_u32) { + let (stored_event, was_inserted) = if let Some(admission) = &world_view_admission { + let channel_id = channel_id.expect("validated world-view bindings have channel scope"); + let d_tag = admission.snapshot.document.scope.d_tag(); + let expected_revision_event_id = admission + .previous_revision_event_id + .as_ref() + .map(|event_id| &event_id.as_bytes()[..]); + state + .db + .replace_scoped_world_view_bindings_event( + tenant.community(), + &event, + channel_id, + &d_tag, + expected_revision_event_id, + ) + .await + .map_err(|error| match error { + buzz_db::DbError::RevisionConflict { expected, current } => { + let mut next_command = format!("buzz world-views get --channel {channel_id}"); + if let Some(thread_root_event_id) = + admission.snapshot.document.scope.thread_root_event_id() + { + next_command.push_str(" --thread-root "); + next_command.push_str(thread_root_event_id); + } + IngestError::Rejected(format!( + "conflict: world-view bindings revision changed \ + (expected {}, current {}); refresh with `{next_command}`", + expected.as_deref().unwrap_or("none"), + current.as_deref().unwrap_or("none") + )) + } + other => IngestError::Internal(format!("error: {other}")), + })? + } else if buzz_core::kind::is_replaceable(kind_u32) { // NIP-16 replaceable event — atomic replace with stale-write protection. // channel_id is None for global kinds (0, 1, 3) due to step 5b above. state diff --git a/crates/buzz-sdk/src/builders.rs b/crates/buzz-sdk/src/builders.rs index f9e54de9c5..f86a387eff 100644 --- a/crates/buzz-sdk/src/builders.rs +++ b/crates/buzz-sdk/src/builders.rs @@ -5,19 +5,20 @@ use buzz_core::{ kind::{ - KIND_AGENT_OBSERVER_FRAME, KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_DELETION, - KIND_DM_ADD_MEMBER, KIND_DM_OPEN, KIND_EMOJI_SET, KIND_GIT_ISSUE, KIND_GIT_PATCH, - KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, + KIND_AGENT_OBSERVER_FRAME, KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_CANVAS, + KIND_DELETION, KIND_DM_ADD_MEMBER, KIND_DM_OPEN, KIND_EMOJI_SET, KIND_GIT_ISSUE, + KIND_GIT_PATCH, KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST, KIND_MODERATION_BAN, KIND_MODERATION_RESOLVE_REPORT, KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN, KIND_MODERATION_UNTIMEOUT, KIND_PRESENCE_UPDATE, KIND_WORKFLOW_DEF, - KIND_WORKFLOW_TRIGGER, + KIND_WORKFLOW_TRIGGER, KIND_WORLD_VIEW_BINDINGS, }, observer::{ content_looks_like_nip44, OBSERVER_AGENT_TAG, OBSERVER_FRAME_CONTROL, OBSERVER_FRAME_TAG, OBSERVER_FRAME_TELEMETRY, }, + world_view::WorldViewBindingsDocument, }; use nostr::{EventBuilder, Kind, Tag}; use uuid::Uuid; @@ -528,7 +529,35 @@ pub fn build_custom_emoji_set(emojis: &[CustomEmoji]) -> Result Result { let tags = vec![tag(&["h", &channel_id.to_string()])?]; - Ok(EventBuilder::new(Kind::Custom(40100), content).tags(tags)) + Ok(EventBuilder::new(Kind::Custom(KIND_CANVAS as u16), content).tags(tags)) +} + +/// Build an optimistic, exact-scope world-view bindings event (kind 40101). +pub fn build_set_world_view_bindings( + channel_id: Uuid, + expected_revision_event_id: Option<&str>, + document: &WorldViewBindingsDocument, +) -> Result { + document.validate().map_err(SdkError::InvalidInput)?; + let content = serde_json::to_string(document).map_err(|error| { + SdkError::InvalidInput(format!("serialize world view bindings: {error}")) + })?; + check_content(&content, 64 * 1024)?; + + let previous = expected_revision_event_id + .map(|event_id| check_hex_exact(event_id, 64, "expected revision event id")) + .transpose()? + .unwrap_or_default(); + let d_tag = document.scope.d_tag(); + let mut tags = vec![ + tag(&["h", &channel_id.to_string()])?, + tag(&["d", &d_tag])?, + tag(&["prev", &previous])?, + ]; + if let Some(thread_root_event_id) = document.scope.thread_root_event_id() { + tags.push(tag(&["e", thread_root_event_id, "", "root"])?); + } + Ok(EventBuilder::new(Kind::Custom(KIND_WORLD_VIEW_BINDINGS as u16), content).tags(tags)) } /// Build a NIP-01 profile metadata event (kind 0). @@ -2316,6 +2345,60 @@ mod tests { assert_eq!(ev.content, "# Canvas\nHello"); } + #[test] + fn set_world_view_bindings_happy_path() { + use buzz_core::world_view::{ + WorldViewBinding, WorldViewBindingScope, WorldViewBindingsDocument, + WorldViewDisplayMode, WorldViewReference, WORLD_VIEW_BINDINGS_VERSION, + }; + + let cid = uuid(); + let mut document = WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: WorldViewBindingScope::Channel, + bindings: vec![WorldViewBinding { + id: Uuid::nil(), + label: Some("Launch".into()), + reference: WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "view-token".into(), + }, + realm_qualified_name: "world::main".into(), + view_qualified_name: "world::main::@Board".into(), + display_mode: WorldViewDisplayMode::Graph, + }], + }; + let ev = sign(build_set_world_view_bindings(cid, None, &document).unwrap()); + assert_eq!(ev.kind.as_u16(), KIND_WORLD_VIEW_BINDINGS as u16); + assert!(has_tag(&ev, "h", &cid.to_string())); + assert!(has_tag(&ev, "d", "world-view-bindings:channel")); + assert!(has_tag(&ev, "prev", "")); + assert_eq!( + serde_json::from_str::(&ev.content).unwrap(), + document + ); + + let root = "a".repeat(64); + let previous = "b".repeat(64); + document.scope = WorldViewBindingScope::thread(root.clone()).unwrap(); + let thread_event = + sign(build_set_world_view_bindings(cid, Some(&previous), &document).unwrap()); + assert!(has_tag( + &thread_event, + "d", + format!("world-view-bindings:thread:{root}").as_str() + )); + assert!(has_tag(&thread_event, "prev", &previous)); + assert!(thread_event.tags.iter().any(|tag| { + let parts = tag.as_slice(); + parts.len() == 4 + && parts[0] == "e" + && parts[1] == root + && parts[2].is_empty() + && parts[3] == "root" + })); + } + #[test] fn profile_all_fields() { let ev = sign( diff --git a/crates/buzz-world-view-resolver/Cargo.toml b/crates/buzz-world-view-resolver/Cargo.toml new file mode 100644 index 0000000000..326f7efe41 --- /dev/null +++ b/crates/buzz-world-view-resolver/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "buzz-world-view-resolver" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Canonical typed Shivai world-view resolver for Buzz" + +[dependencies] +buzz-core = { workspace = true } +chrono = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["process"] } +uuid = { workspace = true } diff --git a/crates/buzz-world-view-resolver/src/lib.rs b/crates/buzz-world-view-resolver/src/lib.rs new file mode 100644 index 0000000000..00a29a5c6d --- /dev/null +++ b/crates/buzz-world-view-resolver/src/lib.rs @@ -0,0 +1,2341 @@ +mod presentation; + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::process::Stdio; + +use buzz_core::world_view::{ + WorldAuthorityRegistry, WorldViewBinding, WorldViewBindingScope, WorldViewBindingsDocument, + WorldViewReference, WORLD_VIEW_BINDINGS_VERSION, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use tokio::io::AsyncWriteExt; +use uuid::Uuid; + +pub use presentation::*; + +const WORLD_VIEW_RESOLUTION_FORMAT_VERSION: u8 = 1; +const WORLD_VIEW_CATALOG_FORMAT_VERSION: u8 = 1; + +/// Everything needed to resolve one binding without consulting ambient UI state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewResolutionRequest { + pub channel_id: Uuid, + pub binding: WorldViewBinding, + pub declared_scope: WorldViewBindingScope, + pub effective_scope: WorldViewBindingScope, + pub binding_revision_event_id: String, +} + +impl WorldViewResolutionRequest { + pub fn validate(&self) -> Result<(), WorldViewResolutionError> { + WorldViewBindingsDocument { + version: WORLD_VIEW_BINDINGS_VERSION, + scope: self.declared_scope.clone(), + bindings: vec![self.binding.clone()], + } + .validate() + .map_err(WorldViewResolutionError::InvalidRequest)?; + self.effective_scope + .validate() + .map_err(WorldViewResolutionError::InvalidRequest)?; + validate_event_id("bindingRevisionEventId", &self.binding_revision_event_id)?; + Ok(()) + } +} +/// Machine-local authorization selected for one exact world-view reference. +#[derive(Debug, Clone, PartialEq, Eq)] +enum WorldViewResolutionAccess { + TrustedPublicOrigin { + origin: String, + }, + LocalSourceRoot { + origin: String, + mirror_id: String, + source_root: PathBuf, + }, + HostedEditShareFile { + origin: String, + hosted_world_id: String, + credential_file: PathBuf, + }, +} + +/// Credential-free authority readback for the source that produced a resolution. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)] +pub enum WorldViewResolutionAuthority { + HostedWorldViewExport { + origin: String, + }, + HostedWorldLiveViewShare { + origin: String, + #[serde(rename = "hostedWorldId")] + hosted_world_id: String, + }, + LocalWorldMirrorLatest { + origin: String, + #[serde(rename = "mirrorId")] + mirror_id: String, + }, + HostedWorldLatest { + origin: String, + #[serde(rename = "hostedWorldId")] + hosted_world_id: String, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewResolutionFreshness { + Pinned, + LatestAtResolution, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvedWorldViewEntity { + pub name: String, + pub qualified_name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewCatalogEntry { + pub name: String, + pub qualified_name: String, + pub realm: ResolvedWorldViewEntity, +} + +/// Canonical authored view identities available through one public source. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewCatalog { + pub format_version: u8, + pub revision: String, + pub world_qualified_name: String, + pub views: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewCounts { + pub nodes: usize, + pub edges: usize, + pub ready: usize, + pub actionable_ready: usize, + pub satisfied: usize, + pub blocked: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ResolvedWorldViewNodeStatus { + Satisfied, + Ready, + Blocked, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewNote { + pub preview: Option, + pub truncated: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewSignalCase { + pub name: String, + pub evidence: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ResolvedWorldViewSignalTarget { + Preference, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ResolvedWorldViewSignalMode { + First, + All, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewSignal { + pub name: String, + pub target: ResolvedWorldViewSignalTarget, + pub mode: ResolvedWorldViewSignalMode, + pub cases: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewNode { + pub preference: String, + pub qualified_name: String, + pub status: ResolvedWorldViewNodeStatus, + pub actionable: bool, + pub leaf: bool, + pub in_focus: bool, + pub in_satisfied: bool, + pub blockers: Vec, + pub enablers: Vec, + pub note: ResolvedWorldViewNote, + pub signals: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewEdge { + pub downstream: String, + pub upstream: String, + pub relation: ResolvedWorldViewEdgeRelation, + pub connection_type: WorldViewConnectionType, + pub flowspace: String, + pub flowspace_qualified_name: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ResolvedWorldViewEdgeRelation { + Blocker, + Enabler, +} + +/// Compact, agent-ready subset of `world view dump` with no untyped JSON payloads. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldViewDump { + pub counts: ResolvedWorldViewCounts, + pub nodes: Vec, + pub ready_leaves: Vec, + pub satisfied_nodes: Vec, + pub blocked_nodes: Vec, + pub edges: Vec, +} + +/// Canonical result shared by Buzz CLI, desktop, and agent prompt delivery. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResolvedWorldView { + pub format_version: u8, + pub binding_id: Uuid, + pub channel_id: Uuid, + pub declared_scope: WorldViewBindingScope, + pub effective_scope: WorldViewBindingScope, + pub binding_revision_event_id: String, + pub source_revision: String, + pub freshness: WorldViewResolutionFreshness, + pub authority: WorldViewResolutionAuthority, + pub realm: ResolvedWorldViewEntity, + pub view: ResolvedWorldViewEntity, + pub view_dump: ResolvedWorldViewDump, + pub presentation: WorldViewPresentationVariants, + pub resolved_at: DateTime, + pub next_command: String, +} + +#[derive(Debug, Error)] +pub enum WorldViewResolutionError { + #[error("invalid world-view resolution request: {0}")] + InvalidRequest(String), + #[error( + "hosted world `{hosted_world_id}` has no private edit-share authority registered on this client" + )] + MissingHostedAuthority { hosted_world_id: String }, + #[error( + "Shivai origin `{origin}` is not trusted on this device; trust it explicitly before resolving this binding" + )] + UntrustedOrigin { origin: String }, + #[error("could not launch the Shivai world resolver `{binary}`: {source}")] + Launch { + binary: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("could not send private Shivai world input to `{binary}` over stdin: {source}")] + Input { + binary: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("Shivai world-view resolution failed: {0}")] + CommandFailed(String), + #[error("Shivai hosted-world revision conflict: {0}")] + RevisionConflict(String), + #[error("Shivai world resolver returned invalid JSON: {0}")] + Decode(#[from] serde_json::Error), + #[error("Shivai world resolver returned an invalid result: {0}")] + InvalidResult(String), +} + +#[derive(Debug, Deserialize)] +struct WorldResultEnvelope { + ok: bool, + result: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldViewDumpResult { + revision: String, + hosted_world_id: Option, + realm: ResolvedWorldViewEntity, + view: ResolvedWorldViewEntity, + counts: ResolvedWorldViewCounts, + presentation: WorldViewPresentationVariants, + nodes: Vec, + ready_leaves: Vec, + satisfied_nodes: Vec, + blocked_nodes: Vec, + edges: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldViewCatalogResult { + command: String, + format_version: u8, + revision: String, + world_qualified_name: String, + views: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct HostedEditShareInspection { + pub hosted_world_id: String, + pub revision: String, +} + +/// Stable public live-view capability minted from private hosted authority. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PublishedHostedLiveViewShare { + pub hosted_world_id: String, + pub source_revision: String, + pub package_revision: String, + pub realm_qualified_name: String, + pub view_qualified_name: String, + pub share_token: String, + pub share_url_path: String, + pub title: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldHostedPublishLiveViewShareResult { + command: String, + live_view_share: WorldHostedPublishLiveViewShareResponse, + revision: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldHostedPublishLiveViewShareResponse { + live_view_share: WorldHostedLiveViewShare, + source: WorldHostedLiveViewShareSource, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldHostedLiveViewShare { + hosted_world_id: String, + realm_qualified_name: String, + share_token: String, + share_url_path: String, + title: String, + view_qualified_name: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldHostedLiveViewShareSource { + hosted_world_id: String, + revision_id: String, + package_revision: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldHostedLatestResult { + projection: WorldHostedLatestProjection, + revision: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WorldHostedLatestProjection { + hosted_world_id: String, +} + +/// Resolve using `SHIVAI_WORLD_BIN`, or `world` when the override is absent. +pub async fn resolve_world_view( + request: WorldViewResolutionRequest, + registry: &WorldAuthorityRegistry, +) -> Result { + let binary = std::env::var_os("SHIVAI_WORLD_BIN") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("world")); + resolve_world_view_with_binary(request, binary, registry).await +} + +/// Resolve through one explicitly selected source `world` binary. +pub async fn resolve_world_view_with_binary( + request: WorldViewResolutionRequest, + binary: impl AsRef, + registry: &WorldAuthorityRegistry, +) -> Result { + request.validate()?; + let access = resolution_access(registry, &request.binding.reference)?; + let binary = binary.as_ref(); + let invocation = world_cli_invocation(&request.binding, &access)?; + let stdout = + run_world_cli_invocation(binary, invocation, &request.binding.reference, &access).await?; + decode_world_view_resolution(&request, &stdout, Utc::now()) +} + +/// List canonical authored views using `SHIVAI_WORLD_BIN`, or `world`. +pub async fn catalog_world_views( + reference: WorldViewReference, + registry: &WorldAuthorityRegistry, +) -> Result { + let binary = std::env::var_os("SHIVAI_WORLD_BIN") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("world")); + catalog_world_views_with_binary(reference, binary, registry).await +} + +/// List canonical authored views through one explicitly selected source binary. +pub async fn catalog_world_views_with_binary( + reference: WorldViewReference, + binary: impl AsRef, + registry: &WorldAuthorityRegistry, +) -> Result { + reference + .validate() + .map_err(WorldViewResolutionError::InvalidRequest)?; + let access = resolution_access(registry, &reference)?; + let binary = binary.as_ref(); + let invocation = world_view_cli_invocation(&reference, &access, "catalog")?; + let stdout = run_world_cli_invocation(binary, invocation, &reference, &access).await?; + decode_world_view_catalog(&stdout) +} + +fn resolution_access( + registry: &WorldAuthorityRegistry, + reference: &WorldViewReference, +) -> Result { + let origin = reference.origin(); + if !registry.is_trusted_origin(origin) { + return Err(WorldViewResolutionError::UntrustedOrigin { + origin: origin.into(), + }); + } + + match reference { + WorldViewReference::LocalWorldMirrorLatest { origin, mirror_id } => { + if let Some(authority) = registry.resolve_local(origin, mirror_id) { + return Ok(WorldViewResolutionAccess::LocalSourceRoot { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + source_root: authority.source_root.clone().into(), + }); + } + Ok(WorldViewResolutionAccess::TrustedPublicOrigin { + origin: origin.clone(), + }) + } + WorldViewReference::HostedWorldViewExport { origin, .. } + | WorldViewReference::HostedWorldLiveViewShare { origin, .. } => { + Ok(WorldViewResolutionAccess::TrustedPublicOrigin { + origin: origin.clone(), + }) + } + WorldViewReference::HostedWorldLatest { + origin, + hosted_world_id, + } => { + let authority = registry + .resolve_hosted(origin, hosted_world_id) + .ok_or_else(|| WorldViewResolutionError::MissingHostedAuthority { + hosted_world_id: hosted_world_id.clone(), + })?; + Ok(WorldViewResolutionAccess::HostedEditShareFile { + origin: origin.clone(), + hosted_world_id: hosted_world_id.clone(), + credential_file: authority.credential_file.clone().into(), + }) + } + } +} + +async fn run_world_cli_invocation( + binary: &Path, + invocation: WorldCliInvocation<'_>, + reference: &WorldViewReference, + access: &WorldViewResolutionAccess, +) -> Result, WorldViewResolutionError> { + let mut command = tokio::process::Command::new(binary); + command + .args(&invocation.args) + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if invocation.stdin.is_some() { + command.stdin(Stdio::piped()); + } + let mut child = command + .spawn() + .map_err(|source| WorldViewResolutionError::Launch { + binary: binary.to_owned(), + source, + })?; + if let Some(input) = invocation.stdin { + let mut stdin = child + .stdin + .take() + .ok_or_else(|| WorldViewResolutionError::Input { + binary: binary.to_owned(), + source: std::io::Error::other("resolver stdin pipe was not available"), + })?; + stdin.write_all(input.as_bytes()).await.map_err(|source| { + WorldViewResolutionError::Input { + binary: binary.to_owned(), + source, + } + })?; + } + let output = + child + .wait_with_output() + .await + .map_err(|source| WorldViewResolutionError::Launch { + binary: binary.to_owned(), + source, + })?; + if !output.status.success() { + let diagnostics = redact_diagnostics( + String::from_utf8_lossy(&output.stderr).trim(), + reference, + access, + ); + return Err(command_failure(diagnostics)); + } + Ok(output.stdout) +} + +/// Inspect one private edit-share credential without placing it in process arguments. +pub async fn inspect_hosted_edit_share( + origin: &str, + credential_file: impl AsRef, +) -> Result { + let binary = std::env::var_os("SHIVAI_WORLD_BIN") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("world")); + inspect_hosted_edit_share_with_binary(origin, credential_file, binary).await +} + +/// Inspect one private edit-share credential through an explicit source `world` binary. +pub async fn inspect_hosted_edit_share_with_binary( + origin: &str, + credential_file: impl AsRef, + binary: impl AsRef, +) -> Result { + let binary = binary.as_ref(); + let output = tokio::process::Command::new(binary) + .args([ + "hosted", + "latest", + "--json", + "--base-url", + origin, + "--edit-share-file", + &credential_file.as_ref().to_string_lossy(), + "--anonymous-session", + ]) + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .map_err(|source| WorldViewResolutionError::Launch { + binary: binary.to_owned(), + source, + })?; + if !output.status.success() { + let diagnostics = redact_credential_file_path( + String::from_utf8_lossy(&output.stderr).trim(), + credential_file.as_ref(), + ); + return Err(command_failure(diagnostics)); + } + + let envelope: WorldResultEnvelope = + serde_json::from_slice(&output.stdout)?; + if !envelope.ok { + return Err(WorldViewResolutionError::InvalidResult( + "the command returned a non-success envelope".into(), + )); + } + let result = envelope.result.ok_or_else(|| { + WorldViewResolutionError::InvalidResult("the success envelope omitted `result`".into()) + })?; + if result.projection.hosted_world_id.trim().is_empty() { + return Err(WorldViewResolutionError::InvalidResult( + "the hosted-world id is blank".into(), + )); + } + if result.revision.trim().is_empty() { + return Err(WorldViewResolutionError::InvalidResult( + "the hosted-world revision is blank".into(), + )); + } + Ok(HostedEditShareInspection { + hosted_world_id: result.projection.hosted_world_id, + revision: result.revision, + }) +} + +/// Apply one revision-checked hosted WorldLang script through machine-local +/// edit authority without exposing the credential path to the caller. +pub async fn apply_hosted_world_script( + origin: &str, + hosted_world_id: &str, + credential_file: impl AsRef, + expected_revision: &str, + script: &str, +) -> Result { + let binary = std::env::var_os("SHIVAI_WORLD_BIN") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("world")); + apply_hosted_world_script_with_binary( + origin, + hosted_world_id, + credential_file, + expected_revision, + script, + binary, + ) + .await +} + +/// Apply one revision-checked hosted WorldLang script through an explicit +/// source `world` binary. +pub async fn apply_hosted_world_script_with_binary( + origin: &str, + hosted_world_id: &str, + credential_file: impl AsRef, + expected_revision: &str, + script: &str, + binary: impl AsRef, +) -> Result { + let credential_file = credential_file.as_ref(); + let binary = binary.as_ref(); + validate_event_id("expectedRevision", expected_revision)?; + if script.trim().is_empty() { + return Err(WorldViewResolutionError::InvalidRequest( + "hosted world script must not be blank".into(), + )); + } + + let inspection = inspect_hosted_edit_share_with_binary(origin, credential_file, binary).await?; + if inspection.hosted_world_id != hosted_world_id { + return Err(WorldViewResolutionError::InvalidResult(format!( + "the private authority resolved hosted world `{}` instead of `{hosted_world_id}`", + inspection.hosted_world_id + ))); + } + if inspection.revision != expected_revision { + return Err(WorldViewResolutionError::RevisionConflict(format!( + "expected revision `{expected_revision}`, current revision `{}`; no mutation was attempted", + inspection.revision + ))); + } + + let reference = WorldViewReference::HostedWorldLatest { + origin: origin.to_owned(), + hosted_world_id: hosted_world_id.to_owned(), + }; + reference + .validate() + .map_err(WorldViewResolutionError::InvalidRequest)?; + let access = WorldViewResolutionAccess::HostedEditShareFile { + origin: origin.to_owned(), + hosted_world_id: hosted_world_id.to_owned(), + credential_file: credential_file.to_owned(), + }; + let invocation = WorldCliInvocation { + args: vec![ + "hosted".into(), + "script".into(), + "--json".into(), + "--base-url".into(), + origin.into(), + "--edit-share-file".into(), + credential_file.to_string_lossy().into_owned(), + "--anonymous-session".into(), + "--expected-revision".into(), + expected_revision.into(), + "--stdin".into(), + ], + stdin: Some(script), + }; + let stdout = run_world_cli_invocation(binary, invocation, &reference, &access).await?; + let mut envelope: serde_json::Value = serde_json::from_slice(&stdout)?; + if envelope.get("ok").and_then(serde_json::Value::as_bool) != Some(true) { + return invalid_result("the hosted script returned a non-success envelope"); + } + let result = envelope + .get("result") + .and_then(serde_json::Value::as_object) + .ok_or_else(|| { + WorldViewResolutionError::InvalidResult( + "the hosted script success envelope omitted `result`".into(), + ) + })?; + if result.get("command").and_then(serde_json::Value::as_str) != Some("hosted script") { + return invalid_result("the hosted script result carried an unexpected command"); + } + if result + .get("revision") + .and_then(serde_json::Value::as_str) + .is_none_or(str::is_empty) + { + return invalid_result("the hosted script result omitted its revision"); + } + redact_credential_file_path_in_json(&mut envelope, credential_file); + Ok(envelope) +} + +/// Apply one revision-checked local WorldLang script through a machine-local +/// broker without exposing the mutable source root to the caller. +pub async fn apply_local_world_script( + origin: &str, + mirror_id: &str, + source_root: impl AsRef, + expected_revision: &str, + script: &str, +) -> Result { + let binary = std::env::var_os("SHIVAI_WORLD_BIN") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("world")); + apply_local_world_script_with_binary( + origin, + mirror_id, + source_root, + expected_revision, + script, + binary, + ) + .await +} + +/// Apply one revision-checked local WorldLang script through an explicit +/// source `world` binary. +pub async fn apply_local_world_script_with_binary( + origin: &str, + mirror_id: &str, + source_root: impl AsRef, + expected_revision: &str, + script: &str, + binary: impl AsRef, +) -> Result { + validate_event_id("expectedRevision", expected_revision)?; + if script.trim().is_empty() { + return Err(WorldViewResolutionError::InvalidRequest( + "local world script must not be blank".into(), + )); + } + let reference = WorldViewReference::LocalWorldMirrorLatest { + origin: origin.to_owned(), + mirror_id: mirror_id.to_owned(), + }; + reference + .validate() + .map_err(WorldViewResolutionError::InvalidRequest)?; + + let source_root = source_root.as_ref(); + let binary = binary.as_ref(); + let output = tokio::process::Command::new(binary) + .args([ + "script", + "--json", + "--root", + source_root.to_string_lossy().as_ref(), + "--expected-revision", + expected_revision, + "--stdin", + ]) + .kill_on_drop(true) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|source| WorldViewResolutionError::Launch { + binary: binary.to_owned(), + source, + })?; + let mut child = output; + let mut stdin = child + .stdin + .take() + .ok_or_else(|| WorldViewResolutionError::Input { + binary: binary.to_owned(), + source: std::io::Error::other("local script stdin pipe was not available"), + })?; + stdin + .write_all(script.as_bytes()) + .await + .map_err(|source| WorldViewResolutionError::Input { + binary: binary.to_owned(), + source, + })?; + drop(stdin); + let output = + child + .wait_with_output() + .await + .map_err(|source| WorldViewResolutionError::Launch { + binary: binary.to_owned(), + source, + })?; + if !output.status.success() { + let diagnostics = + redact_local_world_root(String::from_utf8_lossy(&output.stderr).trim(), source_root); + return Err(command_failure(diagnostics)); + } + + let mut envelope: serde_json::Value = serde_json::from_slice(&output.stdout)?; + if envelope.get("ok").and_then(serde_json::Value::as_bool) != Some(true) { + return invalid_result("the local script returned a non-success envelope"); + } + let result = envelope + .get("result") + .and_then(serde_json::Value::as_object) + .ok_or_else(|| { + WorldViewResolutionError::InvalidResult( + "the local script success envelope omitted `result`".into(), + ) + })?; + if result.get("command").and_then(serde_json::Value::as_str) != Some("script") { + return invalid_result("the local script result carried an unexpected command"); + } + if result + .get("revision") + .and_then(serde_json::Value::as_str) + .is_none_or(str::is_empty) + { + return invalid_result("the local script result omitted its revision"); + } + redact_local_world_root_in_json(&mut envelope, source_root); + Ok(envelope) +} + +/// Mint or reuse a stable public live-view share using `SHIVAI_WORLD_BIN`. +pub async fn publish_hosted_live_view_share( + origin: &str, + credential_file: impl AsRef, + view_qualified_name: &str, +) -> Result { + let binary = std::env::var_os("SHIVAI_WORLD_BIN") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("world")); + publish_hosted_live_view_share_with_binary(origin, credential_file, view_qualified_name, binary) + .await +} + +/// Mint or reuse a stable public live-view share through an explicit `world` binary. +pub async fn publish_hosted_live_view_share_with_binary( + origin: &str, + credential_file: impl AsRef, + view_qualified_name: &str, + binary: impl AsRef, +) -> Result { + let binary = binary.as_ref(); + let output = tokio::process::Command::new(binary) + .args([ + "hosted", + "view", + "share-live", + "--json", + "--base-url", + origin, + "--edit-share-file", + &credential_file.as_ref().to_string_lossy(), + "--anonymous-session", + "--view", + view_qualified_name, + ]) + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .map_err(|source| WorldViewResolutionError::Launch { + binary: binary.to_owned(), + source, + })?; + if !output.status.success() { + let diagnostics = redact_credential_file_path( + String::from_utf8_lossy(&output.stderr).trim(), + credential_file.as_ref(), + ); + return Err(WorldViewResolutionError::CommandFailed( + if diagnostics.is_empty() { + "the command exited without diagnostics".into() + } else { + diagnostics + }, + )); + } + + let envelope: WorldResultEnvelope = + serde_json::from_slice(&output.stdout)?; + if !envelope.ok { + return invalid_result("the command returned a non-success envelope"); + } + let result = envelope.result.ok_or_else(|| { + WorldViewResolutionError::InvalidResult("the success envelope omitted `result`".into()) + })?; + if result.command != "hosted view share-live" { + return invalid_result(format!( + "unexpected live-share command `{}`", + result.command + )); + } + let published = result.live_view_share; + let live_view_share = published.live_view_share; + let source = published.source; + for (field, value) in [ + ("revision", result.revision.as_str()), + ("source.hostedWorldId", source.hosted_world_id.as_str()), + ("source.revisionId", source.revision_id.as_str()), + ("source.packageRevision", source.package_revision.as_str()), + ( + "liveViewShare.hostedWorldId", + live_view_share.hosted_world_id.as_str(), + ), + ( + "liveViewShare.realmQualifiedName", + live_view_share.realm_qualified_name.as_str(), + ), + ( + "liveViewShare.viewQualifiedName", + live_view_share.view_qualified_name.as_str(), + ), + ( + "liveViewShare.shareToken", + live_view_share.share_token.as_str(), + ), + ( + "liveViewShare.shareUrlPath", + live_view_share.share_url_path.as_str(), + ), + ("liveViewShare.title", live_view_share.title.as_str()), + ] { + if value.trim().is_empty() { + return invalid_result(format!("live-share `{field}` is blank")); + } + } + if result.revision != source.package_revision { + return invalid_result( + "live-share result revision did not match its source package revision", + ); + } + if source.revision_id == source.package_revision { + return invalid_result( + "live-share source revision id unexpectedly matched its package revision", + ); + } + if live_view_share.hosted_world_id != source.hosted_world_id { + return invalid_result("live-share hosted world did not match its source hosted world"); + } + if live_view_share.view_qualified_name != view_qualified_name { + return invalid_result(format!( + "live-share view `{}` did not match requested `{view_qualified_name}`", + live_view_share.view_qualified_name + )); + } + + Ok(PublishedHostedLiveViewShare { + hosted_world_id: source.hosted_world_id, + source_revision: source.revision_id, + package_revision: source.package_revision, + realm_qualified_name: live_view_share.realm_qualified_name, + view_qualified_name: live_view_share.view_qualified_name, + share_token: live_view_share.share_token, + share_url_path: live_view_share.share_url_path, + title: live_view_share.title, + }) +} + +fn decode_world_view_catalog(stdout: &[u8]) -> Result { + let envelope: WorldResultEnvelope = serde_json::from_slice(stdout)?; + if !envelope.ok { + return invalid_result("the command returned a non-success envelope"); + } + let result = envelope.result.ok_or_else(|| { + WorldViewResolutionError::InvalidResult("the success envelope omitted `result`".into()) + })?; + if result.command != "view.catalog" { + return invalid_result(format!("unexpected catalog command `{}`", result.command)); + } + if result.format_version != WORLD_VIEW_CATALOG_FORMAT_VERSION { + return invalid_result(format!( + "unsupported catalog format version {}", + result.format_version + )); + } + if result.revision.trim().is_empty() { + return invalid_result("catalog `revision` is blank"); + } + if result.world_qualified_name.trim().is_empty() { + return invalid_result("catalog `worldQualifiedName` is blank"); + } + + let mut qualified_names = HashSet::with_capacity(result.views.len()); + for view in &result.views { + if view.name.trim().is_empty() + || view.qualified_name.trim().is_empty() + || view.realm.name.trim().is_empty() + || view.realm.qualified_name.trim().is_empty() + { + return invalid_result("catalog view names and realm identities must not be blank"); + } + if !qualified_names.insert(&view.qualified_name) { + return invalid_result(format!( + "duplicate catalog view qualified name `{}`", + view.qualified_name + )); + } + } + + Ok(WorldViewCatalog { + format_version: result.format_version, + revision: result.revision, + world_qualified_name: result.world_qualified_name, + views: result.views, + }) +} + +fn decode_world_view_resolution( + request: &WorldViewResolutionRequest, + stdout: &[u8], + resolved_at: DateTime, +) -> Result { + let envelope: WorldResultEnvelope = serde_json::from_slice(stdout)?; + if !envelope.ok { + return Err(WorldViewResolutionError::InvalidResult( + "the command returned a non-success envelope".into(), + )); + } + let result = envelope.result.ok_or_else(|| { + WorldViewResolutionError::InvalidResult("the success envelope omitted `result`".into()) + })?; + validate_dump_result(request, &result)?; + + let (authority, freshness) = authority_readback(&request.binding.reference, &result)?; + Ok(ResolvedWorldView { + format_version: WORLD_VIEW_RESOLUTION_FORMAT_VERSION, + binding_id: request.binding.id, + channel_id: request.channel_id, + declared_scope: request.declared_scope.clone(), + effective_scope: request.effective_scope.clone(), + binding_revision_event_id: request.binding_revision_event_id.clone(), + source_revision: result.revision, + freshness, + authority, + realm: result.realm, + view: result.view, + view_dump: ResolvedWorldViewDump { + counts: result.counts, + nodes: result.nodes, + ready_leaves: result.ready_leaves, + satisfied_nodes: result.satisfied_nodes, + blocked_nodes: result.blocked_nodes, + edges: result.edges, + }, + presentation: result.presentation, + resolved_at, + next_command: next_command(request), + }) +} + +fn validate_dump_result( + request: &WorldViewResolutionRequest, + result: &WorldViewDumpResult, +) -> Result<(), WorldViewResolutionError> { + if result.revision.trim().is_empty() { + return invalid_result("`revision` is blank"); + } + if result.realm.qualified_name != request.binding.realm_qualified_name { + return invalid_result(format!( + "realm `{}` did not match requested `{}`", + result.realm.qualified_name, request.binding.realm_qualified_name + )); + } + if result.view.qualified_name != request.binding.view_qualified_name { + return invalid_result(format!( + "view `{}` did not match requested `{}`", + result.view.qualified_name, request.binding.view_qualified_name + )); + } + if result.counts.nodes != result.nodes.len() { + return invalid_result(format!( + "node count {} did not match {} returned nodes", + result.counts.nodes, + result.nodes.len() + )); + } + if result.counts.edges != result.edges.len() { + return invalid_result(format!( + "edge count {} did not match {} returned edges", + result.counts.edges, + result.edges.len() + )); + } + if result.presentation.format_version != WORLD_VIEW_RESOLUTION_FORMAT_VERSION { + return invalid_result(format!( + "unsupported presentation format version {}", + result.presentation.format_version + )); + } + for (appearance, model) in [ + ("dark", &result.presentation.dark), + ("light", &result.presentation.light), + ] { + if model.selection.realm_qualified_name != request.binding.realm_qualified_name + || model.selection.view_qualified_name != request.binding.view_qualified_name + { + return invalid_result(format!( + "{appearance} presentation selection did not match the requested realm/view" + )); + } + if model.revision.as_deref() != Some(result.revision.as_str()) { + return invalid_result(format!( + "{appearance} presentation revision did not match the resolved source revision" + )); + } + } + Ok(()) +} + +fn invalid_result(message: impl Into) -> Result { + Err(WorldViewResolutionError::InvalidResult(message.into())) +} + +fn authority_readback( + reference: &WorldViewReference, + result: &WorldViewDumpResult, +) -> Result<(WorldViewResolutionAuthority, WorldViewResolutionFreshness), WorldViewResolutionError> +{ + match reference { + WorldViewReference::HostedWorldViewExport { origin, .. } => Ok(( + WorldViewResolutionAuthority::HostedWorldViewExport { + origin: origin.clone(), + }, + WorldViewResolutionFreshness::Pinned, + )), + WorldViewReference::HostedWorldLiveViewShare { origin, .. } => { + let hosted_world_id = result + .hosted_world_id + .as_deref() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + WorldViewResolutionError::InvalidResult( + "hosted live-view resolution omitted `hostedWorldId`".into(), + ) + })?; + Ok(( + WorldViewResolutionAuthority::HostedWorldLiveViewShare { + origin: origin.clone(), + hosted_world_id: hosted_world_id.to_owned(), + }, + WorldViewResolutionFreshness::LatestAtResolution, + )) + } + WorldViewReference::LocalWorldMirrorLatest { origin, mirror_id } => Ok(( + WorldViewResolutionAuthority::LocalWorldMirrorLatest { + origin: origin.clone(), + mirror_id: mirror_id.clone(), + }, + WorldViewResolutionFreshness::LatestAtResolution, + )), + WorldViewReference::HostedWorldLatest { + origin, + hosted_world_id, + } => Ok(( + WorldViewResolutionAuthority::HostedWorldLatest { + origin: origin.clone(), + hosted_world_id: hosted_world_id.clone(), + }, + WorldViewResolutionFreshness::LatestAtResolution, + )), + } +} + +struct WorldCliInvocation<'a> { + args: Vec, + stdin: Option<&'a str>, +} + +fn world_cli_invocation<'a>( + binding: &'a WorldViewBinding, + access: &WorldViewResolutionAccess, +) -> Result, WorldViewResolutionError> { + let mut invocation = world_view_cli_invocation(&binding.reference, access, "dump")?; + invocation.args.extend([ + "--realm".into(), + binding.realm_qualified_name.clone(), + "--view".into(), + binding.view_qualified_name.clone(), + ]); + Ok(invocation) +} + +fn world_view_cli_invocation<'a>( + reference: &'a WorldViewReference, + access: &WorldViewResolutionAccess, + subcommand: &str, +) -> Result, WorldViewResolutionError> { + let (args, stdin) = match reference { + WorldViewReference::LocalWorldMirrorLatest { origin, mirror_id } => match access { + WorldViewResolutionAccess::LocalSourceRoot { + origin: authorized_origin, + mirror_id: authorized_mirror_id, + source_root, + } if authorized_origin == origin && authorized_mirror_id == mirror_id => ( + vec![ + "view".into(), + subcommand.into(), + "--json".into(), + "--root".into(), + source_root.to_string_lossy().into_owned(), + ], + None, + ), + WorldViewResolutionAccess::TrustedPublicOrigin { + origin: authorized_origin, + } if authorized_origin == origin => ( + vec![ + "hosted".into(), + "view".into(), + subcommand.into(), + "--json".into(), + "--base-url".into(), + origin.clone(), + "--local-mirror".into(), + mirror_id.clone(), + ], + None, + ), + _ => return Err(resolution_access_mismatch()), + }, + WorldViewReference::HostedWorldViewExport { + origin, + share_token, + } => { + let WorldViewResolutionAccess::TrustedPublicOrigin { + origin: authorized_origin, + } = access + else { + return Err(resolution_access_mismatch()); + }; + if authorized_origin != origin { + return Err(resolution_access_mismatch()); + } + ( + vec![ + "hosted".into(), + "view".into(), + subcommand.into(), + "--json".into(), + "--base-url".into(), + origin.clone(), + "--share-token-stdin".into(), + ], + Some(share_token.as_str()), + ) + } + WorldViewReference::HostedWorldLiveViewShare { + origin, + share_token, + } => { + let WorldViewResolutionAccess::TrustedPublicOrigin { + origin: authorized_origin, + } = access + else { + return Err(resolution_access_mismatch()); + }; + if authorized_origin != origin { + return Err(resolution_access_mismatch()); + } + ( + vec![ + "hosted".into(), + "view".into(), + subcommand.into(), + "--json".into(), + "--base-url".into(), + origin.clone(), + "--live-share-token-stdin".into(), + ], + Some(share_token.as_str()), + ) + } + WorldViewReference::HostedWorldLatest { + origin, + hosted_world_id, + } => { + let WorldViewResolutionAccess::HostedEditShareFile { + origin: authorized_origin, + hosted_world_id: authorized_hosted_world_id, + credential_file, + } = access + else { + return Err(resolution_access_mismatch()); + }; + if authorized_origin != origin || authorized_hosted_world_id != hosted_world_id { + return Err(resolution_access_mismatch()); + } + ( + vec![ + "hosted".into(), + "view".into(), + subcommand.into(), + "--json".into(), + "--base-url".into(), + origin.clone(), + "--edit-share-file".into(), + credential_file.to_string_lossy().into_owned(), + "--anonymous-session".into(), + ], + None, + ) + } + }; + Ok(WorldCliInvocation { args, stdin }) +} + +fn redact_diagnostics( + diagnostics: &str, + reference: &WorldViewReference, + access: &WorldViewResolutionAccess, +) -> String { + let diagnostics = match reference { + WorldViewReference::HostedWorldViewExport { share_token, .. } => { + diagnostics.replace(share_token, "") + } + WorldViewReference::HostedWorldLiveViewShare { share_token, .. } => { + diagnostics.replace(share_token, "") + } + WorldViewReference::LocalWorldMirrorLatest { .. } + | WorldViewReference::HostedWorldLatest { .. } => diagnostics.to_owned(), + }; + match access { + WorldViewResolutionAccess::HostedEditShareFile { + credential_file, .. + } => redact_credential_file_path(&diagnostics, credential_file), + WorldViewResolutionAccess::LocalSourceRoot { source_root, .. } => { + redact_local_world_root(&diagnostics, source_root) + } + WorldViewResolutionAccess::TrustedPublicOrigin { .. } => diagnostics, + } +} + +fn resolution_access_mismatch() -> WorldViewResolutionError { + WorldViewResolutionError::InvalidRequest( + "world-view resolution authorization does not match the current binding reference".into(), + ) +} + +fn command_failure(diagnostics: String) -> WorldViewResolutionError { + if diagnostics.contains("world.hosted.revision_conflict") + || diagnostics.contains("world.workflow.revision_conflict") + { + WorldViewResolutionError::RevisionConflict(diagnostics) + } else { + WorldViewResolutionError::CommandFailed(diagnostics) + } +} + +fn redact_credential_file_path_in_json(value: &mut serde_json::Value, credential_file: &Path) { + match value { + serde_json::Value::String(text) => { + *text = redact_credential_file_path(text, credential_file); + } + serde_json::Value::Array(values) => { + for value in values { + redact_credential_file_path_in_json(value, credential_file); + } + } + serde_json::Value::Object(values) => { + for value in values.values_mut() { + redact_credential_file_path_in_json(value, credential_file); + } + } + serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {} + } +} + +fn redact_credential_file_path(diagnostics: &str, credential_file: &Path) -> String { + if credential_file.as_os_str().is_empty() { + return diagnostics.to_owned(); + } + diagnostics.replace( + credential_file.to_string_lossy().as_ref(), + "", + ) +} + +fn redact_local_world_root_in_json(value: &mut serde_json::Value, source_root: &Path) { + match value { + serde_json::Value::String(text) => { + *text = redact_local_world_root(text, source_root); + } + serde_json::Value::Array(values) => { + for value in values { + redact_local_world_root_in_json(value, source_root); + } + } + serde_json::Value::Object(values) => { + for value in values.values_mut() { + redact_local_world_root_in_json(value, source_root); + } + } + serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {} + } +} + +fn redact_local_world_root(diagnostics: &str, source_root: &Path) -> String { + if source_root.as_os_str().is_empty() { + return diagnostics.to_owned(); + } + diagnostics.replace( + source_root.to_string_lossy().as_ref(), + "", + ) +} + +fn next_command(request: &WorldViewResolutionRequest) -> String { + let mut command = format!("buzz world-views resolve --channel {}", request.channel_id); + if let Some(thread_root_event_id) = request.declared_scope.thread_root_event_id() { + command.push_str(" --thread-root "); + command.push_str(thread_root_event_id); + } + command.push_str(" --binding "); + command.push_str(&request.binding.id.to_string()); + command +} + +fn validate_event_id(field: &str, value: &str) -> Result<(), WorldViewResolutionError> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(WorldViewResolutionError::InvalidRequest(format!( + "{field} must be 64 lowercase hex characters" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use buzz_core::world_view::WorldViewDisplayMode; + use chrono::TimeZone; + use serde_json::json; + + fn request(reference: WorldViewReference) -> WorldViewResolutionRequest { + WorldViewResolutionRequest { + channel_id: Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").unwrap(), + binding: WorldViewBinding { + id: Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap(), + label: Some("Launch board".into()), + reference, + realm_qualified_name: "world::main".into(), + view_qualified_name: "world::main::@Board".into(), + display_mode: WorldViewDisplayMode::Graph, + }, + declared_scope: WorldViewBindingScope::Channel, + effective_scope: WorldViewBindingScope::Channel, + binding_revision_event_id: "b".repeat(64), + } + } + + fn presentation(revision: &str) -> serde_json::Value { + let graph = json!({ + "kind": "ready", + "graphBackgroundHex": "#111113", + "graphPattern": "dots", + "clusters": [], + "nodes": [{ + "id": "world::main::Ship", + "label": "Ship", + "preferenceQualifiedName": "world::main::Ship", + "status": "ready", + "targetState": null, + "isReady": true, + "isLeaf": true, + "signalCases": [{ + "caseName": "implementing", + "evidence": [{ + "kind": "typedForm", + "appearance": null, + "formQualifiedName": "coordination::AgentAssignment", + "matchedEntries": [], + "value": { + "kind": "object", + "entries": [{ + "key": "state", + "value": { + "kind": "string", + "value": "active" + } + }] + } + }], + "signalName": "CodexThread", + "meanings": [ + { "kind": "targetState", "state": "implementing" }, + { "kind": "codexThread" } + ] + }], + "signalCaseNames": ["implementing"], + "fillHex": "#1c2024", + "borderHex": "#3e63dd", + "textHex": "#f0f0f3", + "deemphasis": null, + "effect": null, + "position": { "x": 150, "y": 57.5 }, + "size": { "width": 300, "height": 115 } + }], + "edges": [], + "bounds": { "width": 420, "height": 235 } + }); + let model = json!({ + "graph": graph, + "revision": revision, + "selection": { + "realmQualifiedName": "world::main", + "viewQualifiedName": "world::main::@Board" + } + }); + json!({ "formatVersion": 1, "dark": model, "light": model }) + } + + fn success_stdout(revision: &str) -> Vec { + serde_json::to_vec(&json!({ + "ok": true, + "result": { + "revision": revision, + "realm": { "name": "main", "qualifiedName": "world::main" }, + "view": { + "name": "Board", + "qualifiedName": "world::main::@Board", + "slots": {}, + "flowspaces": [], + "lenses": [] + }, + "counts": { + "nodes": 1, + "edges": 0, + "ready": 1, + "actionableReady": 1, + "satisfied": 0, + "blocked": 0 + }, + "presentation": presentation(revision), + "nodes": [{ + "preference": "Ship", + "qualifiedName": "world::main::Ship", + "status": "ready", + "actionable": true, + "leaf": true, + "inFocus": false, + "inSatisfied": false, + "blockers": [], + "enablers": [], + "note": { "preview": null, "truncated": false }, + "signals": [] + }], + "readyLeaves": [{ + "preference": "Ship", + "qualifiedName": "world::main::Ship", + "status": "ready", + "actionable": true, + "leaf": true, + "inFocus": false, + "inSatisfied": false, + "blockers": [], + "enablers": [], + "note": { "preview": null, "truncated": false }, + "signals": [] + }], + "satisfiedNodes": [], + "blockedNodes": [], + "edges": [] + } + })) + .unwrap() + } + + #[test] + fn decodes_canonical_view_catalog_identities() { + let stdout = serde_json::to_vec(&json!({ + "ok": true, + "result": { + "command": "view.catalog", + "formatVersion": 1, + "revision": "source-revision-1", + "root": "hosted-local-mirror:mirror-1", + "worldQualifiedName": "world", + "views": [{ + "name": "@Board", + "qualifiedName": "@main::Board", + "realm": { + "name": "main", + "qualifiedName": "world::main" + } + }] + }, + "diagnostics": [] + })) + .unwrap(); + + let catalog = decode_world_view_catalog(&stdout).unwrap(); + + assert_eq!(catalog.world_qualified_name, "world"); + assert_eq!(catalog.views[0].qualified_name, "@main::Board"); + assert_eq!(catalog.views[0].realm.qualified_name, "world::main"); + } + + #[test] + fn catalog_routes_export_capability_over_stdin_without_a_selection() { + let reference = WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "secret-view-token".into(), + }; + + let invocation = world_view_cli_invocation( + &reference, + &WorldViewResolutionAccess::TrustedPublicOrigin { + origin: "https://manifest.shivai.space".into(), + }, + "catalog", + ) + .unwrap(); + + assert_eq!(&invocation.args[..3], ["hosted", "view", "catalog"]); + assert!(invocation + .args + .iter() + .any(|argument| argument == "--share-token-stdin")); + assert!(!invocation.args.iter().any(|argument| argument == "--realm")); + assert_eq!(invocation.stdin, Some("secret-view-token")); + } + + #[test] + fn decodes_one_typed_resolution_and_omits_the_hosted_token() { + let request = request(WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "secret-view-token".into(), + }); + let resolved_at = Utc.with_ymd_and_hms(2026, 7, 24, 12, 0, 0).unwrap(); + let resolved = decode_world_view_resolution( + &request, + &success_stdout("source-revision-1"), + resolved_at, + ) + .unwrap(); + + assert_eq!(resolved.binding_id, request.binding.id); + assert_eq!(resolved.source_revision, "source-revision-1"); + assert_eq!(resolved.view_dump.counts.nodes, 1); + assert_eq!(resolved.freshness, WorldViewResolutionFreshness::Pinned); + let encoded = serde_json::to_string(&resolved).unwrap(); + assert!(!encoded.contains("secret-view-token")); + assert!(encoded.contains("buzz world-views resolve")); + } + + #[test] + fn rejects_a_selection_that_does_not_match_the_binding() { + let mut value: serde_json::Value = + serde_json::from_slice(&success_stdout("source-revision-1")).unwrap(); + value["result"]["view"]["qualifiedName"] = json!("world::main::@Wrong"); + let error = decode_world_view_resolution( + &request(WorldViewReference::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }), + &serde_json::to_vec(&value).unwrap(), + Utc::now(), + ) + .expect_err("mismatched view must fail"); + + assert!(error.to_string().contains("did not match requested")); + } + + #[test] + fn redacts_hosted_tokens_from_command_diagnostics() { + let reference = WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "secret-view-token".into(), + }; + assert_eq!( + redact_diagnostics( + "failed secret-view-token", + &reference, + &WorldViewResolutionAccess::TrustedPublicOrigin { + origin: "https://manifest.shivai.space".into(), + }, + ), + "failed " + ); + } + + #[test] + fn redacts_host_credential_paths_from_command_diagnostics() { + let reference = WorldViewReference::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }; + let access = WorldViewResolutionAccess::HostedEditShareFile { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: PathBuf::from("/private/edit-share.txt"), + }; + + assert_eq!( + redact_diagnostics( + "failed to read /private/edit-share.txt", + &reference, + &access, + ), + "failed to read " + ); + } + #[test] + fn routes_private_local_read_authority_through_the_source_root() { + let request = request(WorldViewReference::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }); + let access = WorldViewResolutionAccess::LocalSourceRoot { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + source_root: PathBuf::from("/private/delivery.world"), + }; + + let invocation = world_cli_invocation(&request.binding, &access).unwrap(); + + assert_eq!(&invocation.args[..3], ["view", "dump", "--json"]); + assert!(invocation + .args + .windows(2) + .any(|pair| pair == ["--root", "/private/delivery.world"])); + assert!(!invocation + .args + .iter() + .any(|argument| argument == "--local-mirror")); + assert_eq!(invocation.stdin, None); + assert_eq!( + redact_diagnostics( + "failed to read /private/delivery.world", + &request.binding.reference, + &access, + ), + "failed to read " + ); + } + + #[test] + fn routes_hosted_export_capability_over_stdin_not_process_arguments() { + let request = request(WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "secret-view-token".into(), + }); + + let invocation = world_cli_invocation( + &request.binding, + &WorldViewResolutionAccess::TrustedPublicOrigin { + origin: "https://manifest.shivai.space".into(), + }, + ) + .unwrap(); + + assert!(invocation + .args + .iter() + .any(|argument| argument == "--share-token-stdin")); + assert!(!invocation.args.join(" ").contains("secret-view-token")); + assert_eq!(invocation.stdin, Some("secret-view-token")); + } + + #[test] + fn routes_private_hosted_read_authority_through_a_credential_file() { + let request = request(WorldViewReference::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }); + let access = WorldViewResolutionAccess::HostedEditShareFile { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: PathBuf::from("/private/edit-share.txt"), + }; + + let invocation = world_cli_invocation(&request.binding, &access).unwrap(); + + assert!(invocation + .args + .windows(2) + .any(|pair| { pair == ["--edit-share-file", "/private/edit-share.txt"] })); + assert!(invocation + .args + .iter() + .any(|argument| argument == "--anonymous-session")); + assert_eq!(invocation.stdin, None); + let envelope: WorldResultEnvelope = + serde_json::from_slice(&success_stdout("source-revision-1")).unwrap(); + let result = envelope.result.unwrap(); + assert_eq!( + authority_readback(&request.binding.reference, &result).unwrap(), + ( + WorldViewResolutionAuthority::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }, + WorldViewResolutionFreshness::LatestAtResolution, + ) + ); + } + + #[tokio::test] + async fn rejects_an_untrusted_binding_origin_before_launching_world() { + let untrusted_origin = "https://attacker.example"; + let error = resolve_world_view_with_binary( + request(WorldViewReference::HostedWorldViewExport { + origin: untrusted_origin.into(), + share_token: "attacker-authored-token".into(), + }), + Path::new("/world-must-not-be-launched"), + &WorldAuthorityRegistry::default(), + ) + .await + .expect_err("untrusted origin must fail before process launch"); + + assert!(matches!( + error, + WorldViewResolutionError::UntrustedOrigin { origin } + if origin == untrusted_origin + )); + } + + #[cfg(unix)] + #[tokio::test] + async fn writes_hosted_export_capability_to_the_child_stdin_pipe() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = + std::env::temp_dir().join(format!("buzz-world-view-resolver-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp resolver root"); + let output_path = temp_root.join("world-output.json"); + std::fs::write(&output_path, success_stdout("source-revision-1")) + .expect("write fake world output"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!( + "#!/bin/sh\n\ + token=$(cat)\n\ + test \"$token\" = \"secret-view-token\" || exit 41\n\ + cat '{}'\n", + output_path.display() + ), + ) + .expect("write fake world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions).expect("make fake world executable"); + + let registry = WorldAuthorityRegistry::default(); + let resolved = resolve_world_view_with_binary( + request(WorldViewReference::HostedWorldViewExport { + origin: "https://manifest.shivai.space".into(), + share_token: "secret-view-token".into(), + }), + &binary_path, + ®istry, + ) + .await + .expect("resolve through stdin-aware child"); + + assert_eq!(resolved.source_revision, "source-revision-1"); + std::fs::remove_dir_all(temp_root).expect("remove temp resolver root"); + } + #[cfg(unix)] + #[tokio::test] + async fn decodes_canonical_nested_hosted_live_share_payload() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = + std::env::temp_dir().join(format!("buzz-hosted-live-share-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp resolver root"); + let output_path = temp_root.join("world-output.json"); + let output = serde_json::to_vec(&json!({ + "ok": true, + "result": { + "baseUrl": "https://manifest.shivai.space", + "command": "hosted view share-live", + "liveViewShare": { + "liveViewShare": { + "id": "live-share-1", + "hostedWorldId": "hosted-1", + "shareToken": "public-live-share-token", + "shareUrlPath": "/world/live/public-live-share-token", + "title": "Focused scope", + "viewQualifiedName": "world::main::@Focused scope", + "viewLocalName": "Focused scope", + "realmQualifiedName": "world::main", + "referencedFlowspaceQualifiedNames": [], + "referencedSpaceQualifiedNames": [], + "createdAt": "2026-07-28T00:00:00.000Z" + }, + "source": { + "hostedWorldId": "hosted-1", + "revisionId": "revision-id-1", + "packageRevision": "package-revision-1", + "manifestWorldQualifiedName": "world" + } + }, + "revision": "package-revision-1", + "target": { "kind": "edit-share" } + }, + "diagnostics": [] + })) + .expect("encode canonical live-share output"); + std::fs::write(&output_path, output).expect("write fake world output"); + let credential_file = temp_root.join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share") + .expect("write private edit-share fixture"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!("#!/bin/sh\ncat '{}'\n", output_path.display()), + ) + .expect("write fake world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions).expect("make fake world executable"); + + let published = publish_hosted_live_view_share_with_binary( + "https://manifest.shivai.space", + &credential_file, + "world::main::@Focused scope", + &binary_path, + ) + .await + .expect("decode canonical hosted live-share payload"); + + assert_eq!( + published, + PublishedHostedLiveViewShare { + hosted_world_id: "hosted-1".into(), + source_revision: "revision-id-1".into(), + package_revision: "package-revision-1".into(), + realm_qualified_name: "world::main".into(), + view_qualified_name: "world::main::@Focused scope".into(), + share_token: "public-live-share-token".into(), + share_url_path: "/world/live/public-live-share-token".into(), + title: "Focused scope".into(), + } + ); + std::fs::remove_dir_all(temp_root).expect("remove temp resolver root"); + } + + #[cfg(unix)] + #[tokio::test] + async fn resolves_a_connected_local_source_without_returning_its_root() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = std::env::temp_dir().join(format!("buzz-local-resolve-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp local resolver root"); + let source_root = temp_root.join("private.world"); + let output_path = temp_root.join("world-output.json"); + std::fs::write(&output_path, success_stdout("local-source-revision")) + .expect("write fake local world output"); + let captured_args = temp_root.join("args.txt"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!( + "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\ncat '{}'\n", + captured_args.display(), + output_path.display(), + ), + ) + .expect("write fake local world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake local world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions) + .expect("make fake local world executable"); + let request = request(WorldViewReference::LocalWorldMirrorLatest { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + }); + + let mut registry = WorldAuthorityRegistry::default(); + registry + .upsert_local(buzz_core::world_view::LocalWorldAuthority { + origin: "https://manifest.shivai.space".into(), + mirror_id: "mirror-1".into(), + source_root: source_root.to_string_lossy().into_owned(), + capability_secret_file: temp_root + .join("local-capability") + .to_string_lossy() + .into_owned(), + }) + .unwrap(); + let resolved = resolve_world_view_with_binary(request, &binary_path, ®istry) + .await + .expect("resolve connected local source"); + + assert_eq!(resolved.source_revision, "local-source-revision"); + let args = std::fs::read_to_string(&captured_args).expect("read captured arguments"); + assert!(args.contains("--root")); + assert!(args.contains(&source_root.to_string_lossy().to_string())); + assert!(!args.contains("--local-mirror")); + let encoded = serde_json::to_string(&resolved).expect("encode resolved local view"); + assert!(!encoded.contains(&source_root.to_string_lossy().to_string())); + std::fs::remove_dir_all(temp_root).expect("remove temp local resolver root"); + } + + #[cfg(unix)] + #[tokio::test] + async fn applies_scoped_hosted_script_without_returning_credential_path() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = std::env::temp_dir().join(format!("buzz-hosted-script-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp hosted script root"); + let credential_file = temp_root.join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share") + .expect("write private authority fixture"); + let expected_revision = "a".repeat(64); + let next_revision = "b".repeat(64); + let latest_output = temp_root.join("latest.json"); + std::fs::write( + &latest_output, + serde_json::to_vec(&json!({ + "ok": true, + "result": { + "projection": { "hostedWorldId": "hosted-1" }, + "revision": expected_revision, + } + })) + .expect("encode latest output"), + ) + .expect("write latest output"); + let script_output = temp_root.join("script.json"); + std::fs::write( + &script_output, + serde_json::to_vec(&json!({ + "ok": true, + "result": { + "command": "hosted script", + "credentialPathDiagnostic": credential_file, + "revision": next_revision, + "script": { "lineCount": 1 }, + } + })) + .expect("encode script output"), + ) + .expect("write script output"); + let captured_stdin = temp_root.join("stdin.txt"); + let captured_args = temp_root.join("args.txt"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!( + "#!/bin/sh\n\ + if [ \"$1\" = hosted ] && [ \"$2\" = latest ]; then\n\ + cat '{}'\n\ + exit 0\n\ + fi\n\ + if [ \"$1\" = hosted ] && [ \"$2\" = script ]; then\n\ + printf '%s\\n' \"$@\" > '{}'\n\ + cat > '{}'\n\ + cat '{}'\n\ + exit 0\n\ + fi\n\ + exit 42\n", + latest_output.display(), + captured_args.display(), + captured_stdin.display(), + script_output.display(), + ), + ) + .expect("write fake world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions).expect("make fake world executable"); + + let script = "world add --disconnected \"Scoped triage\""; + let result = apply_hosted_world_script_with_binary( + "https://manifest.shivai.space", + "hosted-1", + &credential_file, + &expected_revision, + script, + &binary_path, + ) + .await + .expect("apply hosted script"); + + assert_eq!( + result + .pointer("/result/revision") + .and_then(serde_json::Value::as_str), + Some(next_revision.as_str()) + ); + assert_eq!( + std::fs::read_to_string(&captured_stdin).expect("read captured script"), + script + ); + let args = std::fs::read_to_string(&captured_args).expect("read captured arguments"); + assert!(args.contains("--edit-share-file")); + assert!(args.contains(&credential_file.to_string_lossy().to_string())); + let encoded = serde_json::to_string(&result).expect("encode broker result"); + assert!(!encoded.contains(&credential_file.to_string_lossy().to_string())); + assert!(encoded.contains("")); + assert!(!encoded.contains("private-edit-share")); + std::fs::remove_dir_all(temp_root).expect("remove temp hosted script root"); + } + + #[cfg(unix)] + #[tokio::test] + async fn stale_hosted_script_revision_never_launches_mutation() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = std::env::temp_dir().join(format!("buzz-hosted-stale-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp stale script root"); + let credential_file = temp_root.join("authority.edit-share"); + std::fs::write(&credential_file, "private-edit-share") + .expect("write private authority fixture"); + let expected_revision = "a".repeat(64); + let current_revision = "b".repeat(64); + let latest_output = temp_root.join("latest.json"); + std::fs::write( + &latest_output, + serde_json::to_vec(&json!({ + "ok": true, + "result": { + "projection": { "hostedWorldId": "hosted-1" }, + "revision": current_revision, + } + })) + .expect("encode latest output"), + ) + .expect("write latest output"); + let mutation_marker = temp_root.join("mutation-ran"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!( + "#!/bin/sh\n\ + if [ \"$1\" = hosted ] && [ \"$2\" = latest ]; then\n\ + cat '{}'\n\ + exit 0\n\ + fi\n\ + if [ \"$1\" = hosted ] && [ \"$2\" = script ]; then\n\ + touch '{}'\n\ + exit 0\n\ + fi\n\ + exit 42\n", + latest_output.display(), + mutation_marker.display(), + ), + ) + .expect("write fake world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions).expect("make fake world executable"); + + let error = apply_hosted_world_script_with_binary( + "https://manifest.shivai.space", + "hosted-1", + &credential_file, + &expected_revision, + "world add --disconnected \"Stale triage\"", + &binary_path, + ) + .await + .expect_err("stale revision must fail"); + + assert!(matches!( + error, + WorldViewResolutionError::RevisionConflict(_) + )); + assert!(error.to_string().contains(¤t_revision)); + assert!(!mutation_marker.exists()); + std::fs::remove_dir_all(temp_root).expect("remove temp stale script root"); + } + + #[cfg(unix)] + #[tokio::test] + async fn applies_scoped_local_script_without_returning_source_root() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = std::env::temp_dir().join(format!("buzz-local-script-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp local script root"); + let source_root = temp_root.join("private.world"); + let expected_revision = "a".repeat(64); + let next_revision = "b".repeat(64); + let script_output = temp_root.join("script.json"); + std::fs::write( + &script_output, + serde_json::to_vec(&json!({ + "ok": true, + "result": { + "command": "script", + "sourceRootDiagnostic": source_root, + "revision": next_revision, + "script": { "lineCount": 1 }, + } + })) + .expect("encode local script output"), + ) + .expect("write local script output"); + let captured_stdin = temp_root.join("stdin.txt"); + let captured_args = temp_root.join("args.txt"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!( + "#!/bin/sh\n\ + printf '%s\\n' \"$@\" > '{}'\n\ + cat > '{}'\n\ + cat '{}'\n", + captured_args.display(), + captured_stdin.display(), + script_output.display(), + ), + ) + .expect("write fake local world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake local world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions) + .expect("make fake local world executable"); + + let script = "world add --disconnected \"Scoped local triage\""; + let result = apply_local_world_script_with_binary( + "https://manifest.shivai.space", + "mirror-1", + &source_root, + &expected_revision, + script, + &binary_path, + ) + .await + .expect("apply local script"); + + assert_eq!( + result + .pointer("/result/revision") + .and_then(serde_json::Value::as_str), + Some(next_revision.as_str()) + ); + assert_eq!( + std::fs::read_to_string(&captured_stdin).expect("read captured local script"), + script + ); + let args = std::fs::read_to_string(&captured_args).expect("read captured local arguments"); + assert!(args.contains("--root")); + assert!(args.contains(&source_root.to_string_lossy().to_string())); + assert!(args.contains("--expected-revision")); + assert!(args.contains(&expected_revision)); + let encoded = serde_json::to_string(&result).expect("encode local broker result"); + assert!(!encoded.contains(&source_root.to_string_lossy().to_string())); + assert!(encoded.contains("")); + std::fs::remove_dir_all(temp_root).expect("remove temp local script root"); + } + + #[cfg(unix)] + #[tokio::test] + async fn stale_local_script_revision_returns_redacted_conflict_without_mutation() { + use std::os::unix::fs::PermissionsExt; + + let temp_root = std::env::temp_dir().join(format!("buzz-local-stale-{}", Uuid::new_v4())); + std::fs::create_dir(&temp_root).expect("create temp local stale root"); + let source_root = temp_root.join("private.world"); + let expected_revision = "a".repeat(64); + let captured_args = temp_root.join("args.txt"); + let mutation_marker = temp_root.join("mutation-ran"); + let binary_path = temp_root.join("world"); + std::fs::write( + &binary_path, + format!( + "#!/bin/sh\n\ + printf '%s\\n' \"$@\" > '{}'\n\ + echo 'Error (world.workflow.revision_conflict): stale {}' >&2\n\ + exit 1\n\ + touch '{}'\n", + captured_args.display(), + source_root.display(), + mutation_marker.display(), + ), + ) + .expect("write fake stale local world binary"); + let mut permissions = std::fs::metadata(&binary_path) + .expect("read fake stale local world metadata") + .permissions(); + permissions.set_mode(0o700); + std::fs::set_permissions(&binary_path, permissions) + .expect("make fake stale local world executable"); + + let error = apply_local_world_script_with_binary( + "https://manifest.shivai.space", + "mirror-1", + &source_root, + &expected_revision, + "world add --disconnected \"Stale local triage\"", + &binary_path, + ) + .await + .expect_err("stale local revision must fail"); + + assert!(matches!( + error, + WorldViewResolutionError::RevisionConflict(_) + )); + let diagnostics = error.to_string(); + assert!(!diagnostics.contains(&source_root.to_string_lossy().to_string())); + assert!(diagnostics.contains("")); + let args = std::fs::read_to_string(&captured_args).expect("read stale local arguments"); + assert!(args.contains("--expected-revision")); + assert!(args.contains(&expected_revision)); + assert!(!mutation_marker.exists()); + std::fs::remove_dir_all(temp_root).expect("remove temp local stale root"); + } +} diff --git a/crates/buzz-world-view-resolver/src/presentation.rs b/crates/buzz-world-view-resolver/src/presentation.rs new file mode 100644 index 0000000000..3e95eca58c --- /dev/null +++ b/crates/buzz-world-view-resolver/src/presentation.rs @@ -0,0 +1,508 @@ +use serde::{Deserialize, Serialize}; + +/// Shivai's normalized light/dark presentation contract. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewPresentationVariants { + pub format_version: u8, + pub dark: WorldViewPresentationModel, + pub light: WorldViewPresentationModel, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewPresentationModel { + pub graph: WorldViewGraphModel, + pub revision: Option, + pub selection: WorldViewSelection, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewSelection { + pub realm_qualified_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scope_preference_qualified_name: Option, + pub view_qualified_name: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "kebab-case", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewGraphModel { + Ready { + #[serde(rename = "graphBackgroundHex")] + graph_background_hex: String, + #[serde(rename = "graphPattern")] + graph_pattern: WorldViewGraphPattern, + clusters: Vec, + nodes: Vec, + edges: Vec, + bounds: WorldViewGraphBounds, + }, + Unavailable { + reason: WorldViewGraphUnavailableReason, + }, + Empty { + reason: WorldViewGraphEmptyReason, + #[serde( + rename = "graphBackgroundHex", + default, + skip_serializing_if = "Option::is_none" + )] + graph_background_hex: Option, + #[serde( + rename = "graphPattern", + default, + skip_serializing_if = "Option::is_none" + )] + graph_pattern: Option, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewGraphPattern { + None, + Grid, + Dots, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewGraphUnavailableReason { + MissingRealm, + MissingScope, + MissingView, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewGraphEmptyReason { + NoPreferences, + NoVisiblePreferences, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewGraphBounds { + pub width: f64, + pub height: f64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewGraphCluster { + pub background_rgba: String, + pub badge_background_rgba: String, + pub badge_text_hex: String, + pub id: String, + pub label: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewGraphNode { + pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cluster_id: Option, + pub label: String, + pub preference_qualified_name: String, + pub status: WorldViewGraphNodeStatus, + pub target_state: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub readiness_restrictions: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub is_ready: Option, + pub is_leaf: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signal_cases: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signal_case_names: Option>, + pub fill_hex: String, + pub border_hex: String, + pub text_hex: String, + pub deemphasis: Option, + pub effect: Option, + pub position: WorldViewGraphPosition, + pub size: WorldViewGraphSize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewGraphNodeStatus { + Default, + Leaf, + Ready, + Done, + Goal, + Focus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewTargetState { + Actionable, + Implementing, + Satisfied, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewLensDeemphasis { + Fade, + Ghost, + Hide, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewEffect { + Blur, + Dreamy, + Focused, + Glow, + Prismatic, + Clear, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewGraphPosition { + pub x: f64, + pub y: f64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewGraphSize { + pub width: f64, + pub height: f64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewGraphEdge { + pub deemphasis: Option, + pub id: String, + pub line_hex: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flowspace_qualified_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub policy_qualified_names: Option>, + pub source_id: String, + pub target_id: String, + pub connection_type: WorldViewConnectionType, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewConnectionType { + Foundational, + Alternative, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewReadinessRestriction { + Opening { + #[serde(default, skip_serializing_if = "Option::is_none")] + form_qualified_name: Option, + next_refresh_at: Option, + opening: WorldViewOpening, + preference_local_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + preference_qualified_name: Option, + source: WorldViewRestrictionSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + source_appearance_key: Option, + verdict: WorldViewOpeningVerdict, + }, + Hold { + #[serde(default, skip_serializing_if = "Option::is_none")] + form_qualified_name: Option, + hold: WorldViewReadinessHold, + mode: WorldViewReadinessHoldMode, + next_refresh_at: Option, + opening: WorldViewOpening, + preference_local_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + preference_qualified_name: Option, + reason_preference_local_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + reason_preference_qualified_name: Option, + source: WorldViewRestrictionSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + source_appearance_key: Option, + verdict: WorldViewReadinessHoldVerdict, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewRestrictionSource { + Preference, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewOpeningVerdict { + Dormant, + Indeterminate, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewReadinessHoldVerdict { + Active, + Indeterminate, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewOpening { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub place: Option, + pub windows: Vec, + pub zone: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewOpeningPlace { + pub place: String, + pub relation: WorldViewOpeningPlaceRelation, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum WorldViewOpeningPlaceRelation { + At, + AwayFrom, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewOpeningWindow { + pub anchor: WorldViewOpeningAnchor, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub span: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewOpeningAnchor { + Daily, + DateTime { + at: String, + }, + Interval { + end: String, + start: String, + }, + Weekly { + weekdays: Vec, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorldViewOpeningWeekday { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewOpeningTimeSpan { + pub end: String, + pub start: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewReadinessHold { + pub kind: WorldViewReadinessHoldKind, + pub mode: WorldViewReadinessHoldMode, + pub scope: WorldViewReadinessHoldScope, + pub window: WorldViewOpening, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewReadinessHoldKind { + Timebox, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum WorldViewReadinessHoldMode { + WhileActive, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "kebab-case", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewReadinessHoldScope { + Set { + set: WorldViewSetTarget, + }, + View { + #[serde(rename = "viewQualifiedName")] + view_qualified_name: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewSetTarget { + Space { + space_qualified_name: String, + }, + RealmDerived { + realm_qualified_name: String, + selector: WorldViewRealmSelector, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewRealmSelector { + All, + Home, + Refs, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewSignalOutput { + pub case_name: String, + pub evidence: Vec, + pub signal_name: String, + pub meanings: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewSignalMeaning { + TargetState { state: WorldViewTargetState }, + CodexThread, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum WorldViewSignalEvidence { + Slot { + slot_name: String, + slot_space_qualified_name: String, + }, + Ready { + ready: bool, + }, + TypedForm { + form_qualified_name: String, + value: WorldViewTypedObject, + }, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewTypedValueEntry { + pub key: String, + pub value: WorldViewTypedValue, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorldViewTypedObject { + pub kind: WorldViewTypedObjectKind, + pub entries: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum WorldViewTypedObjectKind { + Object, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde( + tag = "kind", + rename_all = "camelCase", + rename_all_fields = "camelCase", + deny_unknown_fields +)] +pub enum WorldViewTypedValue { + Object { + entries: Vec, + }, + Array { + items: Vec, + }, + String { + value: String, + }, + Number { + value: f64, + }, + Boolean { + value: bool, + }, + Null, + PreferenceRef { + preference_qualified_name: String, + }, + SpaceRef { + space_qualified_name: String, + }, + FormRef { + form_qualified_name: String, + }, + SetRef { + set: WorldViewSetTarget, + }, + FlowspaceRef { + flowspace_qualified_name: String, + }, + ViewRef { + view_qualified_name: String, + }, +} diff --git a/desktop/package.json b/desktop/package.json index 6726bfdcad..21bce55c56 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -45,10 +45,12 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.8", + "@shivai.space/world-view-react": "link:../../shivai/realm/packages/shivai-world-view-react", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.168.10", "@tanstack/react-virtual": "^3.14.2", "@tauri-apps/api": "~2.11", + "@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-opener": "~2.5", "@tauri-apps/plugin-process": "^2.3.1", diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 40dd5dd1b1..6652a41004 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -40,6 +40,7 @@ export default defineConfig({ "**/channel-mute.spec.ts", "**/channel-star.spec.ts", "**/channel-controls.spec.ts", + "**/world-views.spec.ts", "**/active-turn-resilience.spec.ts", "**/profile-active-turn.spec.ts", "**/config-bridge-screenshots.spec.ts", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 074b8f739e..a2b3fb45db 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1023,6 +1023,7 @@ dependencies = [ "buzz-media", "buzz-persona", "buzz-sdk", + "buzz-world-view-resolver", "bytes", "bzip2 0.6.1", "chrono", @@ -1143,6 +1144,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "buzz-world-view-resolver" +version = "0.1.0" +dependencies = [ + "buzz-core", + "chrono", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "uuid", +] + [[package]] name = "by_address" version = "1.2.1" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d689544688..6c52f634fa 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -89,6 +89,7 @@ buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" } buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" } buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" } buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" } +buzz_world_view_resolver_pkg = { package = "buzz-world-view-resolver", path = "../../crates/buzz-world-view-resolver" } iroh = { version = "1.0.2", optional = true } mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true } mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true } diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 1c89ee4f77..5f97ba9c04 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -59,6 +59,8 @@ mod window_chrome; mod window_vibrancy; mod workflows; mod workspace; +mod world_authorities; +mod world_views; pub use agent_auth::*; pub use agent_config::*; @@ -110,3 +112,5 @@ pub use window_chrome::*; pub use window_vibrancy::*; pub use workflows::*; pub use workspace::*; +pub use world_authorities::*; +pub use world_views::*; diff --git a/desktop/src-tauri/src/commands/world_authorities.rs b/desktop/src-tauri/src/commands/world_authorities.rs new file mode 100644 index 0000000000..dc6627e170 --- /dev/null +++ b/desktop/src-tauri/src/commands/world_authorities.rs @@ -0,0 +1,1144 @@ +use std::{ + fs::{self, OpenOptions}, + future::Future, + io::{self, Write}, + path::{Path, PathBuf}, + sync::{LazyLock, Mutex}, +}; + +use buzz_core_pkg::world_view::{ + decode_world_authority_registry, derive_trusted_world_origins, HostedWorldAuthority, + LocalWorldAuthority, WorldAuthorityRegistry, WorldMutationAuthority, WorldViewBindingScope, + WorldViewMutationDelegation, WorldViewReference, WORLD_AUTHORITY_REGISTRY_FILE_NAME, + WORLD_AUTHORITY_REGISTRY_VERSION, WORLD_AUTHORITY_SECRET_DIRECTORY, +}; +use buzz_world_view_resolver_pkg::HostedEditShareInspection; +use serde::Deserialize; + +use crate::managed_agents::nest_dir; + +const SHIVAI_LOCAL_MIRROR_BINDING_PATH: &str = ".shivai/local-world-mirror-binding.json"; +const SHIVAI_LOCAL_MIRROR_BINDING_VERSION: u8 = 2; +static REGISTRY_WRITE_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ShivaiLocalMirrorBinding { + origin: String, + source_root: String, + version: u8, + world_ref: ShivaiLocalMirrorRef, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ShivaiLocalMirrorRef { + kind: String, + mirror_id: String, + package_revision: String, + revision_id: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct LegacyLocalWorldAuthority { + origin: String, + mirror_id: String, + source_root: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct WorldAuthorityRegistryV1 { + version: u8, + local_authorities: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct WorldAuthorityRegistryV2 { + version: u8, + local_authorities: Vec, + hosted_authorities: Vec, +} + + +fn validate_world_origin(origin: &str) -> Result<(), String> { + WorldViewReference::HostedWorldLatest { + origin: origin.to_owned(), + hosted_world_id: "origin-validation".into(), + } + .validate() +} + +/// List credential-free world sources already connected to this Buzz client. +#[tauri::command] +pub fn list_world_authorities() -> Result { + let registry = load_world_authority_registry()?; + let local = registry.local_authorities.into_iter().map(|authority| { + serde_json::json!({ + "kind": "local-world-mirror-latest", + "origin": authority.origin, + "mirrorId": authority.mirror_id, + "sourceRoot": authority.source_root, + }) + }); + let hosted = registry.hosted_authorities.into_iter().map(|authority| { + serde_json::json!({ + "kind": "hosted-world-latest", + "origin": authority.origin, + "hostedWorldId": authority.hosted_world_id, + }) + }); + Ok(serde_json::json!({ + "authorities": local.chain(hosted).collect::>(), + "trustedOrigins": registry.trusted_origins, + })) +} + +/// Trust one canonical Shivai origin for public world-view resolution. +#[tauri::command] +pub fn trust_world_origin(origin: String) -> Result { + let changed = trust_world_origin_at(&world_authority_registry_path()?, &origin)?; + Ok(serde_json::json!({ + "origin": origin, + "trusted": true, + "changed": changed, + })) +} + +/// Revoke public world-view resolution trust for one canonical Shivai origin. +#[tauri::command] +pub fn revoke_world_origin_trust(origin: String) -> Result { + let changed = revoke_world_origin_trust_at(&world_authority_registry_path()?, &origin)?; + Ok(serde_json::json!({ + "origin": origin, + "trusted": false, + "changed": changed, + })) +} + +/// List credential-free, device-local agent mutation consent records. +#[tauri::command] +pub fn list_world_view_mutation_delegations() -> Result { + let registry = load_world_authority_registry()?; + Ok(serde_json::json!({ + "delegations": registry.mutation_delegations, + })) +} +/// Allow agents in one exact bound scope to mutate its connected world. +#[tauri::command] +pub fn authorize_world_view_mutation( + channel_id: String, + declared_scope: WorldViewBindingScope, + binding_id: String, + binding_revision_event_id: String, + authority: WorldMutationAuthority, +) -> Result { + let delegation = authorize_world_view_mutation_at( + &world_authority_registry_path()?, + &channel_id, + declared_scope, + &binding_id, + &binding_revision_event_id, + authority, + )?; + Ok(serde_json::json!({ "delegation": delegation })) +} + +/// Revoke device-local agent mutation consent for one exact binding. +#[tauri::command] +pub fn revoke_world_view_mutation( + channel_id: String, + declared_scope: WorldViewBindingScope, + binding_id: String, +) -> Result { + let revoked = revoke_world_view_mutation_at( + &world_authority_registry_path()?, + &channel_id, + &declared_scope, + &binding_id, + )?; + Ok(serde_json::json!({ "revoked": revoked })) +} + +/// Connect a published local `.world` package without asking for its mirror id. +#[tauri::command] +pub fn connect_local_world_authority(source_root: String) -> Result { + let registry_path = world_authority_registry_path()?; + let authority = connect_local_world_authority_at(®istry_path, Path::new(&source_root))?; + let origin = authority.origin.clone(); + let mirror_id = authority.mirror_id.clone(); + + Ok(serde_json::json!({ + "authority": { + "origin": authority.origin, + "mirrorId": authority.mirror_id, + "sourceRoot": authority.source_root, + }, + "worldRef": { + "kind": "local-world-mirror-latest", + "origin": origin, + "mirrorId": mirror_id, + }, + })) +} + +/// Register a private hosted edit-share capability for one public hosted world. +/// +/// The bearer capability is written to an owner-only local file, inspected by +/// the canonical `world hosted latest` boundary, and never crosses into Nostr. +#[tauri::command] +pub async fn register_hosted_world_authority( + origin: String, + credential: String, +) -> Result { + let registry_path = world_authority_registry_path()?; + let (authority, inspection) = register_hosted_world_authority_with_inspector( + ®istry_path, + origin, + &credential, + |origin, credential_file| async move { + buzz_world_view_resolver_pkg::inspect_hosted_edit_share(&origin, credential_file) + .await + .map_err(|error| error.to_string()) + }, + ) + .await?; + let world_origin = authority.origin.clone(); + let hosted_world_id = inspection.hosted_world_id.clone(); + + Ok(serde_json::json!({ + "authority": authority, + "revision": inspection.revision, + "worldRef": { + "kind": "hosted-world-latest", + "origin": world_origin, + "hostedWorldId": hosted_world_id, + }, + })) +} + +async fn register_hosted_world_authority_with_inspector( + registry_path: &Path, + origin: String, + credential: &str, + inspect: Inspect, +) -> Result<(HostedWorldAuthority, HostedEditShareInspection), String> +where + Inspect: FnOnce(String, PathBuf) -> InspectFuture, + InspectFuture: Future>, +{ + validate_world_origin(&origin)?; + + let credential = credential.trim(); + if credential.is_empty() { + return Err("hosted edit-share token or URL must not be blank".into()); + } + if credential.len() > 8192 { + return Err("hosted edit-share token or URL exceeds 8192 bytes".into()); + } + + let nest = registry_path + .parent() + .ok_or_else(|| "world authority registry path has no parent".to_string())?; + let credential_directory = nest.join(WORLD_AUTHORITY_SECRET_DIRECTORY); + ensure_private_directory(&credential_directory)?; + let credential_file = credential_directory.join(format!("{}.edit-share", uuid::Uuid::new_v4())); + write_private_credential(&credential_file, credential)?; + + let inspection = match inspect(origin.clone(), credential_file.clone()).await { + Ok(inspection) => inspection, + Err(error) => { + let _ = fs::remove_file(&credential_file); + return Err(error); + } + }; + let authority = HostedWorldAuthority { + origin, + hosted_world_id: inspection.hosted_world_id.clone(), + credential_file: credential_file.to_string_lossy().into_owned(), + }; + if let Err(error) = register_hosted_world_authority_at(registry_path, authority.clone()) { + let _ = fs::remove_file(&credential_file); + return Err(error); + } + + Ok((authority, inspection)) +} + +fn connect_local_world_authority_at( + registry_path: &Path, + selected_root: &Path, +) -> Result { + let source_root = fs::canonicalize(selected_root) + .map_err(|error| format!("could not resolve local world source root: {error}"))?; + if !source_root.is_dir() { + return Err("local world source root must be a directory".into()); + } + + let binding_path = source_root.join(SHIVAI_LOCAL_MIRROR_BINDING_PATH); + let binding_text = fs::read_to_string(&binding_path).map_err(|error| { + format!( + "could not read Shivai local mirror binding {}: {error}", + binding_path.display() + ) + })?; + let binding: ShivaiLocalMirrorBinding = serde_json::from_str(&binding_text) + .map_err(|error| format!("invalid Shivai local mirror binding: {error}"))?; + if binding.version != SHIVAI_LOCAL_MIRROR_BINDING_VERSION { + return Err(format!( + "unsupported Shivai local mirror binding version: {}", + binding.version + )); + } + if binding.world_ref.kind != "local-world-mirror" { + return Err("Shivai binding does not reference a local-world mirror".into()); + } + if binding.world_ref.package_revision.trim().is_empty() { + return Err("Shivai binding packageRevision must not be blank".into()); + } + if binding.world_ref.revision_id.trim().is_empty() { + return Err("Shivai binding revisionId must not be blank".into()); + } + WorldViewReference::LocalWorldMirrorLatest { + origin: binding.origin.clone(), + mirror_id: binding.world_ref.mirror_id.clone(), + } + .validate()?; + + let bound_root = fs::canonicalize(&binding.source_root) + .map_err(|error| format!("could not resolve Shivai binding sourceRoot: {error}"))?; + if bound_root != source_root { + return Err(format!( + "Shivai binding sourceRoot {} does not match selected root {}", + bound_root.display(), + source_root.display() + )); + } + + let capability_secret_file = create_local_capability_secret(registry_path)?; + let authority = LocalWorldAuthority { + origin: binding.origin, + mirror_id: binding.world_ref.mirror_id, + source_root: source_root.to_string_lossy().into_owned(), + capability_secret_file: capability_secret_file.to_string_lossy().into_owned(), + }; + let registration = register_local_world_authority_at(registry_path, authority); + if registration.is_err() { + let _ = fs::remove_file(&capability_secret_file); + } + registration +} + +fn create_local_capability_secret(registry_path: &Path) -> Result { + let nest = registry_path + .parent() + .ok_or_else(|| "world authority registry path has no parent".to_string())?; + let secret_directory = nest.join(WORLD_AUTHORITY_SECRET_DIRECTORY); + ensure_private_directory(&secret_directory)?; + let secret_file = + secret_directory.join(format!("{}.local-grant", uuid::Uuid::new_v4())); + let secret = nostr::Keys::generate().secret_key().to_secret_hex(); + write_private_credential(&secret_file, &secret)?; + Ok(secret_file) +} + +fn register_local_world_authority_at( + registry_path: &Path, + authority: LocalWorldAuthority, +) -> Result { + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + let mut registry = read_registry_unlocked(registry_path)?; + let replaced = registry + .local_authorities + .iter() + .filter(|candidate| { + (candidate.origin == authority.origin + && candidate.mirror_id == authority.mirror_id) + || candidate.source_root == authority.source_root + || candidate.capability_secret_file == authority.capability_secret_file + }) + .cloned() + .collect::>(); + registry.upsert_local(authority.clone())?; + write_registry(registry_path, ®istry)?; + + for replaced in replaced { + if replaced.capability_secret_file != authority.capability_secret_file { + remove_superseded_secret( + &replaced.capability_secret_file, + "local world capability secret", + ); + } + } + Ok(authority) +} + +fn authorize_world_view_mutation_at( + registry_path: &Path, + channel_id: &str, + declared_scope: WorldViewBindingScope, + binding_id: &str, + binding_revision_event_id: &str, + authority: WorldMutationAuthority, +) -> Result { + let channel_id = uuid::Uuid::parse_str(channel_id) + .map_err(|_| format!("invalid channel UUID: {channel_id}"))?; + let binding_id = uuid::Uuid::parse_str(binding_id) + .map_err(|_| format!("invalid world-view binding UUID: {binding_id}"))?; + declared_scope.validate()?; + let delegation = WorldViewMutationDelegation { + channel_id, + declared_scope, + binding_id, + binding_revision_event_id: binding_revision_event_id.to_owned(), + authority, + }; + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + let mut registry = read_registry_unlocked(registry_path)?; + if !registry.mutation_authority_is_registered(&delegation.authority) { + return Err( + "the mutable world source is not connected on this device; reconnect it before enabling agent edits" + .into(), + ); + } + registry.upsert_mutation_delegation(delegation.clone())?; + write_registry(registry_path, ®istry)?; + Ok(delegation) +} + +fn revoke_world_view_mutation_at( + registry_path: &Path, + channel_id: &str, + declared_scope: &WorldViewBindingScope, + binding_id: &str, +) -> Result { + let channel_id = uuid::Uuid::parse_str(channel_id) + .map_err(|_| format!("invalid channel UUID: {channel_id}"))?; + let binding_id = uuid::Uuid::parse_str(binding_id) + .map_err(|_| format!("invalid world-view binding UUID: {binding_id}"))?; + declared_scope.validate()?; + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + let mut registry = read_registry_unlocked(registry_path)?; + let revoked = + registry.revoke_mutation_delegation(channel_id, declared_scope, binding_id); + if revoked { + write_registry(registry_path, ®istry)?; + } + Ok(revoked) +} + +fn remove_superseded_secret(path: &str, label: &str) { + if let Err(error) = fs::remove_file(path) { + if error.kind() != std::io::ErrorKind::NotFound { + eprintln!("buzz-desktop: could not remove replaced {label} {path}: {error}"); + } + } +} + +fn register_hosted_world_authority_at( + registry_path: &Path, + authority: HostedWorldAuthority, +) -> Result { + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + let mut registry = read_registry_unlocked(registry_path)?; + let replaced = registry + .resolve_hosted(&authority.origin, &authority.hosted_world_id) + .cloned(); + registry.upsert_hosted(authority.clone())?; + write_registry(registry_path, ®istry)?; + + if let Some(replaced) = replaced { + if replaced.credential_file != authority.credential_file { + remove_superseded_secret( + &replaced.credential_file, + "hosted world credential", + ); + } + } + Ok(authority) +} + +fn trust_world_origin_at(registry_path: &Path, origin: &str) -> Result { + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + let mut registry = read_registry_unlocked(registry_path)?; + let trusted = registry.trust_origin(origin.to_owned())?; + if trusted { + write_registry(registry_path, ®istry)?; + } + Ok(trusted) +} + +fn revoke_world_origin_trust_at(registry_path: &Path, origin: &str) -> Result { + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + let mut registry = read_registry_unlocked(registry_path)?; + let revoked = registry.revoke_origin_trust(origin)?; + if revoked { + write_registry(registry_path, ®istry)?; + } + Ok(revoked) +} + +pub(crate) fn load_world_authority_registry() -> Result { + read_registry(&world_authority_registry_path()?) +} + +fn world_authority_registry_path() -> Result { + let nest = nest_dir().ok_or_else(|| "could not resolve the Buzz nest directory".to_string())?; + Ok(nest.join(WORLD_AUTHORITY_REGISTRY_FILE_NAME)) +} + +fn read_registry(path: &Path) -> Result { + let _guard = REGISTRY_WRITE_LOCK + .lock() + .map_err(|_| "world authority registry lock poisoned".to_string())?; + read_registry_unlocked(path) +} + +fn read_registry_unlocked(path: &Path) -> Result { + read_registry_with_migration_writer(path, write_registry) +} + +fn read_registry_with_migration_writer( + path: &Path, + mut write_migration: impl FnMut(&Path, &WorldAuthorityRegistry) -> Result<(), String>, +) -> Result { + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(error) if error.kind() == io::ErrorKind::NotFound => { + return Ok(WorldAuthorityRegistry::default()); + } + Err(error) => return Err(format!("could not read world authority registry: {error}")), + }; + let value: serde_json::Value = serde_json::from_str(&text) + .map_err(|error| format!("invalid world authority registry: {error}"))?; + let version = value + .get("version") + .and_then(serde_json::Value::as_u64) + .ok_or_else(|| { + "invalid world authority registry: version must be an unsigned integer".to_string() + })?; + + match version { + 1 => { + let legacy: WorldAuthorityRegistryV1 = serde_json::from_value(value) + .map_err(|error| format!("invalid v1 world authority registry: {error}"))?; + if legacy.version != 1 { + return Err(format!( + "unsupported world authority registry version: {}", + legacy.version + )); + } + migrate_legacy_registry( + path, + legacy.local_authorities, + Vec::new(), + &mut write_migration, + ) + } + 2 => { + let legacy: WorldAuthorityRegistryV2 = serde_json::from_value(value) + .map_err(|error| format!("invalid v2 world authority registry: {error}"))?; + if legacy.version != 2 { + return Err(format!( + "unsupported world authority registry version: {}", + legacy.version + )); + } + migrate_legacy_registry( + path, + legacy.local_authorities, + legacy.hosted_authorities, + &mut write_migration, + ) + } + current + if current == 3 || current == u64::from(WORLD_AUTHORITY_REGISTRY_VERSION) => + { + let decoded = decode_world_authority_registry(value)?; + if decoded.migrated { + write_migration(path, &decoded.registry)?; + } + Ok(decoded.registry) + } + unsupported => Err(format!( + "unsupported world authority registry version: {unsupported}" + )), + } +} + +fn migrate_legacy_registry( + path: &Path, + legacy_local_authorities: Vec, + hosted_authorities: Vec, + write_migration: &mut impl FnMut(&Path, &WorldAuthorityRegistry) -> Result<(), String>, +) -> Result { + let mut created_secret_files = Vec::with_capacity(legacy_local_authorities.len()); + let mut local_authorities = Vec::with_capacity(legacy_local_authorities.len()); + for legacy in legacy_local_authorities { + let capability_secret_file = match create_local_capability_secret(path) { + Ok(secret_file) => secret_file, + Err(error) => { + for created in created_secret_files { + let _ = fs::remove_file(created); + } + return Err(error); + } + }; + local_authorities.push(LocalWorldAuthority { + origin: legacy.origin, + mirror_id: legacy.mirror_id, + source_root: legacy.source_root, + capability_secret_file: capability_secret_file.to_string_lossy().into_owned(), + }); + created_secret_files.push(capability_secret_file); + } + let trusted_origins = derive_trusted_world_origins(&local_authorities, &hosted_authorities); + let registry = WorldAuthorityRegistry { + version: WORLD_AUTHORITY_REGISTRY_VERSION, + trusted_origins, + local_authorities, + hosted_authorities, + mutation_delegations: Vec::new(), + }; + let migration_result = registry + .validate() + .and_then(|()| write_migration(path, ®istry)); + if let Err(error) = migration_result { + for created in created_secret_files { + let _ = fs::remove_file(created); + } + return Err(error); + } + Ok(registry) +} + + +fn write_registry(path: &Path, registry: &WorldAuthorityRegistry) -> Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| "world authority registry path has no parent".to_string())?; + ensure_private_directory(parent)?; + let temp_path = temporary_registry_path(path); + let write_result = (|| -> Result<(), String> { + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options + .open(&temp_path) + .map_err(|error| format!("could not create world authority registry: {error}"))?; + let mut bytes = serde_json::to_vec_pretty(registry) + .map_err(|error| format!("could not encode world authority registry: {error}"))?; + bytes.push(b'\n'); + file.write_all(&bytes) + .map_err(|error| format!("could not write world authority registry: {error}"))?; + file.sync_all() + .map_err(|error| format!("could not sync world authority registry: {error}"))?; + fs::rename(&temp_path, path) + .map_err(|error| format!("could not replace world authority registry: {error}"))?; + Ok(()) + })(); + if write_result.is_err() { + let _ = fs::remove_file(&temp_path); + } + write_result +} + +fn ensure_private_directory(path: &Path) -> Result<(), String> { + fs::create_dir_all(path) + .map_err(|error| format!("could not create world authority directory: {error}"))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o700)) + .map_err(|error| format!("could not secure world authority directory: {error}"))?; + } + Ok(()) +} + +fn write_private_credential(path: &Path, credential: &str) -> Result<(), String> { + write_private_credential_with(path, |file| { + file.write_all(credential.as_bytes())?; + file.write_all(b"\n")?; + file.sync_all() + }) +} + +fn write_private_credential_with( + path: &Path, + write: impl FnOnce(&mut fs::File) -> io::Result<()>, +) -> Result<(), String> { + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options + .open(path) + .map_err(|error| format!("could not create hosted world credential: {error}"))?; + let write_result = write(&mut file); + drop(file); + if let Err(error) = write_result { + let _ = fs::remove_file(path); + return Err(format!("could not write hosted world credential: {error}")); + } + Ok(()) +} + +fn temporary_registry_path(path: &Path) -> PathBuf { + path.with_extension(format!("json.tmp-{}", uuid::Uuid::new_v4())) +} + +#[cfg(test)] +mod tests { + use std::{ + cell::Cell, + io::Write as _, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, + }, + }; + + use super::*; + use buzz_core_pkg::world_view::DEFAULT_SHIVAI_WORLD_ORIGIN; + + fn write_local_binding(source_root: &Path, origin: &str) { + fs::create_dir_all(source_root.join(".shivai")).unwrap(); + fs::write( + source_root.join(SHIVAI_LOCAL_MIRROR_BINDING_PATH), + serde_json::json!({ + "origin": origin, + "sourceRoot": source_root, + "version": SHIVAI_LOCAL_MIRROR_BINDING_VERSION, + "worldRef": { + "kind": "local-world-mirror", + "mirrorId": "mirror-1", + "packageRevision": "package-1", + "revisionId": "revision-1" + } + }) + .to_string(), + ) + .unwrap(); + } + + #[test] + fn discovers_and_preserves_the_mirror_publishing_origin() { + let temp = tempfile::tempdir().unwrap(); + let source_root = temp.path().join("demo.world"); + let custom_origin = "http://127.0.0.1:8787"; + write_local_binding(&source_root, custom_origin); + let registry_path = temp.path().join("world-authorities.json"); + + let registered = connect_local_world_authority_at(®istry_path, &source_root).unwrap(); + + assert_eq!(registered.origin, custom_origin); + assert_eq!( + registered.source_root, + fs::canonicalize(source_root).unwrap().to_string_lossy() + ); + let registry = read_registry(®istry_path).unwrap(); + assert_eq!(registry.local_authorities, vec![registered.clone()]); + assert!(Path::new(®istered.capability_secret_file).is_file()); + assert!(Path::new(®istered.capability_secret_file).starts_with( + temp.path().join(WORLD_AUTHORITY_SECRET_DIRECTORY) + )); + } + + #[test] + fn rejects_legacy_local_bindings_without_an_origin() { + let temp = tempfile::tempdir().unwrap(); + let source_root = temp.path().join("demo.world"); + fs::create_dir_all(source_root.join(".shivai")).unwrap(); + fs::write( + source_root.join(SHIVAI_LOCAL_MIRROR_BINDING_PATH), + serde_json::json!({ + "sourceRoot": source_root, + "version": 1, + "worldRef": { + "kind": "local-world-mirror", + "mirrorId": "mirror-1", + "packageRevision": "package-1", + "revisionId": "revision-1" + } + }) + .to_string(), + ) + .unwrap(); + + let error = connect_local_world_authority_at( + &temp.path().join("world-authorities.json"), + &source_root, + ) + .unwrap_err(); + + assert!(error.contains("invalid Shivai local mirror binding")); + } + + #[tokio::test] + async fn invalid_hosted_origins_do_not_write_credentials_or_invoke_the_inspector() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let inspector_invoked = Arc::new(AtomicBool::new(false)); + let invoked = Arc::clone(&inspector_invoked); + + let error = register_hosted_world_authority_with_inspector( + ®istry_path, + "http://hosted.example".into(), + "private-token", + move |_, _| { + invoked.store(true, Ordering::SeqCst); + async { + Ok(HostedEditShareInspection { + hosted_world_id: "hosted-1".into(), + revision: "revision-1".into(), + }) + } + }, + ) + .await + .unwrap_err(); + + assert!(error.contains("must use https")); + assert!(!inspector_invoked.load(Ordering::SeqCst)); + assert!(!temp.path().join(WORLD_AUTHORITY_SECRET_DIRECTORY).exists()); + assert!(!registry_path.exists()); + } + + #[test] + fn canonical_origin_validation_allows_https_and_loopback_only() { + assert!(validate_world_origin("https://hosted.example").is_ok()); + assert!(validate_world_origin("http://localhost:8787").is_ok()); + assert!(validate_world_origin("http://127.0.0.1:8787").is_ok()); + assert!(validate_world_origin("http://hosted.example").is_err()); + } + + #[test] + fn partial_credential_write_errors_remove_the_file() { + let temp = tempfile::tempdir().unwrap(); + let credential = temp.path().join("secret.edit-share"); + + let error = write_private_credential_with(&credential, |file| { + file.write_all(b"partial")?; + Err(io::Error::other("injected write failure")) + }) + .unwrap_err(); + + assert!(error.contains("injected write failure")); + assert!(!credential.exists()); + } + + #[tokio::test] + async fn inspector_failures_remove_the_written_credential() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let (credential_path_sender, credential_path_receiver) = mpsc::channel(); + + let error = register_hosted_world_authority_with_inspector( + ®istry_path, + "https://hosted.example".into(), + "private-token", + move |_, path| { + credential_path_sender.send(path).unwrap(); + async { Err("injected inspection failure".into()) } + }, + ) + .await + .unwrap_err(); + + assert_eq!(error, "injected inspection failure"); + assert!(!credential_path_receiver.recv().unwrap().exists()); + assert!(!registry_path.exists()); + } + + #[test] + fn migrates_an_exact_v1_registry_to_v4_once() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let source_root = temp + .path() + .join("demo.world") + .to_string_lossy() + .into_owned(); + fs::write( + ®istry_path, + serde_json::json!({ + "version": 1, + "localAuthorities": [{ + "origin": "https://hosted.example", + "mirrorId": "mirror-1", + "sourceRoot": source_root, + }] + }) + .to_string(), + ) + .unwrap(); + let migration_writes = Cell::new(0); + + let migrated = read_registry_with_migration_writer(®istry_path, |path, registry| { + migration_writes.set(migration_writes.get() + 1); + write_registry(path, registry) + }) + .unwrap(); + let first_v4_bytes = fs::read(®istry_path).unwrap(); + let loaded_again = read_registry_with_migration_writer(®istry_path, |path, registry| { + migration_writes.set(migration_writes.get() + 1); + write_registry(path, registry) + }) + .unwrap(); + + assert_eq!(migration_writes.get(), 1); + assert_eq!(migrated.version, WORLD_AUTHORITY_REGISTRY_VERSION); + assert!(migrated.is_trusted_origin(DEFAULT_SHIVAI_WORLD_ORIGIN)); + assert!(migrated.is_trusted_origin("https://hosted.example")); + assert_eq!(migrated.local_authorities.len(), 1); + assert_eq!(migrated.local_authorities[0].source_root, source_root); + assert!(Path::new(&migrated.local_authorities[0].capability_secret_file).is_file()); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = fs::metadata(&migrated.local_authorities[0].capability_secret_file) + .unwrap() + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600); + } + assert!(migrated.hosted_authorities.is_empty()); + assert!(migrated.mutation_delegations.is_empty()); + assert_eq!(loaded_again, migrated); + assert_eq!(fs::read(registry_path).unwrap(), first_v4_bytes); + } + + #[test] + fn migrates_v2_hosted_authority_without_weakening_its_credential() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let credential_file = temp.path().join("hosted.edit-share"); + fs::write(&credential_file, "private-edit-share").unwrap(); + fs::write( + ®istry_path, + serde_json::json!({ + "version": 2, + "localAuthorities": [], + "hostedAuthorities": [{ + "origin": "https://hosted.example", + "hostedWorldId": "hosted-1", + "credentialFile": credential_file, + }] + }) + .to_string(), + ) + .unwrap(); + + let migrated = read_registry(®istry_path).unwrap(); + + assert_eq!(migrated.version, WORLD_AUTHORITY_REGISTRY_VERSION); + assert_eq!(migrated.hosted_authorities.len(), 1); + assert_eq!( + migrated.hosted_authorities[0].credential_file, + credential_file.to_string_lossy() + ); + assert!(migrated.mutation_delegations.is_empty()); + } + + #[test] + fn migrates_v3_authorities_into_explicit_origin_trust_once() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let credential_file = temp.path().join("hosted.edit-share"); + fs::write(&credential_file, "private-edit-share").unwrap(); + fs::write( + ®istry_path, + serde_json::json!({ + "version": 3, + "localAuthorities": [], + "hostedAuthorities": [{ + "origin": "https://custom.example", + "hostedWorldId": "hosted-1", + "credentialFile": credential_file, + }], + "mutationDelegations": [], + }) + .to_string(), + ) + .unwrap(); + let migration_writes = Cell::new(0); + + let migrated = read_registry_with_migration_writer(®istry_path, |path, registry| { + migration_writes.set(migration_writes.get() + 1); + write_registry(path, registry) + }) + .unwrap(); + let loaded_again = + read_registry_with_migration_writer(®istry_path, |_path, _registry| { + migration_writes.set(migration_writes.get() + 1); + Ok(()) + }) + .unwrap(); + + assert_eq!(migration_writes.get(), 1); + assert!(migrated.is_trusted_origin(DEFAULT_SHIVAI_WORLD_ORIGIN)); + assert!(migrated.is_trusted_origin("https://custom.example")); + assert_eq!(loaded_again, migrated); + } + + #[test] + fn origin_trust_updates_are_explicit_and_persistent() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + + assert!(trust_world_origin_at(®istry_path, "https://custom.example").unwrap()); + assert!(!trust_world_origin_at(®istry_path, "https://custom.example").unwrap()); + assert!(read_registry(®istry_path) + .unwrap() + .is_trusted_origin("https://custom.example")); + assert!(revoke_world_origin_trust_at(®istry_path, "https://custom.example").unwrap()); + assert!(!read_registry(®istry_path) + .unwrap() + .is_trusted_origin("https://custom.example")); + } + + #[test] + fn rejects_unknown_registry_versions_and_non_exact_v1_shapes() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + fs::write( + ®istry_path, + serde_json::json!({ + "version": 99, + "localAuthorities": [], + "hostedAuthorities": [] + }) + .to_string(), + ) + .unwrap(); + + let version_error = read_registry(®istry_path).unwrap_err(); + assert!(version_error.contains("unsupported world authority registry version: 99")); + + fs::write( + ®istry_path, + serde_json::json!({ + "version": 1, + "localAuthorities": [], + "hostedAuthorities": [] + }) + .to_string(), + ) + .unwrap(); + let shape_error = read_registry(®istry_path).unwrap_err(); + assert!(shape_error.contains("invalid v1 world authority registry")); + } + + #[test] + fn replaces_hosted_authority_and_removes_the_superseded_credential() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let old_credential = temp.path().join("old.edit-share"); + let new_credential = temp.path().join("new.edit-share"); + write_private_credential(&old_credential, "old-token").unwrap(); + write_private_credential(&new_credential, "new-token").unwrap(); + let authority = |credential_file: &Path| HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: credential_file.to_string_lossy().into_owned(), + }; + + register_hosted_world_authority_at(®istry_path, authority(&old_credential)).unwrap(); + let registered = + register_hosted_world_authority_at(®istry_path, authority(&new_credential)).unwrap(); + + assert!(!old_credential.exists()); + assert!(new_credential.exists()); + assert_eq!( + read_registry(®istry_path).unwrap().hosted_authorities, + vec![registered] + ); + } + + #[test] + fn persists_and_revokes_explicit_binding_mutation_consent() { + let temp = tempfile::tempdir().unwrap(); + let registry_path = temp.path().join("world-authorities.json"); + let credential_file = temp.path().join("hosted.edit-share"); + write_private_credential(&credential_file, "private-edit-share").unwrap(); + register_hosted_world_authority_at( + ®istry_path, + HostedWorldAuthority { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + credential_file: credential_file.to_string_lossy().into_owned(), + }, + ) + .unwrap(); + let channel_id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; + let binding_id = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"; + let authority = WorldMutationAuthority::HostedWorldLatest { + origin: "https://manifest.shivai.space".into(), + hosted_world_id: "hosted-1".into(), + }; + + let delegated = authorize_world_view_mutation_at( + ®istry_path, + channel_id, + WorldViewBindingScope::Channel, + binding_id, + &"d".repeat(64), + authority.clone(), + ) + .unwrap(); + + assert_eq!(delegated.authority, authority); + assert_eq!( + read_registry(®istry_path).unwrap().mutation_delegations, + vec![delegated] + ); + assert!(revoke_world_view_mutation_at( + ®istry_path, + channel_id, + &WorldViewBindingScope::Channel, + binding_id, + ) + .unwrap()); + assert!(read_registry(®istry_path) + .unwrap() + .mutation_delegations + .is_empty()); + } + + #[cfg(unix)] + #[test] + fn writes_hosted_credentials_with_owner_only_permissions() { + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().unwrap(); + let credential = temp.path().join("secret.edit-share"); + write_private_credential(&credential, "secret-token").unwrap(); + + assert_eq!( + fs::metadata(credential).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } +} diff --git a/desktop/src-tauri/src/commands/world_views.rs b/desktop/src-tauri/src/commands/world_views.rs new file mode 100644 index 0000000000..69c6970526 --- /dev/null +++ b/desktop/src-tauri/src/commands/world_views.rs @@ -0,0 +1,284 @@ +use std::path::{Path, PathBuf}; + +use buzz_core_pkg::{ + kind::KIND_WORLD_VIEW_BINDINGS, + verification::verify_event, + world_view::{ + effective_world_view_bindings, world_view_bindings_snapshot_from_verified_event, + WorldViewBindingScope, WorldViewBindingsDocument, WorldViewBindingsSnapshot, + WorldViewReference, + }, +}; +use buzz_world_view_resolver_pkg::{ + catalog_world_views_with_binary as catalog_typed_world_views, + publish_hosted_live_view_share_with_binary, + resolve_world_view_with_binary as resolve_typed_world_view, PublishedHostedLiveViewShare, + ResolvedWorldView, WorldViewCatalog, WorldViewResolutionRequest, +}; +use tauri::State; + +use crate::{ + app_state::AppState, + commands::world_authorities::load_world_authority_registry, + relay::{query_relay, submit_event}, +}; + +fn bundled_shivai_world_binary(current_exe: &Path) -> PathBuf { + current_exe.with_file_name(format!("shivai-world{}", std::env::consts::EXE_SUFFIX)) +} + +fn shivai_world_binary() -> Result { + if let Some(binary) = std::env::var_os("SHIVAI_WORLD_BIN") { + return Ok(PathBuf::from(binary)); + } + if cfg!(debug_assertions) { + return Ok(PathBuf::from("world")); + } + + let current_exe = std::env::current_exe() + .map_err(|error| format!("resolve Buzz executable for bundled Shivai world: {error}"))?; + let binary = bundled_shivai_world_binary(¤t_exe); + if !binary.is_file() { + return Err(format!( + "bundled Shivai world executable is missing at {}", + binary.display() + )); + } + Ok(binary) +} + +fn exact_scope(thread_root_event_id: Option) -> Result { + thread_root_event_id.map_or(Ok(WorldViewBindingScope::Channel), |event_id| { + WorldViewBindingScope::thread(event_id) + }) +} + +fn read_command(channel_id: &str, scope: &WorldViewBindingScope) -> String { + let mut command = format!("buzz world-views get --channel {channel_id}"); + if let Some(thread_root_event_id) = scope.thread_root_event_id() { + command.push_str(" --thread-root "); + command.push_str(thread_root_event_id); + } + command +} + +async fn query_world_view_bindings_snapshot( + channel_id: &str, + scope: &WorldViewBindingScope, + state: &AppState, +) -> Result { + let d_tag = scope.d_tag(); + let events = query_relay( + state, + &[serde_json::json!({ + "kinds": [KIND_WORLD_VIEW_BINDINGS], + "#h": [channel_id], + "#d": [d_tag], + "limit": 1 + })], + ) + .await?; + + let Some(event) = events.into_iter().next() else { + return Ok(WorldViewBindingsSnapshot::empty(scope.clone())); + }; + let expected_channel_id = uuid::Uuid::parse_str(channel_id) + .map_err(|_| format!("invalid channel UUID: {channel_id}"))?; + let expected_scope = scope.clone(); + tokio::task::spawn_blocking(move || { + verify_event(&event) + .map_err(|error| format!("verify world view bindings event: {error}"))?; + world_view_bindings_snapshot_from_verified_event( + &event, + expected_channel_id, + &expected_scope, + ) + .map_err(|error| format!("decode world view bindings event: {error}")) + }) + .await + .map_err(|error| format!("world view bindings verification task failed: {error}"))? +} + +/// Read one exact channel or thread-root Shivai world-view bindings document. +#[tauri::command] +pub async fn get_world_view_bindings( + channel_id: String, + thread_root_event_id: Option, + state: State<'_, AppState>, +) -> Result { + let uuid = uuid::Uuid::parse_str(&channel_id) + .map_err(|_| format!("invalid channel UUID: {channel_id}"))?; + let scope = exact_scope(thread_root_event_id)?; + let snapshot = query_world_view_bindings_snapshot(&uuid.to_string(), &scope, &state).await?; + let mut value = serde_json::to_value(snapshot) + .map_err(|error| format!("encode world view bindings snapshot: {error}"))?; + value + .as_object_mut() + .expect("snapshot serializes as an object") + .insert( + "nextReadCommand".into(), + serde_json::Value::String(read_command(&channel_id, &scope)), + ); + Ok(value) +} + +/// Read effective channel bindings with exact thread-root shadowing when requested. +#[tauri::command] +pub async fn get_effective_world_view_bindings( + channel_id: String, + thread_root_event_id: Option, + state: State<'_, AppState>, +) -> Result { + let uuid = uuid::Uuid::parse_str(&channel_id) + .map_err(|_| format!("invalid channel UUID: {channel_id}"))?; + let effective_scope = exact_scope(thread_root_event_id)?; + let channel_scope = WorldViewBindingScope::Channel; + let channel = + query_world_view_bindings_snapshot(&uuid.to_string(), &channel_scope, &state).await?; + let thread = match &effective_scope { + WorldViewBindingScope::Channel => None, + WorldViewBindingScope::Thread { .. } => Some( + query_world_view_bindings_snapshot(&uuid.to_string(), &effective_scope, &state).await?, + ), + }; + let effective = effective_world_view_bindings(&channel, thread.as_ref())?; + let mut value = serde_json::to_value(effective) + .map_err(|error| format!("encode effective world view bindings: {error}"))?; + let mut next_read_commands = vec![read_command(&channel_id, &channel_scope)]; + if matches!(effective_scope, WorldViewBindingScope::Thread { .. }) { + next_read_commands.push(read_command(&channel_id, &effective_scope)); + } + value + .as_object_mut() + .expect("effective bindings serialize as an object") + .insert( + "nextReadCommands".into(), + serde_json::to_value(next_read_commands).expect("read commands serialize as strings"), + ); + Ok(value) +} + +/// Publish an optimistic complete replacement for one exact binding scope. +#[tauri::command] +pub async fn set_world_view_bindings( + channel_id: String, + expected_revision_event_id: Option, + document: WorldViewBindingsDocument, + state: State<'_, AppState>, +) -> Result { + let uuid = uuid::Uuid::parse_str(&channel_id) + .map_err(|_| format!("invalid channel UUID: {channel_id}"))?; + document.validate()?; + if let Some(event_id) = &expected_revision_event_id { + if event_id.len() != 64 + || !event_id + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err("expected revision event id must be 64 lowercase hex characters".into()); + } + } + let current = query_world_view_bindings_snapshot(&channel_id, &document.scope, &state).await?; + if current.revision_event_id != expected_revision_event_id { + return Err(format!( + "world-view bindings revision conflict: expected {}, current {}; refresh with `{}`", + expected_revision_event_id.as_deref().unwrap_or("none"), + current.revision_event_id.as_deref().unwrap_or("none"), + read_command(&channel_id, &document.scope) + )); + } + + let builder = buzz_sdk_pkg::build_set_world_view_bindings( + uuid, + expected_revision_event_id.as_deref(), + &document, + ) + .map_err(|error| error.to_string())?; + let result = submit_event(builder, &state).await?; + + Ok(serde_json::json!({ + "ok": true, + "revisionEventId": result.event_id, + "nextReadCommand": read_command(&channel_id, &document.scope), + })) +} + +/// List canonical authored view identities for one public world source. +#[tauri::command] +pub async fn catalog_world_views( + reference: WorldViewReference, +) -> Result { + let registry = load_world_authority_registry()?; + let binary = shivai_world_binary()?; + catalog_typed_world_views(reference, binary, ®istry) + .await + .map_err(|error| error.to_string()) +} + +/// Resolve one bound local or hosted world view through the shared typed resolver. +#[tauri::command] +pub async fn resolve_world_view( + request: WorldViewResolutionRequest, +) -> Result { + let registry = load_world_authority_registry()?; + let binary = shivai_world_binary()?; + resolve_typed_world_view(request, binary, ®istry) + .await + .map_err(|error| error.to_string()) +} + +/// Mint or reuse a stable public live-view capability for one connected hosted world. +#[tauri::command] +pub async fn publish_hosted_world_live_view_share( + reference: WorldViewReference, + view_qualified_name: String, +) -> Result { + let WorldViewReference::HostedWorldLatest { + origin, + hosted_world_id, + } = reference + else { + return Err( + "live-view shares can only be published from a connected hosted world".into(), + ); + }; + let registry = load_world_authority_registry()?; + let authority = registry + .resolve_hosted(&origin, &hosted_world_id) + .ok_or_else(|| { + format!("no private hosted authority is registered for `{hosted_world_id}`") + })?; + let credential_file = PathBuf::from(&authority.credential_file); + let binary = shivai_world_binary()?; + let share = publish_hosted_live_view_share_with_binary( + &origin, + credential_file, + &view_qualified_name, + binary, + ) + .await + .map_err(|error| error.to_string())?; + if share.hosted_world_id != hosted_world_id { + return Err(format!( + "live-view share resolved hosted world `{}` instead of `{hosted_world_id}`", + share.hosted_world_id + )); + } + Ok(share) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn bundled_world_binary_is_a_sibling_of_the_desktop_executable() { + let current = Path::new("/opt/Buzz/Buzz"); + assert_eq!( + bundled_shivai_world_binary(current), + current.with_file_name(format!("shivai-world{}", std::env::consts::EXE_SUFFIX)) + ); + } + +} diff --git a/desktop/src-tauri/src/events.rs b/desktop/src-tauri/src/events.rs index 777d56d02e..4ffd372c79 100644 --- a/desktop/src-tauri/src/events.rs +++ b/desktop/src-tauri/src/events.rs @@ -9,7 +9,7 @@ //! Each function validates inputs and returns a nostr::EventBuilder. //! Signing and submission happen in relay::submit_event. -use buzz_core_pkg::kind::{KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST}; +use buzz_core_pkg::kind::{KIND_CANVAS, KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST}; use nostr::{EventBuilder, EventId, Kind, Tag}; use uuid::Uuid; @@ -465,7 +465,7 @@ pub fn build_remove_reaction(reaction_event_id: EventId) -> Result Result { check_content(content)?; let tags = vec![tag(vec!["h", &channel_id.to_string()])?]; - Ok(EventBuilder::new(Kind::Custom(40100), content).tags(tags)) + Ok(EventBuilder::new(Kind::Custom(KIND_CANVAS as u16), content).tags(tags)) } // ── Profile ────────────────────────────────────────────────────────────────── diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 5346791ccf..f9f8b53d07 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -751,6 +751,20 @@ pub fn run() { leave_channel, get_canvas, set_canvas, + list_world_authorities, + trust_world_origin, + revoke_world_origin_trust, + connect_local_world_authority, + register_hosted_world_authority, + list_world_view_mutation_delegations, + authorize_world_view_mutation, + revoke_world_view_mutation, + get_world_view_bindings, + get_effective_world_view_bindings, + set_world_view_bindings, + catalog_world_views, + resolve_world_view, + publish_hosted_world_live_view_share, get_feed, search_messages, send_channel_message, diff --git a/desktop/src-tauri/src/mesh_llm/recovery.rs b/desktop/src-tauri/src/mesh_llm/recovery.rs index ce6d495a47..9602ec2d4a 100644 --- a/desktop/src-tauri/src/mesh_llm/recovery.rs +++ b/desktop/src-tauri/src/mesh_llm/recovery.rs @@ -286,6 +286,8 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu } let records = crate::managed_agents::load_managed_agents(app).unwrap_or_default(); + let personas = crate::managed_agents::load_personas(app).unwrap_or_default(); + let global = crate::managed_agents::load_global_agent_config(app).unwrap_or_default(); let mesh_records: Vec<_> = records .into_iter() .filter_map(|record| { @@ -293,6 +295,18 @@ pub(crate) async fn rearm_relay_mesh_for_running_agents(app: &AppHandle) -> Resu .map(|mesh_model_id| (record, mesh_model_id)) }) .collect(); + if mesh_records.is_empty() { + return Ok(()); + } + + if recovery == MeshRuntimeRecovery::RestartRequired { + eprintln!( + "buzz-mesh: supervised client startup lost its ingress before the SDK exposed a shutdown handle; restarting Buzz" + ); + app.request_restart(); + return Ok(()); + } + let mut first_error = None; for (record, mesh_model_id) in &mesh_records { match crate::commands::mesh_llm::ensure_relay_mesh_for_record( @@ -484,7 +498,6 @@ mod tests { fn only_running_relay_mesh_agents_trigger_rearm() { let personas: Vec = Vec::new(); let global = crate::managed_agents::GlobalAgentConfig::default(); - let empty = active_set(&[]); assert!(running_relay_mesh_model_id( &mesh_record("stopped", Some(std::process::id())), diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 07b7216346..250e398006 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -57,7 +57,8 @@ "binaries/buzz-agent", "binaries/buzz-dev-mcp", "binaries/git-credential-nostr", - "binaries/buzz" + "binaries/buzz", + "binaries/shivai-world" ], "icon": [ "icons/32x32.png", diff --git a/desktop/src/features/channels/hooks.ts b/desktop/src/features/channels/hooks.ts index 9003d0f5a5..46c59d71ce 100644 --- a/desktop/src/features/channels/hooks.ts +++ b/desktop/src/features/channels/hooks.ts @@ -3,10 +3,15 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { addChannelMembers, + authorizeWorldViewMutation, archiveChannel, createChannel, deleteChannel, getCanvas, + catalogWorldViews, + connectLocalWorldAuthority, + getEffectiveWorldViewBindings, + getWorldViewBindings, getChannelDetails, getChannelMembers, getChannels, @@ -15,12 +20,22 @@ import { leaveChannel, openDm, removeChannelMember, + listWorldViewMutationDelegations, + listWorldAuthorities, + registerHostedWorldAuthority, + revokeWorldViewMutation, + publishHostedWorldLiveViewShare, + resolveWorldView, setCanvas, + trustWorldOrigin, + setWorldViewBindings, setChannelPurpose, setChannelTopic, unarchiveChannel, updateChannel, } from "@/shared/api/tauri"; +import { relayClient } from "@/shared/api/relayClient"; +import { KIND_WORLD_VIEW_BINDINGS } from "@/shared/constants/kinds"; import type { AddChannelMembersInput, Channel, @@ -31,6 +46,17 @@ import type { SetChannelTopicInput, UpdateChannelInput, } from "@/shared/api/types"; +import type { + AuthorizeWorldViewMutationInput, + ConnectLocalWorldAuthorityInput, + RegisterHostedWorldAuthorityInput, + RevokeWorldViewMutationInput, + SetWorldOriginTrustInput, + SetWorldViewBindingsInput, + WorldViewReference, + WorldViewBindingScope, + WorldViewResolutionRequest, +} from "@/shared/api/worldViewTypes"; import { useCommunities } from "@/features/communities/useCommunities"; import { readChannelSnapshot, @@ -38,6 +64,12 @@ import { } from "@/features/channels/channelSnapshot"; export const channelsQueryKey = ["channels"] as const; +export const worldAuthoritiesQueryKey = ["world-authorities"] as const; +export const worldViewMutationDelegationsQueryKey = [ + "world-view-mutation-delegations", +] as const; +const worldViewCatalogQueryKey = (reference: WorldViewReference) => + ["world-view-catalog", reference] as const; const channelDetailQueryKey = (channelId: string) => ["channels", channelId, "detail"] as const; const channelMembersQueryKey = (channelId: string) => @@ -657,3 +689,270 @@ export function useSetCanvasMutation(channelId: string | null) { }, }); } + +// ── Shivai world views ─────────────────────────────────────────────────────── + +export function useWorldViewBindingsQuery( + channelId: string | null, + scope: WorldViewBindingScope = { kind: "channel" }, + enabled = true, +) { + const threadRootEventId = + scope.kind === "thread" ? scope.threadRootEventId : null; + return useQuery({ + queryKey: ["channel-world-view-bindings", channelId, threadRootEventId], + queryFn: () => { + if (!channelId) { + return Promise.reject(new Error("No channel selected")); + } + return getWorldViewBindings(channelId, threadRootEventId); + }, + enabled: enabled && channelId !== null, + }); +} + +export function useLiveWorldViewBindingUpdates(channelId: string | null): void { + const queryClient = useQueryClient(); + + React.useEffect(() => { + if (!channelId) return; + + let cancelled = false; + let disposeLive: (() => Promise) | null = null; + const invalidate = () => { + void queryClient.invalidateQueries({ + queryKey: ["channel-world-view-bindings", channelId], + }); + void queryClient.invalidateQueries({ + queryKey: ["effective-channel-world-view-bindings", channelId], + }); + }; + const disposeReconnect = relayClient.subscribeToReconnects(invalidate); + void relayClient + .subscribeLive( + { + kinds: [KIND_WORLD_VIEW_BINDINGS], + "#h": [channelId], + limit: 0, + }, + invalidate, + ) + .then((dispose) => { + if (cancelled) { + void dispose(); + } else { + disposeLive = dispose; + } + }) + .catch((error: unknown) => { + console.error( + "Failed to subscribe to world-view binding updates", + error, + ); + }); + + return () => { + cancelled = true; + disposeReconnect(); + if (disposeLive) { + void disposeLive(); + } + }; + }, [channelId, queryClient]); +} + +export function useEffectiveWorldViewBindingsQuery( + channelId: string | null, + threadRootEventId: string | null, + enabled = true, +) { + return useQuery({ + queryKey: [ + "effective-channel-world-view-bindings", + channelId, + threadRootEventId, + ], + queryFn: () => { + if (!channelId) { + return Promise.reject(new Error("No channel selected")); + } + return getEffectiveWorldViewBindings(channelId, threadRootEventId); + }, + enabled: enabled && channelId !== null, + }); +} + +export function useWorldAuthoritiesQuery(enabled = true) { + return useQuery({ + queryKey: worldAuthoritiesQueryKey, + queryFn: listWorldAuthorities, + enabled, + }); +} + +export function useTrustWorldOriginMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: SetWorldOriginTrustInput) => trustWorldOrigin(input), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: worldAuthoritiesQueryKey, + }); + }, + }); +} + +export function useWorldViewMutationDelegationsQuery(enabled = true) { + return useQuery({ + queryKey: worldViewMutationDelegationsQueryKey, + queryFn: listWorldViewMutationDelegations, + enabled, + }); +} + +export function useAuthorizeWorldViewMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: AuthorizeWorldViewMutationInput) => + authorizeWorldViewMutation(input), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: worldViewMutationDelegationsQueryKey, + }); + }, + }); +} + +export function useRevokeWorldViewMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: RevokeWorldViewMutationInput) => + revokeWorldViewMutation(input), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: worldViewMutationDelegationsQueryKey, + }); + }, + }); +} + +export function useWorldViewCatalogQuery( + reference: WorldViewReference | null, + enabled = true, +) { + return useQuery({ + queryKey: reference + ? worldViewCatalogQueryKey(reference) + : ["world-view-catalog", null], + queryFn: () => { + if (!reference) { + return Promise.reject(new Error("No world source selected")); + } + return catalogWorldViews(reference); + }, + enabled: enabled && reference !== null, + }); +} + +export function useConnectLocalWorldAuthorityMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: ConnectLocalWorldAuthorityInput) => + connectLocalWorldAuthority(input), + onSuccess: async (result) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: worldAuthoritiesQueryKey, + }), + queryClient.invalidateQueries({ + queryKey: worldViewCatalogQueryKey(result.worldRef), + }), + ]); + }, + }); +} + +export function useRegisterHostedWorldAuthorityMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: RegisterHostedWorldAuthorityInput) => + registerHostedWorldAuthority(input), + onSuccess: async (result) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: worldAuthoritiesQueryKey, + }), + queryClient.invalidateQueries({ + queryKey: worldViewCatalogQueryKey(result.worldRef), + }), + ]); + }, + }); +} + +export function usePublishHostedWorldLiveViewShareMutation() { + return useMutation({ + mutationFn: publishHostedWorldLiveViewShare, + }); +} + +export function useSetWorldViewBindingsMutation(channelId: string | null) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ( + input: Pick< + SetWorldViewBindingsInput, + "document" | "expectedRevisionEventId" + >, + ) => { + if (!channelId) { + return Promise.reject(new Error("No channel selected")); + } + return setWorldViewBindings({ channelId, ...input }); + }, + onSuccess: (_result, input) => { + if (channelId) { + const threadRootEventId = + input.document.scope.kind === "thread" + ? input.document.scope.threadRootEventId + : null; + void queryClient.invalidateQueries({ + queryKey: [ + "channel-world-view-bindings", + channelId, + threadRootEventId, + ], + }); + void queryClient.invalidateQueries({ + queryKey: ["effective-channel-world-view-bindings", channelId], + }); + } + }, + }); +} + +export function useResolvedWorldViewQuery( + request: WorldViewResolutionRequest | null, + enabled = true, +) { + return useQuery({ + queryKey: ["resolved-world-view", request], + queryFn: () => { + if (!request) { + return Promise.reject(new Error("No world-view resolution request")); + } + return resolveWorldView(request); + }, + enabled: enabled && request !== null, + refetchInterval: + request?.binding.reference.kind === "hosted-world-view-export" + ? false + : 10_000, + }); +} diff --git a/desktop/src/features/channels/ui/ChannelMembersBar.tsx b/desktop/src/features/channels/ui/ChannelMembersBar.tsx index c87617ce9b..2b6e6e2477 100644 --- a/desktop/src/features/channels/ui/ChannelMembersBar.tsx +++ b/desktop/src/features/channels/ui/ChannelMembersBar.tsx @@ -198,7 +198,7 @@ export function ChannelMembersBar({ + + + ); +} + +type WorldSourcePickerModel = { + authorities: readonly WorldViewAuthority[]; + hostedCredential: string; + isBusy: boolean; + isFetchingAuthorities: boolean; + isTrustingOrigin: boolean; + isHostedConnecting: boolean; + isHostedConnectionOpen: boolean; + publicReference: string; + publicReferenceError: string | null; + reference: WorldViewReference | null; + trustOrigin: string | null; +}; + +type WorldSourcePickerActions = { + onConnectHostedWorld: () => Promise; + onCancelOriginTrust: () => void; + onConnectLocalWorld: () => Promise; + onHostedCredentialChange: (value: string) => void; + onPublicReferenceChange: (value: string) => void; + onSelectReference: (reference: WorldViewReference) => void; + onToggleHostedConnection: () => void; + onUsePublicReference: () => void; + onTrustOrigin: () => Promise; +}; + +type WorldSourcePickerProps = { + actions: WorldSourcePickerActions; + model: WorldSourcePickerModel; +}; + +function WorldSourcePicker({ actions, model }: WorldSourcePickerProps) { + const hostedCredentialInputId = React.useId(); + const publicReferenceInputId = React.useId(); + const selectedReferenceIsShared = + model.reference?.kind === "hosted-world-view-export" || + model.reference?.kind === "hosted-world-live-view-share"; + + return ( + <> +
+
+

+ Connected worlds +

+ {model.isFetchingAuthorities ? ( + + ) : null} +
+
+ {model.authorities.length === 0 && !selectedReferenceIsShared ? ( +

+ No worlds are connected to this device yet. +

+ ) : ( +
+ {model.authorities.map((authority) => { + const reference = publicWorldViewReference(authority); + return ( + actions.onSelectReference(reference)} + /> + ); + })} + {selectedReferenceIsShared ? ( + + ) : null} +
+ )} +
+
+ + +
+ {model.isHostedConnectionOpen ? ( +
+ + +

+ The capability stays in this client. Buzz publishes only the + stable world reference. +

+
+ ) : null} +
+ +
+ +
+