feat(4b): AZURE-profilen leser miljøet sitt, ikke operatørens laptop

Endepunktet løses som første ikke-tomme av vårt eget
PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT og Foundrys injiserte
FOUNDRY_PROJECT_ENDPOINT — vårt vinner, fallbacken lar samme image kjøre
hostet uten ekstra wiring. Presedensen gjelder verdier, ikke deklarasjoner.
Credential velges av samme miljø: AzureCliCredential lokalt,
ManagedIdentityCredential når FOUNDRY_HOSTING_ENVIRONMENT er satt, fordi
containeren ikke har noen Azure CLI. Ikke DefaultAzureCredential — Learns
MAF-veiledning navngir den spesifikke credentialen for å unngå probing.

Load-bearing målt mot hele suiten, fire mutasjoner alle røde + grønn
kontroll: detach credential-valget · presence i stedet for truthiness ·
detach fallbacken · snu presedensen. Fail-fast-testen var vakuøs først —
vårt variabelnavn inneholder det injiserte som delstreng.

De fire åpne azure.yaml-valgene lukket mot de to JSON-skjemaene og ført i
docs/2026-08-13-fase4-azure-yaml-valg.md. Ingen azure.yaml skrevet (4d).

821 passed / 4 skipped. Ruff + format + mypy rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jk8tauXXAojNKC7Tzq7ziF
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 22:27:21 +02:00
commit 63eec917d2
7 changed files with 373 additions and 12 deletions

View file

@ -0,0 +1,125 @@
# Fase 4· — de fire åpne `azure.yaml`-valgene, lukket mot skjemaet
**Dato:** 2026-08-13 · **Metode:** de to autoritative JSON-skjemaene hentet og lest, ikke referert
fra Learn-prosa · **Status:** beslutninger, ingen fil skrevet. `azure.yaml` skrives i 4d.
Spiken (`docs/2026-08-13-fase4-research-spike.md`) etterlot fire valg under «Ikke verifisert», med
begrunnelsen at Learn og Microsofts eget sample er uenige og at autoriteten —
`schemas/v1.0/azure.yaml.json` — ikke var lest. Den er lest nå.
## 0. Funnet som endrer hvordan de andre skal leses
**Skjemaet er PERMISSIVT, og kan derfor ikke avgjøre tre av de fire valgene.** Målt:
| Felt | Hva skjemaet faktisk sier |
|---|---|
| `host` | `type: string`, **ingen `enum`** — kun `examples`, der `azure.ai.agent` står |
| `language` | `type: string`, **ingen `enum`**`examples` inneholder BÅDE `python` og `docker` |
| service-objektet | `additionalProperties: **true**` |
Et ukjent felt blir altså ikke avvist — det blir *ignorert*. Å skrive `environmentVariables:` ville
ikke gitt en valideringsfeil; det ville gitt en fil som ser konfigurert ut og ikke er det. Det er en
skarpere failure-mode enn den planen fryktet, og den er grunnen til at «samplet er fasit» ikke er
godt nok som regel her.
**Den ekte autoriteten lå ett hopp videre.** `azure.yaml.json` delegerer agent-formen via `$ref`:
```
"if": { "properties": { "host": { "const": "azure.ai.agent" } } },
"then": { "required": ["project"],
"allOf": [ { "$ref": ".../extensions/azure.ai.agents/schemas/azure.ai.agent.json" } ],
"properties": { "config": { "deprecated": true, ... },
"k8s": false, "apiVersion": false, "network": false } }
```
Hentet: `azure.ai.agent.json` (http 200, 16 385 bytes). Den bærer `kind`, `protocols`,
`codeConfiguration`, `container`, `startupCommand` — feltene Learn omtaler, og som hovedskjemaet
ikke kjenner. To ting følger direkte: **`project` er påkrevd** for en hosted agent, og
**`config:` er deprekert** (agent-innstillingene er flyttet opp på service-nivå).
## 1. Protokollversjon: `2.0.0`
Skjemaet lukker ikke valget, men det avgrenser det. `ProtocolVersionRecord`:
```json
{"properties": {"protocol": {"type": "string"}, "version": {"type": "string"}},
"required": ["protocol"], "additionalProperties": false}
```
`version` er en fri streng **uten enum og uten default**, og er **valgfri**. Valget er altså vårt,
og det er et valg mellom å pinne og å la plattformen bestemme.
**Vi pinner `2.0.0`, uttalt.** Grunnen er ikke at skjemaet sier det — det gjør det ikke — men at
spikens §1.5 dokumenterer headere (`x-agent-user-id`, `x-agent-foundry-call-id`, og en gateway som
dropper alt utenfor en allowlist) som er **egenskaper ved 2.0.0**. Kjører plattformen oss under en
annen versjon enn den vi har lest kontrakten for, brytes antakelsene stille. Learn oppgir dessuten
at 1.0.0 er deprekert med blokkering etter fristen. En utelatt versjon ville gjort dette til noe vi
oppdager i drift; en pinnet gjør det til noe vi ser i diffen.
Skrives som `protocols: [{protocol: invocations, version: "2.0.0"}]` — **Invocations, ikke
Responses**, som spikens §5 allerede avgjorde: `run_project` er ett kall med strukturert inn og ut,
ikke en samtale.
## 2. Miljøvariabler: `env:`-map — dette lukkes AV skjemaet
MÅLT, i begge filer: `grep -c environmentVariables`**0 og 0**. Ordet finnes ikke i noen av dem.
`env` derimot er en deklarert service-property:
```json
{"type": "object", "title": "Environment variables for the service",
"description": "Optional. A map of environment variable names to values.
Supports environment variable substitution.", "additionalProperties": {"type": "string"}}
```
**`env:` er den eneste formen skjemaet kjenner.** `environmentVariables:` er ikke ulovlig (jf. §0),
men ingenting validerer den og ingenting leser den. Dette er det ene av de fire valgene som er
avgjort av autoriteten selv, ikke av vår vurdering.
Konsekvens for innholdet: `FOUNDRY_PROJECT_ENDPOINT` skal **ikke** redeklareres (spiken pkt. 2 —
det risikerer å skygge plattformens egen verdi). Modelldeployment-navnet må derimot deklareres, det
injiseres ikke.
## 3. `language: docker` — ikke `python`
Skjemaet tillater begge (ingen enum). Men `azure.ai.agent.json` gjør dem til to **gjensidig
utelukkende deploy-modi**, og sier det rett ut i `CodeConfiguration`:
> «Code deploy configuration. **When present, the agent is deployed from source (ZIP) instead of a
> container image.**» — påkrevd: `runtime` + `entryPoint`; `dependencyResolution` er
> `bundled` eller `remote_build`.
Vi velger container, og grunnen er vår egen avhengighetsform: de to sikkerhetskomponentene er
**git-pinnet via `[tool.uv.sources]`**, ikke publisert på PyPI (spikens §3 og §7.2). En
source-ZIP-deploy overlater resolusjonen til en byggetjeneste vi ikke kontrollerer og som må nå
`git.fromaitochitta.com` — og den formen bærer heller ikke `git` i base-imaget, som spikens §3
målte at pip-veien trenger. Vår Dockerfile er nettopp det stedet den kunnskapen kan bo.
Altså: `language: docker`, **ingen `codeConfiguration`**. `entryPoint`-uenigheten i spikens tabell
(`main.py` vs `["python", "main.py"]`) faller bort med den — feltet tilhører kun ZIP-modusen, og er
`type: string`, ikke en liste, i skjemaet.
## 4. Wrapper-form: IKKE avgjort her — og det er en måling, ikke en utsettelse
Skjemaet har ingenting å si om dette; det er et kodespørsmål. Men det ene alternativet er MÅLT
utilgjengelig i dag:
```
agent_framework_foundry_hosting -> ModuleNotFoundError
agent_framework.foundry -> IMPORT OK, 27 eksporter, 0 med "Host"/"Server" i navnet
agent_framework_foundry -> 1.8.2, 0 host-klasser
```
`InvocationsHostServer` finnes altså ikke i det installerte treet, og pakka som skal bære den har
ingen stabil release (spikens pkt. 8). **Valget kan ikke tas før pakka er installert og målt**, og
det hører uansett til 4c — som spikens §5 allerede har vist er et *signaturproblem*
(`run_project` tar `project_id` + to påkrevde keyword-argumenter og gir en frozen `RunResult`),
ikke et grafproblem. Ført videre som åpent, med en kjørbar grunn.
## Sidefunn, ikke handlet på
`agent_framework.foundry` eksporterer **`AnthropicFoundryClient`** og `RawAnthropicFoundryClient`.
STATEs 1b-blokk slår fast at `FoundryChatClient` ikke kan binde en Claude-deployment fordi Claude på
Foundry er en tredje klientflate (`/anthropic/v1/messages`, målt 13.08). Det står ved lag — men MAF
ser altså ut til å shippe en egen klient for nettopp den flaten. Det endrer potensielt 1b-valget
«Microsoft-solgt modell → ren konfig», og bør måles før 1b, ikke under. **Ikke verifisert:** at
klassen faktisk virker mot en Claude-deployment, og at den komponerer med `backends.py`-sømmen.