@@ -80,6 +80,46 @@ def _get_gateway_container(self) -> Optional[docker.models.containers.Container]
8080 logger .error (f"Error getting Gateway container: { e } " )
8181 return None
8282
83+ def is_running (self ) -> bool :
84+ """True when the Gateway container exists and is running.
85+
86+ This is the direct signal for "is there a Gateway to talk to". Callers must not
87+ infer it from cert presence: certs are generated up front so the mTLS client is
88+ always usable, so their existence says nothing about whether the Gateway is up.
89+ """
90+ container = self ._get_gateway_container ()
91+ return container is not None and container .status == "running"
92+
93+ @staticmethod
94+ def _normalize_mount_source (source : str ) -> str :
95+ """Normalize a Docker-reported bind-mount source to a comparable host path.
96+
97+ Docker Desktop reports macOS/Windows host paths through a ``/host_mnt`` prefix
98+ (host ``/Users/x`` surfaces as ``/host_mnt/Users/x``), so a raw string compare
99+ against a configured host path would never match there.
100+ """
101+ normalized = os .path .normpath (source or "" )
102+ for prefix in ("/host_mnt" , "/run/desktop/mnt/host" ):
103+ if normalized .startswith (prefix + os .sep ):
104+ normalized = normalized [len (prefix ):]
105+ return normalized
106+
107+ def _mounts_shared_certs (self , container ) -> bool :
108+ """True when the container mounts *this* API's shared cert set.
109+
110+ The Gateway reads its server cert from the directory we bind-mount at
111+ ``GATEWAY_CERTS_BIND``. If that mount's source is our cert dir, the Gateway is
112+ serving a cert signed by the CA our clients trust - consistent by construction.
113+ A Gateway started by a different API instance (or a different checkout) mounts a
114+ different source path, which is exactly the mismatch we need to detect.
115+ """
116+ expected = self ._normalize_mount_source (gateway_certs_dir (host = True ))
117+ for mount in container .attrs .get ("Mounts" , []):
118+ if mount .get ("Destination" ) != self .GATEWAY_CERTS_BIND :
119+ continue
120+ return self ._normalize_mount_source (mount .get ("Source" , "" )) == expected
121+ return False
122+
83123 def _get_self_container (self ) -> Optional [docker .models .containers .Container ]:
84124 """Best-effort lookup of the container this API process is running in.
85125
@@ -309,16 +349,17 @@ def start(self, config: GatewayConfig) -> Dict[str, Any]:
309349 def reconcile_certs (self ) -> Dict [str , Any ]:
310350 """Make a running Gateway usable by this API's mTLS client.
311351
312- "Was the Gateway started with this API?" reduces to: does the API hold the shared client
313- cert set? The set lives on the bots/ volume both containers share, so a running Gateway
314- with the certs absent on the API side was not started by (or is inconsistent with) this
315- API instance — every secured request would fail the mTLS handshake.
352+ "Was the Gateway started with this API?" reduces to: does the running container mount
353+ *our* cert dir as its server cert source? Cert presence cannot answer this — the set is
354+ generated up front at API startup, so it is present even for a Gateway this API never
355+ started. A Gateway serving a cert from some other source would fail our mTLS handshake
356+ on every request.
316357
317358 Decision matrix:
318- - container not running -> nothing (start the Gateway to generate certs )
319- - running + certs present -> nothing (already consistent)
320- - running + certs missing -> regenerate the cert set and restart the Gateway so it
321- loads the server cert that matches our client cert
359+ - container not running -> nothing (start the Gateway)
360+ - running + mounts our certs -> nothing (consistent by construction )
361+ - running + mounts other/no certs -> regenerate the cert set and restart the Gateway so it
362+ loads the server cert that matches our client cert
322363
323364 The restart is required: the Gateway reads its server cert/key only at startup, so writing
324365 new certs to the shared volume has no effect on an already-running container.
@@ -331,15 +372,15 @@ def reconcile_certs(self) -> Dict[str, Any]:
331372 "message" : "Gateway container not running; start it to generate certs" ,
332373 }
333374
334- if certs_present (gateway_certs_dir ()):
375+ if self . _mounts_shared_certs ( container ) and certs_present (gateway_certs_dir ()):
335376 return {
336377 "success" : True ,
337378 "action" : "none" ,
338- "message" : "Gateway running and shared mTLS certs present " ,
379+ "message" : "Gateway running against this API's shared mTLS cert set " ,
339380 }
340381
341382 logger .warning (
342- "Gateway container is running but the shared mTLS certs are missing on the API side ; "
383+ "Gateway container is running but is not serving this API's shared mTLS cert set ; "
343384 "regenerating the cert set and restarting the Gateway so it loads a matching server cert"
344385 )
345386 dirs = self ._ensure_gateway_directories ()
0 commit comments