Operator ruling 2026-08-05, which settles decision (g): planning documents are generally never public, and what OUR OWN sessions generate does not go out on the forge at all. The example itself stays public so others can run the process. `.claude/projects/` is the Voyage session workbench -- 25 briefs/plans/reviews this project's own sessions produced. Untracked and gitignored, exactly as STATE.md already is, and for the same stated reason: this repo has a public mirror, so that class of material is local-only rather than tracked. The line is drawn at who wrote the document, and it is drawn deliberately: `docs/plan/`, `docs/research/` and `docs/rapport/` stay tracked. Those are curated, dated documents written for the repo's readers, three of them linked from the README as the decision record. Move that line if it was meant wider. Two files were NOT process artifacts and are not deleted. Both `build_fixture.py` scripts are cited by tracked tests (`test_ingest_golden_sql.py`, `test_ingest_golden_http.py`) as the documented rebuild path for byte-exact goldens -- reproduction code that had landed in the wrong directory. Moved next to the goldens they build; both docstrings updated, so no tracked file is left pointing into an untracked tree (verified: the only remaining `.claude/projects` string in a tracked file is the .gitignore rule itself). One prose reference in the dated Foundry auth recipe was dropped for the same reason. 652 tests still pass. Does NOT address the 27 of these already readable on open/ since the S12 release -- untracking stops future publication only. That retraction is a separate operator decision and is deliberately not taken here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWsexbQjPo9rsV3aUE54ZS
65 lines
3.7 KiB
Markdown
65 lines
3.7 KiB
Markdown
# Azure AI Foundry auth recipe (S4.1)
|
|
|
|
> Verified against Microsoft Learn and the pinned `agent-framework-foundry==1.8.2` in this repo's
|
|
> `.venv`.
|
|
> This note is the operator's manual auth recipe for the single planned live Foundry run (M1). The
|
|
> offline `python -m portfolio_optimiser.preflight --profile azure` checks the config; this doc
|
|
> covers what preflight cannot: the actual authentication.
|
|
|
|
## The recipe (local dev, Intel Mac)
|
|
|
|
1. **`az login`** — sign in to the correct Entra tenant with the Azure CLI. This is a **manual**
|
|
operator step; the code never runs it (no auto-login).
|
|
2. The code passes a **lazy `AzureCliCredential()`** (`azure.identity.aio`) to `FoundryChatClient`
|
|
(`AzureFoundryBackend.create_chat_client`). Constructing the credential acquires **no token** —
|
|
the token is fetched from the `az login` session only on the first live call. So passing a
|
|
credential object is *not* auto-login.
|
|
- `AzureCliCredential` is preferred over `DefaultAzureCredential` on a non-Azure host: the latter
|
|
probes the IMDS managed-identity endpoint (times out / stalls) and can pick a stray identity.
|
|
3. Set the config the preflight validates:
|
|
- `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` — the Foundry **project** endpoint (below).
|
|
- `PORTFOLIO_MODEL_MAP` — path to an out-of-tree model_map with real azure deployment names, so
|
|
tenant-specific names are never committed. (Or replace the `REPLACE-WITH-*` placeholders.)
|
|
|
|
## FoundryChatClient signature (pinned 1.8.2)
|
|
|
|
`FoundryChatClient(*, project_endpoint, model, credential, ...)` — all keyword-only. **`credential`
|
|
is REQUIRED** on the `project_endpoint` path: the 1.8.2 client raises
|
|
`ValueError("Azure credential is required when using project_endpoint without a project_client.")`
|
|
at construction if omitted. There is **no** lazy `DefaultAzureCredential` default. The model
|
|
parameter is `model` (the portal **deployment name**), not `deployment_name`.
|
|
|
|
## Endpoint format
|
|
|
|
Canonical Foundry project endpoint:
|
|
|
|
```
|
|
https://<resource>.services.ai.azure.com/api/projects/<project>
|
|
```
|
|
|
|
The bare-host form `https://<resource>.services.ai.azure.com` also appears in official samples and
|
|
is accepted. The preflight requires `https://` + a host ending `.services.ai.azure.com`; it does
|
|
**not** require the `/api/projects/` path. A `*.openai.azure.com` or `*.cognitiveservices.azure.com`
|
|
endpoint is a **different** client surface (use `OpenAIChatClient`, not `FoundryChatClient`).
|
|
|
|
## RBAC role
|
|
|
|
Assign **`Foundry User`** (role GUID `53ca6127-db72-4b80-b1b0-d745d6d5456d`) to the identity, at the
|
|
Foundry **resource/project** scope. Do **NOT** use `Azure AI Developer` (scoped to hubs/ML
|
|
workspaces, not Foundry projects) or `Cognitive Services User` (the classic Azure OpenAI surface).
|
|
Note the recent rename: `Foundry User` was formerly `Azure AI User` — reference the GUID, not the
|
|
display name, since older community answers use both.
|
|
|
|
## Preflight is necessary-but-not-sufficient
|
|
|
|
A green `preflight` rules out the **offline-detectable** misconfiguration class: unset endpoint,
|
|
malformed/wrong-surface endpoint URL, unresolved `REPLACE-WITH-*` placeholder deployments, and an
|
|
inconsistent model-map. It **cannot** prove the paid live call will succeed. The following surface
|
|
**only at the live call** and are out of scope for an offline check:
|
|
|
|
- **RBAC** — missing/wrong role → 403 (propagation lag can cause 401-then-200).
|
|
- **Token / tenant / consent** — expired or wrong-tenant credential → 401.
|
|
- **`DeploymentNotFound`** — a well-formed deployment name that does not exist in the project → 404.
|
|
- **api-version skew** and regional outages.
|
|
|
|
Treat a green preflight as "config is shaped correctly", never as "M1 will work".
|