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
24 changes: 24 additions & 0 deletions .github/scripts/check-runtime-config-boundary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const forbidden = [
"activate_retrieval_profile_env",
"CODESTORY_EMBED_LLAMACPP_URL_MANAGED",
];
const lateStartupReads = [
'std::env::var(PROJECT_NETWORK_CONFIG_OPT_IN_ENV)',
'std::env::var_os("CODESTORY_STDIO_CACHE_ROOT")',
];

function rustFiles(root) {
return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
Expand All @@ -31,6 +35,26 @@ for (const file of roots.flatMap(rustFiles)) {
}
}

for (const file of [
"crates/codestory-cli/src/runtime.rs",
"crates/codestory-cli/src/stdio_transport.rs",
]) {
const source = fs.readFileSync(file, "utf8");
for (const token of lateStartupReads) {
if (source.includes(token)) violations.push(`${file}: late startup read ${token}`);
}
}

const runtimeSource = fs.readFileSync("crates/codestory-cli/src/runtime.rs", "utf8");
const runtimeSelection = runtimeSource
.split("fn new_with_startup", 2)[1]
?.split("/// Open project", 1)[0];
for (const token of ["user_cache_root()", "for_project_auto_with_defaults("]) {
if (runtimeSelection?.includes(token)) {
violations.push(`crates/codestory-cli/src/runtime.rs: project selection ${token}`);
}
}

if (violations.length) {
console.error("Production runtime configuration must remain immutable:\n" + violations.join("\n"));
process.exit(1);
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
and cooldown across MCP runtimes. Stale heartbeats from a still-live owner
remain fail-closed diagnostics surfaced by canonical MCP status; age alone
never terminates the process.
- Captured CLI user-home, project-network trust, stdio cache, and sidecar
defaults once at process startup so multi-project project selection cannot
drift through later process-environment reads. Repair status, terminal
results, lock arbitration, and broker overlays now consume that same retained
sidecar root instead of polling a second environment-derived cache tree.
- Renewed live local-refresh ownership during long incremental indexing, with
token, PID, and process-start checks that prevent stale workers from
overwriting changed ownership or terminal status.
Expand Down
74 changes: 55 additions & 19 deletions crates/codestory-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ use std::sync::OnceLock;

const PROJECT_NETWORK_CONFIG_OPT_IN_ENV: &str = "CODESTORY_ALLOW_PROJECT_NETWORK_CONFIG";

#[derive(Debug, Clone)]
pub(crate) struct CliStartupConfig {
pub(crate) user_home: Option<PathBuf>,
pub(crate) project_network_config_allowed: bool,
pub(crate) user_cache_root: PathBuf,
pub(crate) stdio_cache_root: Option<PathBuf>,
pub(crate) runtime_defaults: codestory_retrieval::SidecarRuntimeDefaults,
}

impl CliStartupConfig {
pub(crate) fn from_process_env() -> Self {
Self {
user_home: std::env::var_os("USERPROFILE")
.or_else(|| std::env::var_os("HOME"))
.map(PathBuf::from),
project_network_config_allowed: std::env::var(PROJECT_NETWORK_CONFIG_OPT_IN_ENV)
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false),
stdio_cache_root: std::env::var_os("CODESTORY_STDIO_CACHE_ROOT").map(PathBuf::from),
user_cache_root: codestory_retrieval::user_cache_root(),
runtime_defaults: codestory_retrieval::SidecarRuntimeDefaults::from_process_env(),
}
}
}

#[derive(Debug, Clone, Default, Deserialize)]
pub(crate) struct CliConfig {
pub(crate) cache_dir: Option<PathBuf>,
Expand All @@ -31,47 +56,59 @@ enum ConfigSource {
Project,
}

#[cfg(test)]
pub(crate) fn load_config(project_root: &Path) -> Result<CliConfig> {
load_config_with_startup(project_root, &process_startup_config())
}

pub(crate) fn load_config_with_startup(
project_root: &Path,
startup: &CliStartupConfig,
) -> Result<CliConfig> {
let mut config = CliConfig::default();
if let Some(home) = std::env::var_os("USERPROFILE")
.or_else(|| std::env::var_os("HOME"))
.map(PathBuf::from)
{
if let Some(home) = startup.user_home.as_ref() {
merge_config_file(
&mut config,
&home.join(".codestory.toml"),
ConfigSource::TrustedUser,
startup.project_network_config_allowed,
)?;
}
merge_config_file(
&mut config,
&project_root.join(".codestory.toml"),
ConfigSource::Project,
startup.project_network_config_allowed,
)?;
Ok(config)
}

pub(crate) fn process_runtime_defaults() -> codestory_retrieval::SidecarRuntimeDefaults {
pub(crate) fn process_startup_config() -> CliStartupConfig {
#[cfg(test)]
{
codestory_retrieval::SidecarRuntimeDefaults::from_process_env()
CliStartupConfig::from_process_env()
}
#[cfg(not(test))]
{
static DEFAULTS: OnceLock<codestory_retrieval::SidecarRuntimeDefaults> = OnceLock::new();
DEFAULTS
.get_or_init(codestory_retrieval::SidecarRuntimeDefaults::from_process_env)
static STARTUP: OnceLock<CliStartupConfig> = OnceLock::new();
STARTUP
.get_or_init(CliStartupConfig::from_process_env)
.clone()
}
}

fn merge_config_file(config: &mut CliConfig, path: &Path, source: ConfigSource) -> Result<()> {
fn merge_config_file(
config: &mut CliConfig,
path: &Path,
source: ConfigSource,
project_network_config_allowed: bool,
) -> Result<()> {
if !path.exists() {
return Ok(());
}
let raw = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config {}", path.display()))?;
validate_config_trust_boundary(&raw, source, path)?;
validate_config_trust_boundary(&raw, source, path, project_network_config_allowed)?;
let file_config: CliConfig = toml::from_str(&raw)
.with_context(|| format!("Failed to parse config {}", path.display()))?;
if file_config.cache_dir.is_some() {
Expand Down Expand Up @@ -113,7 +150,12 @@ fn merge_config_file(config: &mut CliConfig, path: &Path, source: ConfigSource)
Ok(())
}

fn validate_config_trust_boundary(raw: &str, source: ConfigSource, path: &Path) -> Result<()> {
fn validate_config_trust_boundary(
raw: &str,
source: ConfigSource,
path: &Path,
project_network_config_allowed: bool,
) -> Result<()> {
if source != ConfigSource::Project {
return Ok(());
}
Expand All @@ -128,7 +170,7 @@ fn validate_config_trust_boundary(raw: &str, source: ConfigSource, path: &Path)
);
}
for field in ["summary_endpoint", "summary_model", "embedding_endpoint"] {
if table.contains_key(field) && !project_network_config_allowed() {
if table.contains_key(field) && !project_network_config_allowed {
anyhow::bail!(
"project config field `{field}` is not trusted; set CODESTORY_SUMMARY_ENDPOINT, CODESTORY_SUMMARY_MODEL, CODESTORY_EMBED_LLAMACPP_URL, or pass a trusted CLI option instead"
);
Expand All @@ -137,12 +179,6 @@ fn validate_config_trust_boundary(raw: &str, source: ConfigSource, path: &Path)
Ok(())
}

fn project_network_config_allowed() -> bool {
std::env::var(PROJECT_NETWORK_CONFIG_OPT_IN_ENV)
.map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
}

impl CliConfig {
pub(crate) fn runtime_overrides(&self) -> codestory_retrieval::SidecarRuntimeOverrides {
codestory_retrieval::SidecarRuntimeOverrides {
Expand Down
20 changes: 10 additions & 10 deletions crates/codestory-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7383,7 +7383,7 @@ pub(crate) fn build_readiness_lanes_for_runtime(
let local_status = doctor_sidecar_status_for_runtime(runtime, local_runtime);
let agent_status = selected_agent_status
.cloned()
.unwrap_or_else(|| doctor_sidecar_status_for_runtime(runtime, agent_runtime));
.unwrap_or_else(|| doctor_sidecar_status_for_runtime(runtime, agent_runtime.clone()));
let agent_verdict = readiness
.iter()
.find(|verdict| verdict.goal == ReadinessGoalDto::AgentPacketSearch);
Expand All @@ -7401,15 +7401,14 @@ pub(crate) fn build_readiness_lanes_for_runtime(
&project_arg,
),
);
apply_ready_repair_status_overlay(
&mut lanes,
&runtime.project_root,
&agent_runtime,
&project_arg,
);
if let Some(broker) = broker {
apply_broker_ready_repair_overlay(&mut lanes, broker, &project_arg);
} else {
apply_ready_repair_status_overlay(
&mut lanes,
&runtime.project_root,
agent_run_id,
&project_arg,
);
}
lanes
}
Expand Down Expand Up @@ -7479,10 +7478,11 @@ fn readiness_lane_output(
fn apply_ready_repair_status_overlay(
lanes: &mut BTreeMap<String, ReadinessLaneOutput>,
project_root: &Path,
agent_run_id: Option<&str>,
sidecar: &codestory_retrieval::SidecarRuntimeConfig,
project_arg: &str,
) {
let Some(status) = ready_repair_status::active_ready_repair_status(project_root, agent_run_id)
let Some(status) =
ready_repair_status::active_ready_repair_status_for_sidecar(project_root, sidecar)
else {
return;
};
Expand Down
8 changes: 5 additions & 3 deletions crates/codestory-cli/src/readiness_broker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ pub(crate) use native_lease::{
reusable_native_embedding_resource_pid_for_snapshot, run_with_native_embedding_lease_lifecycle,
transfer_native_embedding_resource_lease,
};
#[allow(unused_imports)]
pub(crate) use paths::machine_resource_cache_fingerprint;
#[allow(unused_imports)]
pub(crate) use reconcile::reconcile_before_enqueue;
pub(crate) use reconcile::{reconcile_before_enqueue, reconcile_before_enqueue_for_sidecar};
#[allow(unused_imports)]
pub(crate) use scope::{
BROKER_SCHEMA_VERSION, agent_repair_scope, broker_operation_id, operation_scope,
};
#[allow(unused_imports)]
pub(crate) use snapshot::{observe_broker_snapshot, refresh_broker_snapshot};
pub(crate) use snapshot::{
observe_broker_snapshot, observe_broker_snapshot_for_sidecar, refresh_broker_snapshot,
refresh_broker_snapshot_for_sidecar,
};
#[allow(unused_imports)]
pub(crate) use types::{
BrokerGpuProofInput, BrokerGpuProofSnapshot, BrokerOperationSnapshot,
Expand Down
68 changes: 57 additions & 11 deletions crates/codestory-cli/src/readiness_broker/reconcile.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::path::Path;

use codestory_retrieval::SidecarRuntimeConfig;

use crate::{local_refresh_status, ready_repair_status};

use super::operations::{
Expand All @@ -17,19 +19,52 @@ pub(crate) fn reconcile_before_enqueue(
) -> BrokerReconciliationSnapshot {
#[cfg(test)]
return super::paths::with_test_broker_root(|| {
reconcile_before_enqueue_inner(project_root, cache_root, run_id, cli_version)
reconcile_before_enqueue_inner(project_root, cache_root, run_id, cli_version, None)
});
#[cfg(not(test))]
reconcile_before_enqueue_inner(project_root, cache_root, run_id, cli_version)
reconcile_before_enqueue_inner(project_root, cache_root, run_id, cli_version, None)
}

pub(crate) fn reconcile_before_enqueue_for_sidecar(
project_root: &Path,
cache_root: &Path,
sidecar: &SidecarRuntimeConfig,
cli_version: &str,
) -> BrokerReconciliationSnapshot {
#[cfg(test)]
return super::paths::with_test_broker_root(|| {
reconcile_before_enqueue_inner(
project_root,
cache_root,
sidecar.run_id.as_deref(),
cli_version,
Some(sidecar),
)
});
#[cfg(not(test))]
reconcile_before_enqueue_inner(
project_root,
cache_root,
sidecar.run_id.as_deref(),
cli_version,
Some(sidecar),
)
}

fn reconcile_before_enqueue_inner(
project_root: &Path,
cache_root: &Path,
run_id: Option<&str>,
cli_version: &str,
sidecar: Option<&SidecarRuntimeConfig>,
) -> BrokerReconciliationSnapshot {
if let Some(active) = ready_repair_status::active_ready_repair_status(project_root, run_id) {
let active = match sidecar {
Some(sidecar) => {
ready_repair_status::active_ready_repair_status_for_sidecar(project_root, sidecar)
}
None => ready_repair_status::active_ready_repair_status(project_root, run_id),
};
if let Some(active) = active {
return BrokerReconciliationSnapshot {
status: "active_repair".to_string(),
cleanup_performed: false,
Expand All @@ -47,18 +82,29 @@ fn reconcile_before_enqueue_inner(
};
}

let cleanups = ready_repair_status::cleanup_abandoned_ready_repair_status(project_root, run_id);
let cleanups = match sidecar {
Some(sidecar) => ready_repair_status::cleanup_abandoned_ready_repair_status_for_sidecar(
project_root,
sidecar,
),
None => ready_repair_status::cleanup_abandoned_ready_repair_status(project_root, run_id),
};
let mut stale_status_paths_removed = Vec::new();
let mut stale_lock_paths_removed = Vec::new();
let mut abandoned_repairs = Vec::new();
let mut local_refresh_cleanups = Vec::new();
let mut unresolved_orphan_reason =
ready_repair_status::stale_live_ready_repair_status(project_root, run_id).map(|status| {
format!(
"live_ready_repair_heartbeat_stale:pid={}:phase={}",
status.pid, status.phase
)
});
let stale_live = match sidecar {
Some(sidecar) => {
ready_repair_status::stale_live_ready_repair_status_for_sidecar(project_root, sidecar)
}
None => ready_repair_status::stale_live_ready_repair_status(project_root, run_id),
};
let mut unresolved_orphan_reason = stale_live.map(|status| {
format!(
"live_ready_repair_heartbeat_stale:pid={}:phase={}",
status.pid, status.phase
)
});
for cleanup in cleanups {
if cleanup.removed_status_path {
stale_status_paths_removed.push(clean_path(&cleanup.status_path));
Expand Down
Loading
Loading