app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.3.3 source + crate build: BOM exported from the SOLIDWORKS assembly, build log §1–11, release script
477e64f
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
//! Caller provenance — WHO asked.
//!
//! A user runs ~20 AI threads against one desktop. ab stamps the caller on every
//! relayed request TWO ways and we read both:
//!
//! * `args.caller = {aiThread, containerName, source}` — what the CLI's
//! `--ai-thread` flag actually produces, and what shows up in practice;
//! * `X-Adom-Caller-Thread` / `-Container` / `-Reason` headers — the form ab
//! uses on its own direct-API callbacks and what the SDK samples forward.
//!
//! args wins when both are present (it carries the explicit identity).
//!
//! TRUST RULE (ab's, and not optional): these values are SELF-ASSERTED, exactly
//! like `reason`. Use them for logging, UX and window labels — NEVER for
//! authorization. `if thread == "admin" { allow() }` is a hole, not a check.
//!
//! Identity is thread-local: each HTTP request is served on its own worker
//! thread, so no request can see another's caller.
use serde_json::Value;
use std::cell::RefCell;
/// What we call ourselves when we forward identity on a callback to ab.
pub const DELEGATE: &str = "solidworks";
pub const HEADERS: [&str; 3] =
["X-Adom-Caller-Thread", "X-Adom-Caller-Container", "X-Adom-Caller-Reason"];
#[derive(Default, Clone, Debug)]
pub struct Identity {
pub thread: String,
pub container: String,
pub reason: String,
}
thread_local! {
static CURRENT: RefCell<Option<Identity>> = const { RefCell::new(None) };
}
/// Capture the caller of the request this thread is about to serve.
pub fn set_from_request(header: impl Fn(&str) -> Option<String>, args: &Value) -> Identity {
let mut ident = Identity {
thread: header(HEADERS[0]).unwrap_or_default(),
container: header(HEADERS[1]).unwrap_or_default(),
reason: header(HEADERS[2]).unwrap_or_default(),
};
if let Some(caller) = args.get("caller").and_then(|c| c.as_object()) {
if let Some(t) = caller.get("aiThread").and_then(|v| v.as_str()) {
if !t.is_empty() {
ident.thread = t.to_string();
}
}
if let Some(c) = caller.get("containerName").and_then(|v| v.as_str()) {
if !c.is_empty() {
ident.container = c.to_string();
}
}
}
if ident.reason.is_empty() {
if let Some(r) = args.get("reason").and_then(|v| v.as_str()) {
ident.reason = r.to_string();
}
}
CURRENT.with(|c| *c.borrow_mut() = Some(ident.clone()));
ident
}
pub fn clear() {
CURRENT.with(|c| *c.borrow_mut() = None);
}
pub fn current() -> Option<Identity> {
CURRENT.with(|c| c.borrow().clone())
}
/// The ` caller="..."` fragment for the one-line-per-verb log, or "" when the
/// caller is anonymous (loopback callers — Hydrogen, another bridge, the local
/// CLI — send nothing, and that is fine).
pub fn log_suffix() -> String {
match current() {
Some(i) if !i.thread.is_empty() => format!(" caller=\"{}\"", i.thread),
_ => String::new(),
}
}
/// Headers to put on a callback we make WHILE serving an AI's verb: echo the
/// three we were handed, and add the Delegate naming us — so ab's Activity Log
/// can show BOTH facts, "chip-fetcher tab 3 (via solidworks)".
pub fn forward_headers() -> Vec<(String, String)> {
let ident = match current() {
Some(i) if !i.thread.is_empty() => i,
_ => return Vec::new(),
};
vec![
(HEADERS[0].to_string(), ident.thread),
(HEADERS[1].to_string(), ident.container),
(HEADERS[2].to_string(), ident.reason),
("X-Adom-Caller-Delegate".to_string(), DELEGATE.to_string()),
]
}
/// The `caller` args block for a callback the bridge makes on its OWN behalf
/// (a poll, a timer, cleanup): an explicit caller in args always beats the
/// forwarded headers, so no stale thread name can leak onto our own work.
pub fn self_caller() -> Value {
serde_json::json!({"aiThread": "solidworks bridge (self)", "containerName": "local"})
}
/// Call an ab verb back over the loopback direct API, forwarding the caller.
///
/// ab injects `ADOM_DIRECT_API_URL` and `ADOM_BRIDGE_TOKEN` into our spawn env.
/// NEVER raises and never fails the verb it is helping: a delegated screenshot
/// that ab refuses is a partial answer, not a broken `swx_screenshot`.
/// Is ab's direct API reachable at all? False when this bridge was started by
/// hand instead of spawned by Adom Bridge, in which case every ab-delegated verb
/// fails for a reason unrelated to SOLIDWORKS — worth being able to say up front.
pub fn ab_available() -> bool {
std::env::var("ADOM_DIRECT_API_URL").map(|v| !v.trim().is_empty()).unwrap_or(false)
}
pub fn ad_callback(command: &str, args: Value, timeout: std::time::Duration) -> Value {
let base = std::env::var("ADOM_DIRECT_API_URL").unwrap_or_default();
let base = base.trim_end_matches('/').to_string();
if base.is_empty() {
return serde_json::json!({
"success": false,
"error": "ADOM_DIRECT_API_URL not in env (inbound-only spawn?)",
});
}
let mut args = args;
let mut headers = forward_headers();
if headers.is_empty() {
// No caller to forward — this is our own initiative, so say so.
if let Value::Object(map) = &mut args {
map.entry("caller").or_insert_with(self_caller);
}
}
if let Ok(token) = std::env::var("ADOM_BRIDGE_TOKEN") {
if !token.is_empty() {
// Attribution badge, not an auth gate.
headers.push(("X-Adom-Bridge-Token".to_string(), token));
}
}
let body = serde_json::json!({"command": command, "args": args}).to_string();
match crate::httpc::post_json(&format!("{base}/command"), &headers, &body, timeout) {
Ok(resp) => resp
.json()
.unwrap_or_else(|e| serde_json::json!({"success": false, "error": e})),
Err(e) => serde_json::json!({"success": false, "error": format!("ab callback failed: {e}")}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn no_headers(_: &str) -> Option<String> {
None
}
#[test]
fn args_caller_beats_headers() {
let ident = set_from_request(
|h| match h {
"X-Adom-Caller-Thread" => Some("from-header".into()),
_ => None,
},
&json!({"caller": {"aiThread": "from-args", "containerName": "box"}}),
);
assert_eq!(ident.thread, "from-args");
assert_eq!(ident.container, "box");
assert_eq!(log_suffix(), " caller=\"from-args\"");
clear();
}
#[test]
fn anonymous_callers_log_and_forward_nothing() {
set_from_request(no_headers, &json!({}));
assert_eq!(log_suffix(), "");
assert!(forward_headers().is_empty());
clear();
}
#[test]
fn reason_falls_back_to_the_arg_and_the_delegate_is_added() {
set_from_request(
|h| (h == "X-Adom-Caller-Thread").then(|| "tab-3".to_string()),
&json!({"reason": "prototyping a verb"}),
);
let fwd = forward_headers();
assert!(fwd.contains(&("X-Adom-Caller-Delegate".into(), "solidworks".into())));
assert!(fwd.contains(&("X-Adom-Caller-Reason".into(), "prototyping a verb".into())));
clear();
}
}