use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::core::path_compat::dedup_key;
use crate::hooks::model_relay_endpoints::{EndpointRecord, ReleasedBy, SlotState, SlotValue};
use crate::model_relay::endpoints::{normalize_origin, Origin, RelayPorts};
use crate::model_relay::wire_format::WireFormat;
pub const RECORD_PREFIX: &str = "cline:";
pub const MAX_STATE_FILE_BYTES: u64 = 8 << 20;
const GLOBAL_STATE_FILE_NAME: &str = "globalState.json";
const ACT_PROVIDER_KEY: &str = "actModeApiProvider";
const PLAN_PROVIDER_KEY: &str = "planModeApiProvider";
const MANAGED_ORGANIZATION_KEY: &str = "lastManagedOrganizationId";
const REMOTE_CONFIG_KEYS: &[&str] = &[
"openAiBaseUrl",
"liteLlmBaseUrl",
"anthropicBaseUrl",
"awsBedrockEndpoint",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LaneTag {
Shared,
Legacy,
Next,
}
impl LaneTag {
pub fn as_str(self) -> &'static str {
match self {
Self::Shared => "shared",
Self::Legacy => "legacy",
Self::Next => "next",
}
}
fn parse(s: &str) -> Option<Self> {
match s {
"shared" => Some(Self::Shared),
"legacy" => Some(Self::Legacy),
"next" => Some(Self::Next),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StateLane {
pub tag: LaneTag,
pub path: PathBuf,
}
pub fn state_lanes() -> Vec<StateLane> {
state_lanes_from(
crate::hooks::cline::store_root().map(|(store, _)| {
store
.join(crate::hooks::cline::DEFAULT_DATA_DIR_NAME)
.join(GLOBAL_STATE_FILE_NAME)
}),
crate::hooks::cline::data_root().map(|(data, _)| data.join(GLOBAL_STATE_FILE_NAME)),
)
}
pub fn state_lanes_from(legacy: Option<PathBuf>, next: Option<PathBuf>) -> Vec<StateLane> {
match (legacy, next) {
(Some(legacy), Some(next)) if dedup_key(&legacy) == dedup_key(&next) => vec![StateLane {
tag: LaneTag::Shared,
path: next,
}],
(legacy, next) => legacy
.map(|path| StateLane {
tag: LaneTag::Legacy,
path,
})
.into_iter()
.chain(next.map(|path| StateLane {
tag: LaneTag::Next,
path,
}))
.collect(),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Evidence {
Live,
Static,
Unverified,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UncoveredReason {
SignedHost,
BundlesDisagree,
SharedWithVertex,
HostDependsOnSetting,
DefaultUnknown,
Unverified,
FirstParty,
NotHttp,
Transport,
InvalidUrl,
NoSetting,
NextBundleOnly,
ManagedOverride,
}
impl UncoveredReason {
pub fn describe(self) -> &'static str {
match self {
Self::SignedHost => "requests are signed over the host name, so they cannot be relayed",
Self::BundlesDisagree => "the two Cline builds default to different hosts",
Self::SharedWithVertex => {
"Vertex is selected and shares this setting in the legacy Cline build"
}
Self::HostDependsOnSetting => "the default host depends on another provider setting",
Self::DefaultUnknown => {
"this OpenLatch build does not know the provider's default host"
}
Self::Unverified => "what Cline builds from this setting is not verified yet",
Self::FirstParty => "Cline's own provider authenticates against its host",
Self::NotHttp => "the provider is not reached over HTTP",
Self::Transport => "the provider streams over a transport the relay does not carry",
Self::InvalidUrl => "the configured value is not an http or https URL",
Self::NoSetting => {
"Cline reads this provider's host from no setting OpenLatch can point"
}
Self::NextBundleOnly => {
"only the next Cline build reads this setting, and no request has come through it yet"
}
Self::ManagedOverride => {
"your Cline organisation's remote configuration can override this setting, and no \
request has come through it yet"
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unset {
Write {
origin: &'static str,
path: &'static str,
},
RequiresUrl,
Uncovered(UncoveredReason),
}
#[derive(Clone, Copy, Debug)]
pub struct ProviderRow {
pub ids: &'static [&'static str],
pub family: Option<WireFormat>,
pub unset: Unset,
pub never: Option<UncoveredReason>,
pub evidence: Evidence,
}
#[derive(Clone, Copy, Debug)]
pub struct GlobalStateRow {
pub key: &'static str,
pub row: ProviderRow,
}
const fn write(origin: &'static str, path: &'static str) -> Unset {
Unset::Write { origin, path }
}
pub const GLOBAL_STATE_ROWS: &[GlobalStateRow] = &[
GlobalStateRow {
key: "anthropicBaseUrl",
row: ProviderRow {
ids: &["anthropic"],
family: Some(WireFormat::AnthropicMessages),
unset: write("https://api.anthropic.com", ""),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "openAiBaseUrl",
row: ProviderRow {
ids: &["openai", "openai-compatible"],
family: Some(WireFormat::OpenAiChatCompletions),
unset: write("https://api.openai.com", "/v1"),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "ollamaBaseUrl",
row: ProviderRow {
ids: &["ollama"],
family: Some(WireFormat::OllamaNative),
unset: write("http://127.0.0.1:11434", ""),
never: None,
evidence: Evidence::Live,
},
},
GlobalStateRow {
key: "lmStudioBaseUrl",
row: ProviderRow {
ids: &["lmstudio"],
family: Some(WireFormat::OpenAiChatCompletions),
unset: write("http://localhost:1234", ""),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "geminiBaseUrl",
row: ProviderRow {
ids: &["gemini"],
family: Some(WireFormat::GoogleGenerateContent),
unset: write("https://generativelanguage.googleapis.com", ""),
never: None,
evidence: Evidence::Live,
},
},
GlobalStateRow {
key: "requestyBaseUrl",
row: ProviderRow {
ids: &["requesty"],
family: Some(WireFormat::OpenAiChatCompletions),
unset: write("https://router.requesty.ai", "/v1"),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "liteLlmBaseUrl",
row: ProviderRow {
ids: &["litellm"],
family: Some(WireFormat::OpenAiChatCompletions),
unset: write("http://localhost:4000", ""),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "asksageApiUrl",
row: ProviderRow {
ids: &["asksage"],
family: None,
unset: write("https://api.asksage.ai", "/server"),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "ocaBaseUrl",
row: ProviderRow {
ids: &["oca"],
family: Some(WireFormat::OpenAiChatCompletions),
unset: Unset::Uncovered(UncoveredReason::HostDependsOnSetting),
never: None,
evidence: Evidence::Unverified,
},
},
GlobalStateRow {
key: "aihubmixBaseUrl",
row: ProviderRow {
ids: &["aihubmix"],
family: None,
unset: Unset::Uncovered(UncoveredReason::BundlesDisagree),
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "difyBaseUrl",
row: ProviderRow {
ids: &["dify"],
family: None,
unset: Unset::RequiresUrl,
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "sapAiCoreBaseUrl",
row: ProviderRow {
ids: &["sapaicore"],
family: None,
unset: Unset::RequiresUrl,
never: None,
evidence: Evidence::Static,
},
},
GlobalStateRow {
key: "awsBedrockEndpoint",
row: ProviderRow {
ids: &["bedrock"],
family: None,
unset: Unset::Uncovered(UncoveredReason::SignedHost),
never: Some(UncoveredReason::SignedHost),
evidence: Evidence::Static,
},
},
];
const NEXT_GLOBAL_STATE_IDS: &[&str] = &[
"anthropic",
"openai",
"openai-compatible",
"ollama",
"lmstudio",
"gemini",
"requesty",
"litellm",
"asksage",
"oca",
"aihubmix",
"dify",
"sapaicore",
];
const fn pj(ids: &'static [&'static str], origin: &'static str, path: &'static str) -> ProviderRow {
ProviderRow {
ids,
family: Some(WireFormat::OpenAiChatCompletions),
unset: write(origin, path),
never: None,
evidence: Evidence::Static,
}
}
const fn pj_never(ids: &'static [&'static str], reason: UncoveredReason) -> ProviderRow {
ProviderRow {
ids,
family: None,
unset: Unset::Uncovered(reason),
never: Some(reason),
evidence: Evidence::Static,
}
}
const fn pj_unset(ids: &'static [&'static str], reason: UncoveredReason) -> ProviderRow {
ProviderRow {
ids,
family: Some(WireFormat::OpenAiChatCompletions),
unset: Unset::Uncovered(reason),
never: None,
evidence: Evidence::Static,
}
}
pub const PROVIDERS_JSON_ROWS: &[ProviderRow] = &[
pj(&["deepseek"], "https://api.deepseek.com", "/v1"),
pj(&["openrouter"], "https://openrouter.ai", "/api/v1"),
pj(&["xai"], "https://api.x.ai", "/v1"),
pj(&["together"], "https://api.together.xyz", "/v1"),
pj(&["groq"], "https://api.groq.com", "/openai/v1"),
pj(&["cerebras"], "https://api.cerebras.ai", "/v1"),
pj(&["sambanova"], "https://api.sambanova.ai", "/v1"),
pj(&["fireworks"], "https://api.fireworks.ai", "/inference/v1"),
pj(&["nebius"], "https://api.tokenfactory.nebius.com", "/v1"),
pj(&["baseten"], "https://inference.baseten.co", "/v1"),
pj(&["huggingface"], "https://router.huggingface.co", "/v1"),
pj(&["wandb"], "https://api.inference.wandb.ai", "/v1"),
pj(
&["vercel-ai-gateway"],
"https://ai-gateway.vercel.sh",
"/v1",
),
pj(&["hicap"], "https://api.hicap.ai", "/v1"),
pj(
&["nousResearch"],
"https://inference-api.nousresearch.com",
"/v1",
),
pj(&["mistral"], "https://api.mistral.ai", "/v1"),
pj(&["openai-native"], "https://api.openai.com", "/v1"),
pj(&["doubao"], "https://ark.cn-beijing.volces.com", "/api/v3"),
pj_unset(
&[
"qwen",
"qwen-code",
"zai",
"moonshot",
"minimax",
"huawei-cloud-maas",
],
UncoveredReason::HostDependsOnSetting,
),
pj_never(&["bedrock"], UncoveredReason::SignedHost),
pj_never(&["vertex"], UncoveredReason::HostDependsOnSetting),
pj_never(&["cline", "cline-pass"], UncoveredReason::FirstParty),
pj_never(&["openai-codex"], UncoveredReason::Transport),
pj_never(
&["claude-code", "opencode", "vscode-lm"],
UncoveredReason::NotHttp,
),
];
pub fn global_state_row(key: &str) -> Option<&'static GlobalStateRow> {
GLOBAL_STATE_ROWS.iter().find(|r| r.key == key)
}
pub fn providers_json_row(id: &str) -> Option<&'static ProviderRow> {
PROVIDERS_JSON_ROWS.iter().find(|r| r.ids.contains(&id))
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SlotId {
GlobalState {
lane: LaneTag,
key: String,
},
ProvidersJson {
id: String,
},
}
impl SlotId {
pub fn record_key(&self) -> String {
match self {
Self::GlobalState { lane, key } => format!("{RECORD_PREFIX}gs:{}:{key}", lane.as_str()),
Self::ProvidersJson { id } => format!("{RECORD_PREFIX}pj:{id}"),
}
}
pub fn from_record_key(record_key: &str) -> Option<Self> {
let rest = record_key.strip_prefix(RECORD_PREFIX)?;
if let Some(rest) = rest.strip_prefix("gs:") {
let (lane, key) = rest.split_once(':')?;
return Some(Self::GlobalState {
lane: LaneTag::parse(lane)?,
key: key.to_string(),
});
}
rest.strip_prefix("pj:")
.map(|id| Self::ProvidersJson { id: id.to_string() })
}
}
#[derive(Clone, Debug)]
pub struct SlotObservation {
pub slot: SlotId,
pub file: PathBuf,
pub file_present: bool,
pub entry_present: bool,
pub value: SlotValue,
pub selected: bool,
pub selected_ids: Vec<String>,
pub row: Option<ProviderRow>,
pub vertex_selected: bool,
pub managed: bool,
}
impl SlotObservation {
pub fn traffic_only(&self) -> Option<UncoveredReason> {
match &self.slot {
SlotId::ProvidersJson { .. } => Some(UncoveredReason::NextBundleOnly),
SlotId::GlobalState { key, .. }
if self.managed && REMOTE_CONFIG_KEYS.contains(&key.as_str()) =>
{
Some(UncoveredReason::ManagedOverride)
}
SlotId::GlobalState { .. } => None,
}
}
pub fn hooks_name_provider(&self) -> bool {
matches!(
self.slot,
SlotId::GlobalState {
lane: LaneTag::Shared | LaneTag::Legacy,
..
}
)
}
pub fn provider_ids(&self) -> Vec<String> {
match (&self.slot, self.row) {
(SlotId::ProvidersJson { id }, _) => vec![id.clone()],
(_, Some(row)) => row.ids.iter().map(|id| (*id).to_string()).collect(),
(_, None) => self.selected_ids.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FileProblem {
TooLarge {
path: PathBuf,
size: u64,
},
Unparseable {
path: PathBuf,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Selection {
pub lane: LaneTag,
pub mode: &'static str,
pub id: String,
}
#[derive(Clone, Debug, Default)]
pub struct Observation {
pub slots: Vec<SlotObservation>,
pub problems: Vec<FileProblem>,
pub selections: Vec<Selection>,
}
pub fn observe_slots(recorded: &BTreeSet<String>) -> Observation {
observe_slots_in(
&state_lanes(),
crate::hooks::cline::providers_json_path().as_deref(),
recorded,
)
}
fn read_bounded(path: &Path) -> Result<Option<String>, FileProblem> {
let size = match std::fs::metadata(path) {
Ok(meta) => meta.len(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(_) => {
return Err(FileProblem::Unparseable {
path: path.to_path_buf(),
})
}
};
if size > MAX_STATE_FILE_BYTES {
return Err(FileProblem::TooLarge {
path: path.to_path_buf(),
size,
});
}
match std::fs::read_to_string(path) {
Ok(raw) if raw.len() as u64 > MAX_STATE_FILE_BYTES => Err(FileProblem::TooLarge {
path: path.to_path_buf(),
size: raw.len() as u64,
}),
Ok(raw) => Ok(Some(raw)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(_) => Err(FileProblem::Unparseable {
path: path.to_path_buf(),
}),
}
}
fn parse(path: &Path, raw: &str) -> Result<serde_json::Value, FileProblem> {
serde_json::from_str(raw)
.or_else(|_| crate::hooks::jsonc::parse_settings_value(raw))
.map_err(|_| FileProblem::Unparseable {
path: path.to_path_buf(),
})
}
fn slot_value(parent: Option<&serde_json::Value>, key: &str) -> SlotValue {
match parent.and_then(|p| p.get(key)) {
None => SlotValue::Absent,
Some(serde_json::Value::Null) => SlotValue::Null,
Some(serde_json::Value::String(s)) => SlotValue::Text(s.clone()),
Some(other) => SlotValue::Text(other.to_string()),
}
}
fn selected_id(doc: &serde_json::Value, key: &str) -> Option<String> {
doc.get(key)?
.as_str()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
}
pub fn observe_slots_in(
lanes: &[StateLane],
providers_json: Option<&Path>,
recorded: &BTreeSet<String>,
) -> Observation {
let mut out = Observation::default();
let mut next_lane_doc: Option<serde_json::Value> = None;
for lane in lanes {
let doc = match read_bounded(&lane.path).and_then(|raw| match raw {
Some(raw) => parse(&lane.path, &raw).map(Some),
None => Ok(None),
}) {
Ok(doc) => doc,
Err(problem) => {
out.problems.push(problem);
continue;
}
};
let present = doc.is_some();
let doc = doc.unwrap_or(serde_json::Value::Null);
if matches!(lane.tag, LaneTag::Next | LaneTag::Shared) {
next_lane_doc = Some(doc.clone());
}
let act = selected_id(&doc, ACT_PROVIDER_KEY);
let plan = selected_id(&doc, PLAN_PROVIDER_KEY);
for (mode, id) in [("act", &act), ("plan", &plan)] {
if let Some(id) = id {
out.selections.push(Selection {
lane: lane.tag,
mode,
id: id.clone(),
});
}
}
let is_selected = |id: &str| act.as_deref() == Some(id) || plan.as_deref() == Some(id);
let vertex_selected = is_selected("vertex");
let managed = selected_id(&doc, MANAGED_ORGANIZATION_KEY).is_some();
let mut keys: Vec<(String, Option<ProviderRow>, Vec<String>)> = GLOBAL_STATE_ROWS
.iter()
.map(|r| {
let ids = r
.row
.ids
.iter()
.filter(|id| is_selected(id))
.map(|id| id.to_string())
.collect();
(r.key.to_string(), Some(r.row), ids)
})
.collect();
for id in [&act, &plan].into_iter().flatten() {
let known = GLOBAL_STATE_ROWS
.iter()
.any(|r| r.row.ids.contains(&id.as_str()))
|| NEXT_GLOBAL_STATE_IDS.contains(&id.as_str())
|| providers_json_row(id).is_some();
if known {
continue;
}
if let Some(key) = base_url_key_of(&doc, id) {
if !keys.iter().any(|(k, _, _)| *k == key) {
keys.push((key, None, vec![id.clone()]));
}
}
}
for (key, row, selected_ids) in keys {
let slot = SlotId::GlobalState {
lane: lane.tag,
key: key.clone(),
};
let value = slot_value(Some(&doc), &key);
let selected = !selected_ids.is_empty();
if !(selected || value.text().is_some() || recorded.contains(&slot.record_key())) {
continue;
}
out.slots.push(SlotObservation {
slot,
file: lane.path.clone(),
file_present: present,
entry_present: true,
value,
selected,
selected_ids,
row,
vertex_selected,
managed,
});
}
}
if let Some(path) = providers_json {
observe_providers_json(path, recorded, next_lane_doc.as_ref(), &mut out);
}
out
}
fn observe_providers_json(
path: &Path,
recorded: &BTreeSet<String>,
next_lane_doc: Option<&serde_json::Value>,
out: &mut Observation,
) {
let doc = match read_bounded(path).and_then(|raw| match raw {
Some(raw) => parse(path, &raw).map(Some),
None => Ok(None),
}) {
Ok(doc) => doc,
Err(problem) => {
out.problems.push(problem);
return;
}
};
let present = doc.is_some();
let providers = doc
.as_ref()
.and_then(|d| d.get(crate::hooks::cline::PROVIDERS_ROOT_KEY))
.and_then(serde_json::Value::as_object);
let managed = next_lane_doc
.and_then(|d| selected_id(d, MANAGED_ORGANIZATION_KEY))
.is_some();
let mut ids: BTreeSet<String> = providers
.map(|p| p.keys().cloned().collect())
.unwrap_or_default();
for key in recorded {
if let Some(SlotId::ProvidersJson { id }) = SlotId::from_record_key(key) {
ids.insert(id);
}
}
for id in ids {
if NEXT_GLOBAL_STATE_IDS.contains(&id.as_str()) {
continue;
}
let entry = providers.and_then(|p| p.get(&id));
let settings = entry.and_then(|e| e.get(crate::hooks::cline::PROVIDER_SETTINGS_KEY));
out.slots.push(SlotObservation {
slot: SlotId::ProvidersJson { id: id.clone() },
file: path.to_path_buf(),
file_present: present,
entry_present: entry.is_some(),
value: slot_value(settings, crate::hooks::cline::BASE_URL_KEY),
selected: entry.is_some(),
selected_ids: vec![id.clone()],
row: providers_json_row(&id).copied(),
vertex_selected: false,
managed,
});
}
}
fn base_url_key_of(doc: &serde_json::Value, id: &str) -> Option<String> {
fn fold(s: &str) -> String {
s.chars()
.filter(char::is_ascii_alphanumeric)
.map(|c| c.to_ascii_lowercase())
.collect()
}
let want = format!("{}baseurl", fold(id));
doc.as_object()?
.keys()
.find(|key| fold(key) == want)
.cloned()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Decision {
Keep,
Skip,
Wire {
path: String,
origin: Origin,
prior: SlotValue,
},
Reapply,
NewPrior {
path: String,
origin: Origin,
prior: SlotValue,
},
Release {
restore: Option<SlotValue>,
},
Uncovered(UncoveredReason),
}
#[derive(Clone, Copy, Debug)]
pub struct DecideCtx {
pub ports: RelayPorts,
pub started_at: u64,
}
pub fn names_relay_listener(value: &str, ports: &RelayPorts, recorded: Option<u16>) -> Option<u16> {
let url = reqwest::Url::parse(value.trim()).ok()?;
if url.scheme() != "http" || url.host_str() != Some("127.0.0.1") {
return None;
}
let port = url.port()?;
(port == ports.main || ports.in_block(port) || Some(port) == recorded).then_some(port)
}
pub fn relay_value(port: u16, path: &str) -> String {
format!("http://127.0.0.1:{port}{path}")
}
fn path_after_authority(raw: &str) -> &str {
let raw = raw.trim();
let Some((_, rest)) = raw.split_once("://") else {
return "";
};
match rest.find(['/', '?', '#']) {
Some(i) => &rest[i..],
None => "",
}
}
pub fn decide(obs: &SlotObservation, rec: Option<&EndpointRecord>, ctx: &DecideCtx) -> Decision {
if !obs.file_present {
return Decision::Skip;
}
let live = rec.filter(|r| r.is_live());
if let Some(reason) = obs.row.and_then(|r| r.never) {
return match live {
Some(r) => Decision::Release {
restore: Some(r.prior.clone()),
},
None if obs.selected => Decision::Uncovered(reason),
None => Decision::Skip,
};
}
let recorded_port = rec.map(|r| r.port);
let ours = obs
.value
.text()
.and_then(|t| names_relay_listener(t, &ctx.ports, recorded_port));
let vertex_guard = |prior: &SlotValue| {
obs.vertex_selected
&& prior.is_unset()
&& matches!(&obs.slot, SlotId::GlobalState { key, .. } if key == "geminiBaseUrl")
};
if let Some(r) = live {
if vertex_guard(&r.prior) {
return Decision::Release {
restore: Some(r.prior.clone()),
};
}
if !obs.entry_present {
return Decision::Release { restore: None };
}
if obs.value.text() == r.last_written.as_deref() && obs.value.text().is_some() {
return Decision::Keep;
}
if ours.is_some() {
return Decision::Reapply;
}
if obs.value.is_unset() {
if r.prior.is_unset() {
return Decision::Reapply;
}
return match unset_target(obs, ctx) {
Ok((path, origin)) => Decision::NewPrior {
path,
origin,
prior: obs.value.clone(),
},
Err(_) => Decision::Release { restore: None },
};
}
if obs.value.text() == r.prior.text() {
return Decision::Reapply;
}
return match custom_target(&obs.value, ctx) {
Ok((path, origin)) => Decision::NewPrior {
path,
origin,
prior: obs.value.clone(),
},
Err(_) => Decision::Release { restore: None },
};
}
if let Some(r) = rec {
let teardown_since_start = r.released_by == Some(ReleasedBy::Teardown)
&& r.state == SlotState::Released
&& r.changed_at >= ctx.started_at;
let unchanged = obs.value == r.prior || (obs.value.is_unset() && r.prior.is_unset());
if teardown_since_start && (unchanged || ours.is_some()) {
return Decision::Skip;
}
}
if !obs.entry_present {
return Decision::Skip;
}
if ours.is_some() {
let prior = rec.map(|r| r.prior.clone()).unwrap_or(SlotValue::Absent);
if vertex_guard(&prior) {
return Decision::Release {
restore: Some(prior),
};
}
let target = match prior.text() {
Some(_) => custom_target(&prior, ctx),
None => {
unset_target(obs, ctx).map_err(|r| r.unwrap_or(UncoveredReason::DefaultUnknown))
}
};
return match target {
Ok((path, origin)) => Decision::Wire {
path,
origin,
prior,
},
Err(reason) => Decision::Uncovered(reason),
};
}
if obs.value.is_unset() {
if !obs.selected {
return Decision::Skip;
}
if vertex_guard(&obs.value) {
return Decision::Uncovered(UncoveredReason::SharedWithVertex);
}
return match unset_target(obs, ctx) {
Ok((path, origin)) => Decision::Wire {
path,
origin,
prior: obs.value.clone(),
},
Err(Some(reason)) => Decision::Uncovered(reason),
Err(None) => Decision::Skip,
};
}
match custom_target(&obs.value, ctx) {
Ok((path, origin)) => Decision::Wire {
path,
origin,
prior: obs.value.clone(),
},
Err(reason) => Decision::Uncovered(reason),
}
}
fn unset_target(
obs: &SlotObservation,
ctx: &DecideCtx,
) -> Result<(String, Origin), Option<UncoveredReason>> {
let Some(row) = obs.row else {
return Err(Some(UncoveredReason::DefaultUnknown));
};
match row.unset {
Unset::RequiresUrl => Err(None),
Unset::Uncovered(reason) => Err(Some(reason)),
Unset::Write { .. } if row.evidence == Evidence::Unverified => {
Err(Some(UncoveredReason::Unverified))
}
Unset::Write { origin, path } => {
let url =
reqwest::Url::parse(origin).map_err(|_| Some(UncoveredReason::DefaultUnknown))?;
let origin = normalize_origin(&url, &ctx.ports)
.map_err(|_| Some(UncoveredReason::DefaultUnknown))?;
Ok((path.to_string(), origin))
}
}
}
fn custom_target(value: &SlotValue, ctx: &DecideCtx) -> Result<(String, Origin), UncoveredReason> {
let text = value.text().ok_or(UncoveredReason::InvalidUrl)?;
let url = reqwest::Url::parse(text.trim()).map_err(|_| UncoveredReason::InvalidUrl)?;
let origin = normalize_origin(&url, &ctx.ports).map_err(|_| UncoveredReason::InvalidUrl)?;
Ok((path_after_authority(text).to_string(), origin))
}
pub fn apply_slot_value(
root: &jsonc_parser::cst::CstRootNode,
slot: &SlotId,
value: &SlotValue,
) -> Result<(), crate::error::OlError> {
use jsonc_parser::cst::CstInputValue;
let input = |v: &SlotValue| match v {
SlotValue::Absent => None,
SlotValue::Null => Some(CstInputValue::Null),
SlotValue::Text(t) => Some(CstInputValue::String(t.clone())),
};
match slot {
SlotId::GlobalState { key, .. } => {
let Some(obj) = root.object_value() else {
return Ok(());
};
match (obj.get(key), input(value)) {
(Some(prop), Some(v)) => prop.set_value(v),
(Some(prop), None) => prop.remove(),
(None, Some(v)) => {
obj.append(key, v);
}
(None, None) => {}
}
Ok(())
}
SlotId::ProvidersJson { id } => {
let Some(entry) = root
.object_value()
.and_then(|o| o.object_value(crate::hooks::cline::PROVIDERS_ROOT_KEY))
.and_then(|p| p.object_value(id))
else {
return Ok(());
};
let key = crate::hooks::cline::BASE_URL_KEY;
match input(value) {
Some(v) => {
let Some(settings) =
entry.object_value_or_create(crate::hooks::cline::PROVIDER_SETTINGS_KEY)
else {
return Ok(());
};
match settings.get(key) {
Some(prop) => prop.set_value(v),
None => {
settings.append(key, v);
}
}
}
None => {
if let Some(prop) = entry
.object_value(crate::hooks::cline::PROVIDER_SETTINGS_KEY)
.and_then(|s| s.get(key))
{
prop.remove();
}
}
}
Ok(())
}
}
}
pub fn read_slot_value(root: &jsonc_parser::cst::CstRootNode, slot: &SlotId) -> SlotValue {
let doc = root.to_serde_value();
match slot {
SlotId::GlobalState { key, .. } => slot_value(doc.as_ref(), key),
SlotId::ProvidersJson { id } => slot_value(
doc.as_ref()
.and_then(|d| d.get(crate::hooks::cline::PROVIDERS_ROOT_KEY))
.and_then(|p| p.get(id))
.and_then(|e| e.get(crate::hooks::cline::PROVIDER_SETTINGS_KEY)),
crate::hooks::cline::BASE_URL_KEY,
),
}
}
pub struct ClineProviderEndpoints {
fixed: Option<(Vec<StateLane>, Option<PathBuf>)>,
}
impl ClineProviderEndpoints {
pub const RESOLVED: Self = Self { fixed: None };
pub fn at(lanes: Vec<StateLane>, providers_json: Option<PathBuf>) -> Self {
Self {
fixed: Some((lanes, providers_json)),
}
}
fn providers_json(&self) -> Option<PathBuf> {
match &self.fixed {
Some((_, pj)) => pj.clone(),
None => crate::hooks::cline::providers_json_path(),
}
}
}
impl crate::hooks::provider_endpoints::ProviderEndpoints for ClineProviderEndpoints {
fn agent_type(&self) -> &'static str {
crate::hooks::bindings::cline::ClineBinding::AGENT_TYPE
}
fn record_prefix(&self) -> &'static str {
RECORD_PREFIX
}
fn observe(&self, recorded: &BTreeSet<String>) -> Observation {
match &self.fixed {
Some((lanes, pj)) => observe_slots_in(lanes, pj.as_deref(), recorded),
None => observe_slots(recorded),
}
}
fn write_slot(
&self,
file: &Path,
slot: &SlotId,
value: &SlotValue,
still: &dyn Fn(&SlotValue) -> bool,
) -> Result<crate::hooks::atomic::RewriteOutcome, crate::error::OlError> {
write_slot_in(file, slot, value, still)
}
fn copies_of(&self, ports: &BTreeSet<u16>) -> Vec<(PathBuf, SlotId, u16)> {
self.providers_json()
.map(|pj| copies_in(&pj, ports))
.unwrap_or_default()
}
fn watch_files(&self) -> Vec<PathBuf> {
let lanes = match &self.fixed {
Some((lanes, _)) => lanes.clone(),
None => state_lanes(),
};
lanes
.into_iter()
.map(|lane| lane.path)
.chain(self.providers_json())
.collect()
}
fn reclaim_retired(&self, main_port: u16) {
if let Some(pj) = self.providers_json() {
reclaim_retired_in(&pj, main_port);
}
}
}
pub fn reclaim_retired_in(providers_json: &Path, main_port: u16) {
use crate::hooks::model_relay_endpoints as records;
let Ok(retired) = records::prior_records(RECORD_PREFIX) else {
return;
};
for (key, prior) in retired {
let Some(id) = key.strip_prefix(RECORD_PREFIX) else {
continue;
};
if id.contains(':') {
continue;
}
let value = match prior {
Some(url) => SlotValue::Text(url),
None => SlotValue::Absent,
};
let slot = SlotId::ProvidersJson { id: id.to_string() };
let restored = write_slot_in(providers_json, &slot, &value, &|current| {
crate::hooks::provider_endpoints::names_port(current, main_port)
});
match restored {
Ok(crate::hooks::atomic::RewriteOutcome::Contended) => continue,
Ok(_) => records::forget(&key),
Err(e) => {
tracing::warn!(key, code = %e.code, error = %e.message, "could not reclaim a retired Cline wiring")
}
}
}
for format in [
WireFormat::OpenAiChatCompletions,
WireFormat::GoogleGenerateContent,
WireFormat::OllamaNative,
] {
records::forget(&crate::hooks::upstream_record_key(format));
}
}
pub fn write_slot_in(
file: &Path,
slot: &SlotId,
value: &SlotValue,
still: &dyn Fn(&SlotValue) -> bool,
) -> Result<crate::hooks::atomic::RewriteOutcome, crate::error::OlError> {
crate::hooks::atomic::rewrite_existing_json(
file,
crate::hooks::atomic::RewriteLimits {
max_bytes: MAX_STATE_FILE_BYTES,
},
|root| {
if !still(&read_slot_value(root, slot)) {
return Ok(());
}
apply_slot_value(root, slot, value)
},
)
}
pub fn copies_in(providers_json: &Path, ports: &BTreeSet<u16>) -> Vec<(PathBuf, SlotId, u16)> {
let Ok(Some(raw)) = read_bounded(providers_json) else {
return Vec::new();
};
let Ok(doc) = parse(providers_json, &raw) else {
return Vec::new();
};
let Some(providers) = doc
.get(crate::hooks::cline::PROVIDERS_ROOT_KEY)
.and_then(serde_json::Value::as_object)
else {
return Vec::new();
};
let mut out = Vec::new();
for id in NEXT_GLOBAL_STATE_IDS {
let value = slot_value(
providers
.get(*id)
.and_then(|e| e.get(crate::hooks::cline::PROVIDER_SETTINGS_KEY)),
crate::hooks::cline::BASE_URL_KEY,
);
if let Some(port) = ports
.iter()
.copied()
.find(|port| crate::hooks::provider_endpoints::names_port(&value, *port))
{
out.push((
providers_json.to_path_buf(),
SlotId::ProvidersJson { id: id.to_string() },
port,
));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
const PORTS: RelayPorts = RelayPorts {
daemon: 7443,
main: 7600,
};
const CTX: DecideCtx = DecideCtx {
ports: PORTS,
started_at: 1_000,
};
struct Tree {
_dir: tempfile::TempDir,
root: PathBuf,
}
impl Tree {
fn new() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path().to_path_buf();
Self { _dir: dir, root }
}
fn write(&self, rel: &str, body: &str) -> PathBuf {
let path = self.root.join(rel);
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, body).expect("write");
path
}
fn shared_lane(&self, body: &str) -> Vec<StateLane> {
let path = self.write("data/globalState.json", body);
state_lanes_from(Some(path.clone()), Some(path))
}
}
fn observe(lanes: &[StateLane], pj: Option<&Path>) -> Observation {
observe_slots_in(lanes, pj, &BTreeSet::new())
}
fn slot<'a>(obs: &'a Observation, key: &str) -> &'a SlotObservation {
obs.slots
.iter()
.find(|s| s.slot.record_key().ends_with(key))
.unwrap_or_else(|| panic!("no slot {key} in {:?}", obs.slots))
}
fn origin(url: &str) -> Origin {
normalize_origin(&reqwest::Url::parse(url).expect("url"), &PORTS).expect("origin")
}
fn record(prior: SlotValue, last_written: &str, state: SlotState) -> EndpointRecord {
EndpointRecord {
port: 7601,
origin: "https://generativelanguage.googleapis.com/".into(),
prior,
last_written: Some(last_written.into()),
file: PathBuf::from("/x/globalState.json"),
state,
released_by: None,
changed_at: 2_000,
proven_at: None,
family: None,
pending_event: None,
proven_by: None,
misconfigured_event: None,
}
}
#[test]
fn an_unset_gemini_is_written_bare_and_forwards_to_google() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"actModeApiProvider":"gemini"}"#);
let obs = observe(&lanes, None);
let gemini = slot(&obs, "geminiBaseUrl");
assert_eq!(gemini.value, SlotValue::Absent);
assert_eq!(
decide(gemini, None, &CTX),
Decision::Wire {
path: String::new(),
origin: origin("https://generativelanguage.googleapis.com"),
prior: SlotValue::Absent,
}
);
assert_eq!(relay_value(7601, ""), "http://127.0.0.1:7601");
}
#[test]
fn requesty_unset_writes_v1_which_legacy_does_not_re_add() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"planModeApiProvider":"requesty","requestyBaseUrl":""}"#);
let obs = observe(&lanes, None);
assert_eq!(
decide(slot(&obs, "requestyBaseUrl"), None, &CTX),
Decision::Wire {
path: "/v1".into(),
origin: origin("https://router.requesty.ai"),
prior: SlotValue::Text(String::new()),
}
);
}
#[test]
fn a_custom_url_keeps_its_path_and_forwards_to_its_origin() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"ollama","anthropicBaseUrl":"https://gw.corp:8443/anthropic/"}"#,
);
let obs = observe(&lanes, None);
let decision = decide(slot(&obs, "anthropicBaseUrl"), None, &CTX);
assert_eq!(
decision,
Decision::Wire {
path: "/anthropic/".into(),
origin: origin("https://gw.corp:8443"),
prior: SlotValue::Text("https://gw.corp:8443/anthropic/".into()),
}
);
assert_eq!(
relay_value(7602, "/anthropic/"),
"http://127.0.0.1:7602/anthropic/"
);
}
#[test]
fn absent_null_and_empty_are_all_unset() {
for body in [
r#"{"actModeApiProvider":"ollama"}"#,
r#"{"actModeApiProvider":"ollama","ollamaBaseUrl":null}"#,
r#"{"actModeApiProvider":"ollama","ollamaBaseUrl":""}"#,
r#"{"actModeApiProvider":"ollama","ollamaBaseUrl":" "}"#,
] {
let tree = Tree::new();
let obs = observe(&tree.shared_lane(body), None);
let ollama = slot(&obs, "ollamaBaseUrl");
assert!(ollama.value.is_unset(), "{body}");
assert!(
matches!(decide(ollama, None, &CTX), Decision::Wire { ref path, .. } if path.is_empty()),
"{body}"
);
}
}
#[test]
fn the_prior_is_restored_in_its_original_representation() {
let slot = SlotId::GlobalState {
lane: LaneTag::Shared,
key: "geminiBaseUrl".into(),
};
for (original, prior) in [
(r#"{"a":1}"#, SlotValue::Absent),
(r#"{"a":1,"geminiBaseUrl":null}"#, SlotValue::Null),
(
r#"{"a":1,"geminiBaseUrl":""}"#,
SlotValue::Text(String::new()),
),
(
r#"{"a":1,"geminiBaseUrl":"https://gw/x"}"#,
SlotValue::Text("https://gw/x".into()),
),
] {
let root = jsonc_parser::cst::CstRootNode::parse(original, &Default::default())
.expect("parse");
assert_eq!(read_slot_value(&root, &slot), prior, "{original}");
apply_slot_value(
&root,
&slot,
&SlotValue::Text("http://127.0.0.1:7601".into()),
)
.expect("wire");
apply_slot_value(&root, &slot, &prior).expect("restore");
let restored: serde_json::Value =
serde_json::from_str(&root.to_string()).expect("json");
let want: serde_json::Value = serde_json::from_str(original).expect("json");
assert_eq!(restored, want, "{original}");
}
}
#[test]
fn an_editor_revert_reapplies_without_changing_the_prior() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"actModeApiProvider":"gemini","geminiBaseUrl":""}"#);
let obs = observe(&lanes, None);
let rec = record(SlotValue::Absent, "http://127.0.0.1:7601", SlotState::Wired);
assert_eq!(
decide(slot(&obs, "geminiBaseUrl"), Some(&rec), &CTX),
Decision::Reapply
);
let tree = Tree::new();
let lanes =
tree.shared_lane(r#"{"actModeApiProvider":"gemini","geminiBaseUrl":"https://gw/g"}"#);
let obs = observe(&lanes, None);
let rec = record(
SlotValue::Text("https://gw/g".into()),
"http://127.0.0.1:7601/g",
SlotState::Wired,
);
assert_eq!(
decide(slot(&obs, "geminiBaseUrl"), Some(&rec), &CTX),
Decision::Reapply
);
}
#[test]
fn our_own_value_is_kept() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"gemini","geminiBaseUrl":"http://127.0.0.1:7601"}"#,
);
let obs = observe(&lanes, None);
let rec = record(SlotValue::Absent, "http://127.0.0.1:7601", SlotState::Wired);
assert_eq!(
decide(slot(&obs, "geminiBaseUrl"), Some(&rec), &CTX),
Decision::Keep
);
}
#[test]
fn a_new_custom_url_becomes_the_prior() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"gemini","geminiBaseUrl":"https://gw.corp/google"}"#,
);
let obs = observe(&lanes, None);
let rec = record(SlotValue::Absent, "http://127.0.0.1:7601", SlotState::Wired);
assert_eq!(
decide(slot(&obs, "geminiBaseUrl"), Some(&rec), &CTX),
Decision::NewPrior {
path: "/google".into(),
origin: origin("https://gw.corp"),
prior: SlotValue::Text("https://gw.corp/google".into()),
}
);
}
#[test]
fn clearing_a_custom_url_makes_unset_the_prior() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"actModeApiProvider":"gemini","geminiBaseUrl":""}"#);
let obs = observe(&lanes, None);
let rec = record(
SlotValue::Text("https://gw.corp/google".into()),
"http://127.0.0.1:7601/google",
SlotState::Wired,
);
assert_eq!(
decide(slot(&obs, "geminiBaseUrl"), Some(&rec), &CTX),
Decision::NewPrior {
path: String::new(),
origin: origin("https://generativelanguage.googleapis.com"),
prior: SlotValue::Text(String::new()),
}
);
}
#[test]
fn bedrock_is_uncovered_because_signing_binds_the_host() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"bedrock","awsBedrockEndpoint":"https://vpce.example"}"#,
);
let obs = observe(&lanes, None);
assert_eq!(
decide(slot(&obs, "awsBedrockEndpoint"), None, &CTX),
Decision::Uncovered(UncoveredReason::SignedHost)
);
}
#[test]
fn vertex_selected_keeps_gemini_base_url_unwired_when_unset() {
let tree = Tree::new();
let lanes =
tree.shared_lane(r#"{"actModeApiProvider":"gemini","planModeApiProvider":"vertex"}"#);
let obs = observe(&lanes, None);
let gemini = slot(&obs, "geminiBaseUrl");
assert_eq!(
decide(gemini, None, &CTX),
Decision::Uncovered(UncoveredReason::SharedWithVertex)
);
let rec = record(SlotValue::Absent, "http://127.0.0.1:7601", SlotState::Wired);
assert_eq!(
decide(gemini, Some(&rec), &CTX),
Decision::Release {
restore: Some(SlotValue::Absent)
}
);
let tree = Tree::new();
let lanes = tree
.shared_lane(r#"{"actModeApiProvider":"vertex","geminiBaseUrl":"https://gw.corp/g"}"#);
let obs = observe(&lanes, None);
assert!(matches!(
decide(slot(&obs, "geminiBaseUrl"), None, &CTX),
Decision::Wire { .. }
));
}
#[test]
fn an_unknown_provider_with_no_url_is_uncovered_default_unknown() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{}"#);
let pj = tree.write(
"data/settings/providers.json",
r#"{"providers":{"futureai":{"settings":{"provider":"futureai"}}}}"#,
);
let obs = observe(&lanes, Some(&pj));
assert_eq!(
decide(slot(&obs, "pj:futureai"), None, &CTX),
Decision::Uncovered(UncoveredReason::DefaultUnknown)
);
}
#[test]
fn an_unknown_provider_with_a_custom_url_is_wired_generically() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"futureai","futureAiBaseUrl":"https://api.future.ai/v2"}"#,
);
let pj = tree.write(
"data/settings/providers.json",
r#"{"providers":{"newgw":{"settings":{"baseUrl":"https://gw.new/openai/v1"}}}}"#,
);
let obs = observe(&lanes, Some(&pj));
assert_eq!(
decide(slot(&obs, "futureAiBaseUrl"), None, &CTX),
Decision::Wire {
path: "/v2".into(),
origin: origin("https://api.future.ai"),
prior: SlotValue::Text("https://api.future.ai/v2".into()),
}
);
assert_eq!(
decide(slot(&obs, "pj:newgw"), None, &CTX),
Decision::Wire {
path: "/openai/v1".into(),
origin: origin("https://gw.new"),
prior: SlotValue::Text("https://gw.new/openai/v1".into()),
}
);
}
#[test]
fn unverified_rows_never_wire_an_unset_slot() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"actModeApiProvider":"oca"}"#);
let obs = observe(&lanes, None);
assert!(matches!(
decide(slot(&obs, "ocaBaseUrl"), None, &CTX),
Decision::Uncovered(_)
));
for row in GLOBAL_STATE_ROWS {
if row.row.evidence == Evidence::Unverified {
assert!(
!matches!(row.row.unset, Unset::Write { .. }) || row.row.never.is_some(),
"{} is unverified but carries a write rule that decide would have to ignore",
row.key
);
}
}
}
#[test]
fn an_unselected_unset_provider_is_not_configured() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"actModeApiProvider":"ollama","geminiBaseUrl":""}"#);
let obs = observe(&lanes, None);
assert!(
obs.slots
.iter()
.all(|s| !s.slot.record_key().ends_with("geminiBaseUrl")),
"an empty key for a provider nobody selected is not configuration"
);
}
#[test]
fn lanes_are_one_slot_on_a_real_host_and_two_in_a_sandbox() {
let tree = Tree::new();
let shared = tree.write("home/.cline/data/globalState.json", "{}");
let lanes = state_lanes_from(Some(shared.clone()), Some(shared));
assert_eq!(lanes.len(), 1);
assert_eq!(lanes[0].tag, LaneTag::Shared);
let legacy = tree.write(
"sb/cline/data/globalState.json",
r#"{"actModeApiProvider":"ollama"}"#,
);
let next = tree.write(
"sb/cline-data/globalState.json",
r#"{"actModeApiProvider":"ollama"}"#,
);
let lanes = state_lanes_from(Some(legacy), Some(next));
assert_eq!(
lanes.iter().map(|l| l.tag).collect::<Vec<_>>(),
vec![LaneTag::Legacy, LaneTag::Next]
);
let obs = observe(&lanes, None);
let keys: Vec<String> = obs.slots.iter().map(|s| s.slot.record_key()).collect();
assert!(
keys.contains(&"cline:gs:legacy:ollamaBaseUrl".to_string()),
"{keys:?}"
);
assert!(
keys.contains(&"cline:gs:next:ollamaBaseUrl".to_string()),
"{keys:?}"
);
for key in keys {
assert_eq!(
SlotId::from_record_key(&key).map(|s| s.record_key()),
Some(key.clone())
);
}
}
#[test]
fn observe_refuses_rather_than_truncates_an_oversized_state_file() {
let tree = Tree::new();
let body = format!(
r#"{{"actModeApiProvider":"ollama","taskHistory":"{}"}}"#,
"x".repeat(MAX_STATE_FILE_BYTES as usize)
);
let lanes = tree.shared_lane(&body);
let obs = observe(&lanes, None);
assert!(obs.slots.is_empty());
assert!(matches!(
obs.problems.as_slice(),
[FileProblem::TooLarge { .. }]
));
}
#[test]
fn a_providers_json_entry_is_a_slot_only_where_next_reads_it() {
let tree = Tree::new();
let lanes = tree.shared_lane("{}");
let pj = tree.write(
"data/settings/providers.json",
r#"{"lastUsedProvider":"deepseek","providers":{
"deepseek":{"settings":{"provider":"deepseek","apiKey":"sk"}},
"gemini":{"settings":{"baseUrl":"http://127.0.0.1:7601"}}}}"#,
);
let obs = observe(&lanes, Some(&pj));
assert_eq!(
decide(slot(&obs, "pj:deepseek"), None, &CTX),
Decision::Wire {
path: "/v1".into(),
origin: origin("https://api.deepseek.com"),
prior: SlotValue::Absent,
}
);
assert!(
obs.slots
.iter()
.all(|s| s.slot.record_key() != "cline:pj:gemini"),
"next reads gemini from globalState; its providers.json entry is a fallback"
);
}
#[test]
fn a_deleted_providers_json_entry_releases_its_endpoint() {
let tree = Tree::new();
let lanes = tree.shared_lane("{}");
let pj = tree.write("data/settings/providers.json", r#"{"providers":{}}"#);
let recorded: BTreeSet<String> = ["cline:pj:deepseek".to_string()].into();
let obs = observe_slots_in(&lanes, Some(&pj), &recorded);
let rec = record(
SlotValue::Absent,
"http://127.0.0.1:7601/v1",
SlotState::Wired,
);
assert_eq!(
decide(slot(&obs, "pj:deepseek"), Some(&rec), &CTX),
Decision::Release { restore: None }
);
}
#[test]
fn a_teardown_this_process_saw_is_not_undone() {
let tree = Tree::new();
let lanes = tree.shared_lane(r#"{"actModeApiProvider":"gemini"}"#);
let obs = observe(&lanes, None);
let gemini = slot(&obs, "geminiBaseUrl");
let mut rec = record(
SlotValue::Absent,
"http://127.0.0.1:7601",
SlotState::Released,
);
rec.released_by = Some(ReleasedBy::Teardown);
rec.changed_at = CTX.started_at + 5;
assert_eq!(decide(gemini, Some(&rec), &CTX), Decision::Skip);
rec.changed_at = CTX.started_at - 5;
assert!(matches!(
decide(gemini, Some(&rec), &CTX),
Decision::Wire { .. }
));
}
#[test]
fn a_relay_url_with_no_record_is_adopted_with_an_unset_prior() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"ollama","ollamaBaseUrl":"http://127.0.0.1:7605"}"#,
);
let obs = observe(&lanes, None);
assert_eq!(
decide(slot(&obs, "ollamaBaseUrl"), None, &CTX),
Decision::Wire {
path: String::new(),
origin: origin("http://127.0.0.1:11434"),
prior: SlotValue::Absent,
}
);
}
#[test]
fn a_customers_ollama_on_127_0_0_1_11434_is_not_ours() {
assert_eq!(
names_relay_listener("http://127.0.0.1:11434", &PORTS, None),
None
);
assert_eq!(
names_relay_listener("http://127.0.0.1:7605", &PORTS, None),
Some(7605)
);
assert_eq!(
names_relay_listener("http://127.0.0.1:7600/v1", &PORTS, None),
Some(7600)
);
assert_eq!(
names_relay_listener("http://localhost:7605", &PORTS, None),
None
);
assert_eq!(
names_relay_listener("http://127.0.0.1:7443", &PORTS, None),
None
);
}
#[test]
fn an_entry_on_a_previous_listener_port_is_still_reclaimable_with_its_record() {
let moved = RelayPorts {
daemon: 7443,
main: 7700,
};
assert_eq!(
names_relay_listener("http://127.0.0.1:7605", &moved, None),
None
);
assert_eq!(
names_relay_listener("http://127.0.0.1:7605", &moved, Some(7605)),
Some(7605)
);
}
#[test]
fn a_retired_main_port_wiring_is_reclaimed() {
let _lock = crate::config::OPENLATCH_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let tree = Tree::new();
let _env = crate::hooks::cline::EnvOverride::apply([(
"OPENLATCH_DIR",
Some(tree.root.join("openlatch").into_os_string()),
)]);
let pj = tree.write(
"data/settings/providers.json",
r#"{"providers":{"ollama":{"settings":{"baseUrl":"http://127.0.0.1:7600/v1"}},"gemini":{"settings":{"baseUrl":"http://127.0.0.1:7600/v1beta"}},"mine":{"settings":{"baseUrl":"http://127.0.0.1:11434"}}}}"#,
);
use crate::hooks::model_relay_endpoints as records;
records::record("cline:ollama", Some("http://127.0.0.1:11434".into())).expect("record");
records::record("cline:gemini", None).expect("record");
records::record(
&crate::hooks::upstream_record_key(WireFormat::OpenAiChatCompletions),
Some("http://127.0.0.1:11434".into()),
)
.expect("record");
reclaim_retired_in(&pj, 7600);
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&pj).expect("read")).expect("json");
assert_eq!(
doc["providers"]["ollama"]["settings"]["baseUrl"],
"http://127.0.0.1:11434"
);
assert!(doc["providers"]["gemini"]["settings"]
.get("baseUrl")
.is_none());
assert_eq!(
doc["providers"]["mine"]["settings"]["baseUrl"],
"http://127.0.0.1:11434"
);
assert_eq!(records::peek("cline:ollama"), None);
assert_eq!(
records::peek(&crate::hooks::upstream_record_key(
WireFormat::OpenAiChatCompletions
)),
None
);
}
#[test]
fn only_traffic_proves_a_slot_the_editor_may_not_route_by() {
let tree = Tree::new();
let lanes = tree.shared_lane(
r#"{"actModeApiProvider":"anthropic","planModeApiProvider":"gemini",
"lastManagedOrganizationId":"org_1"}"#,
);
let pj = tree.write(
"data/settings/providers.json",
r#"{"providers":{"deepseek":{"settings":{"provider":"deepseek"}}}}"#,
);
let obs = observe(&lanes, Some(&pj));
assert_eq!(
slot(&obs, "anthropicBaseUrl").traffic_only(),
Some(UncoveredReason::ManagedOverride)
);
assert_eq!(
slot(&obs, "geminiBaseUrl").traffic_only(),
None,
"remote configuration cannot override gemini's key"
);
assert_eq!(
slot(&obs, "pj:deepseek").traffic_only(),
Some(UncoveredReason::NextBundleOnly)
);
let unmanaged = Tree::new();
let lanes = unmanaged.shared_lane(r#"{"actModeApiProvider":"anthropic"}"#);
assert_eq!(
slot(&observe(&lanes, None), "anthropicBaseUrl").traffic_only(),
None
);
}
#[test]
fn only_a_lane_the_legacy_bundle_reads_is_checked_against_hook_providers() {
let tree = Tree::new();
let legacy = tree.write(
"sb/cline/data/globalState.json",
r#"{"actModeApiProvider":"ollama"}"#,
);
let next = tree.write(
"sb/cline-data/globalState.json",
r#"{"actModeApiProvider":"ollama"}"#,
);
let obs = observe(&state_lanes_from(Some(legacy), Some(next)), None);
assert!(slot(&obs, "legacy:ollamaBaseUrl").hooks_name_provider());
assert!(!slot(&obs, "next:ollamaBaseUrl").hooks_name_provider());
assert_eq!(
slot(&obs, "legacy:ollamaBaseUrl").provider_ids(),
vec!["ollama".to_string()]
);
let shared = Tree::new();
let lanes = shared.shared_lane(r#"{"actModeApiProvider":"ollama"}"#);
assert!(slot(&observe(&lanes, None), "ollamaBaseUrl").hooks_name_provider());
}
#[cfg(unix)]
#[test]
fn observe_slots_never_opens_secrets_json() {
let tree = Tree::new();
let legacy = tree.write(
"cline/data/globalState.json",
r#"{"actModeApiProvider":"gemini"}"#,
);
let next = tree.write(
"cline-data/globalState.json",
r#"{"actModeApiProvider":"gemini"}"#,
);
let lanes = state_lanes_from(Some(legacy.clone()), Some(next.clone()));
let pj = tree.write(
"cline-data/settings/providers.json",
r#"{"providers":{"deepseek":{"settings":{}}}}"#,
);
let excluded = [
tree.write("cline/data/secrets.json", r#"{"geminiApiKey":"SENTINEL"}"#),
tree.write("cline-data/secrets.json", r#"{"geminiApiKey":"SENTINEL"}"#),
tree.write("cline-data/tasks/task.json", "SENTINEL"),
tree.write("cline-data/sessions/s.json", "SENTINEL"),
];
let accessed = |p: &Path| {
std::fs::metadata(p)
.expect("stat")
.accessed()
.expect("atime")
};
let backdate = |p: &Path| {
let status = std::process::Command::new("touch")
.args(["-a", "-t", "202001010000"])
.arg(p)
.status()
.expect("touch");
assert!(status.success());
};
for p in excluded.iter().chain([&legacy, &next]) {
backdate(p);
}
let before: Vec<_> = excluded.iter().map(|p| accessed(p)).collect();
let (legacy_before, next_before) = (accessed(&legacy), accessed(&next));
let obs = observe(&lanes, Some(&pj));
assert!(!obs.slots.is_empty());
assert!(
accessed(&legacy) > legacy_before && accessed(&next) > next_before,
"positive control: both lane files were read, or atime does not record reads here"
);
for (path, was) in excluded.iter().zip(before) {
assert_eq!(accessed(path), was, "{} was opened", path.display());
}
}
}