Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc94445ca0 | ||
|
|
04cf0f663a | ||
|
|
576c642da4 | ||
|
|
12866db84a | ||
|
|
a184254706 |
@@ -28,10 +28,13 @@ app:
|
|||||||
network_policy: isolated
|
network_policy: isolated
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- host: 3000
|
- host: 3030
|
||||||
container: 3000
|
container: 3000
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
bind: 127.0.0.1
|
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
|
# open: the setup wizard and admin console carry AdGuard Home's own
|
||||||
# login; the gate fronts the port (TLS, header fixes) without a
|
# login; the gate fronts the port (TLS, header fixes) without a
|
||||||
# second cookie challenge.
|
# second cookie challenge.
|
||||||
@@ -67,7 +70,7 @@ app:
|
|||||||
|
|
||||||
health_check:
|
health_check:
|
||||||
type: tcp
|
type: tcp
|
||||||
endpoint: localhost:3000
|
endpoint: localhost:3030
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -77,7 +80,7 @@ app:
|
|||||||
name: Admin console
|
name: Admin console
|
||||||
description: AdGuard Home web console
|
description: AdGuard Home web console
|
||||||
type: ui
|
type: ui
|
||||||
port: 3000
|
port: 3030
|
||||||
protocol: http
|
protocol: http
|
||||||
path: /
|
path: /
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ app:
|
|||||||
- storage: 50Gi
|
- storage: 50Gi
|
||||||
|
|
||||||
resources:
|
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
|
disk_limit: 50Gi
|
||||||
|
|
||||||
security:
|
security:
|
||||||
|
|||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.7-alpha"
|
version = "1.8.8-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.7-alpha"
|
version = "1.8.8-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
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,
|
// manifest is what the catalog signed and what the App Store shows,
|
||||||
// so it is also what an installed tile must render.
|
// so it is also what an installed tile must render.
|
||||||
let manifest_icon = real_manifest_metadata(&app_id)
|
let manifest_icon = real_manifest_metadata(&app_id)
|
||||||
.and_then(|m| {
|
.and_then(|m| m.get("icon").and_then(|v| v.as_str()).map(str::to_string))
|
||||||
m.get("icon")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(str::to_string)
|
|
||||||
})
|
|
||||||
.filter(|s| !s.trim().is_empty());
|
.filter(|s| !s.trim().is_empty());
|
||||||
|
|
||||||
// Resolve UI address: separate UI containers > static map > dynamic ports
|
// 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"),
|
.join("manifest.yml"),
|
||||||
);
|
);
|
||||||
for path in candidates {
|
for path in candidates {
|
||||||
let Ok(content) = std::fs::read_to_string(&path) else { continue };
|
let Ok(content) = std::fs::read_to_string(&path) else {
|
||||||
let Ok(value) = serde_yaml::from_str::<serde_json::Value>(&content) else { continue };
|
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();
|
let meta = value.get("app").and_then(|a| a.get("metadata")).cloned();
|
||||||
if meta.is_some() {
|
if meta.is_some() {
|
||||||
return meta;
|
return meta;
|
||||||
|
|||||||
@@ -208,7 +208,11 @@ async fn reload_nft() -> bool {
|
|||||||
Ok(true) => {}
|
Ok(true) => {}
|
||||||
_ => return false,
|
_ => 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) if out.status.success() => true,
|
||||||
Ok(out) => {
|
Ok(out) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -227,7 +231,11 @@ async fn reload_nft() -> bool {
|
|||||||
/// Persist new state and reconcile immediately. Validation happens here so
|
/// Persist new state and reconcile immediately. Validation happens here so
|
||||||
/// an invalid source list can never reach disk, and reconcile reads back
|
/// an invalid source list can never reach disk, and reconcile reads back
|
||||||
/// exactly what was saved.
|
/// 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 {
|
let state = SshMeshState {
|
||||||
enabled,
|
enabled,
|
||||||
sources: validate_sources(sources)?,
|
sources: validate_sources(sources)?,
|
||||||
@@ -268,7 +276,11 @@ pub async fn preflights() -> SshPreflights {
|
|||||||
|
|
||||||
async fn sshd_active() -> bool {
|
async fn sshd_active() -> bool {
|
||||||
for unit in ["ssh", "sshd"] {
|
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() {
|
if out.status.success() {
|
||||||
return true;
|
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>> {
|
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)
|
let prefix = std::path::Path::new(pattern)
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
@@ -338,7 +352,9 @@ async fn glob_sorted(pattern: &str) -> Result<Vec<std::path::PathBuf>> {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
let mut files: Vec<std::path::PathBuf> = Vec::new();
|
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 {
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
let name = entry.file_name();
|
let name = entry.file_name();
|
||||||
let name = name.to_string_lossy();
|
let name = name.to_string_lossy();
|
||||||
@@ -357,14 +373,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn disabled_is_the_default_and_missing_file_is_not_an_error() {
|
fn disabled_is_the_default_and_missing_file_is_not_an_error() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
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.enabled);
|
||||||
assert!(state.sources.is_empty());
|
assert!(state.sources.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn any_peer_dropin_is_an_unrestricted_accept() {
|
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);
|
let out = render_dropin(&state);
|
||||||
assert!(out.contains("tcp dport 22 accept"));
|
assert!(out.contains("tcp dport 22 accept"));
|
||||||
assert!(!out.contains("ip6 saddr"), "no saddr restriction expected");
|
assert!(!out.contains("ip6 saddr"), "no saddr restriction expected");
|
||||||
@@ -398,7 +419,10 @@ mod tests {
|
|||||||
String::new(),
|
String::new(),
|
||||||
])
|
])
|
||||||
.unwrap();
|
.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]
|
#[test]
|
||||||
@@ -408,8 +432,14 @@ mod tests {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sources: vec!["fd00::1".to_string()],
|
sources: vec!["fd00::1".to_string()],
|
||||||
};
|
};
|
||||||
std::fs::write(dir.path().join(STATE_FILE), serde_json::to_string(&state).unwrap()).unwrap();
|
std::fs::write(
|
||||||
let loaded = tokio::runtime::Runtime::new().unwrap().block_on(load(dir.path()));
|
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);
|
assert_eq!(loaded, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.7-alpha",
|
"version": "1.8.8-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.7-alpha",
|
"version": "1.8.8-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.7-alpha",
|
"version": "1.8.8-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
+3232
-3231
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"
|
||||||
|
}
|
||||||
@@ -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.
|
# 3600s leaves headroom; a warm target/ finishes in a fraction of it.
|
||||||
stage "cargo-test-weekly" timeout 3600 env CARGO_INCREMENTAL=0 \
|
stage "cargo-test-weekly" timeout 3600 env CARGO_INCREMENTAL=0 \
|
||||||
cargo test --manifest-path core/Cargo.toml -p archipelago -- \
|
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 ─────────────────────────────────────────
|
# ── Stage 4: live node smoke ─────────────────────────────────────────
|
||||||
if [[ $LIVE -eq 1 ]]; then
|
if [[ $LIVE -eq 1 ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user