Referanse
Agentpakker: felt og regler
Detaljene du trenger når du lager og vedlikeholder en agentpakke.
Artefakttyper
| Type | Form | Hva det er |
|---|---|---|
agents | <navn>.agent.md | Personaer klienten kan startes som, eller underagenter andre kaller |
skills | <navn>/SKILL.md | Kunnskap modellen laster ved behov |
instructions | <navn>.instructions.md | Regler som aktiveres mot matchende filer |
prompts | <navn>.prompt.md eller <navn>/ | Ferdige spørsmål brukeren kan kjøre |
hooks | <navn>.py, valgfritt <navn>.hook.json | Skript som kjører ved verktøykall. Uten sidecaren: ingen matcher, og standard tidsgrense |
extensions | <navn>/extension.mjs | Kode klienten laster inn |
Kjørbar kode
Hooks og extensions er ikke tekst en modell leser. En hook kjører ved verktøykall, en extension lastes av klienten. Den som installerer pakka di kjører koden din på maskinen sin, så si i pakkas description hva den gjør. Installasjonen må skje utenfor cplt: inne i sandkassen nekter cplt å skrive hooks, extensions og skills, og install stopper med en feil som sier det.
Skript i en skill
Sender skillen din med et skript, kopieres det med resten av katalogen, men katalogen havner ulike steder per klient: ~/.copilot/skills/ for copilot, skills/ under konfigurasjonskatalogen for opencode, ~/.nav-pilot/pi/skills for pi, og skills/ i payloadtreet for Tier 2. Skriver du én av stiene i teksten, er skillen feil på de andre. nav-pilot eksporterer derfor NAV_PILOT_SKILLS_DIR ved hver launch, med roten skillene faktisk ble lagt i for den klienten, og sender den gjennom sandkassa. Skriv bash "$NAV_PILOT_SKILLS_DIR/<skill>/<skript>" og den peker riktig overalt. La nav-pilot ingen skills ut for klienten, er variabelen usatt framfor å peke på en katalog som ikke finnes, så en skill kan teste på den og si fra.
Klientoppføringa
Klientnøklene i dag er copilot, opencode og pi. En nav-pilot som ikke kjenner en nøkkel, hopper over den i stedet for å avvise manifestet. En ny klient senere ugyldiggjør derfor ingen pakke som alt er ute.
{
"minNavPilotVersion": "2026.08.17-062831",
"clients": {
"copilot": {
"primaryAgents": ["grillmester"],
"compatibility": ">=1.0.79,<2",
"defaultModel": "inherit"
},
"opencode": {
"primaryAgents": ["grillmester"],
"compatibility": ">=1.18.20,<2",
"defaultModel": "inherit"
},
"pi": {
"primaryAgents": ["grillmester"]
}
}
}Tier utledes av formen og deklareres ikke. En klientoppføring uten payloads er Tier 1: nav-pilot legger inn filene selv fra stiene i layout, som da må finnes. En oppføring med payloads er Tier 2: nav-pilot verifiserer og stager ferdigbygde trær mot en digest, og pinner dem per bruker. En pakke kan blande de to per klient. Hvordan du bygger payload-trær for Tier 2, står i feltreferansen.
compatibility er et versjonsområde for klienten, ikke en versjon: kommaseparerte komparatorer over semver, som ">=1.18.20,<2". "1.18.20" alene har ingen operator og avvises. Området håndheves før hver launch i begge tier: nav-pilot spør klienten om versjonen og nekter en versjon utenfor. Svarer ikke klienten, eller er svaret uleselig, er det også fatalt. Et område nav-pilot ikke kan håndheve, er ikke håndhevet.
owner er attribusjon, ikke tilgangsstyring: kilden til en installasjon er repoet manifestet ble klonet fra. policies.opencodePermissions, profiles og provenance står i skjemaet, men gjør ingenting ennå: stiene sti-sjekkes, og nav-pilot leser dem ikke. Vent med dem.
Hvilken tier skal du velge?
Velg Tier 1 om du ikke har en grunn til noe annet. Innholdet er filer, de havner i repoet eller profilen og er synlige i en diff, konsumenter kan plukke enkeltdeler med items, og du vedlikeholder ingen byggekjede: nav-pilot legger inn filene fra layout.
Velg Tier 2 når pakka di er et ferdig bygget oppsett som skal leveres som én enhet, og ikke plukkes fra. Da får du digestverifisering, én revisjon per bruker framfor filer i repoet, og de tre mekanismene under Stabile releases som i dag bare virker der. Prisen er at du bygger payload-trærne selv og holder dem i takt med kontrakten. nav-pilot har ingen kommando som bygger dem.
defaultModel er per klient. Den literale verdien "inherit" sender ingen --model. En konkret modell-id sendes med. En modell brukeren har pinnet selv, vinner over begge. minNavPilotVersion ligger på pakkenivå, skrives på nav-pilots releaseformat (YYYY.MM.DD-HHMMSS, eventuelt med build-sha) og blokkerer eldre binærer med en melding som sier hva de skal gjøre. Et annet format avvises framfor å ignoreres: nav-pilot kan ikke sammenligne det, og å godta det ville slått av akkurat den gaten manifestet ba om. Et utviklingsbygg (dev) er unntatt gaten, så lokalt arbeid på pakka stopper ikke.
Kjørbar kode når ikke alle klientene. opencode og pi hopper over hooks, med en advarsel på stderr som navngir dem. Det er koblinga som mangler, ikke evnen. Extensions håndteres ikke der i det hele tatt. Har pakka di en hook eller en extension noen er avhengig av, er copilot den eneste klienten som får den.
Pakke uten agent
Deler dere bare skills eller instruksjoner, utelater dere primaryAgents og agents i layout. Dere trenger ikke finne på en persona. Pakka validerer, installeres og synkes som vanlig, men den kan ikke starte klienten: det finnes ingen agent å gi den, og launch stopper med pakkas navn i meldinga. Start klienten selv, eller pek nav-pilot på en pakke som deklarerer en agent. To pakker i samme scope er ingen utvei: et scope installeres fra én kilde, og nav-pilot nekter å blande innhold fra to agentpakker i én installasjon. Vil du ha den andre pakka i stedet, bytter du kilde for scopet.
{
"contractVersion": "1",
"name": "ditt-team",
"description": "Skillene vi deler",
"layout": {
"skills": "skills"
},
"clients": {
"copilot": {}
}
}MCP-servere
Valgfritt. MCP-servere styres sentralt i Navs MCP-register. Du kan ikke definere din egen, men du kan si hvilke av registerets servere agentene og skillene dine forventer. nav-pilot spør registeret når den validerer og installerer: et navn det ikke publiserer, er et funn. Svarer ikke registeret, blir det en advarsel i stedet, så en CI-jobb uten nett ikke feiler på noe den ikke kan sjekke.
{
"mcpServers": ["io.github.navikt/github-mcp", "io.github.navikt/aksel-mcp"]
}Navnene skrives på registerets egen omvendt-DNS-form, <namespace>/<navn>, som io.github.navikt/github-mcp. Et navn uten den formen avvises av schemaet før registeret spørres i det hele tatt.
install navngir serverne pakka trenger, peker på registeret og viser kommandoen som slår dem på: nav-pilot mcp enable <navn> …. Selve installasjonen skriver ingen MCP-konfigurasjon. Det gjør du med den kommandoen. Feltet ligger på pakkenivå, siden det er klientens eget oppsett som avgjør om en server er tilgjengelig.
Sandkassekonfigurasjon
Trenger en skill noe av sandkassa cplt setter rundt klienten, sier pakka det i policies.propose, i stedet for å la brukeren møte feilen midt i arbeidet. Nais-pakka spør Mimir, Loki og Tempo under nav.cloud.nais.io. Navnene slår opp til private adresser over naisdevice, og cplt avviser dem (403 Private target blocked by cplt) til brukeren har gitt et unntak. Pakka navngir hver host. Et suffiks som cloud.nais.io ville gitt unntak for alle hoster under alle organisasjoner på Nais, også dem skillen aldri spør. Et forslag konfigurerer ingenting av seg selv: det blir et launch-flagg først når brukeren har sagt ja.
{
"policies": {
"propose": {
"cplt": {
"reason": "The nais-observability skill queries Mimir, Loki and Tempo. These hosts resolve to private IP addresses over naisdevice and are blocked without this waiver. The preToolUse gate reads naisdevice's agent-status.json to tell whether you are connected.",
"proxy": {
"allow_private_domains": [
"mimir.nav.cloud.nais.io",
"loki.nav.cloud.nais.io",
"tempo.dev-gcp.nav.cloud.nais.io",
"tempo.prod-gcp.nav.cloud.nais.io"
]
},
"allow": {
"read": [
"~/Library/Application Support/naisdevice/agent-status.json",
"~/.config/naisdevice/agent-status.json"
]
}
}
}
},
"minNavPilotVersion": "2026.09.14-131410"
}I v1 får pakka foreslå to ting: proxy.allow_private_domains, 1 til 32 fulle DNS-navn uten wildcard, port eller sti, og allow.read, opptil 8 navngitte filer. reason er påkrevd, høyst 400 tegn, uten linjeskift og kontrolltegn, og er alt brukeren har å avgjøre på: skriv hva som ryker uten unntaket. nav-pilot vasker teksten igjen når den skrives ut, så en pakke kan ikke lage en linje som ser ut som nav-pilots egen. Skjemaet avviser allow.write, allow.exec, allow.socket, deny, preset, repo_dirs, inherit_env, allowed_domains, blocked_domains, proxy.forced, guardene og hele sandbox. Under proxy validerer ingenting annet enn allow_private_domains. Andre nøkler på toppnivå i cplt-blokka validerer, men nav-pilot navngir dem ved install og honorerer dem aldri. Andre verktøynøkler enn cplt ignoreres.
En lesetilgang navngir én fil, aldri en katalog. cplt gir én regel per sti, (allow file-read* (subpath "<sti>")) på macOS og AccessFs::ReadFile | ReadDir på Linux. Det er smalere enn domeneunntaket: ingen skriving, ingen port, ingen utgående trafikk. Men subpath på en katalog er alt under den, og ~/Library/Application Support/naisdevice er ett tegn unna å dele ut private.key. Derfor krever skjemaet en sti som begynner med ~/ og ender i et navn med punktum og filendelse, avviser nav-pilot validate både .. og alt som ligger på eller under cplts DENIED_DOTFILES, DENIED_FILES og DENIED_HOME_SUBPATHS, og stat-er launchen stien og slipper en katalog. Fila finnes ofte ikke ennå når manifestet valideres, så det siste laget er det eneste som kan se hva stien faktisk er.
Stien skrives ~/-relativt, den formen cplt selv forstår. nav-pilot utvider ~ ved launch og sender den absolutte stien, mens samtykkeposten tar vare på ~/-formen brukeren så. naisdevice legger tilstanden under ~/Library/Application Support/naisdevice/ på macOS og ~/.config/naisdevice/ på Linux, så pakka navngir begge og launchen sender bare den som finnes. $XDG_CONFIG_HOME utvides ikke: manifestet har én utvidelse.
Brukeren svarer i terminalen ved install, og ved sync --apply når blokka er endret. Svaret lagres per scope i ~/.nav-pilot/pakke-consent.json, nøklet på en hash av hele blokka: endrer du reason eller legger til en host, kommer spørsmålet tilbake med det som endret seg. Blokka er ett spørsmål: et domene og en lesetilgang i samme blokk vises sammen og besvares én gang. Et nei installerer pakka likevel. Brukeren får vite hva som ryker, og kommandoene som åpner det for hånd:
cplt config set proxy.allow_private_domains mimir.nav.cloud.nais.io cplt config set proxy.allow_private_domains loki.nav.cloud.nais.io cplt config set proxy.allow_private_domains tempo.dev-gcp.nav.cloud.nais.io cplt config set proxy.allow_private_domains tempo.prod-gcp.nav.cloud.nais.io cplt config set allow.read "~/Library/Application Support/naisdevice/agent-status.json"
Uten terminal, og med --json, godkjennes ingenting og noteres ingenting. Er cplt ikke installert ennå, spør ikke nav-pilot. Spørsmålet kommer ved neste install eller sync --apply når cplt er på plass. Etter et nei viser nav-pilot doctor avslaget som informasjon, med kommandoene over, ikke som en advarsel. nav-pilot uninstall sletter svaret. Et ja blir --allow-private-domain <host> og --allow-read <absolutt sti> på cplt-kommandolinja, for launcher fra scopet som svarte, og skrives ut ved hver launch. nav-pilot rører ikke cplt-konfigurasjonen. Unntaket løfter bare DNS-rebinding-vernet for de navnene: tillatelses- og blokklista gjelder fortsatt, ingen port åpnes, ingenting kjøres.
Sett minNavPilotVersion til en nav-pilot-release fra 14. september 2026 eller nyere, og til releasen som innførte allow.read om du bruker den. En eldre nav-pilot ignorerer blokka som et ukjent felt, og brukeren får feilen uten forklaring. En nav-pilot fra før allow.read gjør noe strengere: skjemaet følger binæren, så manifestet avvises i sin helhet. Brukeren trenger dessuten cplt fra 14. september 2026 eller nyere: under det lagres svaret, men unntaket anvendes ikke, og launchen sier hvorfor.
Validering
Advarsler (⚠) feiler ikke kommandoen. En defaultModel nav-pilot ikke kjenner igjen er en av dem: modellkatalogen synkes fra models.dev og henger etter en fersk modell, så et avvist manifest ville tatt oftere feil enn advarselen gjør.
Kilden må være owner/repo eller en absolutt sti. --source . avvises av verdisjekken før noe forsøkes hentet, så bruk "$PWD", eller "$GITHUB_WORKSPACE" i CI. Etiketten i utdataene er kilden du oppga, ikke navnet i manifestet. Skjemaet ligger i cli/nav-pilot/schemas/agentpakke-v1.json, så CI kan linte manifestet mot det uten nav-pilot.
Utelater du --source, velger nav-pilot kilde i denne rekkefølgen: --source, så source-nøkkelen i konfigurasjonen din, så navikt/copilot. Lokal autogjenkjenning gjelder bare en navikt/copilot-checkout, ikke en vanlig agentpakke. Det er derfor en validate uten --source i pakkerepoet ditt validerer Nav-pakka og melder alt grønt: den så aldri på din.
Gjenbruk av en annen pakke
Oppsettet står under Bygg videre på en. Her står reglene for hvordan de to pakkene settes sammen.
Gjenbruker to pakker hverandre, finnes det ingen rekkefølge å løse dem i, og feilen ber om at syklusen brytes i én av dem.
Kollisjoner
Har begge pakkene en agent med samme navn, installeres din. Det er samme regel som overrides i .github/copilot-sync.json: det teamet eier selv, eier de. Å skygge et artefakt er den vanlige måten å endre én ting i en pakke du ellers tar som den er. Det er ikke en feil, og nav-pilot varsler ikke.
Hva som komponerer
Alle installasjonsveiene tar med det gjenbrukte innholdet: install <navn>, install --all, den interaktive plukkeren, install <navn> --type <type> og sync, og list viser det. En konsument kan navngi et arvet artefakt i items. validate komponerer med vilje ikke: den sjekker hva repoet ditt selv sender, så en base kan ikke gjøre en pakke gyldig som ikke er det.
Hold basen oppdatert
Pinnen flytter seg ikke av seg selv, og modellene i agentenes frontmatter blir stående sammen med den. sync og doctor sier fra med én linje når basen har flyttet seg, men flytter ikke pinnen. Du flytter den med nav-pilot pakke bump-base i pakkerepoet ditt. Kommandoen skriver ut hvilke agenter og modeller som er endret. Workflowen agentpakke-base-bump.yaml i navikt/copilot gjør det samme på en tidsplan og åpner en pull request du ser over og merger. Oppsettet står i agentpakke-guiden.
Releases og pensjonering
Når endringen når fram
Konsumentene er pinnet til revisjonen de installerte. En endring du pusher, når dem først når nav-pilot sync --apply flytter pinnen hos dem, som én linje diff i en pull request de leser og godkjenner. En rettelse er derfor ikke ute samme dag. Å endre name i manifestet gjør eksisterende installasjoner til en annen pakke, så det er ikke en omdøping du gjør i forbifarten.
Stabile releases
Uten releases henter sync det standardgrenen holder, så alt du pusher går rett ut til konsumentene. Publiserer du i stedet en GitHub Release med assetet agentpakke-release.json, leser install og sync nyeste stabile release, og du kan jobbe videre på main. Releasen må være publisert, ikke prerelease, og immutable, og taggen må binde versjonen. Et repo uten slike releases fungerer nøyaktig som før, fra standardgrenen.
Er pakka di Tier 1, altså en layout av filer, pinner den ingen revisjon: den installerer filer, og abonnementet avgjør bare hvilken revisjon filene leses fra. install og sync uten --ref leser nyeste stabile release i stedet for standardgrenen, i både bruker- og repo-scope, og sync --apply flytter sha i erklæringa til release-SHA-en. Det finnes ikke noe nedgraderingsvern her: hvert oppslag tar nyeste stabile release uten å sammenligne med det som ligger på disk, så en installasjon som står foran releasen, flyttes tilbake til den ved neste sync --apply. Diffen vises før --apply, som for enhver annen fil.
Tre mekanismer hører sammen med releases, og de virker i dag bare for Tier 2, fordi alle tre er gatet på at scopet pinner en revisjon: nav-pilot rollback tilbake til forrige revisjon på maskinen, oppstartsspørsmålet om å ta en ny release, og det varige valget sync --updates auto|ask|keep. En Tier 1-installasjon fører opp filene den la ned, og faller derfor utenfor gaten: rollback nekter med «your user scope pins none», og de to andre nås aldri. Lov derfor ikke konsumentene dine et rollback en Tier 1-pakke ikke gir dem. Gaten blir ikke utvidet. Vil en Tier 1-konsument tilbake til en eldre revisjon, pinner de den selv med nav-pilot sync --apply --ref <sha>.
Er pakka di Tier 2, gjelder alle tre. Konsumenten kan rulle tilbake uten nett og uten å vente på deg, den forlatte revisjonen tilbys ikke igjen mens neste release gjør det (lever derfor rettelsen som en ny versjon; en revert av taggen når dem ikke), og et team som har valgt keep, blir stående til de selv tar releasen. Regn ikke med at alle er på nyeste versjon dagen etter. Konsumenter som alt står på standardgrenen ligger som regel foran din første release, og nedgraderingsvernet tilbyr den ikke til dem; nav-pilot spør dem én gang ved oppstart om å pinne releasen og følge releases videre, og navngir begge revisjonene.
Feltene og hele kontrakten står i README.agentpakke.md.
Pensjonering
Slett aldri et artefakt uten å føre det opp i .nav-pilot/retired-artifacts.json. En kilde hentes med --depth 1, så brukeren har ingen historikk å slå opp i, og en fil som bare forsvinner blir liggende hos alle som installerte den. Generer lista og commit den: scripts/generate-retired i navikt/copilot er en Go-modul på rundt 200 linjer som leser hashene ut av git-loggen, og mise run retired:check verifiserer i CI at lista stemmer.
Fila navngir hver pensjonerte sti sammen med innholdshashene pakka en gang publiserte, og det er hashene som gjør den trygg: nav-pilot sletter en installert fil bare når bytene matcher en revisjon kilden faktisk har publisert. En utvikler som har skrevet sin egen agent på den stien, beholder den. At kilden ikke lenger har noe med dette navnet, er ikke tillatelse til å slette.