馃悰 Bug Description
In runtime/rust/prompty-foundry/src/arm_discovery.rs, fetch_all accumulates every entry of an ARM page's value array, including non-object ones (null, strings, numbers).
list_foundry_projects then maps the modern ("S1") project results with an infallible .map(parse_s1_project) (line 91), whereas the classic-hub ("S2") results use .filter_map(parse_s2_workspace) (line 103).
The consequence is that a single stray non-object entry in the S1 response becomes a FoundryProject with empty fields. Because projects is then non-empty, the if projects.is_empty() guard at line 95 skips the classic-hub fallback entirely - so a caller gets one junk project instead of the real ones the fallback would have found.
Two things go wrong at once: a bogus entry is surfaced, and a legitimate discovery path is suppressed.
Steps to reproduce
-
Call list_foundry_projects(token, sub, rg, resource) against an account whose projects only exist as classic ML-workspace hubs (so the S1 endpoint has nothing real to return).
-
Have the S1 Microsoft.CognitiveServices/accounts/{name}/projects endpoint return a payload whose value array contains a non-object entry, e.g.:
-
Observe the returned project list.
Expected behavior
The non-object entry is ignored, projects stays empty, the classic-hub fallback runs, and the real projects are returned.
This is what the Java runtime does - its paging helper keeps only object entries, so the fallback stays reachable. Covered by FoundryArmTest.Paging.nonObjectEntriesAreSkipped in runtime/java/prompty-foundry.
Actual behavior
projects contains a single FoundryProject with an empty name, a display_name falling back to the resource name, and an endpoint of the form https://{resource}.services.ai.azure.com/api/projects/ with a trailing slash and no project segment. The classic-hub fallback never runs, so the real projects are never discovered.
Your environment
- Runtime:
runtime/rust/prompty-foundry (arm_discovery.rs)
- Affected functions:
fetch_all (~lines 115-154), list_foundry_projects (~lines 79-109)
Notes for whoever picks this up
- ARM does not emit non-object
value entries in practice, so this is a malformed/hostile-input robustness issue rather than something users hit against a healthy control plane. Filing it because it is a genuine cross-runtime divergence - Rust is the behavioural reference for the other runtimes, and here the reference is the incorrect one, so it should be fixed rather than replicated.
- Suggested fix: filter
fetch_all's accumulation to object entries, or make parse_s1_project fallible and use filter_map for symmetry with parse_s2_workspace. The latter also guards against a well-formed-but-empty object producing the same phantom entry.
- Related, deliberately not fixed in either runtime yet: neither
fetch_all implementation guards against a nextLink that points back at the URL just fetched, so a looping control-plane response would page forever. Both Rust and Java are affected identically. It is a hang in a path a human is usually waiting on (a resource picker), so it is worth a bounded page count or a visited-URL check - but it should land in both runtimes together so a shared bug is not traded for a divergence.
馃悰 Bug Description
In
runtime/rust/prompty-foundry/src/arm_discovery.rs,fetch_allaccumulates every entry of an ARM page'svaluearray, including non-object ones (null, strings, numbers).list_foundry_projectsthen maps the modern ("S1") project results with an infallible.map(parse_s1_project)(line 91), whereas the classic-hub ("S2") results use.filter_map(parse_s2_workspace)(line 103).The consequence is that a single stray non-object entry in the S1 response becomes a
FoundryProjectwith empty fields. Becauseprojectsis then non-empty, theif projects.is_empty()guard at line 95 skips the classic-hub fallback entirely - so a caller gets one junk project instead of the real ones the fallback would have found.Two things go wrong at once: a bogus entry is surfaced, and a legitimate discovery path is suppressed.
Steps to reproduce
Call
list_foundry_projects(token, sub, rg, resource)against an account whose projects only exist as classic ML-workspace hubs (so the S1 endpoint has nothing real to return).Have the S1
Microsoft.CognitiveServices/accounts/{name}/projectsendpoint return a payload whosevaluearray contains a non-object entry, e.g.:{ "value": [null] }Observe the returned project list.
Expected behavior
The non-object entry is ignored,
projectsstays empty, the classic-hub fallback runs, and the real projects are returned.This is what the Java runtime does - its paging helper keeps only object entries, so the fallback stays reachable. Covered by
FoundryArmTest.Paging.nonObjectEntriesAreSkippedinruntime/java/prompty-foundry.Actual behavior
projectscontains a singleFoundryProjectwith an emptyname, adisplay_namefalling back to the resource name, and an endpoint of the formhttps://{resource}.services.ai.azure.com/api/projects/with a trailing slash and no project segment. The classic-hub fallback never runs, so the real projects are never discovered.Your environment
runtime/rust/prompty-foundry(arm_discovery.rs)fetch_all(~lines 115-154),list_foundry_projects(~lines 79-109)Notes for whoever picks this up
valueentries in practice, so this is a malformed/hostile-input robustness issue rather than something users hit against a healthy control plane. Filing it because it is a genuine cross-runtime divergence - Rust is the behavioural reference for the other runtimes, and here the reference is the incorrect one, so it should be fixed rather than replicated.fetch_all's accumulation to object entries, or makeparse_s1_projectfallible and usefilter_mapfor symmetry withparse_s2_workspace. The latter also guards against a well-formed-but-empty object producing the same phantom entry.fetch_allimplementation guards against anextLinkthat points back at the URL just fetched, so a looping control-plane response would page forever. Both Rust and Java are affected identically. It is a hang in a path a human is usually waiting on (a resource picker), so it is worth a bounded page count or a visited-URL check - but it should land in both runtimes together so a shared bug is not traded for a divergence.