feat(5): overleveringspakka er KUN kjørbar Python — container-innpakningen fjernet

Operatørdirektiv 14.08 etter ekstern test: mottakeren skal få kjørbar Python, ikke en
Docker-innpakking. `Dockerfile` og `azure.yaml` er SLETTET fra treet.

Sømmen er valgt av den eksisterende invarianten, ikke av smak: pakka ER `git archive HEAD`,
så å ekskludere filene fra arkivet ville krevd en kurerings-mekanisme — den andre kopien av
«hva mottakeren får», fri til å drifte fra HEAD (kø-(p)). Fjerning holder arkivet ukurert og
gjør fraværet til en egenskap ved HEAD, som er det eneste en gate kan måle.

De to gatene som pinnet flaten er håndtert bevisst:
- 4e-rå-tekst-gaten (`--platform linux/amd64` + ÉN kopi av startkommandoen) er SLETTET, med
  et notat der den sto. En gate som pinner en fjernet flate kan bare bli grønn.
- handover-gatens `_REQUIRED_MEMBERS` er ikke bare fratatt de to navnene, men erstattet av en
  POSITIV fraværs-assert pluss en dokument-gate. Å kun slutte å KREVE dem ville gitt en gate
  som ikke kan skille «fjernet» fra «shippes fortsatt».

Startkommandoen har nå ÉN kopi igjen: DEPLOY.md-ens `python main.py`, som navngir inngangen
subprosess-testen faktisk kjører.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 14:09:35 +02:00
commit 605c02aa62
10 changed files with 215 additions and 205 deletions

View file

@ -459,35 +459,38 @@ 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 task API — runnable Python, no wrapper
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` is 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).
- `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).
```bash
uv sync --frozen --no-dev # the exact locked resolution every measurement ran against
uv run python main.py # serves the task API
```
`git` must be on PATH for the install: two dependencies are git-tag-pinned direct references,
and wheel metadata alone cannot fetch them.
A `Dockerfile` and an `azure.yaml` shipped here until **14 August 2026** and were removed on an
operator directive after an external trial: what is delivered is runnable Python, and how the
process is containerised, supervised or deployed belongs to whoever runs it. Git history keeps
both files. The raw-text gate that pinned them (`--platform linux/amd64`, one copy of the start
command) was **deleted with them** rather than weakened into a check that could only pass — the
start command now has exactly one copy, in [`DEPLOY.md`](DEPLOY.md), and
`tests/test_handover_package_loadbearing.py` is what keeps it there.
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)
credential from the environment at call time, so the same process runs hosted (managed identity)
and locally (`az login`) without rewiring.
### Handing it to someone else
@ -499,8 +502,8 @@ cloning this repository or having an account here:
scripts/make-handover-package.sh # → dist/portfolio-optimiser-foundry-<version>.zip
```
The archive is `git archive HEAD` — tracked files only, which is the same tree the measured docker
build context uses, and the reason local-only files cannot enter it. [`DEPLOY.md`](DEPLOY.md) rides
The archive is `git archive HEAD` — tracked files only, which is why local-only files cannot enter
it and why nothing curates what a receiver sees. [`DEPLOY.md`](DEPLOY.md) rides
along inside it and answers the receiver's first questions: what the three roles do, what the
process is end to end, why there is no chat interface, and the two environment variables that decide
whether the first deployment works. Gated by `tests/test_handover_package_loadbearing.py`.