Setting the file. One moment.
Prompt Read · Foundry Iq · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page File Cu Canary
Bundled file
helpers/ _prompt_read.py
Python · 230 lines · 13 KB
12 from ._common import (
13 MANAGEMENT_AUDIENCE , SEARCH_AUDIENCE , HelperFailure, digest,
14 require_allowed_fields,
15 )
16 except ImportError :
17 from _bootstrap_io import run_cli
18 from _common import (
19 MANAGEMENT_AUDIENCE , SEARCH_AUDIENCE , HelperFailure, digest,
20 require_allowed_fields,
21 )
22
23 PROJECT_API = "2025-10-01-preview"
24 SEARCH_API = "2025-05-01"
25 ROLE_API = "2022-04-01"
26 READER_ROLE = "1407120a-92aa-4202-b7e9-c0e197c71c8f"
27 NAME = re.compile( r " [ A-Za-z0-9 ][ A-Za-z0-9_.- ] {0,127} " )
28 GUID = re.compile( r " [ 0-9a-fA-F ] {8} (?: - [ 0-9a-fA-F ] {4} ) {3} - [ 0-9a-fA-F ] {12} " )
29 SEARCH_ID = re.compile(
30 r "/subscriptions/ [ ^/?# \s] + /resourceGroups/ [ ^/?# \s] + /"
31 r "providers/Microsoft \. Search/searchServices/ ( ?P<name> [ a-z0-9- ] {2,60} ) " ,
32 re. IGNORECASE ,
33 )
34
35
36 def fail (code: str , message: str , response = None , * , status = None , request_id = None ) -> HelperFailure:
37 return HelperFailure(
38 code, message, blocked_at = "reconciliation" ,
39 status = response.status if response is not None else status,
40 request_id = response.request_id if response is not None else request_id,
41 )
42
43
44 def binding (plan: dict[ str , Any]) -> tuple[ str , str , str ]:
45 scope = plan[ "rbac_verified" ].get( "scope" )
46 match = SEARCH_ID .fullmatch(scope) if isinstance (scope, str ) else None
47 target = urlsplit(plan[ "connection" ][ "target" ])
48 parts = target.path.split( "/" )
49 if (
50 match is None or target.hostname != match[ "name" ].lower() + ".search.windows.net"
51 or len (parts) != 4 or parts[ 1 ] != "knowledgebases" or parts[ 3 ] != "mcp"
52 or parse_qs(target.query) != { "api-version" : [ "2026-08-01-preview" ]}
53 or not NAME .fullmatch(unquote(parts[ 2 ]))
54 ):
55 raise fail( "kb-binding-invalid" , "Bind an exact Search service ID and preview KB MCP name, not a generated index." )
56 assignment = plan[ "rbac_verified" ].get( "assignment_id" )
57 prefix = scope + "/providers/Microsoft.Authorization/roleAssignments/"
58 if (
59 not isinstance (assignment, str ) or not assignment.casefold().startswith(prefix.casefold())
60 or not GUID .fullmatch(assignment[ len (prefix):])
61 ):
62 raise fail( "rbac-scope-invalid" , "Select one exact Search-service-scoped role assignment resource ID." )
63 return scope, "https://" + target.hostname, unquote(parts[ 2 ])
64
65
66 def get_object (url, token, transport, * , absent = False , label = "resource" ):
67 try :
68 result = transport(
69 "GET" , url, token, follow_redirects = False , max_response_bytes = 1024 * 1024 ,
70 )
71 except HelperFailure as error:
72 if absent and error.http_status == 404 and not error.partial:
73 return None , [error.request_id] if error.request_id else []
74 raise
75 if result.status == 404 and absent:
76 return None , [result.request_id] if result.request_id else []
77 if result.status != 200 :
78 raise fail(label + "-unavailable" , "Exact " + label + " read failed; no fallback or inferred absence." , result)
79 if not isinstance (result.body, dict ) or not result.body:
80 raise fail(label + "-readback-invalid" , "Exact " + label + " readback must be a nonempty object." , result)
81 return result.body, [result.request_id] if result.request_id else []
82
83
84 def cli_context (project_id, * , cli = run_cli):
85 rc, out, _ = cli([ "account" , "show" ], 60 )
86 if rc:
87 raise fail( "cli-context-unavailable" , "The selected signed-in Azure CLI context could not be read." )
88 try :
89 value = json.loads(out)
90 except ( UnicodeError , ValueError ) as error:
91 raise fail( "cli-context-invalid" , "Azure CLI context is malformed." ) from error
92 user = value.get( "user" ) if isinstance (value, dict ) else None
93 if (
94 not isinstance (user, dict ) or user.get( "type" ) not in { "user" , "servicePrincipal" }
95 or not isinstance (user.get( "name" ), str ) or not 1 <= len (user[ "name" ]) <= 256
96 or value.get( "environmentName" ) != "AzureCloud"
97 or str (value.get( "state" , "" )).casefold() != "enabled"
98 or str (value.get( "id" , "" )).casefold() != project_id.split( "/" )[ 2 ].casefold()
99 or not isinstance (value.get( "tenantId" ), str ) or not GUID .fullmatch(value[ "tenantId" ])
100 ):
101 raise fail( "cli-context-conflict" , "Select the project's enabled subscription and tenant without changing identity." )
102 return { "subscription_id" : value[ "id" ].lower(), "tenant_id" : value[ "tenantId" ].lower(),
103 "principal" : user[ "name" ], "principal_type" : user[ "type" ]}
104
105
106 def kb_state (value, name):
107 if value.get( "name" ) != name:
108 raise fail( "kb-identity-mismatch" , "The exact knowledgebases API returned another KB identity." )
109 sources, models = value.get( "knowledgeSources" ), value.get( "models" , [])
110 if models is None :
111 models = []
112 effort = value.get( "retrievalReasoningEffort" )
113 mode = value.get( "outputMode" )
114 if (
115 not isinstance (sources, list ) or not 1 <= len (sources) <= 200
116 or any ( not isinstance (item, dict ) or not isinstance (item.get( "name" ), str )
117 or not NAME .fullmatch(item[ "name" ]) for item in sources)
118 or len ({item[ "name" ] for item in sources}) != len (sources)
119 or not isinstance (models, list ) or any (
120 not isinstance (item, dict ) or not isinstance (item.get( "@odata.type" ), str )
121 or not item[ "@odata.type" ].strip()
122 for item in models
123 )
124 or not isinstance (effort, dict ) or effort.get( "kind" ) not in { "minimal" , "low" , "medium" }
125 or mode not in { "extractiveData" , "answerSynthesis" }
126 ):
127 raise fail( "kb-configuration-unresolved" , "Read complete KB sources, model configuration, reasoning and output; never infer them from generated indexes." )
128 if not models and (effort[ "kind" ] != "minimal" or mode != "extractiveData" ):
129 raise fail( "kb-model-required" , "This existing KB configuration requires a KB chat model; connection planning never changes its mode or models." )
130 material = copy.deepcopy(value)
131 for field in ( "@odata.etag" , "description" , "tags" ):
132 material.pop(field, None )
133 return { "name" : name, "definition_digest" : digest(material)}, {
134 "source_count" : len (sources), "model_configured" : bool (models),
135 "reasoning" : effort[ "kind" ], "output" : mode,
136 }
137
138
139 def read_dependencies (plan, * , token_provider, transport, cli = run_cli, capture_context = False ):
140 scope, endpoint, kb_name = binding(plan)
141 project_id = plan[ "project_resource_id" ]
142 context = cli_context(project_id, cli = cli) if capture_context else None
143 token = token_provider( MANAGEMENT_AUDIENCE )
144 request_ids = []
145 project, ids = get_object( MANAGEMENT_AUDIENCE + project_id + "?api-version=" + PROJECT_API ,
146 token, transport, label = "project" )
147 request_ids.extend(ids)
148 identity, props = project.get( "identity" ), project.get( "properties" )
149 endpoints = props.get( "endpoints" ) if isinstance (props, dict ) else None
150 identity_types = identity.get( "type" ) if isinstance (identity, dict ) else None
151 identity_types = {item.strip() for item in identity_types.split( "," )} if isinstance (identity_types, str ) else set ()
152 if (
153 str (project.get( "id" , "" )).casefold() != project_id.casefold()
154 or not isinstance (identity, dict ) or identity_types not in ({ "SystemAssigned" }, { "SystemAssigned" , "UserAssigned" })
155 or not isinstance (identity.get( "principalId" ), str ) or not GUID .fullmatch(identity[ "principalId" ])
156 or not isinstance (identity.get( "tenantId" ), str ) or not GUID .fullmatch(identity[ "tenantId" ])
157 or not isinstance (props, dict ) or str (props.get( "provisioningState" , "" )).casefold() != "succeeded"
158 or not isinstance (endpoints, dict ) or plan[ "project_endpoint" ].rstrip( "/" ) not in {
159 value.rstrip( "/" ) for value in endpoints.values() if isinstance (value, str )
160 }
161 ):
162 raise fail( "project-identity-unverified" , "Require the selected ready Foundry PROJECT endpoint and its system-assigned principalId/tenantId." ,
163 request_id = ids[ - 1 ] if ids else None )
164 if context is not None and context[ "tenant_id" ] != identity[ "tenantId" ].lower():
165 raise fail( "cli-context-conflict" , "CLI tenant differs from the observed project identity tenant." )
166 project_state = { "id" : project_id, "endpoint" : plan[ "project_endpoint" ].rstrip( "/" ),
167 "principal_id" : identity[ "principalId" ].lower(), "tenant_id" : identity[ "tenantId" ].lower()}
168
169 search, ids = get_object( MANAGEMENT_AUDIENCE + scope + "?api-version=" + SEARCH_API ,
170 token, transport, label = "search" )
171 request_ids.extend(ids)
172 props = search.get( "properties" )
173 if str (search.get( "id" , "" )).casefold() != scope.casefold() or not isinstance (props, dict ):
174 raise fail( "search-identity-unverified" , "The selected Search resource identity is unresolved." , request_id = ids[ - 1 ] if ids else None )
175 status, provisioning = str (props.get( "status" , "" )).lower(), str (props.get( "provisioningState" , "" )).lower()
176 if status not in { "running" , "provisioning" , "degraded" } or provisioning not in { "succeeded" , "provisioning" }:
177 raise fail( "search-operation-blocked" , "Search is failed, disabled, deleting or unresolved; no connection write is allowed." ,
178 request_id = ids[ - 1 ] if ids else None )
179 warnings = [] if status == "running" and provisioning == "succeeded" else [
180 "Search is provisioning/degraded; healthy KB GET permits connection configuration only, not readiness or retrieval proof."
181 ]
182 search_state = { "id" : scope, "endpoint" : endpoint, "access_digest" : digest({
183 key: props.get(key) for key in ( "disableLocalAuth" , "authOptions" , "publicNetworkAccess" , "networkRuleSet" , "privateEndpointConnections" )
184 })}
185
186 assignment_id = plan[ "rbac_verified" ][ "assignment_id" ]
187 assignment, ids = get_object( MANAGEMENT_AUDIENCE + assignment_id + "?api-version=" + ROLE_API ,
188 token, transport, label = "role-assignment" )
189 request_ids.extend(ids)
190 role = assignment.get( "properties" )
191 if (
192 str (assignment.get( "id" , "" )).casefold() != assignment_id.casefold()
193 or not isinstance (role, dict )
194 or str (role.get( "principalId" , "" )).casefold() != project_state[ "principal_id" ]
195 or str (role.get( "scope" , "" )).casefold() != scope.casefold()
196 or str (role.get( "roleDefinitionId" , "" )).casefold() not in {
197 "/providers/microsoft.authorization/roledefinitions/" + READER_ROLE ,
198 "/subscriptions/" + scope.split( "/" )[ 2 ].lower() + "/providers/microsoft.authorization/roledefinitions/" + READER_ROLE ,
199 }
200 or role.get( "principalType" , "ServicePrincipal" ) != "ServicePrincipal"
201 or role.get( "condition" ) not in ( None , "" )
202 ):
203 raise fail( "project-reader-role-unverified" , "Search Index Data Reader must be an unconditional exact-scope grant to the observed PROJECT principal, not the agent identity." ,
204 request_id = ids[ - 1 ] if ids else None )
205 rbac = { "assignment_id" : assignment_id, "principal_id" : project_state[ "principal_id" ],
206 "scope" : scope, "role_definition_id" : READER_ROLE }
207 url = endpoint + "/knowledgebases('" + quote(kb_name.replace( "'" , "''" ), safe = "" ) + "')?api-version=2026-08-01-preview"
208 kb, ids = get_object(url, token_provider( SEARCH_AUDIENCE ), transport, absent = True , label = "knowledge-base" )
209 request_ids.extend(ids)
210 if kb is None :
211 raise fail( "knowledge-base-absent" , "The exact KB is absent; a generated index is not a knowledge base." ,
212 status = 404 , request_id = ids[ - 1 ] if ids else None )
213 try :
214 knowledge_base, profile = kb_state(kb, kb_name)
215 except HelperFailure as error:
216 error.request_id = ids[ - 1 ] if ids else None
217 raise
218 state = { "project" : project_state, "search" : search_state, "rbac" : rbac, "knowledge_base" : knowledge_base}
219 if context is not None :
220 state[ "cli_context" ] = context
221 expected = plan.get( "verified_dependencies" )
222 if expected is not None :
223 require_allowed_fields(expected, set (state), label = "Verified Prompt dependencies" )
224 if expected != state:
225 raise fail( "connection-prerequisite-drift" , "Project principal, CLI context, KB binding or exact role changed since planning; refresh before approval." )
226 elif plan[ "rbac_verified" ].get( "verified" ) is True and (
227 str (plan[ "rbac_verified" ][ "principal_id" ]).lower() != project_state[ "principal_id" ]
228 ):
229 raise fail( "project-reader-role-unverified" , "The approved principal is not the observed Foundry PROJECT identity." )
230 return state, profile, warnings, request_ids