@@ -79,6 +79,18 @@ def run(self, report: CloneReport) -> PhaseResult:
7979 result .errors .append (f"list target tools: { e } " )
8080 return result
8181
82+ # Source's service-account view hides frictionless adapters; a
83+ # profile-referenced name missing here flags a tool we can't migrate.
84+ try :
85+ self ._src_adapter_names = {
86+ a ["adapter_name" ] for a in self .ctx .source .list_adapters ()
87+ }
88+ except Exception as e :
89+ logger .exception ("Failed to list source adapters: %s" , e )
90+ result .failed += 1
91+ result .errors .append (f"list source adapters for visibility check: { e } " )
92+ return result
93+
8294 # Updated under lock when a fresh create lands so duplicate
8395 # same-name source rows adopt instead of recreating.
8496 target_by_name : dict [str , dict [str , Any ]] = {
@@ -235,7 +247,25 @@ def _create_fresh(
235247 )
236248 return None
237249
238- adapter_ids = self ._resolve_target_adapter_ids (src_tool_id , tool_name )
250+ default_profile = self ._source_default_profile (src_tool_id , tool_name )
251+ if default_profile is None :
252+ with lock :
253+ result .failed += 1
254+ result .errors .append (
255+ f"import { tool_name } : no default profile on source"
256+ )
257+ return None
258+
259+ invisible = self ._invisible_source_adapter_names (default_profile )
260+ if invisible :
261+ self ._register_frictionless_skip (
262+ src_tool_id , tool_name , invisible , result , lock
263+ )
264+ return None
265+
266+ adapter_ids = self ._resolve_target_adapter_ids (
267+ default_profile , tool_name
268+ )
239269 if adapter_ids is None :
240270 with lock :
241271 result .failed += 1
@@ -265,17 +295,9 @@ def _create_fresh(
265295 )
266296 return tgt_tool_id
267297
268- def _resolve_target_adapter_ids (
298+ def _source_default_profile (
269299 self , src_tool_id : str , tool_name : str
270- ) -> dict [str , str ] | None :
271- """Source profile carries adapter NAMES (per serializer); resolve
272- each name to a target adapter UUID via ``list_adapters(name=...)``.
273-
274- Returns ``None`` if any of the four required adapters can't be
275- found on target — caller fails the tool. AdapterPhase preserves
276- names across orgs so this lookup should always hit when the
277- adapter clone ran cleanly.
278- """
300+ ) -> dict [str , Any ] | None :
279301 try :
280302 src_profiles = self .ctx .source .list_profiles (src_tool_id )
281303 except Exception as e :
@@ -293,11 +315,71 @@ def _resolve_target_adapter_ids(
293315 "source tool '%s' has no profiles to derive adapter ids from" ,
294316 tool_name ,
295317 )
296- return None
318+ return default
319+
320+ def _invisible_source_adapter_names (
321+ self , default_profile : dict [str , Any ]
322+ ) -> list [str ]:
323+ """Profile adapter names not in the source's visible adapter set
324+ (typically frictionless) — these can't be migrated.
325+ """
326+ missing : list [str ] = []
327+ for src_field , _ in _PROFILE_ADAPTER_FIELDS :
328+ adapter_name = _extract_adapter_name (default_profile .get (src_field ))
329+ if adapter_name and adapter_name not in self ._src_adapter_names :
330+ missing .append (adapter_name )
331+ return missing
332+
333+ def _register_frictionless_skip (
334+ self ,
335+ src_tool_id : str ,
336+ tool_name : str ,
337+ missing_adapters : list [str ],
338+ result : PhaseResult ,
339+ lock : threading .Lock ,
340+ ) -> None :
341+ """Record the skip + source registry id so dependent workflows
342+ cascade-skip downstream.
343+ """
344+ logger .warning (
345+ "skipping tool '%s' src=%s — default profile references adapters "
346+ "not visible to the source service account (frictionless?): %s. "
347+ "Wire equivalents on target and re-run." ,
348+ tool_name ,
349+ src_tool_id ,
350+ missing_adapters ,
351+ )
352+ try :
353+ src_regs = self .ctx .source .list_registries (custom_tool = src_tool_id )
354+ except Exception as e :
355+ logger .warning (
356+ "registry lookup failed for skipped tool '%s' — "
357+ "downstream cascade-skip may not fire: %s" ,
358+ tool_name ,
359+ e ,
360+ )
361+ src_regs = []
362+ with lock :
363+ result .skipped += 1
364+ for reg in src_regs :
365+ reg_id = reg .get ("prompt_registry_id" )
366+ if reg_id :
367+ self .ctx .skipped_custom_tool_registry_ids .add (reg_id )
368+
369+ def _resolve_target_adapter_ids (
370+ self , default_profile : dict [str , Any ], tool_name : str
371+ ) -> dict [str , str ] | None :
372+ """Source profile carries adapter NAMES (per serializer); resolve
373+ each name to a target adapter UUID via ``list_adapters(name=...)``.
297374
375+ Returns ``None`` if any of the four required adapters can't be
376+ found on target — caller fails the tool. AdapterPhase preserves
377+ names across orgs so this lookup should always hit when the
378+ adapter clone ran cleanly.
379+ """
298380 resolved : dict [str , str ] = {}
299381 for src_field , form_field in _PROFILE_ADAPTER_FIELDS :
300- adapter_name = _extract_adapter_name (default .get (src_field ))
382+ adapter_name = _extract_adapter_name (default_profile .get (src_field ))
301383 if not adapter_name :
302384 logger .warning (
303385 "source default profile for tool '%s' missing adapter '%s'" ,
0 commit comments