Merge remote-tracking branch 'origin/main'

This commit is contained in:
Bryan Helmkamp 2026-04-22 00:46:59 -04:00
commit 702c18cbb4
No known key found for this signature in database
32 changed files with 373 additions and 370 deletions

View file

@ -10,6 +10,7 @@ on:
- "Cargo.lock"
- ".cargo/**"
- ".config/**"
- "bin/dev/**"
- "openapi/**"
- ".github/workflows/rust.yml"
pull_request:
@ -21,6 +22,7 @@ on:
- "Cargo.lock"
- ".cargo/**"
- ".config/**"
- "bin/dev/**"
- "openapi/**"
- ".github/workflows/rust.yml"
workflow_dispatch:
@ -35,6 +37,17 @@ env:
CARGO_TERM_COLOR: always
jobs:
boundary:
name: Boundary
runs-on: ubuntu-24.04-x86-32-cores
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- run: bin/dev/check-boundary.sh
fmt:
name: Format
runs-on: ubuntu-24.04-x86-32-cores

85
bin/dev/check-boundary.sh Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/../.."
symbol_allowlist=(
"lib/crates/fabro-cli/src/local_server.rs"
"lib/crates/fabro-cli/src/commands/install.rs"
"lib/crates/fabro-cli/src/commands/run/runner.rs"
"lib/crates/fabro-cli/src/commands/pr/mod.rs"
"lib/crates/fabro-cli/src/commands/pr/create.rs"
)
deprecated_helper_allowlist=(
"lib/crates/fabro-cli/src/user_config.rs"
"lib/crates/fabro-cli/src/commands/pr/mod.rs"
"lib/crates/fabro-cli/src/commands/pr/create.rs"
)
temporary_exemptions=(
"lib/crates/fabro-cli/src/commands/pr/mod.rs"
"lib/crates/fabro-cli/src/commands/pr/create.rs"
)
exemption_marker="boundary-exempt(pr-api): remove with follow-up #1"
in_array() {
local needle=$1
shift
local item
for item in "$@"; do
if [[ "$item" == "$needle" ]]; then
return 0
fi
done
return 1
}
find_matches() {
local pattern=$1
if command -v rg >/dev/null 2>&1; then
rg -l "$pattern" lib/crates/fabro-cli/src --glob '*.rs' || true
else
grep -R -l -E "$pattern" lib/crates/fabro-cli/src --include='*.rs' || true
fi
}
fail=0
while IFS= read -r path; do
[[ -z "$path" ]] && continue
if ! in_array "$path" "${symbol_allowlist[@]}"; then
echo "boundary check failed: gated server symbol used outside allowlist: $path" >&2
fail=1
fi
done < <(find_matches 'fabro_config::resolve_server_from_file|fabro_config::resolve_server\b|Storage::new')
while IFS= read -r path; do
[[ -z "$path" ]] && continue
if ! in_array "$path" "${deprecated_helper_allowlist[@]}"; then
echo "boundary check failed: deprecated user_config::storage_dir used outside allowlist: $path" >&2
fail=1
fi
done < <(find_matches 'user_config::storage_dir')
for path in "${temporary_exemptions[@]}"; do
if ! grep -q "$exemption_marker" "$path"; then
echo "boundary check failed: missing temporary exemption marker in $path" >&2
fail=1
fi
done
while IFS= read -r path; do
[[ -z "$path" ]] && continue
if ! in_array "$path" "${temporary_exemptions[@]}"; then
echo "boundary check failed: unexpected temporary exemption marker in $path" >&2
fail=1
fi
done < <(find_matches "$exemption_marker")
if [[ $fail -ne 0 ]]; then
exit 1
fi
echo "CLI/server boundary checks passed."

View file

@ -1,9 +1,4 @@
#![expect(
clippy::disallowed_methods,
reason = "CLI `doctor` command: sync directory scan in command handler"
)]
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use anyhow::Result;
use fabro_api::types as api_types;
@ -21,7 +16,6 @@ use fabro_util::version::FABRO_VERSION;
use crate::args::DoctorArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config;
pub(crate) fn check_config(settings_path: Option<PathBuf>) -> CheckResult {
match settings_path {
@ -50,65 +44,6 @@ pub(crate) fn check_config(settings_path: Option<PathBuf>) -> CheckResult {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct StorageDirStatus {
path: PathBuf,
exists: bool,
readable: bool,
writable: bool,
}
fn probe_storage_dir(path: &Path) -> StorageDirStatus {
let exists = path.is_dir();
let readable = exists && std::fs::read_dir(path).is_ok();
let writable = exists && tempfile::tempfile_in(path).is_ok();
StorageDirStatus {
path: path.to_path_buf(),
exists,
readable,
writable,
}
}
fn check_storage_dir(status: &StorageDirStatus) -> CheckResult {
let display = contract_tilde(&status.path);
let display = display.display();
let details = vec![
CheckDetail::new(format!(
"Exists: {}",
if status.exists { "yes" } else { "no" }
)),
CheckDetail::new(format!(
"Readable: {}",
if status.readable { "yes" } else { "no" }
)),
CheckDetail::new(format!(
"Writable: {}",
if status.writable { "yes" } else { "no" }
)),
];
let is_healthy = status.exists && status.readable && status.writable;
CheckResult {
name: "Storage directory".to_string(),
status: if is_healthy {
CheckStatus::Pass
} else {
CheckStatus::Error
},
summary: display.to_string(),
details,
remediation: if is_healthy {
None
} else if !status.exists {
Some(format!("Create the directory: mkdir -p {display}"))
} else {
Some(format!("Fix permissions on {display}"))
},
}
}
fn check_version_parity(server_version: &str) -> CheckResult {
let cli_version = FABRO_VERSION;
if server_version == cli_version {
@ -230,19 +165,11 @@ pub(crate) async fn run_doctor(
let settings_config_path = active_settings_path(None);
let settings = user_config::load_settings().unwrap_or_default();
let storage_dir_path =
user_config::storage_dir(&settings).unwrap_or_else(|_| user_config::default_storage_dir());
let storage_dir = probe_storage_dir(&storage_dir_path);
let local_checks = vec![
check_config(
settings_config_path
.exists()
.then_some(settings_config_path),
),
check_storage_dir(&storage_dir),
];
let local_checks = vec![check_config(
settings_config_path
.exists()
.then_some(settings_config_path),
)];
let mut report = CheckReport {
title: "Fabro Doctor".to_string(),
@ -409,81 +336,6 @@ mod tests {
assert!(result.remediation.is_some());
}
// -- check_storage_dir --
#[test]
fn probe_storage_dir_existing_dir_is_readable_and_writable() {
let dir = tempfile::tempdir().unwrap();
let status = probe_storage_dir(dir.path());
assert_eq!(status, StorageDirStatus {
path: dir.path().to_path_buf(),
exists: true,
readable: true,
writable: true,
});
}
#[test]
fn probe_storage_dir_missing_dir_is_not_usable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing");
let status = probe_storage_dir(&path);
assert_eq!(status, StorageDirStatus {
path,
exists: false,
readable: false,
writable: false,
});
}
#[test]
fn check_storage_dir_pass() {
let result = check_storage_dir(&StorageDirStatus {
path: PathBuf::from("/home/user/.fabro"),
exists: true,
readable: true,
writable: true,
});
assert_eq!(result.status, CheckStatus::Pass);
assert_eq!(result.summary, "/home/user/.fabro");
assert!(result.remediation.is_none());
assert_eq!(result.details.len(), 3);
}
#[test]
fn check_storage_dir_not_exists() {
let result = check_storage_dir(&StorageDirStatus {
path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"),
exists: false,
readable: false,
writable: false,
});
assert_eq!(result.status, CheckStatus::Error);
assert!(result.summary.contains("nonexistent-fabro-doctor-test-xyz"));
assert!(result.remediation.as_deref().unwrap().contains("mkdir -p"));
}
#[test]
fn check_storage_dir_not_writable() {
let result = check_storage_dir(&StorageDirStatus {
path: PathBuf::from("/home/user/.fabro"),
exists: true,
readable: true,
writable: false,
});
assert_eq!(result.status, CheckStatus::Error);
assert_eq!(result.summary, "/home/user/.fabro");
assert_eq!(
result.remediation.as_deref(),
Some("Fix permissions on /home/user/.fabro")
);
}
#[test]
fn check_version_parity_warns_on_mismatch() {
let result = check_version_parity("0.0.0-test");

View file

@ -57,7 +57,7 @@ use crate::shared::provider_auth::{
ApiKeySource, authenticate_provider, authenticate_provider_with_api_key_source,
authenticate_provider_with_method, prompt_confirm, prompt_password, provider_display_name,
};
use crate::{server_client, user_config};
use crate::{local_server, server_client, user_config};
const GITHUB_TOKEN_SECRET_KEY: &str = "GITHUB_TOKEN";
const GITHUB_APP_PRIVATE_KEY_KEY: &str = "GITHUB_APP_PRIVATE_KEY";
@ -1161,7 +1161,7 @@ fn persist_server_env_secrets(storage_dir: &Path, secrets: &[(String, String)])
return Ok(());
}
let env_path = Storage::new(storage_dir).server_state().env_path();
let env_path = Storage::new(storage_dir).runtime_state().env_path();
envfile::merge_env_file(&env_path, secrets.iter().cloned())
.with_context(|| format!("merging server env secrets into {}", env_path.display()))?;
Ok(())
@ -1223,7 +1223,7 @@ fn persist_github_install_changes(
writes: &PendingGitHubInstallWrite<'_>,
) -> Result<()> {
let storage = Storage::new(storage_dir);
let server_env_path = storage.server_state().env_path();
let server_env_path = storage.runtime_state().env_path();
let vault_path = storage.secrets_path();
let previous_server_env = std::fs::read_to_string(&server_env_path).ok();
let previous_vault = std::fs::read_to_string(&vault_path).ok();
@ -1494,7 +1494,7 @@ async fn run_install_github_inner(
.context("failed to parse existing settings.toml")?,
args.storage_dir.as_deref(),
);
let storage_dir = user_config::storage_dir(&parsed_settings).unwrap_or_else(|_| {
let storage_dir = local_server::storage_dir(&parsed_settings).unwrap_or_else(|_| {
args.storage_dir
.clone_path()
.unwrap_or_else(default_storage_dir)
@ -1586,7 +1586,7 @@ async fn run_install_github_inner(
.and_then(|auth| auth.methods)
.unwrap_or_default();
let token = dev_token::read_dev_token_file(
&Storage::new(&storage_dir).server_state().dev_token_path(),
&Storage::new(&storage_dir).runtime_state().dev_token_path(),
);
print_auth_status(&methods, token.as_deref(), &s, printer);
fabro_util::printerr!(printer, "");
@ -1641,7 +1641,7 @@ async fn run_install_inner(
let s = Styles::detect_stderr();
let emoji = console::Emoji("⚒️ ", "");
let cli_settings = user_config::load_settings_with_storage_dir(args.storage_dir.as_deref())?;
let storage_dir = user_config::storage_dir(&cli_settings)?;
let storage_dir = local_server::storage_dir(&cli_settings)?;
let server_was_running = record::active_server_record(&storage_dir)?.is_some();
let fabro_dir = fabro_util::Home::from_env().root().to_path_buf();
let config_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME);
@ -1823,7 +1823,7 @@ async fn run_install_inner(
&fabro_util::Home::from_env().dev_token_path(),
)?;
dev_token::write_dev_token(
&Storage::new(&storage_dir).server_state().dev_token_path(),
&Storage::new(&storage_dir).runtime_state().dev_token_path(),
&token,
)?;
fabro_util::printerr!(
@ -1879,7 +1879,7 @@ async fn run_install_inner(
" {} Saved {} runtime secrets to {}",
s.green.apply_to(""),
server_env_pairs.len(),
path::contract_tilde(&Storage::new(&storage_dir).server_state().env_path()).display()
path::contract_tilde(&Storage::new(&storage_dir).runtime_state().env_path()).display()
);
fabro_util::printerr!(
printer,
@ -1911,7 +1911,7 @@ async fn run_install_inner(
.map(Vec::as_slice)
.unwrap_or_default();
let token = dev_token::read_dev_token_file(
&Storage::new(&storage_dir).server_state().dev_token_path(),
&Storage::new(&storage_dir).runtime_state().dev_token_path(),
);
print_auth_status(methods, token.as_deref(), &s, printer);
fabro_util::printerr!(printer, "");
@ -2561,7 +2561,7 @@ client_id = "client-id"
.unwrap();
let server_env =
std::fs::read_to_string(Storage::new(dir.path()).server_state().env_path()).unwrap();
std::fs::read_to_string(Storage::new(dir.path()).runtime_state().env_path()).unwrap();
assert!(server_env.contains("SESSION_SECRET=session"));
assert!(server_env.contains("FABRO_JWT_PUBLIC_KEY=public-key"));
assert_eq!(created.calls_async().await, 2);
@ -2817,7 +2817,7 @@ client_id = "client-id"
.await;
assert!(result.is_err());
assert!(Storage::new(dir.path()).server_state().env_path().exists());
assert!(Storage::new(dir.path()).runtime_state().env_path().exists());
assert!(!settings_path.exists());
assert!(stop_called.load(Ordering::SeqCst));
}
@ -2861,7 +2861,7 @@ client_id = "client-id"
fn persist_github_install_changes_replaces_app_env_keys_with_token_secret() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path());
let server_env_path = storage.server_state().env_path();
let server_env_path = storage.runtime_state().env_path();
envfile::write_env_file(
&server_env_path,
&std::collections::HashMap::from([
@ -2923,7 +2923,7 @@ client_id = "client-id"
fn persist_github_install_changes_replaces_token_secret_with_app_env_keys() {
let dir = tempfile::tempdir().unwrap();
let storage = Storage::new(dir.path());
let server_env_path = storage.server_state().env_path();
let server_env_path = storage.runtime_state().env_path();
envfile::write_env_file(
&server_env_path,
&std::collections::HashMap::from([("KEEP_ME".to_string(), "1".to_string())]),

View file

@ -21,6 +21,10 @@ use crate::shared::print_json_pretty;
use crate::shared::repo::ensure_matching_repo_origin;
use crate::user_config;
#[allow(
deprecated,
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
)]
pub(super) async fn create_command(
args: PrCreateArgs,
cli: &CliSettings,

View file

@ -37,6 +37,10 @@ pub(crate) async fn dispatch(
}
}
#[allow(
deprecated,
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
)]
fn load_github_credentials_required(
cli: &CliSettings,
cli_layer: &CliLayer,

View file

@ -71,7 +71,6 @@ pub(crate) async fn execute(
super::output::print_run_summary_with_client(
&client,
&created_run.run_id,
created_run.local_run_dir.as_deref(),
styles,
printer,
)

View file

@ -1,6 +1,3 @@
use std::path::PathBuf;
use fabro_config::Storage;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::RunId;
@ -13,11 +10,9 @@ use super::overrides::run_args_layer;
use crate::args::RunArgs;
use crate::command_context::CommandContext;
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, run_manifest_args};
use crate::user_config;
pub(crate) struct CreatedRun {
pub(crate) run_id: RunId,
pub(crate) local_run_dir: Option<PathBuf>,
pub(crate) run_id: RunId,
}
/// Create a workflow run: allocate run directory, persist RunSpec, return
@ -54,7 +49,6 @@ pub(crate) async fn create_run(
user_layer: load_settings_user()?,
user_settings_path: Some(active_settings_path(None)),
})?;
let target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?;
let client = ctx.server().await?;
if !quiet {
let preflight = client.run_preflight(built.manifest.clone()).await?;
@ -73,19 +67,8 @@ pub(crate) async fn create_run(
}
let created_run_id = client.create_run_from_manifest(built.manifest).await?;
let local_run_dir = if target.is_unix_socket() {
Some(
Storage::new(user_config::storage_dir(ctx.machine_settings())?)
.run_scratch(&created_run_id)
.root()
.to_path_buf(),
)
} else {
None
};
Ok(CreatedRun {
run_id: created_run_id,
local_run_dir,
})
}

View file

@ -17,9 +17,7 @@ use fabro_workflow::records::Conclusion;
use indicatif::HumanDuration;
use crate::server_client;
use crate::shared::{
format_tokens_human, format_usd_micros, print_diagnostics, relative_path, tilde_path,
};
use crate::shared::{format_tokens_human, format_usd_micros, print_diagnostics, relative_path};
pub(crate) fn print_preflight_workflow_summary(
workflow: &types::PreflightWorkflowSummary,
@ -133,7 +131,6 @@ pub(crate) fn api_check_report_to_local(report: &types::PreflightCheckReport) ->
pub(crate) async fn print_run_summary_with_client(
client: &server_client::Client,
run_id: &fabro_types::RunId,
local_run_dir: Option<&Path>,
styles: &Styles,
printer: Printer,
) -> Result<()> {
@ -151,7 +148,6 @@ pub(crate) async fn print_run_summary_with_client(
print_run_conclusion(
&conclusion,
run_id,
local_run_dir,
None,
pr_url.as_deref(),
styles,
@ -160,16 +156,13 @@ pub(crate) async fn print_run_summary_with_client(
let final_output =
resolve_final_output_with_client(client, run_id, checkpoint.as_ref()).await?;
print_final_output(final_output.as_deref(), styles, printer);
if local_run_dir.is_some() {
print_assets_with_client(client, run_id, styles, printer).await?;
}
print_assets_with_client(client, run_id, styles, printer).await?;
Ok(())
}
pub(crate) fn print_run_conclusion(
conclusion: &Conclusion,
run_id: impl std::fmt::Display,
run_dir: Option<&Path>,
pushed_branch: Option<&str>,
pr_url: Option<&str>,
styles: &Styles,
@ -251,16 +244,6 @@ pub(crate) fn print_run_conclusion(
}
}
if let Some(run_dir) = run_dir {
fabro_util::printerr!(
printer,
"{}",
styles
.dim
.apply_to(format!("Run: {}", tilde_path(run_dir)))
);
}
if let Some(ref failure) = conclusion.failure_reason {
fabro_util::printerr!(printer, "Failure: {}", styles.red.apply_to(failure));
}

View file

@ -43,14 +43,8 @@ pub(crate) async fn resume_command(
))
.await?;
if !json {
super::output::print_run_summary_with_client(
client.as_ref(),
&run_id,
None,
styles,
printer,
)
.await?;
super::output::print_run_summary_with_client(client.as_ref(), &run_id, styles, printer)
.await?;
}
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);

View file

@ -2,7 +2,7 @@ use std::path::PathBuf;
use anyhow::Result;
use chrono::Utc;
use fabro_config::Storage;
use fabro_config::ServerRuntimeState;
use fabro_server::bind::BindRequest;
use fabro_server::serve;
use fabro_server::serve::ServeArgs;
@ -35,7 +35,7 @@ pub(crate) async fn execute(
None
};
let log_path = Storage::new(&storage_dir).server_state().log_path();
let log_path = ServerRuntimeState::new(&storage_dir).log_path();
let dev_token_path = std::env::var_os("FABRO_DEV_TOKEN_PATH").map(PathBuf::from);
let pid = std::process::id();

View file

@ -22,7 +22,7 @@ use crate::args::{
GlobalArgs, ServerCommand, ServerRestartArgs, ServerServeArgs, ServerStartArgs,
ServerStatusArgs, ServerStopArgs,
};
use crate::user_config;
use crate::{local_server, user_config};
pub(crate) async fn dispatch(
command: ServerCommand,
@ -54,9 +54,8 @@ pub(crate) async fn dispatch(
serve_args.config.as_deref(),
storage_dir.as_deref(),
)?;
let storage_dir = user_config::storage_dir(&settings)?;
let bind_addr =
serve::resolve_bind_request_from_settings(&settings, serve_args.bind.as_deref())?;
let storage_dir = local_server::storage_dir(&settings)?;
let bind_addr = local_server::bind_request(&settings, serve_args.bind.as_deref())?;
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
Box::pin(start::execute(
bind_addr,
@ -74,7 +73,7 @@ pub(crate) async fn dispatch(
timeout,
}) => {
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
let storage_dir = user_config::storage_dir(&settings)?;
let storage_dir = local_server::storage_dir(&settings)?;
stop::execute(&storage_dir, Duration::from_secs(timeout), printer).await
}
ServerCommand::Restart(ServerRestartArgs {
@ -102,10 +101,9 @@ pub(crate) async fn dispatch(
serve_args.config.as_deref(),
storage_dir.as_deref(),
)?;
let storage_dir = user_config::storage_dir(&settings)?;
let storage_dir = local_server::storage_dir(&settings)?;
stop::stop_server(&storage_dir, Duration::from_secs(timeout)).await?;
let bind_addr =
serve::resolve_bind_request_from_settings(&settings, serve_args.bind.as_deref())?;
let bind_addr = local_server::bind_request(&settings, serve_args.bind.as_deref())?;
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
Box::pin(start::execute(
bind_addr,
@ -120,7 +118,7 @@ pub(crate) async fn dispatch(
}
ServerCommand::Status(ServerStatusArgs { storage_dir, json }) => {
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
let storage_dir = user_config::storage_dir(&settings)?;
let storage_dir = local_server::storage_dir(&settings)?;
status::execute(&storage_dir, json, printer)
}
ServerCommand::Serve(ServerServeArgs {
@ -138,9 +136,8 @@ pub(crate) async fn dispatch(
.clone()
.unwrap_or_else(|| user_config::active_settings_path(None)),
);
let storage_dir = user_config::storage_dir(&settings)?;
let bind_addr =
serve::resolve_bind_request_from_settings(&settings, serve_args.bind.as_deref())?;
let storage_dir = local_server::storage_dir(&settings)?;
let bind_addr = local_server::bind_request(&settings, serve_args.bind.as_deref())?;
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
Box::pin(foreground::execute(
record_path,

View file

@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use chrono::{DateTime, Utc};
use fabro_config::Storage;
use fabro_config::ServerRuntimeState;
use fabro_config::user::default_storage_dir;
use fabro_server::bind::Bind;
use fabro_util::Home;
@ -52,7 +52,7 @@ pub(crate) fn server_record_is_running(record: &ServerRecord) -> bool {
}
fn server_record_path(storage_dir: &Path) -> PathBuf {
Storage::new(storage_dir).server_state().record_path()
ServerRuntimeState::new(storage_dir).record_path()
}
fn legacy_record_path(storage_dir: &Path) -> Option<PathBuf> {
@ -140,7 +140,7 @@ mod tests {
#[test]
fn write_and_read_round_trip() {
let dir = tempfile::tempdir().unwrap();
let path = Storage::new(dir.path()).server_state().record_path();
let path = ServerRuntimeState::new(dir.path()).record_path();
let record = test_record(Bind::Tcp("127.0.0.1:3000".parse().unwrap()));
write_server_record(&path, &record).unwrap();
@ -158,7 +158,7 @@ mod tests {
#[test]
fn active_server_record_cleans_stale_dead_pid() {
let dir = tempfile::tempdir().unwrap();
let path = Storage::new(dir.path()).server_state().record_path();
let path = ServerRuntimeState::new(dir.path()).record_path();
let mut record = test_record(Bind::Tcp("127.0.0.1:3000".parse().unwrap()));
record.pid = u32::MAX; // definitely not alive
write_server_record(&path, &record).unwrap();

View file

@ -9,7 +9,7 @@ use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail};
use chrono::Utc;
use fabro_config::user::{FABRO_CONFIG_ENV, default_settings_path, load_settings_config};
use fabro_config::{Storage, envfile, resolve_server_from_file};
use fabro_config::{ServerRuntimeState, envfile};
use fabro_server::bind::{Bind, BindRequest};
use fabro_server::jwt_auth::auth_method_name;
use fabro_server::serve;
@ -23,6 +23,7 @@ use tokio::task::spawn_blocking;
use tokio::time;
use super::record;
use crate::local_server;
pub(crate) struct ForegroundServerLogBootstrap {
#[expect(dead_code, reason = "held for its Drop to release the server lock")]
@ -76,7 +77,7 @@ pub(crate) async fn prepare_foreground_server_log(
);
}
let log_path = Storage::new(storage_dir).server_state().log_path();
let log_path = ServerRuntimeState::new(storage_dir).log_path();
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating log directory {}", parent.display()))?;
@ -148,7 +149,7 @@ async fn ensure_server_running_with_bind(
bind_request
} else {
let settings = load_settings_config(Some(config_path))?;
serve::resolve_bind_request_from_settings(&settings, None)?
local_server::bind_request(&settings, None)?
};
match execute_daemon(
@ -222,8 +223,8 @@ fn load_or_create_local_dev_token(storage_dir: &Path, home: &Home) -> Result<Str
return Ok(token);
}
let storage = Storage::new(storage_dir);
let server_env_path = storage.server_state().env_path();
let runtime_state = ServerRuntimeState::new(storage_dir);
let server_env_path = runtime_state.env_path();
if let Some(token) = envfile::read_env_file(&server_env_path)
.ok()
.and_then(|entries| entries.get("FABRO_DEV_TOKEN").cloned())
@ -231,14 +232,9 @@ fn load_or_create_local_dev_token(storage_dir: &Path, home: &Home) -> Result<Str
{
dev_token::write_dev_token(&home.dev_token_path(), &token)
.with_context(|| format!("writing dev token to {}", home.dev_token_path().display()))?;
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token).with_context(
|| {
format!(
"writing dev token to {}",
storage.server_state().dev_token_path().display()
)
},
)?;
let storage_token_path = runtime_state.dev_token_path();
dev_token::write_dev_token(&storage_token_path, &token)
.with_context(|| format!("writing dev token to {}", storage_token_path.display()))?;
return Ok(token);
}
@ -248,14 +244,9 @@ fn load_or_create_local_dev_token(storage_dir: &Path, home: &Home) -> Result<Str
home.dev_token_path().display()
)
})?;
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token).with_context(
|| {
format!(
"writing dev token to {}",
storage.server_state().dev_token_path().display()
)
},
)?;
let storage_token_path = runtime_state.dev_token_path();
dev_token::write_dev_token(&storage_token_path, &token)
.with_context(|| format!("writing dev token to {}", storage_token_path.display()))?;
Ok(token)
}
@ -271,8 +262,7 @@ fn load_or_create_local_session_secret(storage_dir: &Path) -> Result<String> {
return Ok(secret);
}
let storage = Storage::new(storage_dir);
let server_env_path = storage.server_state().env_path();
let server_env_path = ServerRuntimeState::new(storage_dir).env_path();
if let Some(secret) = envfile::read_env_file(&server_env_path)
.ok()
.and_then(|entries| entries.get("SESSION_SECRET").cloned())
@ -320,9 +310,9 @@ async fn execute_foreground(
},
);
let server_state = Storage::new(&storage_dir).server_state();
let record_path = server_state.record_path();
let log_path = server_state.log_path();
let runtime_state = ServerRuntimeState::new(&storage_dir);
let record_path = runtime_state.record_path();
let log_path = runtime_state.log_path();
let pid = std::process::id();
let _record_guard = scopeguard::guard(record_path.clone(), |path| {
@ -382,14 +372,14 @@ async fn execute_daemon(
return Ok(());
}
let server_state = Storage::new(storage_dir).server_state();
let log_path = server_state.log_path();
let runtime_state = ServerRuntimeState::new(storage_dir);
let log_path = runtime_state.log_path();
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating log directory {}", parent.display()))?;
}
let record_path = server_state.record_path();
let record_path = runtime_state.record_path();
let log_file = std::fs::File::create(&log_path)
.with_context(|| format!("creating server log file {}", log_path.display()))?;
let stdout_log = log_file
@ -517,8 +507,7 @@ fn print_auth_methods(printer: Printer, serve_args: &ServeArgs) {
let settings = load_settings_config(serve_args.config.as_deref()).ok();
let auth_methods = settings
.as_ref()
.and_then(|s| resolve_server_from_file(s).ok())
.map(|s| s.auth.methods)
.map(local_server::auth_methods)
.unwrap_or_default();
let names: Vec<&str> = auth_methods.iter().map(|m| auth_method_name(*m)).collect();
fabro_util::printerr!(printer, "Auth: {}", names.join(", "));
@ -534,7 +523,7 @@ fn print_dev_token(printer: Printer, home: &Home, token: &str) {
// ---------------------------------------------------------------------------
async fn acquire_lock(storage_dir: &Path) -> Result<std::fs::File> {
let lock_path = Storage::new(storage_dir).server_state().lock_path();
let lock_path = ServerRuntimeState::new(storage_dir).lock_path();
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating server lock directory {}", parent.display()))?;

View file

@ -23,7 +23,7 @@ use tracing::warn;
use crate::args::UninstallArgs;
use crate::commands::server;
use crate::shared::{format_size, print_json_pretty, tilde_path};
use crate::user_config;
use crate::{local_server, user_config};
#[derive(Debug, Serialize)]
struct Inventory {
@ -59,13 +59,10 @@ pub(crate) async fn run_uninstall(
return Ok(());
}
let storage_dir = user_config::load_settings().map_or_else(
|_| user_config::default_storage_dir(),
|settings| {
user_config::storage_dir(&settings)
.unwrap_or_else(|_| user_config::default_storage_dir())
},
);
let storage_dir = user_config::load_settings()
.ok()
.and_then(|settings| local_server::storage_dir(&settings).ok())
.unwrap_or_else(user_config::default_storage_dir);
let inventory = build_inventory(&home_root, &storage_dir)?;

View file

@ -0,0 +1,60 @@
//! Helpers for CLI code that manages the local Fabro server on this host.
//!
//! This module is the only generic CLI lifecycle surface allowed to read
//! `[server.*]` settings. User-facing CLI commands outside same-host server
//! lifecycle should not call into it.
use std::path::PathBuf;
use anyhow::Result;
use fabro_server::bind::BindRequest;
use fabro_server::serve::resolve_bind_request_from_settings;
use fabro_types::settings::{ServerAuthMethod, SettingsLayer};
fn render_server_resolve_errors(errors: Vec<fabro_config::ResolveError>) -> anyhow::Error {
anyhow::anyhow!(
"failed to resolve server settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
}
pub(crate) fn storage_dir(settings: &SettingsLayer) -> Result<PathBuf> {
let resolved =
fabro_config::resolve_server_from_file(settings).map_err(render_server_resolve_errors)?;
let resolved_root = resolved
.storage
.root
.resolve(|name| std::env::var(name).ok())
.map_err(|err| {
anyhow::anyhow!(
"failed to resolve {}: {err}",
resolved.storage.root.as_source()
)
})?;
Ok(PathBuf::from(resolved_root.value))
}
pub(crate) fn bind_request(
settings: &SettingsLayer,
cli_override: Option<&str>,
) -> Result<BindRequest> {
resolve_bind_request_from_settings(settings, cli_override)
}
pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec<ServerAuthMethod> {
fabro_config::resolve_server_from_file(settings)
.map(|resolved| resolved.auth.methods)
.unwrap_or_default()
}
pub(crate) fn config_log_level(settings: &SettingsLayer) -> Option<String> {
settings
.server
.as_ref()
.and_then(|server| server.logging.as_ref())
.and_then(|logging| logging.level.clone())
}

View file

@ -8,6 +8,7 @@ mod command_context;
mod commands;
mod gh;
mod landing;
mod local_server;
mod logging;
mod manifest_builder;
mod server_client;
@ -481,8 +482,8 @@ async fn prepare_server_bootstrap(
) -> Result<PreTracingBootstrap> {
let settings =
user_config::load_settings_with_config_and_storage_dir(config_path, storage_dir)?;
let storage_dir = user_config::storage_dir(&settings)?;
let storage = fabro_config::Storage::new(&storage_dir);
let storage_dir = local_server::storage_dir(&settings)?;
let runtime_state = fabro_config::ServerRuntimeState::new(storage_dir.clone());
let foreground_server_log_bootstrap = if foreground {
Some(commands::server::start::prepare_foreground_server_log(&storage_dir).await?)
} else {
@ -491,21 +492,13 @@ async fn prepare_server_bootstrap(
Ok(PreTracingBootstrap {
sink: logging::InternalLogSink::Server {
path: storage.server_state().log_path(),
path: runtime_state.log_path(),
},
config_log_level: server_config_log_level(&settings),
config_log_level: local_server::config_log_level(&settings),
foreground_server_log_bootstrap,
})
}
fn server_config_log_level(settings: &SettingsLayer) -> Option<String> {
settings
.server
.as_ref()
.and_then(|server| server.logging.as_ref())
.and_then(|logging| logging.level.clone())
}
#[cfg(test)]
#[expect(
clippy::disallowed_methods,

View file

@ -8,7 +8,7 @@ use fabro_client::{
apply_bearer_token_auth,
};
pub(crate) use fabro_client::{Client, RunEventStream};
use fabro_config::Storage;
use fabro_config::ServerRuntimeState;
use fabro_server::bind::Bind;
pub(crate) use fabro_types::RunProjection;
use fabro_types::settings::SettingsLayer;
@ -18,6 +18,7 @@ use tokio::time::sleep;
use crate::args::ServerTargetArgs;
use crate::commands::server::{record, start};
use crate::local_server;
use crate::user_config::{self, cli_http_client_builder};
#[derive(Debug)]
@ -69,7 +70,7 @@ pub(crate) async fn connect_server_with_settings(
if let Some(path) = target.as_unix_socket_path() {
return connect_managed_unix_socket_api_client_bundle(
path,
&user_config::storage_dir(settings)?,
&local_server::storage_dir(settings)?,
base_config_path,
)
.await;
@ -77,7 +78,7 @@ pub(crate) async fn connect_server_with_settings(
return connect_target_api_client_bundle(&target).await;
}
connect_local_api_client_bundle(&user_config::storage_dir(settings)?, base_config_path).await
connect_local_api_client_bundle(&local_server::storage_dir(settings)?, base_config_path).await
}
async fn connect_managed_unix_socket_api_client_bundle(
@ -139,16 +140,6 @@ async fn connect_local_api_client_bundle(
}
}
#[allow(
dead_code,
reason = "Retained for pending storage-backed internal callers and referenced in existing design docs."
)]
pub(crate) async fn connect_api_client(storage_dir: &Path) -> Result<fabro_api::ApiClient> {
connect_local_api_client_bundle(storage_dir, &user_config::active_settings_path(None))
.await
.map(|client| client.api_client())
}
async fn connect_target_api_client_bundle(target: &ServerTarget) -> Result<Client> {
let credential = resolve_target_credential(target, None, local_dev_token_fallback(target))?;
let oauth_session = refreshable_oauth(target, credential.as_ref());
@ -230,12 +221,13 @@ fn load_dev_token_if_available_from_sources(
}
if let Some(storage_dir) = storage_dir {
let storage_token_path = Storage::new(storage_dir).server_state().dev_token_path();
let runtime_state = ServerRuntimeState::new(storage_dir);
let storage_token_path = runtime_state.dev_token_path();
if let Some(token) = dev_token::read_dev_token_file(&storage_token_path) {
return Some(token);
}
let record_path = Storage::new(storage_dir).server_state().record_path();
let record_path = runtime_state.record_path();
if let Some(token) = record::read_server_record(&record_path)
.and_then(|server| server.dev_token_path)
.as_deref()
@ -413,15 +405,12 @@ mod tests {
let token_path = token_dir.path().join("dev-token");
std::fs::write(&token_path, token).unwrap();
let record_path = fabro_config::Storage::new(storage.path())
.server_state()
.record_path();
let runtime_state = fabro_config::ServerRuntimeState::new(storage.path());
let record_path = runtime_state.record_path();
record::write_server_record(&record_path, &record::ServerRecord {
pid: std::process::id(),
bind: Bind::Unix(temp_home.path().join("fabro.sock")),
log_path: fabro_config::Storage::new(storage.path())
.server_state()
.log_path(),
log_path: runtime_state.log_path(),
dev_token_path: Some(token_path),
started_at: Utc::now(),
})

View file

@ -10,6 +10,7 @@ use fabro_util::version::FABRO_VERSION;
use tracing::debug;
use crate::args::ServerTargetArgs;
use crate::local_server;
pub(crate) fn load_settings() -> anyhow::Result<SettingsLayer> {
load_settings_with_config_and_storage_dir(None, None)
@ -83,28 +84,11 @@ pub(crate) fn default_server_target() -> ServerTarget {
ServerTarget::unix_socket_path(default_socket_path()).expect("default socket path is absolute")
}
#[deprecated(
note = "use local_server::storage_dir for lifecycle; PR commands must move to server-side API"
)]
pub(crate) fn storage_dir(settings: &SettingsLayer) -> anyhow::Result<PathBuf> {
let resolved = fabro_config::resolve_server_from_file(settings).map_err(|errors| {
anyhow::anyhow!(
"failed to resolve server settings:\n{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
)
})?;
let resolved_root = resolved
.storage
.root
.resolve(|name| std::env::var(name).ok())
.map_err(|err| {
anyhow::anyhow!(
"failed to resolve {}: {err}",
resolved.storage.root.as_source()
)
})?;
Ok(PathBuf::from(resolved_root.value))
local_server::storage_dir(settings)
}
fn parse_server_target(value: &str) -> Result<ServerTarget> {

View file

@ -237,7 +237,7 @@ mode = "keep-me"
),
);
let server_env_path = Storage::new(&storage_dir).server_state().env_path();
let server_env_path = Storage::new(&storage_dir).runtime_state().env_path();
envfile::write_env_file(
&server_env_path,
&std::collections::HashMap::from([

View file

@ -530,6 +530,13 @@ fn remote_foreground_run_consumes_paginated_events_and_prints_server_backed_summ
.header("Content-Type", "application/json")
.body(remote_run_state_response().to_string());
});
server.mock(|when, then| {
when.method("GET")
.path(format!("/api/v1/runs/{run_id}/artifacts"));
then.status(200)
.header("Content-Type", "application/json")
.body(serde_json::json!({ "data": [] }).to_string());
});
let workflow = context.install_fixture("simple.fabro");
let output = context
@ -665,7 +672,6 @@ fn dry_run_simple() {
Run: [ULID]
Status: SUCCESS
Duration: [DURATION]
Run: [RUN_DIR]
=== Output ===
[Simulated] Response for stage: report

View file

@ -666,7 +666,7 @@ struct TestServerRecord {
}
pub(crate) fn local_dev_token(storage_dir: &Path) -> Option<String> {
let server_state = Storage::new(storage_dir).server_state();
let server_state = Storage::new(storage_dir).runtime_state();
fabro_util::dev_token::read_dev_token_file(&server_state.dev_token_path()).or_else(|| {
std::fs::read_to_string(server_state.record_path())
@ -679,7 +679,7 @@ pub(crate) fn local_dev_token(storage_dir: &Path) -> Option<String> {
}
pub(crate) fn server_endpoint(storage_dir: &Path) -> Option<(fabro_http::HttpClient, String)> {
let record_path = Storage::new(storage_dir).server_state().record_path();
let record_path = Storage::new(storage_dir).runtime_state().record_path();
let record = std::fs::read_to_string(record_path)
.ok()
.and_then(|content| serde_json::from_str::<TestServerRecord>(&content).ok())?;
@ -714,7 +714,7 @@ pub(crate) fn server_endpoint(storage_dir: &Path) -> Option<(fabro_http::HttpCli
}
pub(crate) fn server_target(storage_dir: &Path) -> String {
let record_path = Storage::new(storage_dir).server_state().record_path();
let record_path = Storage::new(storage_dir).runtime_state().record_path();
let record = std::fs::read_to_string(record_path)
.ok()
.and_then(|content| serde_json::from_str::<TestServerRecord>(&content).ok())

View file

@ -32,7 +32,6 @@ fn dry_run_branching() {
Run: [ULID]
Status: SUCCESS
Duration: [DURATION]
Run: [RUN_DIR]
=== Output ===
[Simulated] Response for stage: validate
@ -66,7 +65,6 @@ fn dry_run_conditions() {
Run: [ULID]
Status: SUCCESS
Duration: [DURATION]
Run: [RUN_DIR]
=== Output ===
[Simulated] Response for stage: path_b
@ -101,7 +99,6 @@ fn dry_run_parallel() {
Run: [ULID]
Status: SUCCESS
Duration: [DURATION]
Run: [RUN_DIR]
=== Output ===
[Simulated] Response for stage: review
@ -136,7 +133,6 @@ fn dry_run_styled() {
Run: [ULID]
Status: SUCCESS
Duration: [DURATION]
Run: [RUN_DIR]
=== Output ===
[Simulated] Response for stage: critical_review
@ -169,6 +165,5 @@ fn dry_run_legacy_tool() {
Run: [ULID]
Status: SUCCESS
Duration: [DURATION]
Run: [RUN_DIR]
");
}

View file

@ -38,7 +38,7 @@ pub use resolve::{
resolve_workflow_from_file,
};
use serde::de::DeserializeOwned;
pub use storage::{RunScratch, ServerState, Storage};
pub use storage::{RunScratch, ServerRuntimeState, Storage};
pub fn load_and_resolve(
layers: effective_settings::EffectiveSettingsLayers,

View file

@ -9,7 +9,7 @@ pub struct Storage {
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerState {
pub struct ServerRuntimeState {
root: PathBuf,
}
@ -29,11 +29,6 @@ impl Storage {
&self.root
}
#[must_use]
pub fn logs_dir(&self) -> PathBuf {
self.root.join("logs")
}
#[must_use]
pub fn cache_dir(&self) -> PathBuf {
self.root.join("cache")
@ -53,8 +48,8 @@ impl Storage {
}
#[must_use]
pub fn server_state(&self) -> ServerState {
ServerState::new(self.root.clone())
pub fn runtime_state(&self) -> ServerRuntimeState {
ServerRuntimeState::new(self.root.clone())
}
#[must_use]
@ -83,12 +78,17 @@ impl Storage {
}
}
impl ServerState {
impl ServerRuntimeState {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
#[must_use]
pub fn logs_dir(&self) -> PathBuf {
self.root.join("logs")
}
#[must_use]
pub fn record_path(&self) -> PathBuf {
self.root.join("server.json")
@ -101,7 +101,7 @@ impl ServerState {
#[must_use]
pub fn log_path(&self) -> PathBuf {
self.root.join("logs").join("server.log")
self.logs_dir().join("server.log")
}
#[must_use]
@ -163,17 +163,14 @@ mod tests {
use chrono::Local;
use fabro_types::RunId;
use super::{RunScratch, Storage};
use super::{RunScratch, ServerRuntimeState, Storage};
#[test]
fn storage_accessors_are_relative_to_root() {
let storage = Storage::new("/tmp/fabro-data");
let runtime = ServerRuntimeState::new("/tmp/fabro-data");
assert_eq!(storage.root(), std::path::Path::new("/tmp/fabro-data"));
assert_eq!(
storage.logs_dir(),
std::path::Path::new("/tmp/fabro-data/logs")
);
assert_eq!(
storage.cache_dir(),
std::path::Path::new("/tmp/fabro-data/cache")
@ -199,19 +196,23 @@ mod tests {
std::path::Path::new("/tmp/fabro-data/objects/artifacts")
);
assert_eq!(
storage.server_state().record_path(),
runtime.logs_dir(),
std::path::Path::new("/tmp/fabro-data/logs")
);
assert_eq!(
runtime.record_path(),
std::path::Path::new("/tmp/fabro-data/server.json")
);
assert_eq!(
storage.server_state().lock_path(),
runtime.lock_path(),
std::path::Path::new("/tmp/fabro-data/server.lock")
);
assert_eq!(
storage.server_state().log_path(),
runtime.log_path(),
std::path::Path::new("/tmp/fabro-data/logs/server.log")
);
assert_eq!(
storage.server_state().env_path(),
runtime.env_path(),
std::path::Path::new("/tmp/fabro-data/server.env")
);
}

View file

@ -266,7 +266,7 @@ fn persist_server_env_secrets(storage_dir: &Path, secrets: &[(String, String)])
return Ok(());
}
let env_path = Storage::new(storage_dir).server_state().env_path();
let env_path = Storage::new(storage_dir).runtime_state().env_path();
envfile::merge_env_file(&env_path, secrets.iter().cloned())
.with_context(|| format!("merging server env secrets into {}", env_path.display()))?;
Ok(())
@ -500,7 +500,7 @@ name = "custom"
assert_eq!(restored.get("EXISTING_SECRET"), Some("keep"));
assert_eq!(restored.get("bad-secret-name"), None);
let server_env = envfile::read_env_file(&storage.server_state().env_path()).unwrap();
let server_env = envfile::read_env_file(&storage.runtime_state().env_path()).unwrap();
assert_eq!(
server_env.get("SESSION_SECRET").map(String::as_str),
Some("session")

View file

@ -68,7 +68,7 @@ pub async fn run_all(state: &AppState) -> DiagnosticsReport {
},
CheckSection {
title: "Configuration".to_string(),
checks: vec![crypto],
checks: vec![crypto, check_storage_dir(state)],
},
],
}
@ -398,6 +398,45 @@ fn check_sandbox(state: &AppState) -> CheckResult {
}
}
fn check_storage_dir(state: &AppState) -> CheckResult {
check_storage_dir_path(&state.server_storage_dir())
}
#[expect(
clippy::disallowed_methods,
reason = "Server diagnostics deliberately performs a synchronous local filesystem probe."
)]
fn check_storage_dir_path(path: &std::path::Path) -> CheckResult {
let exists = path.is_dir();
let readable = exists && std::fs::read_dir(path).is_ok();
let writable = exists && tempfile::tempfile_in(path).is_ok();
let details = vec![
CheckDetail::new(format!("Exists: {}", if exists { "yes" } else { "no" })),
CheckDetail::new(format!("Readable: {}", if readable { "yes" } else { "no" })),
CheckDetail::new(format!("Writable: {}", if writable { "yes" } else { "no" })),
];
let is_healthy = exists && readable && writable;
let display = path.display();
CheckResult {
name: "Storage directory".to_string(),
status: if is_healthy {
CheckStatus::Pass
} else {
CheckStatus::Error
},
summary: display.to_string(),
details,
remediation: if is_healthy {
None
} else if !exists {
Some(format!("Create the directory: mkdir -p {display}"))
} else {
Some(format!("Fix permissions on {display}"))
},
}
}
async fn check_brave_search(state: &AppState) -> CheckResult {
let Some(api_key) = state.vault_or_env("BRAVE_SEARCH_API_KEY") else {
return CheckResult {
@ -543,3 +582,39 @@ fn check_crypto(state: &AppState) -> CheckResult {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_storage_dir_path_passes_for_readable_writable_directory() {
let dir = tempfile::tempdir().unwrap();
let result = check_storage_dir_path(dir.path());
assert_eq!(result.name, "Storage directory");
assert_eq!(result.status, CheckStatus::Pass);
assert_eq!(result.summary, dir.path().display().to_string());
assert!(result.remediation.is_none());
}
#[test]
fn check_storage_dir_path_errors_for_missing_directory() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("missing");
let result = check_storage_dir_path(&missing);
assert_eq!(result.name, "Storage directory");
assert_eq!(result.status, CheckStatus::Error);
assert_eq!(result.summary, missing.display().to_string());
assert_eq!(
result.remediation,
Some(format!(
"Create the directory: mkdir -p {}",
missing.display()
))
);
}
}

View file

@ -826,7 +826,7 @@ async fn post_install_finish(
};
if let Err(err) = dev_token::write_dev_token(
&Storage::new(state.storage_dir.as_ref())
.server_state()
.runtime_state()
.dev_token_path(),
&token,
) {

View file

@ -470,7 +470,7 @@ where
};
let storage = Storage::new(&data_dir);
let vault_path = storage.secrets_path();
let server_env_path = storage.server_state().env_path();
let server_env_path = storage.runtime_state().env_path();
let server_secrets = ServerSecrets::load(server_env_path.clone())?;
let webhook_secret_present = server_secrets.get(WEBHOOK_SECRET_ENV).is_some();

View file

@ -1566,7 +1566,7 @@ fn build_disk_usage_response(
verbose: bool,
) -> anyhow::Result<DiskUsageResponse> {
let scratch_base_dir = scratch_base(storage_dir);
let logs_base_dir = Storage::new(storage_dir).logs_dir();
let logs_base_dir = Storage::new(storage_dir).runtime_state().logs_dir();
let runs = scan_runs_with_summaries(summaries, &scratch_base_dir)?;
let mut active_count = 0u64;
@ -3744,7 +3744,7 @@ struct WorkerServerRecord {
}
fn current_server_target(storage_dir: &std::path::Path) -> anyhow::Result<String> {
let record_path = Storage::new(storage_dir).server_state().record_path();
let record_path = Storage::new(storage_dir).runtime_state().record_path();
#[expect(
clippy::disallowed_methods,
reason = "sync helper invoked from worker_command (sync) via spawn_blocking at the async \

View file

@ -395,7 +395,7 @@ async fn token_install_finish_persists_settings_env_and_vault() {
let server_env = std::fs::read_to_string(
fabro_config::Storage::new(temp_dir.path())
.server_state()
.runtime_state()
.env_path(),
)
.unwrap();
@ -556,7 +556,7 @@ async fn app_install_finish_omits_dev_token_and_does_not_write_it() {
);
let server_env =
std::fs::read_to_string(Storage::new(temp_dir.path()).server_state().env_path()).unwrap();
std::fs::read_to_string(Storage::new(temp_dir.path()).runtime_state().env_path()).unwrap();
assert!(!server_env.contains("FABRO_DEV_TOKEN="));
assert!(
@ -565,7 +565,7 @@ async fn app_install_finish_omits_dev_token_and_does_not_write_it() {
);
assert!(
!Storage::new(temp_dir.path())
.server_state()
.runtime_state()
.dev_token_path()
.exists(),
"storage dev token file should not be created for App installs"
@ -1372,7 +1372,7 @@ async fn install_finish_failure_restores_settings_and_vault_but_leaves_env_keys(
"{ not valid json"
);
let server_env = std::fs::read_to_string(storage.server_state().env_path()).unwrap();
let server_env = std::fs::read_to_string(storage.runtime_state().env_path()).unwrap();
assert!(server_env.contains("SESSION_SECRET="));
assert!(server_env.contains("FABRO_DEV_TOKEN="));
assert!(!callback_invoked.load(Ordering::Acquire));
@ -1441,10 +1441,10 @@ async fn install_finish_failure_leaves_home_dev_token_mirror_written() {
let home_dev_token = dev_token::read_dev_token_file(&home.dev_token_path())
.expect("home dev token should exist");
let storage_dev_token =
dev_token::read_dev_token_file(&storage.server_state().dev_token_path())
dev_token::read_dev_token_file(&storage.runtime_state().dev_token_path())
.expect("storage dev token should exist");
assert_eq!(home_dev_token, storage_dev_token);
let server_env = std::fs::read_to_string(storage.server_state().env_path()).unwrap();
let server_env = std::fs::read_to_string(storage.runtime_state().env_path()).unwrap();
assert!(server_env.contains(&format!("FABRO_DEV_TOKEN={home_dev_token}")));
}

View file

@ -9,7 +9,7 @@ use std::sync::Arc;
use std::time::Duration;
use axum::http::StatusCode;
use fabro_config::{ServerState, parse_settings_layer, resolve_server_from_file};
use fabro_config::{ServerRuntimeState, parse_settings_layer, resolve_server_from_file};
use fabro_server::bind::Bind;
use fabro_server::ip_allowlist::{IpAllowlist, IpAllowlistConfig};
use fabro_server::jwt_auth::{AuthMode, resolve_auth_mode_with_lookup};
@ -64,7 +64,7 @@ fn write_test_config(tempdir: &TempDir, settings: &str) -> PathBuf {
let config_path = tempdir.path().join("settings.toml");
std::fs::write(&config_path, settings).expect("test settings should write");
std::fs::write(
ServerState::new(tempdir.path()).env_path(),
ServerRuntimeState::new(tempdir.path()).env_path(),
format!("FABRO_DEV_TOKEN={TEST_DEV_TOKEN}\nSESSION_SECRET={TEST_SESSION_SECRET}\n"),
)
.expect("test env file should write");