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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/rucelium-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ rucelium-worldgraph = { workspace = true }
rucelium-federation = { workspace = true }
rucelium-policy = { workspace = true }
rucelium-store = { workspace = true }
rufield-core = { workspace = true }
rufield-fusion = { workspace = true }
rufield-provenance = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

Expand Down
27 changes: 27 additions & 0 deletions crates/rucelium-gateway/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub fn router(state: GatewayState) -> Router {
.route("/api/federation/announce", post(fed_announce))
.route("/api/admin/revoke/:node_id", post(admin_revoke))
.route("/api/admin/command", post(admin_command))
.route("/api/rf-context", get(rf_context))
.with_state(state)
}

Expand Down Expand Up @@ -124,6 +125,32 @@ async fn stats(State(state): State<GatewayState>) -> Json<Value> {
}))
}

/// `GET /api/rf-context` — the RuView RF-context bridge's state (ADR-264 §8,
/// see `rf_bridge`): the configured upstream (if any), ingest counters, the
/// bounded recent ring, the current within-RF-domain fused inferences
/// (`rf_bridge::current_inferences` — RuField's own room-state rules over
/// this upstream's own verified events; needs no external pairing), and a
/// bounded history of past inference snapshots so a transient inference is
/// still visible after its own TTL expires (in-memory only — does not
/// survive a restart; see [`crate::rf_bridge::InferenceSnapshot`] docs for
/// why not durable yet). Each ring entry is honestly tagged `verified` — an unverified event
/// is visible here but excluded from fusion, never silently dropped or
/// upgraded. `fused_into_worldgraph` is always `false`: this bridge does not
/// (yet) correlate RF context against any biome environmental sample — see
/// `rf_bridge`'s module docs for why that's a separate, still-unbuilt
/// decision. Empty and idle when `--rf-upstream` was not set.
async fn rf_context(State(state): State<GatewayState>) -> Json<Value> {
let inner = state.inner.lock().await;
Json(json!({
"upstream": inner.rf_upstream,
"fused_into_worldgraph": false,
"stats": inner.rf_stats,
"recent": inner.rf_recent.iter().collect::<Vec<_>>(),
"room_state_inferences": crate::rf_bridge::current_inferences(&inner.rf_fusion),
"inference_history": inner.rf_inference_history.iter().collect::<Vec<_>>(),
}))
}

/// `GET /api/observations/recent?limit=50` — most recent stored samples in
/// append order.
async fn observations_recent(
Expand Down
24 changes: 24 additions & 0 deletions crates/rucelium-gateway/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub const DEFAULT_ACTUATOR_ID: &str = "sluice-gate-1";
/// Default durability mode for the durable stores: the daemon fsyncs every
/// accepted append, so an accepted record survives power loss.
pub const DEFAULT_FSYNC: bool = true;
/// Default poll interval for the RuView RF-context bridge (ADR-264 §8),
/// matching `rufield-viewer`'s own default `/api/field` poll cadence.
pub const DEFAULT_RF_POLL_MS: u64 = 500;

/// Runtime configuration of one gateway daemon instance.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -61,6 +64,13 @@ pub struct GatewayConfig {
/// Durability mode for `ObservationStore` / `EventStore`: `true` fsyncs
/// every accepted append before it is acknowledged.
pub fsync: bool,
/// Base URL of a RuView `wifi-densepose-sensing-server` to poll for RF
/// context (ADR-264 §8). `None` (default) disables the bridge entirely —
/// this is opt-in, not a dependency the gateway assumes exists.
pub rf_upstream: Option<String>,
/// Poll interval for the RF-context bridge, in milliseconds. Unused when
/// `rf_upstream` is `None`.
pub rf_poll_ms: u64,
}

impl Default for GatewayConfig {
Expand All @@ -79,6 +89,8 @@ impl Default for GatewayConfig {
federation_backfill_ms: None,
actuator_id: DEFAULT_ACTUATOR_ID.to_string(),
fsync: DEFAULT_FSYNC,
rf_upstream: None,
rf_poll_ms: DEFAULT_RF_POLL_MS,
}
}
}
Expand Down Expand Up @@ -129,6 +141,10 @@ impl GatewayConfig {
}
"--actuator" => config.actuator_id = value("--actuator")?,
"--fsync" => config.fsync = parse_num(&value("--fsync")?, "--fsync")?,
"--rf-upstream" => config.rf_upstream = Some(value("--rf-upstream")?),
"--rf-poll-ms" => {
config.rf_poll_ms = parse_num(&value("--rf-poll-ms")?, "--rf-poll-ms")?;
}
unknown => return Err(format!("unknown flag {unknown}")),
}
}
Expand Down Expand Up @@ -169,6 +185,8 @@ mod tests {
assert_eq!(c.federation_backfill_ms(), 30_000);
assert_eq!(c.actuator_id, "sluice-gate-1");
assert!(c.fsync, "the daemon fsyncs accepted appends by default");
assert_eq!(c.rf_upstream, None, "the RF bridge is opt-in, off by default");
assert_eq!(c.rf_poll_ms, 500);
}

#[test]
Expand Down Expand Up @@ -210,6 +228,10 @@ mod tests {
"weir-3",
"--fsync",
"false",
"--rf-upstream",
"http://127.0.0.1:8080",
"--rf-poll-ms",
"250",
]))
.unwrap();
assert_eq!(c.biome_id, "biome/x");
Expand All @@ -223,6 +245,8 @@ mod tests {
assert_eq!(c.federation_poll_ms, 200);
assert_eq!(c.actuator_id, "weir-3");
assert!(!c.fsync);
assert_eq!(c.rf_upstream, Some("http://127.0.0.1:8080".to_string()));
assert_eq!(c.rf_poll_ms, 250);
}

#[test]
Expand Down
20 changes: 20 additions & 0 deletions crates/rucelium-gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@
//! ──► /api/federation/{pubkey,summary,revocations,peers}
//! ──► /api/federation/announce (push inbox, ADR-269 §3)
//! ──► /api/admin/{revoke/:node_id,command}
//! ──► /api/rf-context (ADR-264 §8, see `rf_bridge`)
//! ```
//!
//! ## RuView RF context (ADR-264 §8)
//!
//! `--rf-upstream <url>` starts [`rf_bridge::run_rf_ingest`]: it polls a
//! RuView `wifi-densepose-sensing-server`'s `/api/field` (ADR-262 P3),
//! verifies each event's provenance receipt, and keeps a bounded, honestly
//! verified/unverified-tagged ring at `GET /api/rf-context`. This is storage
//! only — it does **not** fuse into the WorldGraph yet; see [`rf_bridge`]'s
//! module docs for why that pairing decision is deliberately not guessed at
//! here.
//!
//! ## Push federation (ADR-269 §3)
//!
//! Federation is push-first over a swappable [`transport::FederationTransport`]:
Expand Down Expand Up @@ -61,6 +72,7 @@ pub mod federation;
pub mod journal;
pub mod net;
pub mod pipeline;
pub mod rf_bridge;
pub mod simulate;
pub mod state;
pub mod transport;
Expand Down Expand Up @@ -185,6 +197,14 @@ pub async fn spawn_gateway_with_transport(
)));
}

if let Some(upstream) = config.rf_upstream.clone() {
tasks.push(tokio::spawn(rf_bridge::run_rf_ingest(
state.clone(),
upstream,
config.rf_poll_ms,
)));
}

Ok(GatewayHandle {
state,
udp_port,
Expand Down
7 changes: 6 additions & 1 deletion crates/rucelium-gateway/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ async fn main() {
"usage: rucelium-gateway [--biome-id <s>] [--udp <port>] [--http <port>] \
[--data-dir <path>] [--peer <url>]... [--simulate <n>] [--seed <u64>] \
[--sim-interval-ms <u64>] [--retention-check-secs <u64>] \
[--federation-poll-ms <u64>] [--actuator <id>] [--fsync <bool>]"
[--federation-poll-ms <u64>] [--actuator <id>] [--fsync <bool>] \
[--rf-upstream <url>] [--rf-poll-ms <u64>]"
);
std::process::exit(2);
}
Expand All @@ -35,6 +36,10 @@ async fn main() {
println!(" peer: {peer}");
}
}
match &config.rf_upstream {
Some(url) => println!(" rf-context: polling {url} every {}ms (ADR-264 §8)", config.rf_poll_ms),
None => println!(" rf-context: disabled (set --rf-upstream <url> to enable)"),
}
println!(" WARNING: admin endpoints are UNAUTHENTICATED in v0.1 — bind");
println!(" the http port to localhost or firewall it.");

Expand Down
Loading