Skip to content

Commit 4001253

Browse files
committed
fix(resolver): Phase 1 — Root-cause and fix Tier 1 binding mismatch
When X-Mcpmux-Machine-Id names a device with no binding for the reported root, fall through to client/local machine lookup for identified OAuth clients instead of skipping straight to Unbound. Preserves tunnel isolation for anonymous header-only callers. Autonomous decisions: - Root cause from live logs + DB: header branch `continue` skipped client machine fallback while binding exists on Gondor — likely stale/wrong machine header on native Cursor OAuth after Cloud Agents work - Removed temporary debug instrumentation; kept fix-only diff - Added client_icon: None to test InboundClient builders (branch WIP field) Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 259a3c0 commit 4001253

6 files changed

Lines changed: 106 additions & 10 deletions

File tree

crates/mcpmux-gateway/src/services/feature_set_resolver.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ impl FeatureSetResolverService {
264264
request_machine_id: Option<Uuid>,
265265
) -> Result<Option<mcpmux_core::WorkspaceBinding>> {
266266
for root in roots {
267+
let registered_machine = if let Some(cid) = client_id {
268+
self.client_repo.get_machine_id(cid).await?
269+
} else {
270+
None
271+
};
272+
267273
if let Some(header_machine) = request_machine_id {
268274
if let Some(binding) = self
269275
.binding_repo
@@ -275,17 +281,24 @@ impl FeatureSetResolverService {
275281
if let Some(binding) = self.binding_repo.find_exact_global(root).await? {
276282
return Ok(Some(binding));
277283
}
278-
continue;
284+
// Header-only semantics when the header matches the OAuth
285+
// client's registered machine (tunneled caller on the wrong
286+
// box stays Unbound) or the caller is anonymous. When the
287+
// header disagrees with the registered tag it is treated as
288+
// stale — e.g. cloud-agent config on a native localhost
289+
// session — and we fall through to client/local/global below.
290+
if client_id.is_none() || registered_machine.is_some_and(|m| m == header_machine) {
291+
continue;
292+
}
279293
}
280-
if let Some(cid) = client_id {
281-
if let Some(client_machine) = self.client_repo.get_machine_id(cid).await? {
282-
if let Some(binding) = self
283-
.binding_repo
284-
.find_exact_for_machine(&client_machine, root, Some(cid))
285-
.await?
286-
{
287-
return Ok(Some(binding));
288-
}
294+
295+
if let Some(client_machine) = registered_machine {
296+
if let Some(binding) = self
297+
.binding_repo
298+
.find_exact_for_machine(&client_machine, root, client_id)
299+
.await?
300+
{
301+
return Ok(Some(binding));
289302
}
290303
}
291304
if let Some(local_id) = *self.local_machine_id.read().await {

crates/mcpmux-storage/src/repositories/workspace_binding_repository.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,49 @@ mod tests {
606606
assert!(hits_other.is_empty());
607607
}
608608

609+
/// Regression: machine-scoped canonical binding (`client_id IS NULL`) must
610+
/// match when the caller passes its OAuth `client_id` for the specific
611+
/// lookup — the repo falls back to the machine-only row after no
612+
/// client+machine row exists.
613+
#[tokio::test]
614+
async fn test_find_exact_for_machine_canonical_matches_registered_client() {
615+
let (repo, space_id, fs_id) = fixture().await;
616+
let db = repo.db.clone();
617+
let machine_id = Uuid::new_v4();
618+
let client_id = "mcp_36740f70";
619+
let root = if cfg!(windows) {
620+
"d:\\jsg-tech-check"
621+
} else {
622+
"/Users/joe/Desktop/Repos/Personal/jsg-tech-check"
623+
};
624+
let now = Utc::now().to_rfc3339();
625+
626+
{
627+
let guard = db.lock().await;
628+
guard
629+
.connection()
630+
.execute(
631+
"INSERT INTO machines (id, name, created_at, updated_at)
632+
VALUES (?1, 'Gondor', ?2, ?2)",
633+
params![machine_id.to_string(), now],
634+
)
635+
.unwrap();
636+
}
637+
638+
let binding =
639+
WorkspaceBinding::new_machine_scoped_multi(root, space_id, machine_id, vec![fs_id]);
640+
repo.create(&binding).await.unwrap();
641+
642+
let hit = repo
643+
.find_exact_for_machine(&machine_id, root, Some(client_id))
644+
.await
645+
.unwrap()
646+
.expect("machine-scoped canonical binding should match registered client");
647+
assert_eq!(hit.workspace_root, root);
648+
assert_eq!(hit.machine_id, Some(machine_id));
649+
assert!(hit.client_id.is_none());
650+
}
651+
609652
#[tokio::test]
610653
async fn test_find_exact_for_roots_is_exact_only() {
611654
// No ancestor inheritance: a binding on `outer` must NOT match a

tests/rust/tests/database/inbound_client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ fn create_test_client(name: &str) -> InboundClient {
4040
reports_roots: false,
4141
roots_capability_known: false,
4242
machine_id: None,
43+
client_icon: None,
4344
}
4445
}
4546

tests/rust/tests/integration/feature_set_resolver.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ impl Fixture {
196196
reports_roots: false,
197197
roots_capability_known: false,
198198
machine_id: None,
199+
client_icon: None,
199200
};
200201
self.client_repo.save_client(&c).await.unwrap();
201202
}
@@ -1146,6 +1147,42 @@ async fn request_machine_header_outranks_client_and_local_machine() {
11461147
assert_eq!(with_rohan_header.feature_set_ids, vec![f.fs_b_id]);
11471148
}
11481149

1150+
#[tokio::test]
1151+
async fn wrong_request_machine_header_falls_back_to_client_machine_binding() {
1152+
// Native Cursor OAuth can inherit a stale X-Mcpmux-Machine-Id (e.g. Rohan
1153+
// from a shared tunnel config) while the binding + client tag live on Gondor.
1154+
let f = Fixture::new().await;
1155+
let gondor_id = f.make_machine("Gondor").await;
1156+
let rohan_id = f.make_machine("Rohan").await;
1157+
let root = normalize_workspace_root(if cfg!(windows) {
1158+
"d:\\jsg-tech-check"
1159+
} else {
1160+
"/Users/joe/Desktop/Repos/Personal/jsg-tech-check"
1161+
});
1162+
let client_id = "mcp_36740f70";
1163+
1164+
f.make_client(client_id).await;
1165+
f.client_repo
1166+
.set_machine_id(client_id, Some(gondor_id))
1167+
.await
1168+
.unwrap();
1169+
1170+
let mut binding = WorkspaceBinding::new(root.clone(), f.space_id, f.fs_a_id.clone());
1171+
binding.machine_id = Some(gondor_id);
1172+
f.binding_repo.create(&binding).await.unwrap();
1173+
1174+
f.session_roots.set("s", [root.as_str()]);
1175+
f.session_roots.set_roots_capable("s", true);
1176+
1177+
let resolver = f.resolver_with_local_machine(gondor_id);
1178+
let r = resolver
1179+
.resolve(Some("s"), Some(client_id), Some(rohan_id))
1180+
.await
1181+
.unwrap();
1182+
assert_eq!(r.source, ResolutionSource::WorkspaceBinding);
1183+
assert_eq!(r.feature_set_ids, vec![f.fs_a_id]);
1184+
}
1185+
11491186
#[tokio::test]
11501187
async fn request_machine_header_enables_deny_when_only_other_machine_bound() {
11511188
let f = Fixture::new().await;

tests/rust/tests/streamable_http/api_key_auth.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl Harness {
109109
reports_roots: false,
110110
roots_capability_known: false,
111111
machine_id: None,
112+
client_icon: None,
112113
};
113114
client_repo.save_client(&client).await.expect("save client");
114115
let key_id = Uuid::new_v4().to_string();

tests/rust/tests/streamable_http/gateway_notifications.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ impl TestGateway {
159159
reports_roots: false,
160160
roots_capability_known: false,
161161
machine_id: None,
162+
client_icon: None,
162163
};
163164
inbound_client_repo
164165
.save_client(&test_client)

0 commit comments

Comments
 (0)