Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions src/bin/opencompany.rs
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,23 @@ async fn main() -> Result<()> {
"{}",
opencompany::app::journal::pin_keyring(&journal).summary()
);
// Tag every request the embedded openhuman_core makes to the
// TinyHumans backend as opencompany's, not the vendored
// runtime's own `openhuman` default (issue #376). This must run
// here — before any company runtime, agent harness or HTTP
// listener exists — because core's `IntegrationClient` reads the
// identity into its default headers AT CONSTRUCTION
// (`harness/toolbelt.rs`, `harness/composio.rs`,
// `harness/search.rs` each build one the first time a company
// needs it), so a call after the first client already exists
// would not retroactively re-tag it. Same startup-ordering
// reasoning as the keyring pin directly above: say it here, once,
// rather than leaving it implicit in which line happens to run
// first.
// The call itself lives in the library so a test can reach it —
// this arm cannot be exercised from one.
#[cfg(feature = "openhuman")]
opencompany::product::install_into_embedded_core();
// Soft disk-quota alerting. Hard enforcement is the container /
// StorageClass layer's job (EFS access point, k8s ResourceQuota);
// here we surface an operator-visible warning when a workspace
Expand Down
9 changes: 9 additions & 0 deletions src/brain/medulla/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,18 @@ impl HttpSocketTransport {
// Belt-and-suspenders: never send a body carrying a `model` field.
wire::assert_no_model(&envelope)?;

let (product_header_name, product_header_value) = crate::product::product_identity_header();
let response = self
.client
.post(self.endpoint(path))
// `HttpSocketTransport` always targets the TinyHumans-owned
// Medulla endpoint (`base_url` is `https://api.tinyhumans.ai`),
// never a third party, so tagging it is unconditional. This is a
// bespoke `reqwest::Client`, not one built through
// `openhuman_core`'s `IntegrationClient`, so it never inherits
// the header `set_product_identity` attaches elsewhere — see
// `crate::product`.
.header(product_header_name, product_header_value)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.bearer_auth(self.credential.expose())
.json(&envelope)
.send()
Expand Down
21 changes: 20 additions & 1 deletion src/feedback/tinyhumans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ use crate::feedback::types::FeedbackCategory;

/// The product discriminator the hub routes on. Feedback from this runtime is
/// always attributed to opencompany, whichever company reported it.
pub const PRODUCT: &str = "opencompany";
///
/// Re-exported from [`crate::product::PRODUCT_IDENTITY`] rather than holding
/// its own `"opencompany"` literal: that module is this crate's single
/// source of truth for the product name, and a second copy of the string
/// here would be exactly the kind of duplicate this task exists to remove
/// (issue #376). If the two ever need to differ — e.g. the hub's `product`
/// field wants a different spelling than the `x-sdk-name` header — that is a
/// deliberate divergence to introduce explicitly, not something to default
/// into by leaving a stale literal in place.
pub const PRODUCT: &str = crate::product::PRODUCT_IDENTITY;

/// One feedback report to forward.
///
Expand Down Expand Up @@ -217,9 +226,19 @@ mod http {
"origin": request.origin,
"externalRef": request.external_ref,
});
let (product_header_name, product_header_value) =
crate::product::product_identity_header();
let resp = self
.http
.post(&url)
// This client bypasses the embedded openhuman_core entirely, so
// unlike the harness's `IntegrationClient`-backed calls it must
// tag itself with our product identity directly — see
// `crate::product`. `body` already carries the same value under
// `"product"`, but that is the hub's own routing field over the
// JSON payload; this header is the transport-level marker every
// backend endpoint reads, feedback or otherwise.
.header(product_header_name, product_header_value)
// The credential rides the header and only the header.
.bearer_auth(self.credential.expose())
.json(&body)
Expand Down
80 changes: 79 additions & 1 deletion src/harness/embeddings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,19 @@ impl HostedEmbeddings {
_ => text,
};

let mut http = self.client.post(&url).json(body);
let (product_header_name, product_header_value) = crate::product::product_identity_header();
let mut http = self
.client
.post(&url)
.json(body)
// `hosted_endpoint_from_env` (the sole resolver for `base_url`
// here) always points at the managed TinyHumans endpoint, never a
// BYOK third party, so tagging every request is unconditional.
// This is a bespoke `reqwest::Client`, not one built through
// `openhuman_core`'s `IntegrationClient`, so it never inherits
// the header `set_product_identity` attaches elsewhere — see
// `crate::product`.
.header(product_header_name, product_header_value);
if let Some(bearer) = &bearer {
http = http.bearer_auth(bearer);
}
Expand Down Expand Up @@ -457,6 +469,72 @@ mod tests {
);
}

/// Wire-level proof for issue #376 (AC #1 + AC #3): a real HTTP request
/// this client sends must carry `x-sdk-name: opencompany` — not the
/// vendored `openhuman_core` crate's own `openhuman` default, and not
/// nothing. `HostedEmbeddings` was picked for this proof, of the three
/// direct backend clients this issue touches (feedback, the Medulla HTTP
/// transport, and this one): the other two live behind the `tinyhumans`
/// / `medulla` Cargo features respectively, so their tests only compile
/// under a feature set this crate's `openhuman` build does not enable by
/// default, while `harness/` — and therefore this file's tests — already
/// compiles and runs under plain `--features openhuman` (`reqwest` rides
/// in on that feature; see `Cargo.toml`).
///
/// Deliberately a small standalone server rather than reusing
/// `spawn_server`/`Mode` above: this test cares about exactly one header
/// on exactly one request, and threading header capture through the
/// shared multi-mode fixture would only add indirection for it.
#[tokio::test]
async fn embed_request_carries_the_product_identity_header() {
use axum::Json;
use axum::http::HeaderMap;
use axum::routing::post;

let captured_header: Arc<StdMutex<Option<String>>> = Arc::new(StdMutex::new(None));
let capture = Arc::clone(&captured_header);
let app = axum::Router::new().route(
"/embeddings",
post(
move |headers: HeaderMap, Json(body): Json<serde_json::Value>| {
let capture = Arc::clone(&capture);
async move {
*capture.lock().unwrap() = headers
.get(crate::product::PRODUCT_IDENTITY_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::to_string);
// One 4-dim embedding per input, so this stays compatible
// with the `dim: 4` backend built below regardless of how
// many strings a future edit passes to `embed`.
let inputs = body
.get("input")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(1);
let data: Vec<serde_json::Value> = (0..inputs)
.map(|i| serde_json::json!({ "index": i, "embedding": vec![0.0_f64; 4] }))
.collect();
Json(serde_json::json!({ "data": data }))
}
},
),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});

let be = backend(format!("http://{addr}"), 4);
be.embed(&["hello"]).await.expect("embeds against the stub");

assert_eq!(
captured_header.lock().unwrap().as_deref(),
Some("opencompany"),
"the embeddings client must attach x-sdk-name: opencompany on every request"
);
}

#[tokio::test]
async fn retries_once_after_unauthorized() {
let (url, seen) = spawn_server(Mode::UnauthorizedThenOk { dim: 4 }).await;
Expand Down
149 changes: 148 additions & 1 deletion src/harness/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,20 @@ impl ChatModel<()> for HostedProvider {
"{}/chat/completions",
self.config.base_url.trim_end_matches('/')
);
let mut http = self.client.post(&url).json(&body);
let (product_header_name, product_header_value) = crate::product::product_identity_header();
Comment thread
senamakel marked this conversation as resolved.
let mut http = self
.client
.post(&url)
.json(&body)
// `HostedProvider` always speaks to the TinyHumans-owned managed
// endpoint (`DEFAULT_TINYHUMANS_INFERENCE_URL`), never a
// third-party BYOK host, so tagging it with our product identity
// is unconditional. This client is a bespoke `reqwest::Client`,
// not one built through `openhuman_core`'s `IntegrationClient`,
// so it does not inherit the header set by
// `set_product_identity` and must attach it itself — see
// `crate::product`.
.header(product_header_name, product_header_value);
// Resolved per request, never captured: on the hosted platform this reads
// a token file the cluster rewrites in place every few minutes.
let bearer = self.config.credential.current().await.map_err(|e| {
Expand Down Expand Up @@ -771,6 +784,18 @@ pub async fn request_plan(
("HTTP-Referer", OPENROUTER_REFERER.to_string()),
("X-Title", OPENROUTER_TITLE.to_string()),
]
} else if decl.provider == "managed" {
// Only `"managed"` is a TinyHumans-owned endpoint. The other three
// `INFERENCE_PROVIDERS` (`openrouter`, `openai_compatible`, `ollama`
// — see `company::types::INFERENCE_PROVIDERS`) are bring-your-own-key
// THIRD-PARTY endpoints (OpenAI, OpenRouter, DeepSeek, a
// self-hosted/local Ollama); sending them our `x-sdk-name` would leak
// which product a tenant is running to an operator who has no
// relationship with TinyHumans and gains nothing from knowing it.
// `openrouter` already gets its own attribution headers above — those
// are OpenRouter's own dashboard/rankings feature, unrelated to this.
let (name, value) = crate::product::product_identity_header();
vec![(name, value.to_string())]
} else {
Vec::new()
};
Expand Down Expand Up @@ -1723,6 +1748,128 @@ mod tests {
assert!(plan.headers.is_empty(), "no OpenRouter headers for Ollama");
}

/// The positive half of issue #376 (AC #1): `provider = "managed"` always
/// targets a TinyHumans-owned endpoint, so [`request_plan`] must attach
/// our `x-sdk-name: opencompany` product header alongside the tier's
/// other headers.
#[tokio::test]
async fn request_plan_attaches_the_product_header_for_managed() {
let company = CompanyId::new("acme");
let secrets = MemSecrets::default();
let env = crate::company::inference::EnvDefault {
base_url: "https://env.example/openai/v1".into(),
credential: Credential::from_value("platform-key"),
};
// A hand-written `provider = "managed"` still resolves through the
// env default (mirrors `manifest_managed_inherits_env_credential` in
// `company::inference`'s own test suite) — this is the shape a real
// company manifest produces, not a synthetic decl.
let decl = inference::resolve_effective(
&company,
&manifest_inference("managed"),
Some(&env),
&secrets,
)
.await
.unwrap()
.expect("managed resolves via the env default");
assert_eq!(decl.provider, "managed");

let plan = request_plan(
&decl,
"chat-v1",
Vec::new(),
0.2,
None,
Vec::new(),
&ToolChoice::Auto,
)
.await
.expect("plan");
assert!(
plan.headers
.contains(&("x-sdk-name", "opencompany".to_string())),
"managed provider must carry the product header: {:?}",
plan.headers
);
}

/// The negative half of issue #376 (AC #1) — and the important one, per
/// the task: `openrouter` and `openai_compatible` are bring-your-own-key
/// THIRD-PARTY endpoints (OpenRouter's own API, and any OpenAI-compatible
/// host an operator points at — OpenAI, DeepSeek, a self-hosted proxy,
/// …). Sending them our product identity would tell a company we have no
/// relationship with which product a tenant is running, for no benefit to
/// anyone. Only `"managed"` (see the test above) may ever carry the
/// header.
#[tokio::test]
async fn request_plan_never_attaches_the_product_header_for_third_party_providers() {
let company = CompanyId::new("acme");
let secrets = MemSecrets::default();

// openrouter: gets ITS OWN attribution headers, never ours.
let mut or_manifest = manifest_inference("openrouter");
or_manifest.models =
BTreeMap::from([("chat-v1".to_string(), "deepseek/deepseek-chat".to_string())]);
inference::store_key(&company, &secrets, "or-key")
.await
.unwrap();
let or_decl = inference::resolve_effective(&company, &or_manifest, None, &secrets)
.await
.unwrap()
.unwrap();
let or_plan = request_plan(
&or_decl,
"chat-v1",
Vec::new(),
0.2,
None,
Vec::new(),
&ToolChoice::Auto,
)
.await
.expect("plan");
assert!(
!or_plan
.headers
.iter()
.any(|(name, _)| *name == "x-sdk-name"),
"openrouter is third-party and must never see our product identity: {:?}",
or_plan.headers
);
assert!(
or_plan
.headers
.contains(&("HTTP-Referer", OPENROUTER_REFERER.to_string())),
"openrouter's own attribution headers must be unaffected: {:?}",
or_plan.headers
);

// openai_compatible: a bring-your-own-endpoint host — no headers at all.
let mut compat_manifest = manifest_inference("openai_compatible");
compat_manifest.base_url = Some("https://byok.example/v1".into());
let compat_decl = inference::resolve_effective(&company, &compat_manifest, None, &secrets)
.await
.unwrap()
.unwrap();
let compat_plan = request_plan(
&compat_decl,
"chat-v1",
Vec::new(),
0.2,
None,
Vec::new(),
&ToolChoice::Auto,
)
.await
.expect("plan");
assert!(
compat_plan.headers.is_empty(),
"openai_compatible is third-party and must carry no headers at all: {:?}",
compat_plan.headers
);
}

/// Spawns an in-process OpenAI-compatible stub that echoes `marker` as the
/// completion content. The listener is bound before the task spawns, so the
/// OS accepts connections into the backlog immediately.
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub mod metering;
pub mod openhuman;
pub mod policy;
pub mod ports;
/// The `x-sdk-name: opencompany` identity attached to this crate's own
/// backend HTTP clients. Ungated (no `openhuman` feature requirement) because
/// `brain/` and `feedback/` need it and neither is feature-gated.
pub mod product;
pub mod runtime;
pub mod server;
pub mod store;
Expand Down
Loading
Loading