Setting the file. One moment.
Hosted Connect · Foundry Iq · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page File Cu Canary
This file
Number 10.52
Position 52 of 77
Type Python
Size 31 KB
Lines 488 helpers/ hosted_connect.py
Python · 488 lines · 31 KB
13
14 try :
15 from . import _prompt_read as read
16 from ._bootstrap_io import read_json, run_cli
17 from ._common import (
18 MANAGEMENT_AUDIENCE , SEARCH_AUDIENCE , HelperFailure, azure_cli_token,
19 blocked_result, digest, emit_result, http_request, reject_secrets,
20 )
21 from .prompt_connect import _project_identity, _project_endpoint, PROJECT_ID
22 except ImportError :
23 import _prompt_read as read
24 from _bootstrap_io import read_json, run_cli
25 from _common import (
26 MANAGEMENT_AUDIENCE , SEARCH_AUDIENCE , HelperFailure, azure_cli_token,
27 blocked_result, digest, emit_result, http_request, reject_secrets,
28 )
29 from prompt_connect import _project_identity, _project_endpoint, PROJECT_ID
30
31 AI_AUDIENCE = "https://ai.azure.com"
32 FOUNDRY_USER = "53ca6127-db72-4b80-b1b0-d745d6d5456d"
33 OUTCOME = "assess-existing-hosted-toolbox"
34 FIELDS = {
35 "schema_version" , "scope" , "project" , "search_service" , "knowledge_base" ,
36 "agent_name" , "agent_version" , "toolbox_name" , "tool_label" , "connection_name" ,
37 "reader_assignment_id" , "project_assignment_id" , "retention_owner" ,
38 }
39 OPTIONAL = { "known_agents" , "supported_question" , "unrelated_question" }
40
41
42 class ProjectSelectionFailure ( HelperFailure ):
43 def __init__ (self, candidates):
44 super (). __init__ (
45 "project-ambiguous" if candidates else "project-absent" ,
46 "Scoped project name did not resolve uniquely; select an exact account/project without widening scope." ,
47 blocked_at = "input-resolution" ,
48 )
49 self .candidates = candidates
50
51
52 def fail (code, message, ** metadata):
53 return read.fail(code, message, ** metadata)
54
55
56 def name (value):
57 return isinstance (value, str ) and read. NAME .fullmatch(value) is not None
58
59
60 def url (value):
61 try :
62 parsed = urlsplit(value)
63 if (parsed.scheme != "https" or not parsed.hostname or parsed.username
64 or parsed.password or parsed.port not in ( None , 443 ) or parsed.fragment):
65 raise ValueError ()
66 return parsed
67 except ValueError as error:
68 raise fail( "selection-invalid" , "Use an exact public-cloud HTTPS endpoint without credentials or fragments." ) from error
69
70
71 def validate (request):
72 if not isinstance (request, dict ):
73 raise fail( "input-schema-invalid" , "Hosted assessment requires a resolved intent object." )
74 reject_secrets(request)
75 if set (request) - FIELDS - OPTIONAL or not FIELDS <= set (request) or request[ "schema_version" ] != "1.0" :
76 raise fail( "input-schema-invalid" , "Use only the documented Hosted assessment decisions." )
77 scope = request[ "scope" ]
78 if ( not isinstance (scope, dict ) or not { "subscription_id" , "resource_group" } <= set (scope)
79 or set (scope) - { "subscription_id" , "resource_group" , "account_name" }
80 or any ( not name(value) for value in scope.values())):
81 raise fail( "scope-invalid" , "Select one subscription/resource group and optionally one account; no automatic widening." )
82 for key in ( "agent_name" , "agent_version" , "toolbox_name" , "tool_label" , "connection_name" ):
83 if not name(request[key]):
84 raise fail( "input-schema-invalid" , "Select bounded exact agent/version, toolbox and connection names." )
85 if request[ "agent_version" ].casefold() in { "latest" , "default" }:
86 raise fail( "agent-version-unresolved" , "Pin the observed agent version, never latest." )
87 for key in ( "project" , "search_service" , "knowledge_base" ):
88 value = request[key]
89 if not isinstance (value, str ) or not 1 <= len (value) <= 2048 :
90 raise fail( "selection-invalid" , "Supply a name, exact resource ID or documented endpoint." )
91 if value.startswith( "https:" ):
92 parsed = url(value)
93 if key == "project" :
94 _project_endpoint(value)
95 if ( not name(parsed.hostname.removesuffix( ".services.ai.azure.com" ))
96 or not name(unquote(parsed.path.rstrip( "/" ).rsplit( "/" , 1 )[ - 1 ]))):
97 raise fail( "selection-invalid" , "The project endpoint must identify a valid account/project name." )
98 elif key == "search_service" :
99 if ( not parsed.hostname.endswith( ".search.windows.net" ) or parsed.path not in ( "" , "/" )
100 or parsed.query):
101 raise fail( "selection-invalid" , "Select one Search service endpoint, not an index." )
102 elif ( not parsed.hostname.endswith( ".search.windows.net" )
103 or re.fullmatch( r "/knowledgebases/ [ A-Za-z0-9_.- ] + /mcp" , parsed.path) is None
104 or parse_qs(parsed.query) != { "api-version" : [ "2026-08-01-preview" ]}):
105 raise fail( "selection-invalid" , "Select an exact preview KB MCP endpoint." )
106 elif value.startswith( "/" ):
107 pattern = PROJECT_ID if key == "project" else read. SEARCH_ID if key == "search_service" else None
108 if pattern is None or pattern.fullmatch(value) is None :
109 raise fail( "selection-invalid" , "The supplied resource ID does not identify the selected resource kind." )
110 elif not name(value):
111 raise fail( "selection-invalid" , "The selected name is malformed." )
112 if key == "search_service" and not value.startswith( "/" ):
113 service = url(value).hostname.removesuffix( ".search.windows.net" ) if value.startswith( "https:" ) else value
114 resource_id = ( f "/subscriptions/ { scope[ 'subscription_id' ] } /resourceGroups/ { scope[ 'resource_group' ] } "
115 "/providers/Microsoft.Search/searchServices/" + service)
116 if read. SEARCH_ID .fullmatch(resource_id) is None :
117 raise fail( "selection-invalid" , "Use a valid Search service name, not a display label or index." )
118 for key in ( "reader_assignment_id" , "project_assignment_id" ):
119 value = request[key]
120 if ( not isinstance (value, str ) or len (value) > 2048
121 or re.fullmatch( r "/subscriptions/ [ ^/?# \s] + /resourceGroups/ [ ^?# \s] + /providers/"
122 r "Microsoft . Authorization/roleAssignments/ [ 0-9a-fA-F- ] + " , value, re.I) is None
123 or not read. GUID .fullmatch(value.rsplit( "/" , 1 )[ - 1 ])):
124 raise fail( "role-selection-invalid" , "Select exact resource-scoped role-assignment IDs, not principals to invent." )
125 if ( not isinstance (request[ "retention_owner" ], str ) or not request[ "retention_owner" ].strip()
126 or len (request[ "retention_owner" ]) > 256 ):
127 raise fail( "owner-unresolved" , "Name the retained connection/toolbox-version owner; cleanup is unsupported." )
128 agents = request.get( "known_agents" , [])
129 if ( not isinstance (agents, list ) or len (agents) > 20
130 or any ( not isinstance (item, dict ) or set (item) != { "name" , "version" }
131 or not name(item[ "name" ]) or not name(item[ "version" ])
132 or item[ "version" ].casefold() in { "latest" , "default" } for item in agents)):
133 raise fail( "known-bindings-invalid" , "Supply at most 20 known exact agent/version bindings; no exclusive-consumer inference." )
134 for key in ( "supported_question" , "unrelated_question" ):
135 if key in request and ( not isinstance (request[key], str ) or not request[key].strip() or len (request[key]) > 4096 ):
136 raise fail( "acceptance-invalid" , "Questions are optional resolved candidates, never implicit invocation approval." )
137
138
139 class Reads :
140 def __init__ (self, token_provider, transport):
141 self .token_provider, self .transport = token_provider, transport
142 self .tokens, self .request_ids = {}, []
143 self .last_request_id = None
144
145 def get (self, endpoint, audience, * , absent = False , label = "resource" ):
146 if audience not in self .tokens:
147 self .tokens[audience] = self .token_provider(audience)
148 value, ids = read.get_object(endpoint, self .tokens[audience], self .transport, absent = absent, label = label)
149 self .request_ids.extend(ids)
150 self .last_request_id = ids[ - 1 ] if ids else None
151 return value
152
153 def collection (self, endpoint):
154 initial = urlsplit(endpoint)
155 result, seen = [], set ()
156 while endpoint:
157 current = url(endpoint)
158 if (endpoint in seen or len (seen) >= 20 or current.netloc != initial.netloc
159 or current.path != initial.path
160 or parse_qs(current.query).get( "api-version" ) != parse_qs(initial.query).get( "api-version" )):
161 raise fail( "inventory-unverified" , "Scoped inventory continuation is unsafe or exceeds 20 pages." )
162 seen.add(endpoint)
163 page = self .get(endpoint, MANAGEMENT_AUDIENCE , label = "scoped-inventory" )
164 items = page.get( "value" )
165 if not isinstance (items, list ) or any ( not isinstance (item, dict ) for item in items):
166 raise fail( "inventory-unverified" , "Scoped inventory is malformed; no inferred absence." )
167 result.extend(items)
168 if len (result) > 100 :
169 raise fail( "inventory-limit" , "Scoped inventory exceeds 100 resources; select an exact parent instead." )
170 endpoint = page.get( "nextLink" )
171 if endpoint is not None and not isinstance (endpoint, str ):
172 raise fail( "inventory-unverified" , "Scoped continuation must be a URL." )
173 return result
174
175
176 def resolve (request, reads):
177 scope = request[ "scope" ]
178 prefix = f "/subscriptions/ { scope[ 'subscription_id' ] } /resourceGroups/ { scope[ 'resource_group' ] } "
179 account_prefix = prefix + "/providers/Microsoft.CognitiveServices/accounts/"
180 selection = request[ "project" ]
181 if selection.startswith( "/" ):
182 project_id = selection
183 elif selection.startswith( "https:" ):
184 parsed = url(selection)
185 project_id = account_prefix + parsed.hostname.removesuffix( ".services.ai.azure.com" ) + "/projects/" + unquote(parsed.path.rstrip( "/" ).rsplit( "/" , 1 )[ - 1 ])
186 elif scope.get( "account_name" ):
187 project_id = account_prefix + scope[ "account_name" ] + "/projects/" + selection
188 else :
189 accounts = reads.collection( MANAGEMENT_AUDIENCE + account_prefix.rstrip( "/" ) + "?api-version=2025-06-01" )
190 if len (accounts) > 20 :
191 raise fail( "account-selection-required" , "Select one account; do not enumerate more than 20 accounts for a project name." )
192 matches, seen = [], set ()
193 for account in accounts:
194 account_id = account.get( "id" , "" )
195 if ( not isinstance (account_id, str ) or not account_id.casefold().startswith(account_prefix.casefold())
196 or not name(account_id[ len (account_prefix):]) or account_id.casefold() in seen):
197 raise fail( "inventory-unverified" , "Account inventory contains a duplicate or out-of-scope resource." )
198 seen.add(account_id.casefold())
199 projects_seen = set ()
200 for project in reads.collection( MANAGEMENT_AUDIENCE + account_id + "/projects?api-version=" + read. PROJECT_API ):
201 candidate = project.get( "id" , "" )
202 if ( not isinstance (candidate, str ) or PROJECT_ID .fullmatch(candidate) is None
203 or not candidate.casefold().startswith((account_id + "/projects/" ).casefold())
204 or candidate.casefold() in projects_seen):
205 raise fail( "inventory-unverified" , "Project inventory contains an unverified identity." )
206 projects_seen.add(candidate.casefold())
207 if candidate.rsplit( "/" , 1 )[ - 1 ].casefold() == selection.casefold():
208 matches.append(candidate)
209 if len (matches) != 1 :
210 raise ProjectSelectionFailure(matches)
211 project_id = matches[ 0 ]
212 match = PROJECT_ID .fullmatch(project_id)
213 if match is None :
214 raise fail( "selection-invalid" , "The resolved project resource identity is malformed." )
215 endpoint = f "https:// { match[ 'account' ] } .services.ai.azure.com/api/projects/ { quote(match[ 'project' ], safe = '' ) } "
216 _project_identity({ "project_resource_id" : project_id, "project_endpoint" : endpoint})
217 selection = request[ "search_service" ]
218 if selection.startswith( "/" ):
219 search_id = selection
220 else :
221 service = url(selection).hostname.removesuffix( ".search.windows.net" ) if selection.startswith( "https:" ) else selection
222 search_id = prefix + "/providers/Microsoft.Search/searchServices/" + service
223 match = read. SEARCH_ID .fullmatch(search_id)
224 if match is None :
225 raise fail( "selection-invalid" , "The selected Search service identity is invalid." )
226 search_endpoint = "https://" + match[ "name" ].lower() + ".search.windows.net"
227 selection = request[ "knowledge_base" ]
228 if selection.startswith( "https:" ):
229 parsed = url(selection)
230 if parsed.hostname != urlsplit(search_endpoint).hostname:
231 raise fail( "kb-binding-invalid" , "The KB endpoint belongs to another selected Search service." )
232 kb_name = parsed.path.split( "/" )[ 2 ]
233 else :
234 kb_name = selection
235 return project_id, endpoint, search_id, search_endpoint, kb_name
236
237
238 def role (reads, assignment_id, scope, principal, role_id):
239 prefix = scope + "/providers/Microsoft.Authorization/roleAssignments/"
240 if ( not assignment_id.casefold().startswith(prefix.casefold())
241 or not read. GUID .fullmatch(assignment_id[ len (prefix):])):
242 raise fail( "role-scope-invalid" , "Role assignment must use the exact selected resource scope." )
243 value = reads.get( MANAGEMENT_AUDIENCE + assignment_id + "?api-version=" + read. ROLE_API ,
244 MANAGEMENT_AUDIENCE , label = "role-assignment" )
245 properties = value.get( "properties" )
246 definitions = {
247 "/providers/microsoft.authorization/roledefinitions/" + role_id,
248 "/subscriptions/" + scope.split( "/" )[ 2 ].lower() + "/providers/microsoft.authorization/roledefinitions/" + role_id,
249 }
250 if ( str (value.get( "id" , "" )).casefold() != assignment_id.casefold() or not isinstance (properties, dict )
251 or str (properties.get( "principalId" , "" )).lower() != principal
252 or str (properties.get( "scope" , "" )).casefold() != scope.casefold()
253 or str (properties.get( "roleDefinitionId" , "" )).casefold() not in definitions
254 or properties.get( "principalType" , "ServicePrincipal" ) != "ServicePrincipal"
255 or properties.get( "condition" ) not in ( None , "" )):
256 raise fail( "hosted-runtime-role-unverified" , "Require exact-scope grants to the published Hosted principal, not the project or blueprint." )
257 return value
258
259
260 def agent (reads, endpoint, agent_name, version):
261 value = reads.get(endpoint + f "/agents/ { quote(agent_name, safe = '' ) } /versions/ { quote(version, safe = '' ) } ?api-version=v1" ,
262 AI_AUDIENCE , label = "hosted-agent" )
263 if value.get( "name" ) != agent_name or str (value.get( "version" )) != version or not isinstance (value.get( "definition" ), dict ):
264 raise fail( "agent-identity-unverified" , "Read the exact observed agent/version; never select latest." )
265 return value
266
267
268 def toolbox_binding (environment, endpoint, toolbox_name):
269 """Match the first-party FoundryToolbox environment resolver, not arbitrary code."""
270 if not isinstance (environment, dict ):
271 return None
272 consumer = endpoint + "/toolboxes/" + toolbox_name + "/mcp?api-version=v1"
273 if "TOOLBOX_ENDPOINT" in environment:
274 return "endpoint" if environment[ "TOOLBOX_ENDPOINT" ] == consumer else None
275 project = environment.get( "FOUNDRY_PROJECT_ENDPOINT" )
276 if ( isinstance (project, str ) and project.rstrip( "/" ) == endpoint
277 and environment.get( "TOOLBOX_NAME" ) == toolbox_name):
278 return "name"
279 return None
280
281
282 def assess (request, * , token_provider = azure_cli_token, transport = http_request, cli = run_cli):
283 validate(request)
284 scope = request[ "scope" ]
285 selected_project = request[ "project" ]
286 context_id = selected_project if selected_project.startswith( "/" ) else "/subscriptions/" + scope[ "subscription_id" ]
287 context = read.cli_context(context_id, cli = cli)
288 reads = Reads(token_provider, transport)
289 project_id, endpoint, search_id, search_endpoint, kb_name = resolve(request, reads)
290 project = reads.get( MANAGEMENT_AUDIENCE + project_id + "?api-version=" + read. PROJECT_API , MANAGEMENT_AUDIENCE , label = "project" )
291 properties = project.get( "properties" , {})
292 endpoints = properties.get( "endpoints" ) if isinstance (properties, dict ) else None
293 if ( str (project.get( "id" , "" )).casefold() != project_id.casefold() or not isinstance (properties, dict )
294 or str (properties.get( "provisioningState" , "" )).lower() != "succeeded"
295 or not isinstance (endpoints, dict ) or endpoint not in endpoints.values()):
296 raise fail( "project-unverified" , "The exact ready project endpoint must be confirmed remotely." )
297 selected = agent(reads, endpoint, request[ "agent_name" ], request[ "agent_version" ])
298 definition, identity = selected[ "definition" ], selected.get( "instance_identity" )
299 other_principals = [
300 value.get( "principalId" ) or value.get( "principal_id" )
301 for value in (project.get( "identity" ), selected.get( "blueprint" ))
302 if isinstance (value, dict )
303 ]
304 if (definition.get( "kind" ) != "hosted" or selected.get( "status" ) != "active"
305 or not isinstance (identity, dict ) or identity.get( "status" ) != "active"
306 or not isinstance (identity.get( "principal_id" ), str ) or not read. GUID .fullmatch(identity[ "principal_id" ])
307 or not isinstance (identity.get( "client_id" ), str ) or not read. GUID .fullmatch(identity[ "client_id" ])
308 or identity[ "principal_id" ].casefold() in {
309 value.casefold() for value in other_principals if isinstance (value, str )
310 }):
311 raise fail( "hosted-principal-unverified" , "Require an active published Hosted agent and observed instance principal." )
312 principal = identity[ "principal_id" ].lower()
313 consumer = endpoint + "/toolboxes/" + request[ "toolbox_name" ] + "/mcp?api-version=v1"
314 environment = definition.get( "environment_variables" )
315 binding_mode = toolbox_binding(environment, endpoint, request[ "toolbox_name" ])
316 if binding_mode is None :
317 raise fail( "hosted-runtime-change-required" , "Observed settings do not establish this unversioned FoundryToolbox binding. The runtime owner must verify custom/overridden behavior or the actual configuration change before requesting source; a version-pinned developer endpoint is not equivalent." )
318 search = reads.get( MANAGEMENT_AUDIENCE + search_id + "?api-version=" + read. SEARCH_API , MANAGEMENT_AUDIENCE , label = "search" )
319 properties = search.get( "properties" )
320 if ( str (search.get( "id" , "" )).casefold() != search_id.casefold() or not isinstance (properties, dict )
321 or str (properties.get( "status" , "" )).lower() not in { "running" , "provisioning" , "degraded" }
322 or str (properties.get( "provisioningState" , "" )).lower() not in { "succeeded" , "provisioning" }):
323 raise fail( "search-operation-blocked" , "Search failed/deleting/disabled/unresolved state blocks this assessment." )
324 kb = reads.get(search_endpoint + "/knowledgebases('" + quote(kb_name, safe = "" ) + "')?api-version=2026-08-01-preview" ,
325 SEARCH_AUDIENCE , absent = True , label = "knowledge-base" )
326 if kb is None :
327 raise fail( "knowledge-base-absent" , "The exact KB is absent; Search indexes are not KB evidence." ,
328 status = 404 , request_id = reads.last_request_id)
329 _, profile = read.kb_state(kb, kb_name)
330 reader = role(reads, request[ "reader_assignment_id" ], search_id, principal, read. READER_ROLE )
331 project_role = role(reads, request[ "project_assignment_id" ], project_id, principal, FOUNDRY_USER )
332 toolbox_url = endpoint + "/toolboxes/" + request[ "toolbox_name" ]
333 toolbox = reads.get(toolbox_url + "?api-version=v1" , AI_AUDIENCE , label = "toolbox" )
334 default = toolbox.get( "default_version" )
335 if (toolbox.get( "name" ) != request[ "toolbox_name" ] or not name(default)
336 or default.casefold() in { "latest" , "default" }):
337 raise fail( "toolbox-default-unverified" , "Read the exact toolbox and its current default version." )
338 version = reads.get(toolbox_url + "/versions/" + default + "?api-version=v1" , AI_AUDIENCE , label = "toolbox-version" )
339 tools = version.get( "tools" )
340 if (version.get( "name" ) != request[ "toolbox_name" ] or str (version.get( "version" )) != default
341 or not isinstance (tools, list ) or len (tools) > 200 or any ( not isinstance (tool, dict ) for tool in tools)):
342 raise fail( "toolbox-version-unverified" , "The default immutable version and its complete tool list must be verified." )
343 matching = [tool for tool in tools if tool.get( "server_label" ) == request[ "tool_label" ]]
344 if len (matching) > 1 :
345 raise fail( "toolbox-binding-ambiguous" , "Multiple selected KB tool labels require explicit reconciliation." )
346 if matching:
347 tool = matching[ 0 ]
348 allowed = tool.get( "allowed_tools" ) or []
349 allowed = allowed.get( "tool_names" , []) if isinstance (allowed, dict ) else allowed
350 if (tool.get( "type" ) != "mcp" or not isinstance (allowed, list )
351 or any ( not isinstance (item, str ) for item in allowed)
352 or not isinstance (tool.get( "headers" ) or {}, dict )
353 or not isinstance (tool.get( "server_url" ), str ) or len (tool[ "server_url" ]) > 2048
354 or not isinstance (tool.get( "project_connection_id" ), str )):
355 raise fail( "toolbox-binding-unverified" , "The selected MCP tool definition is malformed or has another type." )
356 if tool.get( "authorization" ) is not None or tool.get( "connector_id" ) is not None or tool.get( "headers" ):
357 raise fail( "toolbox-auth-unverified" , "Inline authorization, connectors or custom headers are outside this agentic-identity recipe; preserve them rather than silently replacing their auth." )
358 if "allowed_tools" in tool and tool[ "allowed_tools" ] is not None and "knowledge_base_retrieve" not in allowed:
359 raise fail( "toolbox-policy-unverified" , "The explicit tool filter does not establish access to knowledge_base_retrieve; review the policy delta separately, never silently widen it." )
360 old_endpoint = url(tool[ "server_url" ])
361 api = parse_qs(old_endpoint.query)
362 reference = tool[ "project_connection_id" ]
363 prefix = project_id + "/connections/"
364 if ( not old_endpoint.hostname.endswith( ".search.windows.net" )
365 or re.fullmatch( r "/knowledgebases/ [ A-Za-z0-9_.- ] + /mcp" , old_endpoint.path) is None
366 or set (api) != { "api-version" } or len (api[ "api-version" ]) != 1
367 or re.fullmatch( r " \d {4} - \d {2} - \d {2} (?: -preview ) ? " , api[ "api-version" ][ 0 ]) is None
368 or not (name(reference) or (reference.casefold().startswith(prefix.casefold())
369 and name(reference[ len (prefix):])))):
370 raise fail( "toolbox-binding-unverified" , "The selected label is not a verified Search KB MCP binding." )
371 target = search_endpoint + "/knowledgebases/" + kb_name + "/mcp?api-version=2026-08-01-preview"
372 connection_url = MANAGEMENT_AUDIENCE + project_id + "/connections/" + request[ "connection_name" ] + "?api-version=" + read. PROJECT_API
373 connection = reads.get(connection_url, MANAGEMENT_AUDIENCE , absent = True , label = "connection" )
374 if connection is not None :
375 props = connection.get( "properties" )
376 expected_id = project_id + "/connections/" + request[ "connection_name" ]
377 if (connection.get( "name" ) != request[ "connection_name" ] or not isinstance (props, dict )
378 or str (connection.get( "id" , expected_id)).casefold() != expected_id.casefold()
379 or any (props.get(key) != value for key, value in {
380 "category" : "RemoteTool" , "authType" : "AgenticIdentityToken" ,
381 "target" : target, "audience" : "https://search.azure.com/" ,
382 }.items())):
383 raise fail( "connection-conflict" , "This exact connection NAME has incompatible Hosted recipe auth/binding. Preserve it; explicitly choose a new name, even for the same KB endpoint." )
384 old_connection, old_connection_url = None , None
385 if matching:
386 old_name = matching[ 0 ][ "project_connection_id" ].rsplit( "/" , 1 )[ - 1 ]
387 old_connection_url = MANAGEMENT_AUDIENCE + project_id + "/connections/" + old_name + "?api-version=" + read. PROJECT_API
388 old_connection = connection if old_name == request[ "connection_name" ] else reads.get(
389 old_connection_url, MANAGEMENT_AUDIENCE , label = "previous-connection" ,
390 )
391 old_id = project_id + "/connections/" + old_name
392 if (old_connection is None or old_connection.get( "name" ) != old_name
393 or str (old_connection.get( "id" , old_id)).casefold() != old_id.casefold()
394 or not isinstance (old_connection.get( "properties" ), dict )
395 or old_connection[ "properties" ].get( "target" ) != matching[ 0 ][ "server_url" ]):
396 raise fail( "toolbox-existing-binding-unverified" , "The previous connection and selected toolbox tool do not establish one consistent KB binding." )
397 exact = bool (connection and matching
398 and matching[ 0 ].get( "project_connection_id" ) in {
399 request[ "connection_name" ], project_id + "/connections/" + request[ "connection_name" ],
400 }
401 and matching[ 0 ][ "server_url" ] == target)
402 known = [{ "name" : request[ "agent_name" ], "version" : request[ "agent_version" ]}]
403 known_states = []
404 for item in request.get( "known_agents" , []):
405 observed = agent(reads, endpoint, item[ "name" ], item[ "version" ])
406 known_states.append(observed)
407 env = observed[ "definition" ].get( "environment_variables" , {})
408 configured_tools = observed[ "definition" ].get( "tools" ) or []
409 tool_binding = isinstance (configured_tools, list ) and any (
410 isinstance (tool, dict ) and tool.get( "type" ) == "mcp" and tool.get( "server_url" ) == consumer
411 for tool in configured_tools
412 )
413 if (toolbox_binding(env, endpoint, request[ "toolbox_name" ]) or tool_binding) and item not in known:
414 known.append(item)
415 refreshed = reads.get(toolbox_url + "?api-version=v1" , AI_AUDIENCE , label = "toolbox" )
416 refreshed_agent = agent(reads, endpoint, request[ "agent_name" ], request[ "agent_version" ])
417 refreshed_connection = reads.get(connection_url, MANAGEMENT_AUDIENCE , absent = True , label = "connection" )
418 refreshed_old = old_connection
419 if old_connection_url and old_connection_url != connection_url:
420 refreshed_old = reads.get(old_connection_url, MANAGEMENT_AUDIENCE , label = "previous-connection" )
421 refreshed_version = reads.get(toolbox_url + "/versions/" + default + "?api-version=v1" , AI_AUDIENCE , label = "toolbox-version" )
422 if (refreshed != toolbox or refreshed_agent != selected or refreshed_connection != connection
423 or refreshed_old != old_connection or refreshed_version != version
424 or read.cli_context(project_id, cli = cli) != context):
425 raise fail( "hosted-protected-state-drift" , "Agent, toolbox default/metadata or CLI context changed during assessment." )
426 summary = {
427 "branch" : "toolbox-only" , "agent" : request[ "agent_name" ], "agent_version" : request[ "agent_version" ],
428 "project_resource_id" : project_id, "project_endpoint" : endpoint, "search_resource_id" : search_id,
429 "toolbox_name" : request[ "toolbox_name" ], "consumer_endpoint" : consumer,
430 "runtime_binding" : { "resolver" : "FoundryToolbox environment" , "mode" : binding_mode,
431 "runtime_usage_verified" : False },
432 "agent_change" : "none" , "runtime_principal_id" : principal, "kb_profile" : profile,
433 "before" : { "kb_endpoint" : matching[ 0 ].get( "server_url" ) if matching else None , "default_version" : default,
434 "connection" : matching[ 0 ].get( "project_connection_id" ) if matching else None },
435 "after" : { "kb_endpoint" : target, "default_version" : default if exact else "new immutable version (not created)" ,
436 "connection" : request[ "connection_name" ]},
437 "connection_action" : "reuse" if connection else "create (blocked)" ,
438 "known_consumers" : known, "unknown_consumers" : True ,
439 "shared_default_approval_required" : not exact, "mutation_approval_required" : not exact,
440 "retention_owner" : request[ "retention_owner" ], "cleanup" : "unsupported; retain previous versions and legacy connections" ,
441 "rollback" : "separate explicit plan; never automatic" ,
442 "tool_policy" : "Preserve existing tool approval/filter/configuration; runtime enforcement is not proven by this assessment." ,
443 "acceptance" : {
444 "supported_candidate_needed" : "supported_question" not in request,
445 "unrelated_question_needed" : "unrelated_question" not in request,
446 "invocation_approval" : "not granted by assessment" , "agent_tool_retrieval" : "not-run" ,
447 },
448 }
449 result = {
450 "status" : "planned" if exact else "blocked" , "outcome" : OUTCOME , "approval_summary" : summary,
451 "writes_performed" : [], "execution_input" : None , "execution_available" : False ,
452 "private_evidence" : { "fingerprint" : digest({
453 "project" : project, "agent" : selected, "search" : search, "kb" : kb,
454 "reader" : reader, "project_role" : project_role, "toolbox" : toolbox,
455 "version" : version, "connection" : connection, "previous_connection" : old_connection,
456 "known_agents" : known_states, "cli" : context,
457 })},
458 "request_ids" : reads.request_ids,
459 "warnings" : [ "Configured runtime binding is not actual agent tool-use proof; acceptance invocations remain separate." ,
460 "Known bindings are not a complete consumer inventory; external consumers may follow this default." ],
461 }
462 if ( str (search[ "properties" ].get( "status" )).lower() != "running"
463 or str (search[ "properties" ].get( "provisioningState" )).lower() != "succeeded" ):
464 result[ "warnings" ].append( "Search is provisioning/degraded; healthy KB GET is configuration evidence, not retrieval readiness." )
465 if not exact:
466 result[ "first_blocker" ] = {
467 "code" : "toolbox-promotion-concurrency-unverified" ,
468 "message" : "SDK/REST version creation and default promotion exist, but their conditional ETag/default update contract is unverified. Obtain service-owner confirmation before any connection/version creation; arbitrary If-Match headers are not proof." ,
469 }
470 return result
471
472
473 def main (argv = None ):
474 parser = argparse.ArgumentParser()
475 parser.add_argument( "--plan" , type = Path, required = True )
476 args = parser.parse_args(argv)
477 try :
478 result = assess(read_json(args.plan))
479 except HelperFailure as error:
480 result = blocked_result(error, outcome = OUTCOME , fingerprint = None , owner = None )
481 if isinstance (error, ProjectSelectionFailure):
482 result[ "selection_candidates" ] = error.candidates
483 emit_result(result)
484 return 3 if result[ "status" ] == "partial" else 2 if result[ "status" ] == "blocked" else 0
485
486
487 if __name__ == "__main__" :
488 sys.exit(main())