use std::collections::{BTreeMap, HashMap};
use crate::api::editor::EditorBindingRow;
use crate::error::{OlError, OL_4222_BINDING_NOT_CONFIGURED};
use crate::generated::Manifest;
pub const DEFAULT_LOCAL_ENDPOINT: &str = "http://127.0.0.1:8080/event";
#[derive(Debug, Clone)]
pub struct LocalRoute {
pub binding_id: String,
pub tool_slug: String,
pub provider_slug: String,
pub local_url: String,
}
#[derive(Debug, Clone, Default)]
pub struct RouteTable {
by_binding_id: HashMap<String, LocalRoute>,
}
impl RouteTable {
pub fn empty() -> Self {
Self::default()
}
pub fn lookup(&self, binding_id: &str) -> Option<&LocalRoute> {
self.by_binding_id.get(binding_id)
}
pub fn len(&self) -> usize {
self.by_binding_id.len()
}
pub fn is_empty(&self) -> bool {
self.by_binding_id.is_empty()
}
pub fn binding_ids(&self) -> impl Iterator<Item = &String> {
self.by_binding_id.keys()
}
pub fn routes(&self) -> impl Iterator<Item = &LocalRoute> {
self.by_binding_id.values()
}
}
pub fn resolve_routes(
manifest: &Manifest,
live_bindings: &[EditorBindingRow],
known_secret_ids: &[String],
) -> Result<RouteTable, OlError> {
let known_secrets: std::collections::HashSet<&str> =
known_secret_ids.iter().map(String::as_str).collect();
let mut by_pair: BTreeMap<(String, String), &EditorBindingRow> = BTreeMap::new();
for row in live_bindings {
by_pair.insert((row.tool.clone(), row.provider.clone()), row);
}
let mut table = HashMap::new();
let mut missing_ids: Vec<String> = Vec::new();
for binding in manifest.bindings.iter() {
let tool_slug = binding.tool.clone();
let provider_slug = binding.provider.clone();
let pair_key = (tool_slug.clone(), provider_slug.clone());
let Some(live) = by_pair.get(&pair_key) else {
return Err(OlError::new(
OL_4222_BINDING_NOT_CONFIGURED,
format!(
"manifest binding (tool=`{}`, provider=`{}`) is not registered with the \
platform — run `openlatch-provider register` first",
tool_slug, provider_slug,
),
));
};
if !known_secrets.contains(live.id.as_str()) {
missing_ids.push(live.id.clone());
continue;
}
let local_url = local_endpoint_for(binding);
table.insert(
live.id.clone(),
LocalRoute {
binding_id: live.id.clone(),
tool_slug: tool_slug.clone(),
provider_slug: provider_slug.clone(),
local_url,
},
);
}
if !missing_ids.is_empty() {
let joined = missing_ids.join(", ");
return Err(OlError::new(
OL_4222_BINDING_NOT_CONFIGURED,
format!(
"no local secret available for binding(s) [{joined}] — \
the runtime cannot verify inbound webhook signatures"
),
)
.with_suggestion(
"Re-issue the secret with `openlatch-provider bindings rotate-secret <id>`. \
The platform-side secret rotates atomically and the new plaintext is \
revealed to you exactly once.",
));
}
Ok(RouteTable {
by_binding_id: table,
})
}
fn local_endpoint_for(binding: &crate::generated::ManifestBinding) -> String {
binding
.local_endpoint
.clone()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_LOCAL_ENDPOINT.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest_with_one_binding() -> Manifest {
let yaml = r#"
schema_version: 1
editor:
slug: my-editor
display_name: My Editor
description: One-binding test fixture used to exercise resolve_routes.
tools:
- slug: pii-fast
version: 1.0.0
license: apache-2.0
description: Fixture tool for the runtime route-resolver tests.
hooks_supported: [pre_tool_use]
agents_supported: [claude-code]
capabilities:
- threat_category: pii_outbound
execution_mode: sync
declared_latency_p95_ms: 80
needs_raw_payload: false
providers:
- slug: example-prv-us
display_name: Example US
region: us-east-1
total_capacity_qps: 500
endpoint_url: https://provider-us.example.io/v1/event
bindings:
- tool: pii-fast
provider: example-prv-us
local_endpoint: http://127.0.0.1:8081/event
declared_latency_p95_ms: 80
capacity_qps: 500
priority: 100
pricing:
unit: per_event
amount_usd: 0.0002
process:
command: ["./pii-fast"]
health_check:
http:
port: 8081
"#;
crate::manifest::parse(yaml.as_bytes()).expect("fixture parses")
}
fn live_row(id: &str, tool: &str, provider: &str) -> EditorBindingRow {
EditorBindingRow {
id: id.into(),
tool: tool.into(),
provider: provider.into(),
state: Some("active".into()),
routing_score: None,
}
}
#[test]
fn resolve_routes_happy_path() {
let m = manifest_with_one_binding();
let live = vec![live_row("bnd_42", "pii-fast", "example-prv-us")];
let secrets = vec!["bnd_42".to_string()];
let table = resolve_routes(&m, &live, &secrets).unwrap();
assert_eq!(table.len(), 1);
let route = table.lookup("bnd_42").expect("present");
assert_eq!(route.tool_slug, "pii-fast");
assert_eq!(route.provider_slug, "example-prv-us");
assert_eq!(route.local_url, "http://127.0.0.1:8081/event");
}
#[test]
fn resolve_routes_uses_default_local_endpoint() {
let yaml_no_override = r#"
schema_version: 1
editor:
slug: my-editor
display_name: My Editor
description: Fixture exercising the local_endpoint default fallback.
tools:
- slug: pii-fast
version: 1.0.0
license: apache-2.0
description: Fixture tool for the runtime route-resolver tests.
hooks_supported: [pre_tool_use]
agents_supported: [claude-code]
capabilities:
- threat_category: pii_outbound
execution_mode: sync
declared_latency_p95_ms: 80
needs_raw_payload: false
providers:
- slug: example-prv-us
display_name: Example US
region: us-east-1
total_capacity_qps: 500
endpoint_url: https://provider-us.example.io/v1/event
bindings:
- tool: pii-fast
provider: example-prv-us
declared_latency_p95_ms: 80
capacity_qps: 500
priority: 100
pricing:
unit: per_event
amount_usd: 0.0002
process:
command: ["./pii-fast"]
health_check:
http:
port: 8081
"#;
let m = crate::manifest::parse(yaml_no_override.as_bytes()).unwrap();
let live = vec![live_row("bnd_42", "pii-fast", "example-prv-us")];
let secrets = vec!["bnd_42".to_string()];
let table = resolve_routes(&m, &live, &secrets).unwrap();
let route = table.lookup("bnd_42").unwrap();
assert_eq!(route.local_url, DEFAULT_LOCAL_ENDPOINT);
}
#[test]
fn resolve_routes_4222_when_platform_doesnt_know_pair() {
let m = manifest_with_one_binding();
let live = vec![]; let err = resolve_routes(&m, &live, &[]).unwrap_err();
assert_eq!(err.code.code, "OL-4222");
}
#[test]
fn resolve_routes_4222_when_secret_missing_locally() {
let m = manifest_with_one_binding();
let live = vec![live_row("bnd_42", "pii-fast", "example-prv-us")];
let err = resolve_routes(&m, &live, &[]).unwrap_err();
assert_eq!(err.code.code, "OL-4222");
}
}