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

@ -41,7 +41,7 @@ The package has three distinct audiences. Only the first one is technical.
| Role | What they do | Where it is documented |
|---|---|---|
| **Platform operator** | Deploys the container into Microsoft Foundry, sets two environment variables, verifies with the offline preflight. One-time work. | This document, sections 35. |
| **Platform operator** | Installs the package, sets two environment variables, starts the service, verifies with the offline preflight. One-time work. | This document, sections 36. |
| **Ordering domain expert** (*bestiller*) | Writes the mandate file: the objective, which approaches are worth trying, and why. The *why* is the part the system cannot read out of cost figures. | `docs/bestille-en-kjoring.md` |
| **Reviewing domain expert** (*fagperson*) | Receives a validated proposal and answers `approved` or `rejected` with a written rationale. That verdict is what the system learns from. | `docs/ekspert-svar.md` |
@ -59,7 +59,7 @@ back with its reason. That property is the point of the whole design; do not bui
your own.
2. **Order a run** with a mandate file (optional but recommended). See
`docs/bestille-en-kjoring.md`.
3. **Run it** — from the CLI, or by `POST /invocations` against the deployed container (section 5).
3. **Run it** — from the CLI, or by `POST /invocations` against the running service (section 6).
The run navigates the knowledge base, debates candidate measures, validates the numbers against
the project's actual cost baseline, and returns either a **validated proposal** or a
**rejection with a reason**. A rejection is a successful run: the negative outcome belongs to the
@ -99,8 +99,8 @@ PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT # yours; wins if set
FOUNDRY_PROJECT_ENDPOINT # injected by the Foundry hosting platform
```
Inside a hosted Foundry container the platform injects the second one and you need to set nothing.
Outside it — a laptop, a pipeline, a container you run yourself — set the first. The value looks
Where something injects the second one for you — the Foundry hosting platform does — you need to
set nothing. Everywhere else (a laptop, a pipeline, your own runtime) set the first. The value looks
like `https://<resource>.services.ai.azure.com/api/projects/<project>`.
**5.2 The model map — this is the one that catches people.** The packaged
@ -125,7 +125,7 @@ own file:
export PORTFOLIO_MODEL_MAP=/path/to/model_map.json
```
If you skip this, the container starts and answers `/readiness` — and fails every invocation. That
If you skip this, the service starts and answers `/readiness` — and fails every invocation. That
is the single most likely first-deployment failure, which is why it has its own section.
**5.3 Verify before you pay for a model call:**
@ -140,24 +140,32 @@ no placeholders left. It is **necessary but not sufficient** — RBAC (403), tok
---
## 6. Deploying
## 6. Running it
The image must be built for x86_64 regardless of your workstation's architecture:
**This package is Python and nothing else.** There is no image to build and no platform-specific
deployment manifest — that was removed deliberately after an external trial, because it forced a
container toolchain on receivers who only needed to run the code. How the process is supervised,
packaged or placed in your environment is your decision, and this package does not pre-empt it.
Install the locked environment and start the service:
```bash
git archive HEAD | docker build --platform linux/amd64 -t portfolio-optimiser -
uv sync --frozen --no-dev
uv run python main.py
```
If you unzipped this package rather than cloning it, build from the directory instead:
`uv sync --frozen` installs the exact resolution every measurement in this repository ran against —
never a fresh resolve, which picks other versions. Two dependencies are git-tag-pinned direct
references, so **`git` must be on PATH** for the install to work.
`main.py` serves port 8088 (override with `PORT`), answers `GET /readiness`, accepts
`POST /invocations`, and exits cleanly on SIGTERM. For a single run without the service, the CLI is
the same code path:
```bash
docker build --platform linux/amd64 -t portfolio-optimiser .
uv run portfolio-optimiser --help
```
`azure.yaml` describes the hosted-agent deployment (`azd`). The container serves port 8088 (override
with `PORT`), answers `GET /readiness`, accepts `POST /invocations`, and shuts down cleanly on
SIGTERM.
**The invocations payload** is whitelisted against the run signature — unknown fields are refused by
name with HTTP 400, never silently ignored:
@ -167,7 +175,7 @@ name with HTTP 400, never silently ignored:
| `docs_dir` | yes | Source documents. |
| `verdict_input` | yes | The expert verdict for this run. |
| `bundle_dir` | no | Knowledge base to navigate. |
| `profile` | no | Defaults to `azure` in the hosted container. |
| `profile` | no | Defaults to `azure` on this HTTP surface. |
| `max_rounds` | no | Debate round cap. |
| `max_tokens` | no | **Per-invocation token ceiling.** Set it. The built-in default is 100 000 tokens per run, which is a ceiling nobody chose for your workload. |
| `top_k` | no | Retrieval breadth. |
@ -180,7 +188,7 @@ HTTP 400 means the request was malformed; 500 means the run itself failed, with
## 7. Honest limits
- **This framework has never been run against a live model in our environment.** Everything above is
measured against tests, an offline simulation and container builds. The first live run in *your*
measured against tests and an offline simulation. The first live run in *your*
tenant is the first live run, full stop.
- The system is a **technical framework**. Data protection assessments, lawful basis and risk
ownership belong to whoever deploys it — see `README.md` and `SECURITY.md`.