merge(13): bring main forward — unblocks the crate's test build

The lane merged main at 0c4826f8, one commit before 0de67ca6 added
auth/auth_rationale to PortMapping's test constructors in prod_orchestrator.rs.
That left the lane unable to compile ANY test in the archipelago crate, which
is why 13-05 could not observe its 13 tests pass (window 19). Not a defect in
this phase's work — just staleness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	core/archipelago/src/main.rs
This commit is contained in:
archipelago
2026-08-04 01:50:39 -04:00
25 changed files with 2094 additions and 35 deletions
+59
View File
@@ -0,0 +1,59 @@
//! `security.app-gate-status` — what the app gate is actually enforcing.
//!
//! The gate rolls out per app (an app must be pinned to loopback before the
//! gate can claim its port — see `appgate::listener`), so for a while every
//! node is partially protected. "Partially" is only safe if it is *visible*:
//! this is the RPC that lets the UI say which app ports are still reachable
//! without a credential, instead of the operator having to port-scan their
//! own node to find out.
use anyhow::Result;
use super::RpcHandler;
impl RpcHandler {
pub(in crate::api::rpc) async fn handle_app_gate_status(&self) -> Result<serde_json::Value> {
let status = crate::appgate::listener::shared_status();
let status = status.read().await.clone();
let port_map = self.app_gate.port_map().await;
// Exemptions are reported alongside, and with their manifest
// rationale, because "which ports are open and why" is the actual
// question — a list of unprotected ports without the deliberate ones
// next to it invites someone to "fix" LND's gRPC port and break every
// remote wallet.
let exempt: Vec<serde_json::Value> = port_map
.exempt_ports()
.iter()
.map(|e| {
serde_json::json!({
"port": e.port,
"app_id": e.app_id,
"protocol": e.protocol,
"rationale": e.rationale,
})
})
.collect();
let gated: Vec<serde_json::Value> = port_map
.gated_ports()
.map(|g| {
serde_json::json!({
"port": g.port,
"app_id": g.app_id,
"app_name": g.app_name,
})
})
.collect();
Ok(serde_json::json!({
// The headline. False means this node still has app ports that
// answer without authentication.
"fully_enforced": status.is_fully_enforced(),
"claimed": status.claimed,
"unprotected": status.unprotected,
"gated": gated,
"exempt": exempt,
}))
}
}
@@ -470,6 +470,7 @@ impl RpcHandler {
"server.set-location" => self.handle_server_set_location(params).await,
// System monitoring
"security.app-gate-status" => self.handle_app_gate_status().await,
"system.get-hostname" => self.handle_system_get_hostname().await,
"system.stats" => self.handle_system_stats().await,
"system.processes" => self.handle_system_processes().await,
+14
View File
@@ -1,4 +1,5 @@
mod analytics;
mod appgate;
mod ark;
mod assistant_chat;
mod auth;
@@ -94,6 +95,11 @@ pub struct RpcHandler {
port_allocator: Arc<tokio::sync::Mutex<PortAllocator>>,
pub session_store: SessionStore,
login_rate_limiter: LoginRateLimiter,
/// Authentication in front of every app port. Built here rather than in
/// `server.rs` so it shares this handler's session store and login rate
/// limiter — an attacker must not get a fresh budget of password guesses
/// by moving from the dashboard to an app port.
pub(crate) app_gate: Arc<crate::appgate::AppGate>,
endpoint_rate_limiter: EndpointRateLimiter,
response_cache: ResponseCache,
mesh_service: Arc<tokio::sync::RwLock<Option<crate::mesh::MeshService>>>,
@@ -158,6 +164,13 @@ impl RpcHandler {
});
}
let app_gate = Arc::new(crate::appgate::AppGate::new(
session_store.clone(),
auth_manager.clone(),
login_rate_limiter.clone(),
config.data_dir.clone(),
));
Ok(Self {
config,
auth_manager,
@@ -168,6 +181,7 @@ impl RpcHandler {
port_allocator,
session_store,
login_rate_limiter,
app_gate,
endpoint_rate_limiter,
response_cache: ResponseCache::new(5),
mesh_service: Arc::new(tokio::sync::RwLock::new(None)),