Skip to content
Merged
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
94 changes: 91 additions & 3 deletions src/anolisa/crates/anolisa-cli/src/commands/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,38 @@ fn validate_component_path_segment(component: &str, command: &str) -> Result<(),
/// that the bundled layer is always-present and overlays stack on top.
pub fn load_bundled_catalog(ctx: &CliContext, command: &str) -> Result<Catalog, CliError> {
let layout = resolve_layout(ctx);
let bundled = packaged_manifests_root(&layout)
let bundled = discovered_packaged_manifests_root(&layout)
.or_else(dev_tree_manifests)
.unwrap_or_else(|| layout.datadir.join(MANIFESTS_SUBDIR));
load_catalog_from_layers(&layout, ctx.install_mode, bundled, command)
}

/// Load the layered catalog for a concrete filesystem layout.
///
/// Scoped read-only views may inspect records from a physical root that is
/// not the current write root. Callers pass the root's layout and scope so
/// bundled and overlay manifests are loaded from the same physical root as
/// the state record being projected.
pub fn load_catalog_for_layout(
layout: &FsLayout,
install_mode: InstallMode,
command: &str,
) -> Result<Catalog, CliError> {
let bundled = trusted_manifests_root(layout, install_mode)
.or_else(dev_tree_manifests)
.unwrap_or_else(|| layout.datadir.join(MANIFESTS_SUBDIR));
load_catalog_from_layers(layout, install_mode, bundled, command)
}

fn load_catalog_from_layers(
layout: &FsLayout,
install_mode: InstallMode,
bundled: PathBuf,
command: &str,
) -> Result<Catalog, CliError> {
let overlay = layout.manifests_overlay.clone();
let overlay = overlay.is_dir().then_some(overlay);
let (system, user) = match ctx.install_mode {
let (system, user) = match install_mode {
InstallMode::System => (overlay, None),
InstallMode::User => (None, overlay),
};
Expand All @@ -304,7 +329,25 @@ pub fn load_bundled_catalog(ctx: &CliContext, command: &str) -> Result<Catalog,
})
}

fn packaged_manifests_root(layout: &FsLayout) -> Option<PathBuf> {
fn trusted_manifests_root(layout: &FsLayout, install_mode: InstallMode) -> Option<PathBuf> {
match install_mode {
InstallMode::System => trusted_system_manifests_root(layout),
InstallMode::User => discovered_packaged_manifests_root(layout),
}
}

fn trusted_system_manifests_root(layout: &FsLayout) -> Option<PathBuf> {
let local = layout.datadir.join(MANIFESTS_SUBDIR);
if local.is_dir() {
return Some(local);
}
layout
.package_datadir()
.map(|datadir| datadir.join(MANIFESTS_SUBDIR))
.filter(|candidate| candidate.is_dir())
}

fn discovered_packaged_manifests_root(layout: &FsLayout) -> Option<PathBuf> {
// Discover the packaged datadir (`<prefix>/share/anolisa/`) using
// the shared probe in [`crate::packaged`] — that helper honors the
// `ANOLISA_DATA_DIR` env override and binary-location lookup so a
Expand Down Expand Up @@ -682,6 +725,51 @@ dest = "{datadir}/adapters/sec-core/openclaw/"
);
}

#[test]
fn system_scoped_catalog_ignores_data_dir_env_override() {
let tmp = tempfile::tempdir().expect("tempdir");
let layout = FsLayout::system(Some(tmp.path().join("system")));
let env_datadir = tmp.path().join("env-datadir");
let env_runtime = env_datadir.join("manifests/runtime");
let system_runtime = layout.datadir.join("manifests/runtime");
std::fs::create_dir_all(&env_runtime).expect("mkdir env runtime");
std::fs::create_dir_all(&system_runtime).expect("mkdir system runtime");
std::fs::write(
env_runtime.join("agentsight.toml"),
r#"
[component]
name = "agentsight"
version = "env"
"#,
)
.expect("write env manifest");
std::fs::write(
system_runtime.join("agentsight.toml"),
r#"
[component]
name = "agentsight"
version = "system"
"#,
)
.expect("write system manifest");

let _guard = crate::packaged::DataDirEnvGuard::set(&env_datadir);
let catalog =
load_catalog_for_layout(&layout, InstallMode::System, "status").expect("catalog");

assert_eq!(
catalog.layers.bundled,
layout.datadir.join("manifests"),
"system-scoped catalog must use the system layout, not ANOLISA_DATA_DIR",
);
assert_eq!(
catalog
.component("agentsight")
.map(|m| m.component.version.as_str()),
Some("system")
);
}

/// `object_status_str` must cover every variant of `ObjectStatus` and
/// produce the exact wire vocabulary the spec promises. If a new variant
/// is added, this test forces us to extend the mapping.
Expand Down
2 changes: 2 additions & 0 deletions src/anolisa/crates/anolisa-cli/src/commands/state_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub(crate) enum StateVisibility {
#[derive(Debug, Clone)]
pub(crate) struct ScopedStateRoot {
pub(crate) scope: StateScope,
pub(crate) layout: FsLayout,
pub(crate) state_path: PathBuf,
pub(crate) writable: bool,
pub(crate) state: InstalledState,
Expand Down Expand Up @@ -123,6 +124,7 @@ impl StateView {
};
let root = ScopedStateRoot {
scope: spec.scope,
layout,
state_path,
writable: spec.writable,
state,
Expand Down
5 changes: 5 additions & 0 deletions src/anolisa/crates/anolisa-cli/src/commands/tier1/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,11 @@ mod tests {
ComponentRecord {
name: name.to_string(),
status: status.to_string(),
scope: "system".to_string(),
active: true,
mutable_by_current_invocation: true,
shadowed_by: None,
state_path: Some("/tmp/anolisa-system-state/installed.toml".to_string()),
version: Some("1.0.0".to_string()),
installed_at: None,
last_operation_id: None,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

use anolisa_core::state::{InstalledState, ObjectStatus, Ownership};
use anolisa_platform::fs_layout::FsLayout;

use crate::commands::state_view::{ScopedStateRoot, StateScope, StateView};
use crate::commands::tier1::list::{ListArgs, build_rows_from_view};
Expand Down Expand Up @@ -62,12 +63,21 @@ fn user_record_shadows_system_record_in_list_rows() {
fn user_plus_system_view(user_state: InstalledState, system_state: InstalledState) -> StateView {
let user_root = ScopedStateRoot {
scope: StateScope::User,
layout: FsLayout::user_with_overrides(
PathBuf::from("/tmp/anolisa-home"),
None,
None,
Some(PathBuf::from("/tmp/anolisa-user-state")),
None,
None,
),
state_path: PathBuf::from("/tmp/anolisa-user-state/installed.toml"),
writable: true,
state: user_state,
};
let system_root = ScopedStateRoot {
scope: StateScope::System,
layout: FsLayout::system(Some(PathBuf::from("/tmp/anolisa-system"))),
state_path: PathBuf::from("/tmp/anolisa-system-state/installed.toml"),
writable: false,
state: system_state,
Expand Down
Loading
Loading