Krav 3, and the operator chose the run path explicitly: the external service must be reachable WHILE the run works, not only when documents are ingested. Until now the run path had one in-process tool against a local folder — and on the bundle path the agents had no tools at all. MAF already ships the client (MCPStdioTool / MCPStreamableHTTPTool, verified in the pinned 1.9.0 with allowed_tools and request_timeout), so `mcp_tools.py` owns only what MAF cannot decide for us: which servers a run may contact, which of their tools it may call, how long it waits, and where the credential comes from. This is a DIFFERENT seam from ingest_mcp.py on purpose — that one pulls source documents before a run and speaks to null-argument tools. Same protocol, different job. Every refusal is a live hazard, not tidiness. An empty allowlist would let the far end decide what the agents may call, so naming the tools is mandatory. A non-positive timeout is an unbounded wait against a third party. An unknown field is refused rather than ignored, which is also what keeps a literal secret from being parked in the config — there is no field for one, only the NAME of an env var. A named-but-unset credential refuses instead of calling anonymously, because an anonymous call can succeed with the wrong scope. Egress is declared, always. Every server and permitted tool is named in the run announcement before the first call — including when no --mandate is given, which was a real hole: the announcement only printed with a commission, so configuring servers without one would have contacted third parties with nothing printed at all. --live-dry-run still opens nothing, because the tools are entered after the dry-run cut: the promise to stop before the first call now covers egress too. Threaded through BOTH modes. A flag accepted in one mode and silently dropped in the other is the defect class this CLI refuses by name. Load-bearing MEASURED against the whole 744-test suite, four mutations all red: build the tools but never hand them to the agents (2) · never enter the AsyncExitStack, so they are constructed and useless (1) · never declare the egress (2) · drop the allowlist on the built client (1). Two live docs claimed MCP was unwired in the run path; both corrected rather than left to rot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULCqjLF61rehj5cZmdUoR3
7.4 KiB
Bestille en kjøring
Dette dokumentet er for deg som bestiller en kjøring — fagpersonen som vet hvilke tilnærminger det er verdt å prøve på et prosjekt, og som vil vite nøyaktig hva systemet skal gjøre før det gjør det. Søsterdokumentet ekspert-svar.md handler om det motsatte enden: å avgi dommen etterpå.
Ærlighetsmerking. Bestillingen styrer hva som vurderes. Den styrer aldri hva som godkjennes. Den deterministiske validatoren gjelder uendret for en tilnærming du selv har bedt om — ber du om noe tallene ikke bærer, blir det avvist, og avvisningen kommer tilbake til deg med begrunnelsen.
Oppdragsfila
En bestilling er én JSON-fil. Du gir den til kjøringen med --mandate.
{
"objective": "Kutt energikostnad i BYGG-KONTOR-NORD uten ombygging, med tiltak som kan gjennomføres i 2026.",
"approaches": [
{
"id": "led-retrofit",
"label": "LED-retrofit av kontorbelysning",
"description": "Drift mener armaturene er originale og har gått langt forbi oppgitt levetid."
},
{
"id": "driftsavtale",
"label": "Reforhandling av driftsavtale",
"description": "Avtalen er ikke justert siden 2019 og dekker areal vi ikke lenger disponerer."
}
],
"allow_own_proposals": true,
"success_criteria": "Minst ett tiltak som passerer validatoren og som drift kan stå inne for."
}
| Felt | Hva det gjør |
|---|---|
objective |
Hva kjøringen er til for, i klartekst. Påkrevd — en kjøring uten uttalt hensikt er nettopp det denne fila finnes for å hindre. |
approaches |
Tilnærmingene du vil ha vurdert. Hver får sin egen vurdering og sin egen linje i oppgjøret. |
allow_own_proposals |
true (standard): systemet foreslår også sitt eget, i tillegg til dine. false: kun dine. |
success_criteria |
Hva du vil regne som vellykket. Gjengis ordrett i kunngjøringen; ingenting tolker den maskinelt. |
Om id: den er nøkkelen tilnærmingen rapporteres under. To tilnærminger kan ikke dele id
(da ville de kollapset til én linje i oppgjøret), og own-proposal er reservert for systemets eget
forslag.
Om description: den mates ordrett inn til modellen. Det er her fagkunnskapen din faktisk
gjør en forskjell — hvorfor du tror tilnærmingen er verdt å prøve er nøyaktig det systemet ikke
kan lese seg til av kostnadstallene.
Enten tilnærminger eller egne forslag. En fil med tom approaches og
allow_own_proposals: false blir avvist: da har kjøringen ingenting å gjøre.
Å kjøre den
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
--docs-dir shared/examples/bygg-energi-mikro \
--bundle-dir shared/examples/bygg-energi-mikro \
--mandate oppdrag.json \
--scripted-replies replies.json
--mandate virker i begge moduser. I porteføljemodus (--portfolio) gjelder bestillingen hvert
prosjekt i passet, og hvert prosjekt gjør opp for seg.
Vil du se hva kjøringen ville gjort, uten å bruke penger? Legg til --live-dry-run. Da skrives
kunngjøringen, og kjøringen stopper før første modellkall.
Fila leses strengt. Mangler den, eller er den feilformet, blir kjøringen nektet (rc 1) — den går aldri videre på en halvlest bestilling. Det er med vilje: et oppgjør som beskriver arbeid ingen har bestilt, er verre enn ingen bestilling.
Hva du får: kunngjøringen
Før noe koster penger, skriver kjøringen hva den skal gjøre:
Run mandate for BYGG-KONTOR-NORD
Objective: Kutt energikostnad i BYGG-KONTOR-NORD uten ombygging, ...
Evaluates: 2 expert-proposed approach(es) + the system's own proposals
1. led-retrofit — LED-retrofit av kontorbelysning
2. driftsavtale — Reforhandling av driftsavtale
Stops at: 3 rounds / 100000 tokens
Contacts: no external services
Success: Minst ett tiltak som passerer validatoren ...
Kjører du med --dimension-config, kommer det en Scoped to:-linje til. Linjer for ting kjøringen
ikke har — avgrensning, tallmål — utelates i stedet for å stå tomme: en tom linje ville lest som en
beslutning ingen har tatt.
Contacts er egress-erklæringen: en kjøring når aldri en ekstern tjeneste den ikke har navngitt
her. Stops at er takene — systemet kjører aldri en ubegrenset løkke.
Hva du får: oppgjøret
Etter kjøringen svarer den for bestillingen, én linje per tilnærming:
Mandate outcome
led-retrofit VALIDATED 30000 NOK LED-retrofit av kontorbelysning
driftsavtale REJECTED claimed 240000 exceeds feasible 90000
own-proposal NOT EVALUATED budget exhausted before this approach was evaluated
Validated: 1 of 3 approaches — best 30000 NOK (the outcome this run carries)
De tre statusene betyr forskjellige ting, og forskjellen er poenget:
- VALIDATED — tallene holdt. Beløpet er det validatoren slapp gjennom.
- REJECTED — tilnærmingen ble vurdert, og falt på tallene. Grunnen står der; det er den du kan gjøre noe med (ofte er den «regnet mot feil grunnlag», ikke «dårlig idé»).
- NOT EVALUATED — kjøringen rakk den aldri. Den er ikke avvist, og den er ikke glemt.
En tilnærming du bestilte forsvinner aldri i stillhet. Det er hele grunnen til at oppgjøret finnes.
Tilnærmingene summeres ikke. Flere av dem angriper som regel samme kostnadslinje, så de er alternativer — ikke besparelser som legges oppå hverandre. Derfor står det hvor mange som holdt og hvilken kjøringen bærer videre, aldri en sum. (Dette ble oppdaget ved å faktisk kjøre den: tre tilnærminger mot samme linje ga «totalt 90 000», som ingen av dem kunne innfri.)
Hva bestillingen ikke gjør
- Den overstyrer ikke validatoren. Se ærlighetsmerkingen øverst.
- Den avgjør ikke om et tiltak skal gjennomføres. Det gjør et menneske, etterpå — se ekspert-svar.md.
- Den setter ikke et tallmål. Sparemålet har ett hjem, målkonfigurasjonen (
--goals, som gjelder porteføljemodus). To kopier av samme tall drifter fra hverandre, så oppdraget bærer hensikten i ord og gjengir tallet hvis det finnes. - Den lagrer seg ikke som kunnskap. Bestillingen er en instruks for én kjøring. Det som lærer systemet noe, er dommen du avgir etterpå.
Hvis kjøringen skal spørre en ekstern tjeneste
Skal agentene kunne slå opp i en tjeneste dere allerede har — et prisregister, målerdata, et
avtaleregister — settes den opp med --mcp-config:
{"servers": [
{"name": "prisregister", "transport": "http", "url": "https://intern.example/mcp",
"allowed_tools": ["lookup_unit_price"], "timeout_seconds": 15,
"credential_env": "PRISREGISTER_TOKEN"}
]}
Tre ting er verdt å vite som bestiller:
- Du må navngi hvilke verktøy som er tillatt. Tom liste er ikke lov. Uten det ville tjenesten selv bestemt hva agentene får lov til å kalle.
- Alt som vil bli kontaktet, står i kunngjøringen — før første kall, på
Contacts:-linja, med både servernavn og tillatte verktøy. En kjøring når aldri en tjeneste den ikke har navngitt. Det gjelder også når du ikke bruker--mandate. - Passord og nøkler skal aldri i fila.
credential_envnavngir en miljøvariabel; verdien leses derfra. Er variabelen ikke satt, blir kjøringen nektet — heller det enn å ringe tjenesten anonymt og få svar med feil tilgang.
Uten --mcp-config gjøres ingen nettverkskall i det hele tatt.