feat(4d): hostet inngang — main.py wrapper rundt run_project på én asyncio-løkke

To målinger avgjorde formen FØR koden: (1) hosting-pakkas InvocationsHostServer
finnes kun i bygg som krever agent-framework-core>=1.13.0 (treet låser 1.9.0;
eneste 1.9-kompatible bygg er en forlatt alfa som importerer mcp udeklarert),
(2) et gjenbrukt bygget workflow er single-use på 1.9.0 (kall-serie [2,0,0] —
rundetaket persisterer; ferskt objekt per kall er ren kontroll). Derfor spikens
§5-fallback: hosting.py serverer kontrakten (8088/PORT, /readiness,
/invocations, SIGTERM→0) selv, stdlib asyncio på ÉN løkke — aldri as_agent()
(gatene ligger utenfor grafen), aldri tråder (NG1-guarden fanget første utkast
med ThreadingHTTPServer; asyncio-formen består den by construction).

Payload whitelistes på run_projects signatur — ukjente felt nektes ved navn
(400), aldri stille droppet; profile defaulter til azure kun her. ValueError →
400, alt annet → 500 {error_type, error}; Rejection er vellykket kjøring → 200.
outbox.outcome_payload ekstrahert som den ENE kopien av validated/rejected-
forgreningen (kø-(p)-regelen). azure.yaml validert GRØNN mot begge autoritative
skjemaer (jsonschema, hentet ferskt); ingen env:, ingen startupCommand (imagets
CMD er den ene kopien). Dockerfile: 3.12-slim-bookworm + git + uv==0.9.8 +
uv sync --frozen --no-dev; git archive <indeks-tre> | docker build
--platform linux/amd64 grønn på nøyaktig de stagede bytene.

Iron Law fulgt: testfila rød ved collection FØR modulen fantes. 835 passed /
4 skipped (fra 821), ruff+format+mypy rene. Seks mutasjoner mot HELE suiten,
alle røde på riktig test: detach felt-mappingen · dropp ukjente felt stille ·
flipp 400/500 · detach azure-defaulten · detach SIGTERM-handleren · detach
main.py-shimen (de to siste kun fanget av subprosess-testen, P4-presedensen).
Deploy IKKE utført — azd-steget er operatørens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEiiSGRShizKc771ZBa1iq
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 23:31:02 +02:00
commit 426ccb0ad6
8 changed files with 766 additions and 14 deletions

View file

@ -430,6 +430,37 @@ only an expert has:
- **Vendor-neutral knowledge** — the same bundles drive two different agent stacks; switching
frameworks does not orphan the organization's curated knowledge.
## Hosted on Foundry Agent Service
The repo ships the three files a hosted-agent deployment needs. All three are measured —
schema-validated and container-built — but an actual `azd` deploy has **not** been run; that
step is the operator's.
- `main.py` — the ONE process entry. It serves the hosted-agent runtime contract (port 8088 /
`PORT`, `GET /readiness`, `POST /invocations`, clean SIGTERM exit) from
`portfolio_optimiser.hosting`, wrapping `run_project` — never `Workflow.as_agent()`, which
would bypass the deterministic validator and the rest of the gate chain. No protocol library
is used: the prerelease `agent-framework-foundry-hosting` requires
`agent-framework-core>=1.13.0` while this tree locks 1.9.0, so the contract is a small
stdlib-asyncio server instead (single event loop, no threads — the same concurrency model as
the portfolio waves).
- `Dockerfile``python:3.12-slim-bookworm` + git (the two security components are
git-tag-pinned direct references; wheel metadata alone cannot install them) +
`uv sync --frozen --no-dev` (the exact locked resolution every measurement ran against).
Build it the way a stranger would:
`git archive HEAD | docker build --platform linux/amd64 -t portfolio-optimiser -`
- `azure.yaml` — closed against the two authoritative JSON schemas (see
`docs/2026-08-13-fase4-azure-yaml-valg.md`): `language: docker`, invocations protocol pinned
to `2.0.0`, no `env:` block (the platform injects `FOUNDRY_PROJECT_ENDPOINT`; model choice is
config, not environment), no `startupCommand` (the image `CMD` is the one copy).
An invocation is a JSON object whitelisted onto `run_project`'s signature — `project_id`,
`docs_dir` and `verdict_input` required; `bundle_dir`, `profile`, `max_rounds`, `max_tokens`
and `top_k` optional. Unknown fields are refused by name (400), never silently dropped.
`profile` defaults to `azure` on this surface: the AZURE profile reads its endpoint and
credential from the environment at call time, so the same image runs hosted (managed identity)
and locally (`az login`) without rewiring.
## Docs
- [Bestille en kjøring](docs/bestille-en-kjoring.md) *(norsk)* — for the domain expert who