Compare commits

...
5 Commits
Author SHA1 Message Date
archipelago bc94445ca0 chore: sign the v1.8.8 app catalog + release manifest 2026-09-01 03:57:23 -04:00
archipelago 04cf0f663a chore: drop the superseded v1.8.8 prep for rebuild 2026-09-01 03:49:16 -04:00
archipelago 576c642da4 fix(apps): ollama resource type + adguardhome port; gate on collisions
Demo images / Build & push demo images (push) Failing after 38s
Ollama's embedded manifest failed the typed parse (memory_limit wants a
string) so the catalog overlay was skipped for it; AdGuard Home's
conventional :3000 collided with Grafana's. The release gate now runs
the host-port collision test (repo_app_manifests_have_no_host_port_collisions)
so this class can never ship untested again.
2026-09-01 03:31:02 -04:00
archipelago 12866db84a chore: sign the v1.8.8 app catalog + release manifest 2026-09-01 03:16:10 -04:00
archipelago a184254706 style: rustfmt the ssh-mesh module 2026-09-01 02:42:39 -04:00
11 changed files with 3323 additions and 3258 deletions
+6 -3
View File
@@ -28,10 +28,13 @@ app:
network_policy: isolated
ports:
- host: 3000
- host: 3030
container: 3000
protocol: tcp
bind: 127.0.0.1
# 3030, not AdGuard Home's conventional 3000: Grafana owns :3000 on a
# node, and both being installable means the host ports must not
# collide (the orchestrator refuses/loads warn on overlap).
# open: the setup wizard and admin console carry AdGuard Home's own
# login; the gate fronts the port (TLS, header fixes) without a
# second cookie challenge.
@@ -67,7 +70,7 @@ app:
health_check:
type: tcp
endpoint: localhost:3000
endpoint: localhost:3030
interval: 30s
timeout: 5s
retries: 3
@@ -77,7 +80,7 @@ app:
name: Admin console
description: AdGuard Home web console
type: ui
port: 3000
port: 3030
protocol: http
path: /
+2 -1
View File
@@ -20,7 +20,8 @@ app:
- storage: 50Gi
resources:
memory_limit: 0
# No memory limit: models are sized by the disk allowance below, and a
# RAM ceiling would just OOM-kill long inferences.
disk_limit: 50Gi
security:
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]]
name = "archipelago"
version = "1.8.7-alpha"
version = "1.8.8-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.8.7-alpha"
version = "1.8.8-alpha"
edition = "2021"
license.workspace = true
description = "Archipelago Bitcoin Node OS - Native backend"
@@ -145,11 +145,7 @@ impl DockerPackageScanner {
// manifest is what the catalog signed and what the App Store shows,
// so it is also what an installed tile must render.
let manifest_icon = real_manifest_metadata(&app_id)
.and_then(|m| {
m.get("icon")
.and_then(|v| v.as_str())
.map(str::to_string)
})
.and_then(|m| m.get("icon").and_then(|v| v.as_str()).map(str::to_string))
.filter(|s| !s.trim().is_empty());
// Resolve UI address: separate UI containers > static map > dynamic ports
@@ -365,8 +361,12 @@ fn real_manifest_metadata(app_id: &str) -> Option<serde_json::Value> {
.join("manifest.yml"),
);
for path in candidates {
let Ok(content) = std::fs::read_to_string(&path) else { continue };
let Ok(value) = serde_yaml::from_str::<serde_json::Value>(&content) else { continue };
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(value) = serde_yaml::from_str::<serde_json::Value>(&content) else {
continue;
};
let meta = value.get("app").and_then(|a| a.get("metadata")).cloned();
if meta.is_some() {
return meta;
+40 -10
View File
@@ -208,7 +208,11 @@ async fn reload_nft() -> bool {
Ok(true) => {}
_ => return false,
}
match Command::new("sudo").args(["nft", "-f", FIPS_NFT]).output().await {
match Command::new("sudo")
.args(["nft", "-f", FIPS_NFT])
.output()
.await
{
Ok(out) if out.status.success() => true,
Ok(out) => {
tracing::warn!(
@@ -227,7 +231,11 @@ async fn reload_nft() -> bool {
/// Persist new state and reconcile immediately. Validation happens here so
/// an invalid source list can never reach disk, and reconcile reads back
/// exactly what was saved.
pub async fn set(data_dir: &Path, enabled: bool, sources: &[String]) -> Result<(SshMeshState, ReconcileOutcome)> {
pub async fn set(
data_dir: &Path,
enabled: bool,
sources: &[String],
) -> Result<(SshMeshState, ReconcileOutcome)> {
let state = SshMeshState {
enabled,
sources: validate_sources(sources)?,
@@ -268,7 +276,11 @@ pub async fn preflights() -> SshPreflights {
async fn sshd_active() -> bool {
for unit in ["ssh", "sshd"] {
if let Ok(out) = Command::new("systemctl").args(["is-active", "--quiet", unit]).output().await {
if let Ok(out) = Command::new("systemctl")
.args(["is-active", "--quiet", unit])
.output()
.await
{
if out.status.success() {
return true;
}
@@ -330,7 +342,9 @@ fn collect_password_auth(content: &str, out: &mut Vec<bool>) {
}
async fn glob_sorted(pattern: &str) -> Result<Vec<std::path::PathBuf>> {
let dir = std::path::Path::new(pattern).parent().unwrap_or_else(|| Path::new("/"));
let dir = std::path::Path::new(pattern)
.parent()
.unwrap_or_else(|| Path::new("/"));
let prefix = std::path::Path::new(pattern)
.file_name()
.and_then(|n| n.to_str())
@@ -338,7 +352,9 @@ async fn glob_sorted(pattern: &str) -> Result<Vec<std::path::PathBuf>> {
.unwrap_or("")
.to_string();
let mut files: Vec<std::path::PathBuf> = Vec::new();
let mut entries = tokio::fs::read_dir(dir).await.context("read sshd_config.d")?;
let mut entries = tokio::fs::read_dir(dir)
.await
.context("read sshd_config.d")?;
while let Ok(Some(entry)) = entries.next_entry().await {
let name = entry.file_name();
let name = name.to_string_lossy();
@@ -357,14 +373,19 @@ mod tests {
#[test]
fn disabled_is_the_default_and_missing_file_is_not_an_error() {
let dir = tempfile::tempdir().unwrap();
let state = tokio::runtime::Runtime::new().unwrap().block_on(load(dir.path()));
let state = tokio::runtime::Runtime::new()
.unwrap()
.block_on(load(dir.path()));
assert!(!state.enabled);
assert!(state.sources.is_empty());
}
#[test]
fn any_peer_dropin_is_an_unrestricted_accept() {
let state = SshMeshState { enabled: true, sources: vec![] };
let state = SshMeshState {
enabled: true,
sources: vec![],
};
let out = render_dropin(&state);
assert!(out.contains("tcp dport 22 accept"));
assert!(!out.contains("ip6 saddr"), "no saddr restriction expected");
@@ -398,7 +419,10 @@ mod tests {
String::new(),
])
.unwrap();
assert_eq!(ok, vec!["fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586".to_string()]);
assert_eq!(
ok,
vec!["fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586".to_string()]
);
}
#[test]
@@ -408,8 +432,14 @@ mod tests {
enabled: true,
sources: vec!["fd00::1".to_string()],
};
std::fs::write(dir.path().join(STATE_FILE), serde_json::to_string(&state).unwrap()).unwrap();
let loaded = tokio::runtime::Runtime::new().unwrap().block_on(load(dir.path()));
std::fs::write(
dir.path().join(STATE_FILE),
serde_json::to_string(&state).unwrap(),
)
.unwrap();
let loaded = tokio::runtime::Runtime::new()
.unwrap()
.block_on(load(dir.path()));
assert_eq!(loaded, state);
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.8.7-alpha",
"version": "1.8.8-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.8.7-alpha",
"version": "1.8.8-alpha",
"dependencies": {
"@scure/bip39": "^2.2.0",
"@types/dompurify": "^3.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.8.7-alpha",
"version": "1.8.8-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",
+3232 -3231
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,30 @@
{
"changelog": [
"**SSH over the mesh is now a first-class setting.** Settings gains an \"SSH over mesh\" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit \"I understand\" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the `90-ssh.nft` drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, `fipssh <user>@<node-npub>` connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.",
"**The App Store now lists apps — not parts of apps.** The signed catalog carries every manifest because the node's update layer needs their pins, and the store briefly listed them all: Mempool API, LND UI, Bitcoin UI, the Pine voice engines, the IndeeHub and Immich backends, the mesh router and friends. Components are hidden from the store listing (they still appear where they belong — the Services tab of My Apps, once installed), and four entries that never earned a tile are gone outright: MorphOS server (old), the Web5 DID wallet, Lightning Stack (an untracked upstream bundle — LND covers the need), and CryptPad (never tested).",
"**App icons now persist everywhere, in the proper container style.** Two fixes: installed apps render the icon from their own manifest — Cuprate no longer falls back to the generic A-mark on its Services tile — and the store grids (the Discover page) apply the same icon container treatment (backdrop, border, shadow) as My Apps, the detail pages, and Home. Manifest-declared UI apps also classify correctly again: Alby Hub installs into My Apps with a working tile, not into Services, because a probe miss no longer buries an app the manifest itself says has a frontend.",
"**Installing from the store keeps you on the store page.** The install progress lives on the tile itself and the app appears in My Apps when it lands — no more being yanked to My Apps mid-browse."
],
"components": [
{
"current_version": "1.8.8-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.8-alpha/archipelago",
"name": "archipelago",
"new_version": "1.8.8-alpha",
"sha256": "96f39b8db6f08386200e1eab91c8444a7758526e6034100c8a33907ff9263530",
"size_bytes": 64175864
},
{
"current_version": "1.8.8-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.8-alpha/archipelago-frontend-1.8.8-alpha.tar.gz",
"name": "archipelago-frontend-1.8.8-alpha.tar.gz",
"new_version": "1.8.8-alpha",
"sha256": "7829b67edf8dec27997dd821650ed4d61aea721f802d46a8d47014f4b4246db1",
"size_bytes": 97730549
}
],
"release_date": "2026-09-01",
"signature": "c839cbdcb356a503d87bc17f52b6e5f3a934ae1e72a891f2d21d85366f23debb224a2a40b9124bab50fe95444e01e27711690b1bc50062f40b7ed4f34e078d06",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.8-alpha"
}
+1 -1
View File
@@ -127,7 +127,7 @@ stage "cargo-check" timeout 580 cargo check --manifest-path core/Cargo.toml
# 3600s leaves headroom; a warm target/ finishes in a fraction of it.
stage "cargo-test-weekly" timeout 3600 env CARGO_INCREMENTAL=0 \
cargo test --manifest-path core/Cargo.toml -p archipelago -- \
update:: lnd container::image_versions scanner drift missing_secret
update:: lnd container::image_versions scanner drift missing_secret collision
# ── Stage 4: live node smoke ─────────────────────────────────────────
if [[ $LIVE -eq 1 ]]; then