Setting the file. One moment.
File Source · Foundry Iq · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page File Cu Canary
def main
— line 770
This file
Number 10.49
Position 49 of 77
Type Python
Size 44 KB
Lines 858 helpers/ file_source.py
Python · 858 lines · 44 KB
._progress
import
Progress, add_progress_argument, reporting
13 from . import file_ingest, search_reconcile, source_vector, file_cu_mi
14 from . import cu_ingestion_auth as file_cu_auth
15 from ._common import (
16 SEARCH_AUDIENCE ,
17 MANAGEMENT_AUDIENCE ,
18 HelperFailure,
19 TokenProvider,
20 Transport,
21 azure_cli_token,
22 blocked_result,
23 digest,
24 emit_result,
25 http_request,
26 load_approved_input,
27 normalize_azure_location,
28 reject_secrets,
29 require_allowed_fields,
30 )
31 except ImportError :
32 from _progress import Progress, add_progress_argument, reporting
33 import file_ingest # type: ignore[no-redef]
34 import search_reconcile # type: ignore[no-redef]
35 import source_vector
36 import file_cu_mi
37 import cu_ingestion_auth as file_cu_auth
38 from _common import ( # type: ignore[no-redef]
39 SEARCH_AUDIENCE ,
40 MANAGEMENT_AUDIENCE ,
41 HelperFailure,
42 TokenProvider,
43 Transport,
44 azure_cli_token,
45 blocked_result,
46 digest,
47 emit_result,
48 http_request,
49 load_approved_input,
50 normalize_azure_location,
51 reject_secrets,
52 require_allowed_fields,
53 )
54
55
56 def _cu_failure (code: str , message: str , * , request_id: str | None = None ) -> HelperFailure:
57 return HelperFailure(code, message, blocked_at = "cu-prerequisites" , request_id = request_id)
58
59
60 def validate_content_understanding (value: Any, * , enabled: bool ) -> dict[ str , Any] | None :
61 if not enabled:
62 if value is not None :
63 raise _cu_failure( "cu-choice-conflict" , "Minimal extraction must omit CU choices." )
64 return None
65 if not isinstance (value, dict ):
66 raise _cu_failure( "cu-prerequisite-missing" , "Standard planning requires a resolved CU account, disclosed auth channel and owner-verified prerequisites." )
67 value = copy.deepcopy(value)
68 value.setdefault( "auth" , "system-assigned" )
69 reject_secrets(value)
70 require_allowed_fields(value, {
71 "endpoint" , "resource_id" , "auth" , "api_key_environment" , "prerequisites" , "managed_identity" ,
72 }, label = "File CU choice" )
73 if (
74 not isinstance (value.get( "endpoint" ), str )
75 or re.fullmatch( r "https:// [ a-z0-9 ][ a-z0-9- ] {0,62} \. services \. ai \. azure \. com/ ? " , value[ "endpoint" ]) is None
76 or not isinstance (value.get( "resource_id" ), str )
77 or re.fullmatch(
78 r "/subscriptions/ [ 0-9a-fA-F- ] {36} /resourceGroups/ [ A-Za-z0-9_.()- ] {1,90} "
79 r "/providers/Microsoft \. CognitiveServices/accounts/ [ A-Za-z0-9 ][ A-Za-z0-9_.- ] {1,63} " ,
80 value[ "resource_id" ], re. IGNORECASE ,
81 ) is None
82 or value.get( "auth" ) not in ( "api-key-environment" , "api-key-arm" , "system-assigned" )
83 or (value.get( "auth" ) == "api-key-environment" and (
84 not isinstance (value.get( "api_key_environment" ), str )
85 or search_reconcile. ENVIRONMENT_NAME .fullmatch(value[ "api_key_environment" ]) is None
86 ))
87 or (value.get( "auth" ) != "api-key-environment" and "api_key_environment" in value)
88 or (value.get( "auth" ) != "system-assigned" and "managed_identity" in value)
89 ):
90 raise _cu_failure( "cu-choice-invalid" , "Select exact AIServices and system-assigned MI, or explicitly retain approved ARM/ENV key auth. No automatic auth fallback or setup changes." )
91 if value[ "auth" ] == "system-assigned" :
92 file_cu_mi.validate_choice(value.get( "managed_identity" ), value[ "resource_id" ])
93 prerequisites = value.get( "prerequisites" )
94 if not isinstance (prerequisites, dict ):
95 raise _cu_failure( "cu-prerequisite-missing" , "Supply CU region/capability, selected processing/required deployments, identity/local-auth and network evidence references." )
96 fields = { "resource" , "configuration" , "identity" , "network" }
97 require_allowed_fields(prerequisites, fields, label = "File CU prerequisites" )
98 if any ( not source_vector._text(prerequisites.get(key)) for key in fields):
99 raise _cu_failure( "cu-prerequisite-missing" , "Owner-verified CU capability/region, selected processing/required deployments, auth/access and reachability evidence is required." )
100 source_vector._json_valid(value)
101 return copy.deepcopy(value)
102
103
104 def _cu_account_state (choice: dict[ str , Any], account: Any) -> dict[ str , Any]:
105 properties = account.get( "properties" ) if isinstance (account, dict ) else None
106 if not isinstance (properties, dict ):
107 raise _cu_failure( "cu-prerequisite-invalid" , "CU account readback is incomplete." )
108 endpoints = properties.get( "endpoints" , {})
109 candidates = [properties.get( "endpoint" )]
110 if isinstance (endpoints, dict ):
111 candidates.extend(endpoints.values())
112 if (
113 str (account.get( "id" , "" )).casefold() != choice[ "resource_id" ].casefold()
114 or account.get( "kind" ) != "AIServices"
115 or normalize_azure_location(account.get( "location" )) is None
116 or properties.get( "provisioningState" ) != "Succeeded"
117 or (choice[ "auth" ] != "system-assigned" and properties.get( "disableLocalAuth" ) is not False )
118 or properties.get( "publicNetworkAccess" ) not in ( "Enabled" , "Disabled" )
119 or choice[ "endpoint" ].rstrip( "/" ) not in [v.rstrip( "/" ) for v in candidates if isinstance (v, str )]
120 ):
121 raise _cu_failure( "cu-prerequisite-invalid" , "Readback must bind the selected ready AIServices account/endpoint/location/network. Key modes also need enabled local auth; MI does not. Any required setup change needs separate approval." )
122 state = {
123 "id" : choice[ "resource_id" ], "kind" : "AIServices" , "location" : account[ "location" ],
124 "identity" : copy.deepcopy(account.get( "identity" )),
125 "properties" : {
126 "endpoint" : choice[ "endpoint" ].rstrip( "/" ), "provisioningState" : "Succeeded" ,
127 "disableLocalAuth" : properties.get( "disableLocalAuth" ), "publicNetworkAccess" : properties[ "publicNetworkAccess" ],
128 "networkAcls" : copy.deepcopy(properties.get( "networkAcls" )),
129 },
130 }
131 if choice[ "auth" ] == "system-assigned" :
132 acl = properties.get( "networkAcls" )
133 if properties[ "publicNetworkAccess" ] != "Enabled" or (
134 acl is not None and ( not isinstance (acl, dict ) or acl.get( "defaultAction" ) != "Allow" )
135 ):
136 raise _cu_failure( "cu-mi-network-unverified" , "This MI path requires existing public CU reachability without default-deny ACLs. Restricted/private network compatibility needs separate verified setup and approval; no network changes or key fallback." )
137 reject_secrets(state)
138 source_vector._json_valid(state)
139 return state
140
141
142 def _cu_states_match (current: dict[ str , Any], retained: dict[ str , Any]) -> bool :
143 location = normalize_azure_location(retained.get( "location" ))
144 return location is not None and (
145 { ** current, "location" : normalize_azure_location(current.get( "location" ))}
146 == { ** retained, "location" : location}
147 )
148
149
150 def read_content_understanding (
151 choice: dict[ str , Any], * , token_provider: TokenProvider, transport: Transport,
152 ) -> tuple[dict[ str , Any], list[ str ]]:
153 url = f " { MANAGEMENT_AUDIENCE }{ choice[ 'resource_id' ] } ?api-version=2024-10-01"
154 response = transport( "GET" , url, token_provider( MANAGEMENT_AUDIENCE ))
155 try :
156 if response.status != 200 :
157 raise _cu_failure( "cu-prerequisite-unavailable" , "Selected CU account metadata could not be read; no provisioning or auth changes are allowed." )
158 state = _cu_account_state(choice, response.body)
159 except HelperFailure as failure:
160 failure.request_id = response.request_id
161 if response.status != 200 :
162 failure.http_status = response.status
163 raise
164 return state, [response.request_id] if response.request_id else []
165
166
167 def verify_content_understanding_readback (choice: dict[ str , Any], current: Any) -> None :
168 parameters = current.get( "fileParameters" ) if isinstance (current, dict ) else None
169 ingestion = parameters.get( "ingestionParameters" ) if isinstance (parameters, dict ) else None
170 ai = ingestion.get( "aiServices" ) if isinstance (ingestion, dict ) else None
171 if (
172 not isinstance (ai, dict ) or ingestion.get( "contentExtractionMode" ) != "standard"
173 or not isinstance (ai.get( "uri" ), str )
174 or ai[ "uri" ].rstrip( "/" ) != choice[ "endpoint" ].rstrip( "/" )
175 or ingestion.get( "identity" ) is not None
176 or (choice[ "auth" ] == "system-assigned" and ai.get( "apiKey" ) not in file_cu_mi. REDACTED )
177 ):
178 raise _cu_failure( "cu-readback-mismatch" , "Observed File CU endpoint/extraction/auth conflicts with the selected configuration; credential details withheld." )
179 # File's approved key may be redacted in GET. It is not embedding auth,
180 # and source readback cannot prove its value or CU processing readiness.
181
182
183 def plan_source (
184 request: dict[ str , Any],
185 * ,
186 token_provider: TokenProvider = azure_cli_token,
187 transport: Transport = http_request,
188 context_provider = file_cu_auth.account_context,
189 ) -> dict[ str , Any]:
190 """Build an unapproved File workflow using only local reads and GETs."""
191 if not isinstance (request, dict ):
192 raise HelperFailure(
193 "input-schema-invalid" , "Planning input must be an object." ,
194 blocked_at = "input-resolution" ,
195 )
196 reject_secrets(request)
197 require_allowed_fields(
198 request,
199 { "schema_version" , "api_version" , "endpoint" , "name" , "owner" , "local_root" , "paths" ,
200 "service_tier" , "extraction_mode" , "vectorization" , "embedding" , "rbac" , "network" ,
201 "content_understanding" , "reuse_input_file" , "reuse_result_file" },
202 label = "File planning input" ,
203 )
204 if request.get( "schema_version" ) != "1.0" :
205 raise HelperFailure(
206 "input-schema-invalid" , "Planning requires schema_version 1.0." ,
207 blocked_at = "input-resolution" ,
208 )
209 file_ingest.validate_api_version(request.get( "api_version" , file_ingest. API_VERSION ))
210 for field in ( "name" , "owner" ):
211 if not isinstance (request.get(field), str ) or not request[field].strip():
212 raise HelperFailure(
213 "input-schema-invalid" , f "An explicit non-empty { field } is required." ,
214 blocked_at = "input-resolution" ,
215 )
216 if request.get( "extraction_mode" ) not in ( "minimal" , "standard" ) or request.get( "vectorization" ) not in ( "none" , "azureOpenAI" ):
217 raise HelperFailure(
218 "planning-processing-unsupported" ,
219 "Select minimal or standard extraction and independent vectorization none or azureOpenAI." ,
220 blocked_at = "input-resolution" ,
221 )
222 embedding = source_vector.validate_choice(
223 request.get( "embedding" ), enabled = request[ "vectorization" ] == "azureOpenAI" ,
224 api_version = file_ingest. API_VERSION ,
225 )
226 cu = validate_content_understanding(
227 request.get( "content_understanding" ), enabled = request[ "extraction_mode" ] == "standard" ,
228 )
229 if (request.get( "reuse_input_file" ) is not None or request.get( "reuse_result_file" ) is not None ) and (
230 cu is None or cu[ "auth" ] != "system-assigned"
231 ):
232 raise _cu_failure( "cu-choice-conflict" , "File MI provenance inputs are only for managed-identity exact reuse." )
233 rbac, network = request.get( "rbac" ), request.get( "network" )
234 if (
235 not isinstance (rbac, dict )
236 or not isinstance (rbac.get( "assignments" ), list )
237 or not rbac[ "assignments" ]
238 or not all ( isinstance (item, dict ) and item for item in rbac[ "assignments" ])
239 or not isinstance (network, dict )
240 or not isinstance (network.get( "posture" ), str )
241 or not network[ "posture" ].strip()
242 or not isinstance (network.get( "evidence" ), str )
243 or not network[ "evidence" ].strip()
244 ):
245 raise HelperFailure(
246 "planning-evidence-missing" ,
247 "Supply observed RBAC assignments and network posture/evidence; the policy owner must refresh and verify them before approval." ,
248 blocked_at = "input-resolution" ,
249 )
250 root = file_ingest.resolve_local_root(request.get( "local_root" ))
251 records = file_ingest.snapshot_inventory(
252 root, request.get( "paths" ), service_tier = request.get( "service_tier" )
253 )
254 common = {
255 "endpoint" : request.get( "endpoint" ), "name" : request.get( "name" ),
256 "api_version" : file_ingest. API_VERSION , "owner" : request.get( "owner" ),
257 "cleanup_approved" : False , "rbac" : copy.deepcopy(rbac),
258 "network" : copy.deepcopy(network),
259 }
260 source = {
261 ** common, "operation" : "reconcile" , "resource_type" : "knowledge-source" ,
262 "outcome" : "create-file-knowledge-source" , "action" : "create" ,
263 "desired" : {
264 "name" : common[ "name" ], "kind" : "file" ,
265 "fileParameters" : { "ingestionParameters" : { "contentExtractionMode" : request[ "extraction_mode" ]}},
266 },
267 }
268 ingestion = {
269 ** copy.deepcopy(common), "operation" : "ingest" , "local_root" : str (root),
270 "files" : records, "inventory_digest" : digest(records),
271 "expected_server_inventory_digest" : file_ingest.inventory_digest([]),
272 "service_tier" : request[ "service_tier" ], "extraction_mode" : request[ "extraction_mode" ],
273 }
274 plan = {
275 "operation" : "reconcile-and-ingest" , "outcome" : "create-file-knowledge-source" ,
276 "owner" : common[ "owner" ], "cleanup_approved" : False ,
277 "source" : source, "ingestion" : ingestion,
278 }
279 if embedding is not None :
280 plan[ "embedding" ] = embedding
281 source[ "desired" ][ "fileParameters" ][ "ingestionParameters" ][ "embeddingModel" ] = source_vector.model_definition(embedding)
282 if cu is not None :
283 automatic = cu[ "auth" ] == "api-key-arm"
284 mi = cu[ "auth" ] == "system-assigned"
285 plan.update( file_cu_plan_version = "1.2" if mi else "1.1" if automatic else "1.0" , content_understanding = cu)
286 if automatic:
287 source[ "ai_services_key_acquisition" ] = file_cu_auth.acquisition(cu, context_provider())
288 elif mi:
289 source[ "ai_services_managed_identity" ] = True
290 else :
291 source[ "ai_services_api_key_environment" ] = cu[ "api_key_environment" ]
292 source[ "desired" ][ "fileParameters" ][ "ingestionParameters" ].update(
293 aiServices = { "uri" : cu[ "endpoint" ].rstrip( "/" )}, disableImageVerbalization = True ,
294 )
295 # Validate the local inventory and all choices before any authentication.
296 _validate_plan(plan, require_cu_readback = False )
297 cu_request_ids = []
298 if cu is not None :
299 state, cu_request_ids = read_content_understanding(cu, token_provider = token_provider, transport = transport)
300 plan[ "cu_resource_state" ] = state
301 if cu[ "auth" ] == "system-assigned" :
302 plan[ "cu_identity_state" ], ids = file_cu_mi.read_binding(
303 cu, common[ "endpoint" ], token_provider = token_provider, transport = transport,
304 )
305 cu_request_ids.extend(ids)
306 _validate_plan(plan)
307 transport = source_vector.guard_readback_transport(plan, transport)
308 url = search_reconcile.resource_url(source)
309 token = token_provider( SEARCH_AUDIENCE )
310 current, request_id = search_reconcile.read_resource(url, token, transport = transport)
311 request_ids = cu_request_ids + ([request_id] if request_id else [])
312 matched = {}
313 if current is not None :
314 if cu is not None and cu[ "auth" ] == "system-assigned" :
315 file_cu_mi.verify_reuse(request, plan, current)
316 if not search_reconcile.definitions_match(source[ "desired" ], current):
317 raise HelperFailure(
318 "definition-conflict" ,
319 "The exact source has a different definition; planning never overwrites or chooses another name." ,
320 blocked_at = "reconciliation" ,
321 )
322 etag = current.get( "@odata.etag" )
323 if not isinstance (etag, str ) or not etag:
324 raise HelperFailure(
325 "definition-evidence-missing" , "Exact reuse requires the current source ETag." ,
326 blocked_at = "reconciliation" ,
327 )
328 before, ids = file_ingest.read_inventory(ingestion, token, transport = transport)
329 request_ids.extend(ids)
330 matched = file_ingest.reconcile_inventory(ingestion, before)
331 file_ids = [item.get( "fileId" ) for item in matched.values()]
332 if (
333 not all ( isinstance (value, str ) and value for value in file_ids)
334 or len ( set (file_ids)) != len (file_ids)
335 ):
336 raise HelperFailure(
337 "file-identity-ambiguous" , "Exact reuse requires unique non-empty server file IDs." ,
338 blocked_at = "reconciliation" ,
339 )
340 source.update( action = "reuse" , expected_etag = etag)
341 ingestion[ "expected_server_inventory_digest" ] = file_ingest.inventory_digest(before)
342 refreshed, refresh_id = search_reconcile.read_resource(url, token, transport = transport)
343 if refresh_id:
344 request_ids.append(refresh_id)
345 if (
346 refreshed is None
347 or refreshed.get( "@odata.etag" ) != etag
348 or not search_reconcile.definitions_match(source[ "desired" ], refreshed)
349 ):
350 raise HelperFailure(
351 "definition-drift" ,
352 "Source definition or ETag changed during file inventory readback." ,
353 blocked_at = "reconciliation" ,
354 )
355 # Do not return a snapshot that changed while Search discovery was running.
356 _validate_plan(plan)
357 if cu is not None :
358 state, ids = read_content_understanding(cu, token_provider = token_provider, transport = transport)
359 request_ids.extend(ids)
360 if not _cu_states_match(state, plan[ "cu_resource_state" ]):
361 raise _cu_failure(
362 "cu-prerequisite-drift" , "CU account access or configuration changed during planning; refresh the plan." ,
363 request_id = ids[ - 1 ] if ids else None ,
364 )
365 if cu[ "auth" ] == "api-key-arm" :
366 file_cu_auth.check_context(source[ "ai_services_key_acquisition" ][ "context" ], context_provider)
367 elif cu[ "auth" ] == "system-assigned" :
368 binding, ids = file_cu_mi.read_binding(cu, common[ "endpoint" ], token_provider = token_provider, transport = transport)
369 request_ids.extend(ids)
370 if binding != plan[ "cu_identity_state" ]:
371 raise _cu_failure( "cu-mi-identity-drift" , "Search identity, CU scoped role or network changed during planning." )
372 fingerprint = digest(plan)
373 mutation_required = source[ "action" ] == "create"
374 return {
375 "status" : "planned" , "outcome" : plan[ "outcome" ],
376 "plan_fingerprint" : fingerprint,
377 "execution_input" : {
378 "schema_version" : "1.0" , "plan" : plan,
379 "approval" : { "confirmed" : False , "fingerprint" : fingerprint},
380 },
381 "approval_summary" : {
382 "target" : { "endpoint" : common[ "endpoint" ], "name" : common[ "name" ],
383 "api_version" : common[ "api_version" ], "preview" : True },
384 "source_action" : source[ "action" ], "owner" : common[ "owner" ],
385 "execution_required" : mutation_required,
386 "mutation_approval_required" : mutation_required,
387 "processing" : (
388 "standard CU extraction" + ( " with source embeddings" if embedding else "; source vectors off" )
389 if cu else "minimal extraction with embeddings" if embedding else "minimal lexical; no models"
390 ),
391 ** ({ "content_understanding" : {
392 "purpose" : "Standard document extraction only; not source vectorization or KB answer synthesis." ,
393 "endpoint" : cu[ "endpoint" ],
394 "authentication" : file_cu_auth.approval_summary( "file" , cu[ "auth" ], creating = mutation_required),
395 "auth" : (
396 "Search system-assigned MI for CU; no API key or credential reads, no local-auth requirement. Service implementation inspected; live compatibility unverified."
397 if cu[ "auth" ] == "system-assigned" else
398 "Existing File CU key-auth configuration reused; no credential acquisition. Search remains keyless."
399 if not mutation_required else
400 "Approved private ARM listKeys acquisition of key1 for this source PUT only; Search remains keyless. Local authentication means key auth, not manual local setup."
401 if cu[ "auth" ] == "api-key-arm" else "Explicit existing CU API-key ENV channel; Search remains keyless."
402 ),
403 ** ({ "credential_acquisition" : copy.deepcopy(source[ "ai_services_key_acquisition" ])} if cu[ "auth" ] == "api-key-arm" and mutation_required else {}),
404 ** ({ "managed_identity" : copy.deepcopy(plan[ "cu_identity_state" ])} if cu[ "auth" ] == "system-assigned" else {}),
405 "cost_and_data" : "Billable CU processing, no daily free document allowance; uploaded content moves from Search to CU, possibly across regions. Search retains outputs." ,
406 "verification" : (
407 "ARM verifies Search identity, exact CU-scoped role and account/network metadata, not backend MI rollout, effective access or extraction. A separately approved bounded OCR canary can validate functionality."
408 if cu[ "auth" ] == "system-assigned" else
409 "ARM verifies account binding/local-auth/network metadata, not effective access, key validity or processing success. Verify selected processing/required deployment evidence; no blanket account-defaults confirmation."
410 ),
411 "kb_reasoning" : "Unchanged; source CU does not enable KB chat or source vectors." ,
412 }} if cu else {}),
413 ** ({ "embedding" : source_vector.summary(embedding)} if embedding else {}),
414 "data_boundary" : { "paths" : [r[ "path" ] for r in records],
415 "file_count" : len (records), "total_bytes" : sum (r[ "size" ] for r in records)},
416 "uploads" : 0 if matched else len (records),
417 "reused_files" : [{ "path" : path, "fileId" : item[ "fileId" ]} for path, item in matched.items()],
418 "service_tier" : request[ "service_tier" ],
419 "rbac" : rbac, "network" : network,
420 "cost_and_retention" : (
421 "Existing Search charges remain; review File ingestion/storage charges and retention before approval."
422 if mutation_required else "Existing charges and retention are unchanged; no new uploads or resources."
423 ),
424 "ownership" : "New source and uploaded files only; reused Search/source/files are not run-owned." ,
425 "format_verification" : "Filename/MIME hints are not detected types; Search checks actual content support during ingestion." ,
426 "verification" : (
427 "Execution rechecks definition/ETag and local/server inventories, then verifies uploaded file markers."
428 if mutation_required else
429 "Fresh source definition/ETag and complete file markers/IDs match the local inventory; this does not verify ingestion readiness or retrieval."
430 ),
431 "cleanup" : "Excluded; separate run-owned source cleanup plan and approval required." ,
432 "next_step" : (
433 "Creation owner refreshes identity, RBAC, network, source state and cost/data consent, then obtains explicit approval of these changes before applying the unchanged execution input."
434 if mutation_required else
435 "Reuse the verified identity and file markers without mutation approval or invoking the mutation helper. Refresh discovery before later use; this observation is not future consent."
436 ),
437 },
438 "read_only_evidence" : { "request_ids" : request_ids, "source_state" : source[ "action" ]},
439 "writes_performed" : [],
440 "warnings" : [
441 "Planning is not approval or completed ingestion. Uploader RBAC/network remain supplied evidence; MI identity/role metadata readbacks do not prove backend attribution or effective access."
442 if cu is not None and cu[ "auth" ] == "system-assigned" else
443 "Planning is not approval, policy evaluation, or proof of completed ingestion. RBAC/network are caller-supplied evidence, not verified by this helper."
444 ],
445 }
446
447
448 def _validate_plan (
449 plan: dict[ str , Any],
450 * ,
451 require_cu_readback: bool = True ,
452 ) -> tuple[dict[ str , Any], dict[ str , Any]]:
453 reject_secrets(plan)
454 require_allowed_fields(
455 plan,
456 {
457 "operation" ,
458 "outcome" ,
459 "cleanup_approved" ,
460 "owner" ,
461 "source" ,
462 "ingestion" ,
463 "embedding" ,
464 "content_understanding" ,
465 "file_cu_plan_version" ,
466 "cu_resource_state" ,
467 "cu_identity_state" ,
468 },
469 label = "File source plan" ,
470 )
471 if (
472 plan.get( "operation" ) != "reconcile-and-ingest"
473 or plan.get( "cleanup_approved" ) is not False
474 ):
475 raise HelperFailure(
476 "operation-invalid" ,
477 "File source application requires reconcile-and-ingest with cleanup excluded." ,
478 blocked_at = "input-resolution" ,
479 )
480 source = plan.get( "source" )
481 ingestion = plan.get( "ingestion" )
482 if not isinstance (source, dict ) or not isinstance (ingestion, dict ):
483 raise HelperFailure(
484 "input-schema-invalid" ,
485 "File source application requires source and ingestion objects." ,
486 blocked_at = "input-resolution" ,
487 )
488 desired = source.get( "desired" )
489 if (
490 source.get( "operation" ) != "reconcile"
491 or source.get( "resource_type" ) != "knowledge-source"
492 or source.get( "action" ) not in { "create" , "reuse" }
493 or not isinstance (desired, dict )
494 or desired.get( "kind" ) != "file"
495 or ingestion.get( "operation" ) != "ingest"
496 or plan.get( "owner" ) != source.get( "owner" )
497 or any (
498 source.get(field) != ingestion.get(field)
499 for field in ( "endpoint" , "name" , "api_version" , "owner" )
500 )
501 ):
502 raise HelperFailure(
503 "step-contract-mismatch" ,
504 "Source reconciliation and ingestion must target the same approved File source." ,
505 blocked_at = "input-resolution" ,
506 )
507 source_mode = desired.get( "fileParameters" , {}).get(
508 "ingestionParameters" , {}
509 ).get( "contentExtractionMode" )
510 if source_mode != ingestion.get( "extraction_mode" ):
511 raise HelperFailure(
512 "step-contract-mismatch" ,
513 "Source and ingestion extraction modes must match exactly." ,
514 blocked_at = "input-resolution" ,
515 )
516 search_reconcile._validate_plan(source)
517 file_ingest._validate_plan(ingestion)
518 source_vector.validate_plan_choice(plan)
519 if any (field in plan for field in ( "file_cu_plan_version" , "content_understanding" , "cu_resource_state" )):
520 if plan.get( "file_cu_plan_version" ) not in ( "1.0" , "1.1" , "1.2" ) or source_mode != "standard" :
521 raise _cu_failure( "cu-plan-mismatch" , "New File CU plans require their supported CU-specific version and standard extraction." )
522 cu = validate_content_understanding(plan.get( "content_understanding" ), enabled = True )
523 automatic = cu[ "auth" ] == "api-key-arm"
524 mi = cu[ "auth" ] == "system-assigned"
525 acquisition = source.get( "ai_services_key_acquisition" )
526 if automatic:
527 file_cu_auth.validate_acquisition(acquisition, cu[ "endpoint" ])
528 if (
529 plan[ "file_cu_plan_version" ] != ( "1.2" if mi else "1.1" if automatic else "1.0" )
530 or (automatic and acquisition[ "resource_id" ] != cu[ "resource_id" ])
531 or ( not automatic and acquisition is not None )
532 or source.get( "ai_services_managed_identity" ) is not ( True if mi else None )
533 or ( not mi and "cu_identity_state" in plan)
534 ):
535 raise _cu_failure( "cu-plan-mismatch" , "CU auth mode, version and exact acquisition scope must match the approved plan." )
536 settings = desired[ "fileParameters" ][ "ingestionParameters" ]
537 if (
538 settings.get( "aiServices" ) != { "uri" : cu[ "endpoint" ].rstrip( "/" )}
539 or settings.get( "identity" ) is not None
540 or settings.get( "disableImageVerbalization" ) is not True
541 or settings.get( "chatCompletionModel" ) is not None
542 or source.get( "ai_services_api_key_environment" ) != cu.get( "api_key_environment" )
543 or ( "embedding" in plan) != (settings.get( "embeddingModel" ) is not None )
544 ):
545 raise _cu_failure( "cu-plan-mismatch" , "CU/embedding choices, credential channel and source processing must match the approved definition." )
546 if require_cu_readback:
547 state = plan.get( "cu_resource_state" )
548 if not isinstance (state, dict ) or state != _cu_account_state(cu, state):
549 raise _cu_failure( "cu-prerequisite-missing" , "Retain the planner's selected CU account readback." )
550 if mi:
551 file_cu_mi.validate_state(plan.get( "cu_identity_state" ), cu, source[ "endpoint" ])
552 elif source.get( "ai_services_key_acquisition" ) is not None or source.get( "ai_services_managed_identity" ) is not None or "cu_identity_state" in plan:
553 raise _cu_failure( "cu-plan-mismatch" , "Automatic acquisition requires the complete versioned File CU workflow." )
554 return source, ingestion
555
556
557 def _writes (result: dict[ str , Any]) -> list[dict[ str , Any]]:
558 writes: list[dict[ str , Any]] = []
559 for action in ( "created" , "updated" ):
560 for resource in result[ "resources" ].get(action, []):
561 writes.append(
562 {
563 "action" : action,
564 "type" : resource[ "type" ],
565 "name" : resource[ "name" ],
566 }
567 )
568 return writes
569
570
571 @reporting ( "file-source" )
572 def execute (
573 document: dict[ str , Any],
574 * ,
575 token_provider: TokenProvider = azure_cli_token,
576 transport: Transport = http_request,
577 progress: Progress | None = None ,
578 context_provider = file_cu_auth.account_context,
579 mi_on_created = None ,
580 cleanup_capture = None ,
581 upload_receipt_dir = None ,
582 allow_upload_retry = True ,
583 ) -> dict[ str , Any]:
584 progress.update( "validation" )
585 plan = document[ "plan" ]
586 fingerprint = document[ "_computed_fingerprint" ]
587 source, ingestion = _validate_plan(plan)
588 upload_session = None
589 if upload_receipt_dir is not None :
590 try :
591 from .file_upload import Session
592 except ImportError :
593 from file_upload import Session
594 upload_session = Session(upload_receipt_dir, document, context_provider = context_provider)
595 private_key = None
596 acquisition = source.get( "ai_services_key_acquisition" )
597 mi = source.get( "ai_services_managed_identity" ) is True
598 mi_callback = None
599 if mi_on_created is not None and ( not mi or not callable (mi_on_created)):
600 raise _cu_failure( "creation-callback-unsupported" , "Private MI checkpoints cannot receive File key-auth wire." )
601 if acquisition is not None or mi:
602 approval = document.get( "approval" )
603 if (
604 not isinstance (approval, dict ) or approval.get( "confirmed" ) is not True
605 or approval.get( "fingerprint" ) != digest(plan) or fingerprint != digest(plan)
606 ):
607 raise _cu_failure( "approval-missing" , "Private credential acquisition requires the unchanged fingerprinted source approval." )
608 if acquisition is not None :
609 file_cu_auth.check_context(acquisition[ "context" ], context_provider)
610 child = { "_computed_fingerprint" : fingerprint}
611 cu_ids = []
612 if "content_understanding" in plan:
613 state, cu_ids = read_content_understanding(
614 plan[ "content_understanding" ], token_provider = token_provider, transport = transport,
615 )
616 if not _cu_states_match(state, plan[ "cu_resource_state" ]):
617 raise _cu_failure(
618 "cu-prerequisite-drift" , "CU account access or configuration changed since approval; refresh the plan." ,
619 request_id = cu_ids[ - 1 ] if cu_ids else None ,
620 )
621 if mi:
622 binding, ids = file_cu_mi.read_binding(
623 plan[ "content_understanding" ], source[ "endpoint" ], token_provider = token_provider, transport = transport,
624 )
625 cu_ids.extend(ids)
626 if binding != plan[ "cu_identity_state" ]:
627 raise _cu_failure( "cu-mi-identity-drift" , "Search identity, CU role assignment or network changed since approval; refresh the concrete plan." )
628 raw_transport = transport
629
630 def recheck_mi ():
631 state, _ = read_content_understanding(
632 plan[ "content_understanding" ], token_provider = token_provider, transport = raw_transport,
633 )
634 binding, _ = file_cu_mi.read_binding(
635 plan[ "content_understanding" ], source[ "endpoint" ], token_provider = token_provider, transport = raw_transport,
636 )
637 if not _cu_states_match(state, plan[ "cu_resource_state" ]) or binding != plan[ "cu_identity_state" ]:
638 raise _cu_failure( "cu-mi-identity-drift" , "CU account or Search identity/role/network changed immediately before source PUT." )
639
640 transport, mi_callback = file_cu_mi.guard_create(plan, transport, recheck_mi, mi_on_created)
641 if acquisition is not None :
642 def recheck ():
643 current, _ = read_content_understanding(
644 plan[ "content_understanding" ], token_provider = token_provider, transport = transport,
645 )
646 if not _cu_states_match(current, plan[ "cu_resource_state" ]):
647 raise _cu_failure( "cu-prerequisite-drift" , "CU account changed before credential acquisition; refresh the plan and approval." )
648
649 private_key = file_cu_auth.PrivateKey(
650 acquisition, token_provider = token_provider, transport = transport,
651 context_provider = context_provider, recheck = recheck,
652 )
653 transport = private_key.transport
654
655 if upload_session is not None :
656 transport = upload_session.transport(transport)
657 progress.update( "source-reconciliation" )
658 source_result = search_reconcile.execute(
659 { ** child, "plan" : source},
660 token_provider = token_provider,
661 transport = source_vector.guard_readback_transport(plan, transport),
662 credential_provider = private_key.acquire if private_key else None ,
663 ** ({ "managed_identity_verified" : True , "on_created" : mi_callback} if mi else {}),
664 ** ({ "cleanup_capture" : cleanup_capture} if cleanup_capture is not None else {}),
665 ** ({ "on_file_acknowledged" : upload_session.acknowledge} if upload_session is not None else {}),
666 )
667 source_writes = _writes(source_result)
668
669 def check_retry_source (recovery, token):
670 _validate_plan(plan)
671 current, _ = search_reconcile._get(
672 search_reconcile.resource_url(source), token,
673 transport = source_vector.guard_readback_transport(plan, transport), recovery = recovery,
674 )
675 etag = source_result[ "verification" ][ "readback" ][ "etag" ]
676 if ( not etag or current is None or current.get( "@odata.etag" ) != etag
677 or not search_reconcile.definitions_match(source[ "desired" ], current)):
678 raise HelperFailure( "file-upload-source-drift" , "Retry requires the unchanged acknowledged source version/definition." ,
679 blocked_at = "verification" )
680
681 try :
682 if upload_session is not None :
683 etag = upload_session.require_ack()
684 if source_result[ "verification" ][ "readback" ][ "etag" ] != etag:
685 raise HelperFailure( "file-upload-source-drift" , "Source readback differs from the retained creation ACK; retain the source." ,
686 blocked_at = "verification" )
687 ingestion_result = file_ingest.execute(
688 { ** child, "plan" : ingestion},
689 token_provider = token_provider,
690 transport = transport,
691 allow_new_uploads = bool (source_result[ "resources" ][ "created" ]),
692 progress = progress,
693 source_check = check_retry_source,
694 allow_upload_retry = allow_upload_retry,
695 ** ({ "upload_session" : upload_session} if upload_session is not None else {}),
696 )
697 except HelperFailure as failure:
698 combined = HelperFailure(
699 failure.code,
700 failure.message,
701 blocked_at = failure.blocked_at,
702 writes = source_writes + failure.writes,
703 resources_remaining = (
704 [
705 {
706 "type" : str (write[ "type" ]),
707 "name" : str (write[ "name" ]),
708 }
709 for write in source_writes
710 ]
711 + failure.resources_remaining
712 ),
713 resources_reused = failure.resources_reused,
714 resources_unverified = failure.resources_unverified,
715 warnings = failure.warnings,
716 request_id = failure.request_id,
717 status = failure.http_status,
718 partial = bool (source_writes or failure.writes or failure.partial),
719 )
720 combined.file_batch = failure.file_batch
721 raise combined from failure
722
723 resources = { "created" : [], "reused" : [], "updated" : [], "skipped" : []}
724 for action in resources:
725 resources[action].extend(source_result[ "resources" ].get(action, []))
726 resources[action].extend(ingestion_result[ "resources" ].get(action, []))
727 return {
728 "status" : "completed" ,
729 "outcome" : str (plan.get( "outcome" ) or "create-file-knowledge-source" ),
730 "approved_plan" : { "fingerprint" : fingerprint, "confirmed" : True },
731 "resources" : resources,
732 "api_contracts" : source_result[ "api_contracts" ]
733 + ingestion_result[ "api_contracts" ],
734 "data_movement" : ingestion_result[ "data_movement" ],
735 "auth" : ingestion_result[ "auth" ],
736 "rbac" : ingestion_result[ "rbac" ],
737 "network" : ingestion_result[ "network" ],
738 "verification" : {
739 ** ({ "cu_managed_identity" : {
740 "mode" : "system-assigned" , "binding_digest" : digest(plan[ "cu_identity_state" ]),
741 "processing" : "unverified until indexed OCR marker validation" ,
742 "principal_attribution" : "unverified; no backend identity telemetry collected" ,
743 }} if mi else {}),
744 "readback" : {
745 "source" : source_result[ "verification" ][ "readback" ],
746 "files" : ingestion_result[ "verification" ][ "readback" ],
747 },
748 "request_ids" : cu_ids + source_result[ "verification" ][ "request_ids" ]
749 + ingestion_result[ "verification" ][ "request_ids" ],
750 "idempotency" : (
751 "exact source and file marker readback is zero-write"
752 ),
753 },
754 "warnings" : source_result[ "warnings" ] + ingestion_result[ "warnings" ],
755 ** ({ "file_batch" : ingestion_result[ "file_batch" ]} if "file_batch" in ingestion_result else {}),
756 "ownership" : {
757 "run_owned" : source_result[ "ownership" ][ "run_owned" ]
758 + ingestion_result[ "ownership" ][ "run_owned" ],
759 "reused_not_owned" : source_result[ "ownership" ][ "reused_not_owned" ]
760 + ingestion_result[ "ownership" ][ "reused_not_owned" ],
761 "owner" : plan.get( "owner" ),
762 },
763 "cleanup" : {
764 "status" : "not-requested" ,
765 "separate_confirmation_required" : True ,
766 },
767 }
768
769
770 def main (argv: list[ str ] | None = None ) -> int :
771 try :
772 from . import _cleanup_receipts as cleanup_receipts
773 from .private_artifacts import add_execution_output_argument, emit_plan_result, validate_execution_output_mode
774 except ImportError :
775 import _cleanup_receipts as cleanup_receipts
776 from private_artifacts import add_execution_output_argument, emit_plan_result, validate_execution_output_mode
777 parser = argparse.ArgumentParser()
778 modes = parser.add_mutually_exclusive_group( required = True )
779 modes.add_argument( "--input" , type = Path)
780 modes.add_argument( "--plan" , type = Path)
781 add_execution_output_argument(parser)
782 add_progress_argument(parser)
783 cleanup_receipts.add_argument(parser)
784 parser.add_argument( "--upload-receipt-dir" , type = Path,
785 help = "Required for creation: existing empty private directory for original File ACK and pre-upload attempts." )
786 args = parser.parse_args(argv)
787 fingerprint: str | None = None
788 owner: Any = None
789 outcome = "create-file-knowledge-source"
790 capture = None
791 try :
792 validate_execution_output_mode(args)
793 if args.plan and (args.cleanup_receipt_dir or args.upload_receipt_dir):
794 raise HelperFailure( "input-schema-invalid" , "Creation capture requires approved --input." , blocked_at = "confirmation" )
795 if args.plan:
796 try :
797 request = json.loads(args.plan.read_text( encoding = "utf-8" ))
798 except ( OSError , UnicodeError , json.JSONDecodeError) as exc:
799 raise HelperFailure(
800 "input-unreadable" , "Planning input must be readable UTF-8 JSON." ,
801 blocked_at = "input-resolution" ,
802 ) from exc
803 result = plan_source(request)
804 emit_plan_result(result, args.execution_output)
805 return 0
806 document, plan, fingerprint = load_approved_input(args.input)
807 document[ "_computed_fingerprint" ] = fingerprint
808 owner = plan.get( "owner" )
809 outcome = str (plan.get( "outcome" ) or outcome)
810 source, _ = _validate_plan(plan)
811 if source[ "action" ] == "create" and args.cleanup_receipt_dir is None :
812 raise HelperFailure(
813 "file-creation-receipt-required" ,
814 "File creation requires explicit --cleanup-receipt-dir pointing to an existing protected private directory; no default is inferred." ,
815 blocked_at = "confirmation" ,
816 )
817 if args.cleanup_receipt_dir is not None :
818 directory = cleanup_receipts.private_io.validate_private_artifact_directory( str (args.cleanup_receipt_dir))
819 if args.upload_receipt_dir is not None :
820 upload_directory = cleanup_receipts.private_io.validate_private_artifact_directory( str (args.upload_receipt_dir))
821 if directory == upload_directory:
822 raise HelperFailure( "file-receipt-directory-conflict" ,
823 "Use separate private cleanup and upload journal directories." ,
824 blocked_at = "confirmation" )
825 elif source[ "action" ] == "create" :
826 raise HelperFailure(
827 "file-upload-receipt-required" ,
828 "File creation requires explicit --upload-receipt-dir for durable original ACK and pre-upload attempt records; use a separate existing empty private directory." ,
829 blocked_at = "confirmation" ,
830 )
831 capture = cleanup_receipts.Capture(directory, document)
832 result = execute(document, progress = Progress( "file-source" , enabled = args.progress),
833 ** ({ "cleanup_capture" : capture} if capture else {}),
834 ** ({ "upload_receipt_dir" : args.upload_receipt_dir} if args.upload_receipt_dir else {}))
835 except HelperFailure as failure:
836 result = blocked_result(
837 failure,
838 outcome = outcome,
839 fingerprint = fingerprint,
840 owner = owner,
841 )
842 if capture is not None :
843 result[ "cleanup_receipts" ] = capture.summaries
844 result[ "safe_next_decision" ] = (
845 "Retain any acknowledged source and confirmed uploads. Do not rerun the creation envelope, "
846 "replay uncertain uploads, reset or delete resources. Use file_upload.py --plan with the "
847 "original upload journal for newly approved never-attempted files; missing evidence blocks continuation, not retention."
848 )
849 emit_result(result)
850 return 3 if result[ "status" ] == "partial" else 2
851 if capture is not None :
852 result[ "cleanup_receipts" ] = capture.summaries
853 emit_result(result)
854 return 0
855
856
857 if __name__ == "__main__" :
858 sys.exit(main())