Skip to content

nvt M1-kjerne: nvt-bridge + agents/nvt-fat-developer bak filkontrakten - #110

Open
olebhansen-agent wants to merge 2 commits into
v2.0from
agent/nvt-m1-core
Open

nvt M1-kjerne: nvt-bridge + agents/nvt-fat-developer bak filkontrakten#110
olebhansen-agent wants to merge 2 commits into
v2.0from
agent/nvt-m1-core

Conversation

@olebhansen-agent

@olebhansen-agent olebhansen-agent commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Miljøuavhengig del av M1: ny utførende agent nvt-fat-developer bak uendret
filkontrakt
, med en deterministisk nvt-bridge som mapper topic → levende
nvt-instans.

Refs #97 (ikke Closes — M1 lukkes først etter E2E-verifisering mot ekte
nvt-instanser).

Leveranser

1. apps/nvt-bridge/ (Node/TS, samme stack og testoppsett som
integrations/: native type-stripping, ingen byggesteg, node --test)

  • Poller agents/nvt-fat-developer/triggers/inbox.jsonl med samme dedupe-regel
    som i dag: id uten linje i results.jsonl.
  • topic = payload.origin.event_id uten delta-suffiks, ellers eventets egen
    id. Regelen er portet fra scripts/agent-runner.ps1 (Get-TopicKey) —
    inkludert at strippingen er ankret og skjer én gang, så x-d1-d2x-d1.
  • topic→instans i state/topics.json.
  • Serielt per topic, parallelt på tvers (NVT_BRIDGE_MAX_PARALLEL).
  • TTL-basert agent-down for inaktive topics; workspacet beholdes.
  • Fallback-resultatlinje med status:"error" og forklaring — aldri en
    fabrikert suksess.
    status:"ok" kan kun komme fra agenten selv.

2. All nvt-interaksjon bak NvtDriver med FakeNvtDriver for tester og en
DryRunNvtDriver for tørrkjøring. Den ekte adapteren src/nvt/docker.ts er
tynn og merket «kalibreres mot M0-funn» (#96) i et blokk-kommentarhode som
lister nøyaktig hva som er ubekreftet (make-mål, containernavn,
agentdctl subscribe-format). Ingen antakelser om compose-stier eller nettverk
utover det planen sier; adapteren gjør ingen sti-oversetting.

3. agents/nvt-fat-developer/: triggers/-kontrakt, README, .env.example
og AGENTS.local.md.tmpl — instruks-malen. Kodeagent-protokollen gjenbrukes
ordrett der den kan («kjenn din begrensning», agent/<navn>,
gh pr create --base, Closes #<nr>, aldri merge, retro/KB-steget). To ting
skiller, som spesifisert: ingen innboks-polling, og agentdctl signal done etter resultatlinja.

4. 77 tester mot fake-implementasjonen (dedupe, topic-avledning,
serialisering per topic, fallback-linja) + oppføring i doc/log.md.

integrations/src/ er urørt — verifisert med
git diff --stat -- integrations/. Ruta i AGENT_ROUTES er driftskonfig og
tas ved utrulling.

Review er kjørt (som prosessen krever)

En reviewer-subagent med ferske øyne gikk over diffen og fant 1 blocker og 4
majors
— alle reelle, alle reprodusert, alle fikset i fdc1fae med
regresjonstest (62 → 77 tester). Funnene og verifikasjonen er postet som
kommentar på denne PR-en (audit-sporet). Kort:

  • Bridge-loggen lå utenfor try → en feilende logg-skriving etterlot eventet
    uten resultatlinje (evig ubehandlet, poller i ring).
  • Rå event-id i log-stien → log-verdi utenfor triggers/ + skriving utenfor
    katalogen. safeId() fantes, men var ubrukt i produksjonskode.
  • appendResult antok avsluttende linjeskift. Agenten skriver linja frihånds;
    uten linjeskift limte vår append seg på dens linje og gjorde begge uleselige
    — agentens suksess tapt.
  • Serialisering per topic overlevde ikke omstart → samme prompt kunne havne i
    en levende sesjon to ganger.
  • Prompt-injeksjon: fast avgrenser lot oppgaveteksten utgi seg for broen. Siden
    integrations matcher kun på id, kunne en lurt agent postet falsk suksess i
    en annen tråd.

Designvalg jeg måtte ta (ikke avgjort i planen)

  1. apps/nvt-bridge/ framfor integrations/-søsken (planen sa «apps/
    eller …»). Egen app holder brua unna CODEOWNERS-stien integrations/src/.
  2. Instansnavn = kort slug + 8 hex av sha256(topic), maks 40 tegn,
    lowercase. Topic-id-ene er lange (github-digdir-digdir-ai-agents-97-c… er
    45 tegn), og nvt bygger både compose-prosjektnavn og Traefik-vertsnavn
    (<navn>.agent.localhost) av navnet — DNS-etiketter tar maks 63.
    Deterministisk med vilje: mister vi state/topics.json, peker samme topic
    fortsatt på samme instans og dermed samme workspace.
  3. Eksplisitt in-flight-vakt på event-id. ps1-runneren slapp unna med «én
    container per topic»; brua køer per topic og trenger vakten eksplisitt,
    ellers re-dispatcher neste polling et event som er under arbeid (det har
    ennå ingen resultatlinje).
  4. Egen loggfil logs/<id>.bridge.log for broens diagnostikk, så den ikke
    kolliderer med agentens egen logs/<id>.log.
  5. Omstart midt i en oppgave melder «ukjent utfall» framfor å re-prompte.
    En andre prompt inn i en levende sesjon som står midt i arbeidet er verre
    enn en ærlig feilmelding med peker til instansen.
  6. Ingen --experimental-strip-types-flagg: Node ≥ 23.6 stripper selv, men
    strip-only-modus avviser TS parameter properties. Alle klasser bruker
    derfor eksplisitte felt + tilordning i konstruktøren — samme stil som
    integrations/src/, som unngår dem konsekvent.
  7. Sikkerhet: ingen shell noe sted (spawn med argv-array, aldri
    shell: true), --external er ubetinget på det ene injeksjonspunktet, og
    ingen hemmeligheter i logger, resultatlinjer eller state. Bot-kontonavnet er
    holdt utenfor repoet; .env.example inneholder kun gateway-konsumentnøkkelen
    (samme mønster som jr).

Til deg som reviewer

  • Ikke sett auto-merge. PR-en rører **/Dockerfile og
    **/docker-compose.yml (CODEOWNERS), så labelen ville vært virkningsløs
    uansett — og det er forventet her.

  • CI dekker ikke de nye testene. Jeg la til en nvt-bridge-jobb i
    ci.yml, men GitHub avviste pushen: agent-tokenet mangler workflow-scope.
    Endringen er derfor rullet tilbake, og .github/ er urørt. Foreslått tekst,
    til manuell innlegging etter integrations-jobben:

      nvt-bridge:
        name: nvt-bridge
        runs-on: ubuntu-latest
        defaults:
          run:
            working-directory: apps/nvt-bridge
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: 24
              cache: npm
              cache-dependency-path: apps/nvt-bridge/package-lock.json
          - run: npm ci
          - run: npm run typecheck
          - run: npm test
  • CODEOWNERS-gap å vurdere: agents/*/CLAUDE.md fanger ikke
    agents/nvt-fat-developer/AGENTS.local.md.tmpl, som er agentens instruks.
    Framtidige endringer i den ville sluppet unna code-owner-review. Jeg har
    ikke rørt CODEOWNERS selv — hvem som må godkjenne hva er en
    governance-beslutning, ikke en ingeniørbeslutning.

  • Planen sier noe issue-teksten ikke sier: doc/plans/nvt-agent-integrasjon.md
    finnesv2.0 (delegeringsprompten hevdet at den manglet — den ble nok
    sjekket mot main). Jeg har fulgt planen, inkludert beslutning 3 fra
    2026-07-27: llm-gatewayen med subscription-OAuth, ikke LM Studio, som
    modell-backend. Issue nvt M0: sandkasse-bevis — nvt-instans med bot-PAT-grant og LM Studio på WSL2 #96-teksten sier fortsatt LM Studio.

Ikke med i M1 (bevisst)

Refs #97

Summary by CodeRabbit

  • New Features
    • Added the nvt-bridge service to route trigger events to isolated agent sessions.
    • Added per-topic scheduling, serialization, parallel processing limits, persistence, restart recovery, and idle-session cleanup.
    • Added Docker-backed execution and a dry-run mode.
    • Added configuration templates and deployment setup.
  • Documentation
    • Added setup guides, agent instructions, configuration examples, and changelog details.
  • Bug Fixes
    • Improved duplicate prevention, timeout/error reporting, JSONL handling, path safety, and recovery from interrupted work.

olebhansen-agent and others added 2 commits July 29, 2026 08:35
…1-kjerne)

Miljøuavhengig del av M1: ny utførende agent bak uendret filkontrakt, med en
deterministisk bro (ingen LLM) som mapper topic til levende nvt-instans.

apps/nvt-bridge/ (Node/TS, samme stack og testoppsett som integrations/):
- dedupe som resten av pipelinen: id uten linje i results.jsonl
- topic = payload.origin.event_id uten delta-suffiks (-dN), ellers eventets id
- topic -> instans i state/topics.json; instansnavnet er deterministisk avledet
  og lengdebegrenset, så tapt state gjenfinner samme workspace
- serielt per topic (én levende sesjon, én arbeidskopi), parallelt på tvers
- TTL-basert agent-down som beholder workspacet
- fallback: status:"error" med forklaring når signal done kommer uten
  resultatlinje, ved timeout, eller ved intern feil. Aldri en fabrikert
  suksess — status:"ok" kan kun komme fra agenten selv

All nvt-interaksjon bak NvtDriver med fake for tester. Ekte adapter
(src/nvt/docker.ts) er tynn og merket «kalibreres mot M0-funn» (#96): den er
det eneste stedet antakelser om make-mål, containernavn og agentdctl-format
bor.

agents/nvt-fat-developer/: triggers-kontrakt, README, .env.example og
instruks-malen for instansens AGENTS.local.md — kodeagent-protokollen ordrett
der den kan, men uten innboks-polling og med agentdctl signal done etter
resultatlinja.

62 tester dekker dedupe, topic-avledning, serialisering per topic og
fallback-linja. integrations/src/ er urørt; ruta i AGENT_ROUTES er
driftskonfig som tas ved utrulling.

Refs #97

Co-Authored-By: Claude <noreply@anthropic.com>
Funn fra reviewer-subagent på diffen (ferske øyne). Alle fikser har
regresjonstest; 62 -> 77 tester.

- Blocker: bridge-loggen ble skrevet utenfor try/catch, så en feilende
  logg-skriving (f.eks. logs/ som fil -> ENOTDIR) etterlot eventet UTEN
  resultatlinje — dvs. evig ubehandlet, og polleren dispatcher i ring. Nå er
  loggingen best effort og innenfor try-en, og fallback-linja skrives før
  logging/opprydding i catch-en.
- Major: bridgeLogRelPath brukte rå event-id. En id med `/` eller `..` ga en
  `log`-verdi integrations avviser (utenfor triggers/) og fikk loggen skrevet
  utenfor katalogen. Bruker nå safeId(), som fantes men var ubrukt.
- Major: appendResult antok at fila slutter med linjeskift. Agenten skriver
  linja frihånds; glemte den linjeskiftet, limte vår append seg på dens linje
  og gjorde begge uleselige — agentens suksess tapt, eventet re-dispatchet.
  Reparerer nå manglende linjeskift først.
- Major: serialisering per topic overlevde ikke omstart. Injiserte events
  markeres nå i state (in_flight_event_id) før prompten sendes; etter omstart
  promptes de ikke på nytt (en andre prompt inn i en levende sesjon er verre
  enn ærlig «ukjent utfall»). SIGTERM venter nå på events under arbeid, og
  poll-sleepen er abort-aware.
- Major: prompt-injeksjon. Fast avgrenser lot oppgaveteksten «lukke» blokka og
  utgi seg for broen; siden integrations matcher kun på id, kunne en lurt
  agent postet falsk suksess i en ANNEN tråd. Avgrenserne har nå en nonce, og
  kontrakten med den eneste gyldige id-en gjentas ETTER blokka.
- Minor: /triggers var hardkodet i prompt.ts stikk i strid med at alle
  nvt-antakelser skal ligge i docker.ts — nå konfigurerbart
  (NVT_INSTANCE_TRIGGERS_PATH).
- Nits: .catch() på runTopic, grace-polling 500ms -> 1000ms (hvert forsøk
  leser hele results.jsonl), tomme event-felter rendres ikke som "undefined".

Testhull lukket: nådefristen ble aldri utøvd (resultGraceMs var 0 overalt), og
maks-parallell var utestet på bridge-nivå.

Refs #97

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 061c4e70-954a-40e1-8634-23a2a0497972

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@olebhansen-agent

Copy link
Copy Markdown
Collaborator Author

Review av denne PR-en (reviewer-subagent, ferske øyne)

Kjørt som prosessen krever, før rapportering. Reviewer fikk diffen uten
konteksten som skrev koden, med issue #97s krav og filkontrakten
(integrations/src/agent/{types,queue}.ts + scripts/agent-runner.ps1) som
målestokk, og verifiserte funnene ved å reprodusere dem i /tmp — ikke i
repoet.

Konklusjon: 1 blocker + 4 majors, alle reelle og alle reprodusert. Alle er
fikset i fdc1fae, hver med regresjonstest (62 → 77 tester). Reviewen var
altså ikke ren, og labelen auto-merge er ikke satt (PR-en rører også
CODEOWNERS-stier, så den ville vært virkningsløs uansett).

Funn og hva som ble gjort

# Alvor Funn Fiks
1 Blocker appendBridgeLog lå utenfor try i handleEvent, og som første setning i catch. Feilet logg-skrivingen (reprodusert med logs/ som fil → ENOTDIR), endte eventet uten resultatlinje → evig ubehandlet, polleren dispatcher i ring, brukeren får aldri svar. Loggingen er nå best effort (svelger feil) og flyttet innenfor try; i catch skrives resultatlinja først. To uavhengige lag — verifisert at det triggers-nære laget har tenner ved å reversere det.
2 Major bridgeLogRelPath() interpolerte rå event-id. Med id = "slack-C1/../../../etc-x" ble log = "../etc-x.bridge.log" (integrations avviser den, queue.ts:454), og appendBridgeLog skrev faktisk en fil utenfor triggers-katalogen. safeId() fantes allerede, men var ubrukt i produksjonskode. bridgeLogRelPath bruker nå safeId(). Test asserterer at verdien normaliserer til ett segment under logs/. Ikke utnyttbart via dagens integrations (ids saneres der), så defense-in-depth — men fiksen var ett kall.
3 Major appendResult skrev blindt. Agenten skriver resultatlinja frihånds (ulikt jq-entrypointene). Glemte den avsluttende linjeskiftet, limte vår fallback seg på dens linje → én linje, 0 parsebare. Reprodusert: agentens status:"ok" tapt, ingen id funnet, eventet re-dispatchet (prompts 1 → 2). Sjekker siste byte og reparerer manglende linjeskift før append.
4 Major Serialisering per topic overlevde ikke omstart: inFlight var kun in-memory. Reprodusert — bridge #2 injiserte samme prompt inn i den fortsatt levende sesjonen. Nettopp den «to prompts i én tmux-sesjon / én arbeidskopi»-faren serialiseringen finnes for. Dessuten avsluttet run() uten å vente på arbeid under utførelse, og sleep var ikke abort-aware — normal redeploy-vei med restart: unless-stopped. in_flight_event_id persisteres i state før prompten sendes; etter omstart promptes eventet ikke på nytt, men får nådefristen til å levere selv og meldes ellers som ukjent utfall med peker til instansen. SIGTERM venter nå på aktive topics; poll-sleepen er abort-aware.
5 Major Prompt-injeksjon: avgrenseren var fast og gjettbar, upålitelig tekst lå sist, og ingenting fulgte etter den. Reviewer viste en oppgavetekst som «lukker» blokka og ber om en resultatlinje for et annet event. Siden integrations matcher kun på pending.get(result.id), ville en lydig agent postet fabrikert suksess i en annen Slack-tråd / issue. Avgrenserne har nå en nonce avledet av event-id-en, og kontrakten med den eneste gyldige id-en gjentas etter blokka. --external var og er ubetinget.
6 Minor /triggers var hardkodet i prompt.ts, stikk i strid med at alle nvt-antakelser skal ligge i docker.ts. Konfigurerbart (NVT_INSTANCE_TRIGGERS_PATH).
7 Minor Dublett-vindu: agentens linje og fallbacken kan begge bli skrevet hvis de treffer samme øyeblikk. Utfall: «agenten lyktes, brukeren fikk feilmeldingen». Vinduet var alt smalt (ny sjekk rett før skriving); nå dokumentert eksplisitt som kjent restrisiko i README.
8 Minor waitForDone i den ekte adapteren kan miste et done som kom i siste chunk (exit løser timeout uten å drenere stdout). Notert; ligger i den eksplisitt ukalibrerte fila og tas med M0-kalibreringen.
9 Minor Testhull: resultGraceMs var 0 i hele harnessen, så «innen fristen» — selve fristen — var aldri utøvd. maxParallel var utestet på bridge-nivå. Begge dekket nå, inkl. at nådefristen respekteres presist (sum av ventetid = fristen) og at timeout hopper over den.
10 Minor (prosess) agents/*/CLAUDE.md i CODEOWNERS fanger ikke agents/nvt-fat-developer/AGENTS.local.md.tmpl, som er agentens instruks. Ikke fikset med vilje — hvem som må godkjenne hva er en governance-beslutning. Løftet i PR-beskrivelsen for din avgjørelse.
11 Nits Manglende .catch()void runTopic(...); hasResult leste hele results.jsonl hvert 500. ms gjennom nådefristen; tomme event-felter rendret som undefined; Number() godtok 1e3 som heltall. De tre første fikset (.catch(), 1000 ms, fallback-verdier). Tallparsing beholdt — Number.isInteger fanger det som betyr noe.

Verifisert OK (med bevis)

  • Scheduler er korrekt: et topic kan ikke startes to ganger (active.add
    er synkront før første await), ingen plasslekkasje (finally), pump()
    fra den frigjorte arbeideren slipper inn ventende topic, og idle() verken
    resolver for tidlig eller henger. maxParallel=1 med 3 topics → 3 instanser,
    3 resultatlinjer.
  • Topic-avledning matcher Get-TopicKey ordrett, inkludert det ankrede
    ett-gangs -dN-strippet og identisk tegnsett. Ids med regex-metategn er
    harmløse (ingen dynamisk regex).
  • «Aldri fabrikert suksess»: begge appendResult-kallstedene i broen
    hardkoder status:"error"; fake- og dry-run-driverne skriver aldri
    resultater. Ingen kodevei gir ok fra broen.
  • Kontrakten mot integrations: én komplett \n-terminert linje per
    appendFile (O_APPEND, ingen fletting), lesing stopper ved siste \n som i
    queue.ts:211, ids returneres verbatim.
  • Sikkerhet: ingen shell noe sted — hver spawn bruker argv-array uten
    shell-opsjon (testen for dette er en ekte enkelt-argv-assertion, ikke
    tautologisk). Ingen hemmeligheter i logger, resultatlinjer eller state.
  • Testene har tenner: reviewer bekreftet at fake-driverens
    samtidighetsinstrument faktisk kan nå 2 under reell overlapp, så
    maxConcurrentPrompts == 1 er en reell assertion og ikke alltid-sann.
  • Konvensjoner: tsconfig.json identisk med integrations/; ingen TS
    parameter properties noe sted (strip-only-modus avviser dem); .ts-endelser
    og import type konsekvent; agentkatalogen speiler
    agents/local-cc-jr-developer/; doc/log.md-formatet stemmer.
  • integrations/src/ er urørt: git diff --stat -- integrations/ scripts/ .github/ er tom.

Status etter fiks

77 tester, 77 pass, 0 fail    (node --test)
tsc --noEmit                  rent

Ende-til-ende tørrkjørt mot et ekte delegerings-event: topic avledet riktig,
instansnavn 39 tegn og gyldig DNS-etikett, status:"error"-fallback med
log-peker innenfor triggers/, state/topics.json skrevet.

Merk at grønne tester her ikke er E2E-bevis: adapteren mot ekte
nvt-instanser er uverifisert til M0 (#96) er kjørt. Det er derfor Refs #97
og ikke Closes #97.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
apps/nvt-bridge/src/topic.ts (1)

62-73: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

instanceNameFor can exceed maxLength when prefix alone leaves no room.

room is clamped to ≥0 (Line 70), but the final prefix-hash (slug dropped) is never re-checked against maxLength. E.g. prefix.length = 25, maxLength = 20, hash.length = 8fixed = 35 > 20room = 0, slug = "", but the returned name is still 25 + 1 + 8 = 34 chars — well over the configured cap. Since this cap exists specifically to satisfy the DNS-label limit that compose project names and code-server hostnames rely on (per the docstring), a silent overflow here defeats the whole point of the function.

🛡️ Proposed fix — fail fast instead of silently overflowing
   const hash = createHash("sha256").update(topic).digest("hex").slice(0, 8);
   const prefix = slugify(opts.prefix);
   // Fast del: <prefix>- ... -<hash8>
   const fixed = (prefix === "" ? 0 : prefix.length + 1) + 1 + hash.length;
+  if (fixed > opts.maxLength) {
+    throw new Error(
+      `instanceNameFor: prefix "${opts.prefix}" leaves no room for a slug within maxLength=${opts.maxLength}`,
+    );
+  }
   const room = Math.max(0, opts.maxLength - fixed);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nvt-bridge/src/topic.ts` around lines 62 - 73, Update instanceNameFor to
validate that the prefix, separator, and hash can fit within opts.maxLength
before constructing the result; when the fixed prefix-hash portion exceeds the
limit, fail fast with a clear error instead of returning an overlong name.
Preserve the existing slug truncation behavior when sufficient room remains.
apps/nvt-bridge/src/nvt/docker.ts (1)

115-169: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

waitForDone's streaming/timeout/abort logic has zero test coverage because it isn't injectable.

Every other method in DockerNvtDriver (ensureInstance, sendPrompt, stopInstance, isRunning, make) is testable because it goes through the injectable this.exec/ExecFn seam. waitForDone instead calls the imported spawn directly, so its partial-line buffering, timeout, and abort-signal wiring — the most stateful logic in the file — has no unit test today.

  • apps/nvt-bridge/src/nvt/docker.ts#L115-L169: extract the spawn call behind an injectable function (mirroring ExecFn, e.g. a SpawnFn passed via DockerNvtDriverOptions) so it can be stubbed in tests.
  • apps/nvt-bridge/src/nvt/docker.test.ts#L1-L118: once injectable, add tests for done-detection across chunked/partial stdout lines, timeout expiry, and abort-signal cancellation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nvt-bridge/src/nvt/docker.ts` around lines 115 - 169, Make waitForDone
testable by introducing an injectable SpawnFn through DockerNvtDriverOptions,
mirroring the existing ExecFn seam, and use it instead of calling imported spawn
directly in waitForDone. In apps/nvt-bridge/src/nvt/docker.ts lines 115-169,
preserve the existing streaming, timeout, abort, and exit behavior. In
apps/nvt-bridge/src/nvt/docker.test.ts lines 1-118, add tests using the injected
spawn stub for done detection across chunked or partial stdout lines, timeout
expiry, and abort-signal cancellation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agents/nvt-fat-developer/.env.example`:
- Around line 28-38: Remove the concrete credential from ANTHROPIC_AUTH_TOKEN in
agents/nvt-fat-developer/.env.example, leaving an empty placeholder for
deployment-time injection; also add .env to agents/nvt-fat-developer/.gitignore
so copied environment files are not tracked.

In `@agents/nvt-fat-developer/AGENTS.local.md.tmpl`:
- Around line 151-166: Add agents/nvt-fat-developer/AGENTS.local.md.tmpl to the
CODEOWNERS-protected paths and update the auto-merge sensitivity predicate in
this template’s auto-merge instructions to explicitly include that exact path.
Preserve the existing reviewer, audit-comment, and label workflow for
non-sensitive changes.

In `@apps/nvt-bridge/.env.example`:
- Around line 35-59: Declare documented PIPELINE_ROOT and RESTART_POLICY entries
in apps/nvt-bridge/.env.example, replace the personal NVT_ROOT value with a
<bruker> placeholder, and update apps/nvt-bridge/docker-compose.yml lines 18-29
to require both root variables with `${VAR:?...}` guards so startup fails when
either is unset.

In `@apps/nvt-bridge/package.json`:
- Around line 7-13: Raise the engines.node minimum in package.json from >=22.6
to >=22.18.0 so the start, test, and typecheck scripts run on a runtime with
default type stripping, or explicitly document a later required runtime if that
is the project’s chosen policy.

In `@apps/nvt-bridge/src/bridge.ts`:
- Around line 155-163: Propagate the abort signal from run() into handleEvent
and pass it as the signal option to driver.waitForDone(). Ensure an abort
produces the existing timeout-style outcome so handleEvent still writes the
fallback result line before shutdown.

In `@apps/nvt-bridge/src/nvt/docker.ts`:
- Around line 200-212: Update isDoneEvent to verify the parsed JSON value is a
non-null object before accessing obj.type or obj.event, returning false for null
and other non-object values; also add an isDoneEvent("null") case alongside the
existing invalid-input tests.

---

Nitpick comments:
In `@apps/nvt-bridge/src/nvt/docker.ts`:
- Around line 115-169: Make waitForDone testable by introducing an injectable
SpawnFn through DockerNvtDriverOptions, mirroring the existing ExecFn seam, and
use it instead of calling imported spawn directly in waitForDone. In
apps/nvt-bridge/src/nvt/docker.ts lines 115-169, preserve the existing
streaming, timeout, abort, and exit behavior. In
apps/nvt-bridge/src/nvt/docker.test.ts lines 1-118, add tests using the injected
spawn stub for done detection across chunked or partial stdout lines, timeout
expiry, and abort-signal cancellation.

In `@apps/nvt-bridge/src/topic.ts`:
- Around line 62-73: Update instanceNameFor to validate that the prefix,
separator, and hash can fit within opts.maxLength before constructing the
result; when the fixed prefix-hash portion exceeds the limit, fail fast with a
clear error instead of returning an overlong name. Preserve the existing slug
truncation behavior when sufficient room remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ffa42ab8-e1f2-4637-83d2-ab2458a79195

📥 Commits

Reviewing files that changed from the base of the PR and between 762952f and fdc1fae.

⛔ Files ignored due to path filters (1)
  • apps/nvt-bridge/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • agents/nvt-fat-developer/.env.example
  • agents/nvt-fat-developer/.gitattributes
  • agents/nvt-fat-developer/.gitignore
  • agents/nvt-fat-developer/AGENTS.local.md.tmpl
  • agents/nvt-fat-developer/README.md
  • agents/nvt-fat-developer/triggers/.gitkeep
  • apps/nvt-bridge/.env.example
  • apps/nvt-bridge/.gitignore
  • apps/nvt-bridge/Dockerfile
  • apps/nvt-bridge/README.md
  • apps/nvt-bridge/docker-compose.yml
  • apps/nvt-bridge/package.json
  • apps/nvt-bridge/src/bridge.test.ts
  • apps/nvt-bridge/src/bridge.ts
  • apps/nvt-bridge/src/config.ts
  • apps/nvt-bridge/src/index.ts
  • apps/nvt-bridge/src/nvt/docker.test.ts
  • apps/nvt-bridge/src/nvt/docker.ts
  • apps/nvt-bridge/src/nvt/driver.ts
  • apps/nvt-bridge/src/nvt/dryrun.ts
  • apps/nvt-bridge/src/nvt/fake.ts
  • apps/nvt-bridge/src/prompt.ts
  • apps/nvt-bridge/src/scheduler.test.ts
  • apps/nvt-bridge/src/scheduler.ts
  • apps/nvt-bridge/src/state.test.ts
  • apps/nvt-bridge/src/state.ts
  • apps/nvt-bridge/src/topic.test.ts
  • apps/nvt-bridge/src/topic.ts
  • apps/nvt-bridge/src/triggers.test.ts
  • apps/nvt-bridge/src/triggers.ts
  • apps/nvt-bridge/src/types.ts
  • apps/nvt-bridge/tsconfig.json
  • doc/log.md

Comment on lines +28 to +38
# AUTH_TOKEN er fat-devs EGEN konsument-nøkkel i gatewayens routes.json, slik
# at trafikken kan skilles i loggen og nøkkelen revokeres alene. Det ekte
# OAuth-tokenet bor kun i gatewayens .env og er aldri inne i denne
# containeren. Modell-allowlisten håndheves i gatewayen (fail closed) —
# agenten kan ikke velge en dyrere modell selv.
#
# ⚠️ Host-oppslaget må VERIFISERES i M0 (issue #96): nvt-runtimen kjører med
# `network_mode: service:docker`, så det er ikke gitt at
# host.docker.internal løses her. Er den ikke det, brukes gateway-IP-en.
ANTHROPIC_BASE_URL=http://host.docker.internal:8787
ANTHROPIC_AUTH_TOKEN=fat-developer

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not publish or track the gateway authentication credential.

ANTHROPIC_AUTH_TOKEN is documented as a per-agent consumer key, yet the example assigns a concrete value; copied .env files are also not ignored despite line 1 claiming otherwise. This enables unauthorized gateway use and accidental credential commits.

  • agents/nvt-fat-developer/.env.example#L28-L38: use an empty placeholder and inject the real token at deployment.
  • agents/nvt-fat-developer/.gitignore#L1-L4: add .env.
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 38-38: [UnorderedKey] The ANTHROPIC_AUTH_TOKEN key should go before the ANTHROPIC_BASE_URL key

(UnorderedKey)

📍 Affects 2 files
  • agents/nvt-fat-developer/.env.example#L28-L38 (this comment)
  • agents/nvt-fat-developer/.gitignore#L1-L4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/nvt-fat-developer/.env.example` around lines 28 - 38, Remove the
concrete credential from ANTHROPIC_AUTH_TOKEN in
agents/nvt-fat-developer/.env.example, leaving an empty placeholder for
deployment-time injection; also add .env to agents/nvt-fat-developer/.gitignore
so copied environment files are not tracked.

Comment on lines +151 to +166
## Auto-merge av trygge PR-er

PR-er som **ikke** rører noen sti i `.github/CODEOWNERS` (agent-instrukser,
skills, Docker-filer, `integrations/src/`, `scripts/`, `.github/`) kan
merges uten menneskelig godkjenning — se `doc/pr-prosess.md`. Prosessen er:

1. Kjør en reviewer-subagent på PR-diffen — ferske øyne, ikke samme
kontekst som skrev koden.
2. Post reviewens funn og konklusjon som kommentar på PR-en
(`gh pr comment`) — kommentaren er audit-sporet.
3. Er reviewen ren: sett labelen `auto-merge`
(`gh pr edit <nr> --add-label auto-merge`). En GitHub Action merger når
required checks er grønne.

Rører PR-en en sensitiv sti, er labelen virkningsløs (branch protection
krever code owner uansett) — utelat den og pek på PR-en i svaret som før.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Protect this instruction template before allowing auto-merge.

AGENTS.local.md.tmpl is currently outside CODEOWNERS, but this flow permits non-owned PRs to receive auto-merge. A change weakening this template could therefore merge without human ownership and affect every rendered agent. Add this exact path to CODEOWNERS and treat it as sensitive in the auto-merge predicate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/nvt-fat-developer/AGENTS.local.md.tmpl` around lines 151 - 166, Add
agents/nvt-fat-developer/AGENTS.local.md.tmpl to the CODEOWNERS-protected paths
and update the auto-merge sensitivity predicate in this template’s auto-merge
instructions to explicitly include that exact path. Preserve the existing
reviewer, audit-comment, and label workflow for non-sensitive changes.

Comment on lines +35 to +59
# --- nvt-oppsettet (kalibreres mot M0-funn, se issue #96) ---
# Sjekkouten av nvt-agent. Make-flyten (agent-init/agent-up) kjøres herfra.
# MERK: når bridgen kjører i container, må denne stien være IDENTISK med
# stien på hosten — compose-bind-mounts løses av hostens daemon.
NVT_ROOT=/home/ole/src/nvt-agent

# Agent-type nvt skal initialisere instansene med.
NVT_AGENT_TYPE=claude

# Hvor agentens triggers/ er mountet INNE i instansen. Brukes i prompten som
# forteller agenten hvor resultatlinja skal. Mounten settes opp i nvt-configen
# (kalibreres mot M0-funn).
NVT_INSTANCE_TRIGGERS_PATH=/triggers

# Prefiks for instansnavn (compose-prosjekt blir agent-<navn>, og code-server
# rutes som http://<navn>.agent.localhost:4090).
NVT_INSTANCE_PREFIX=fatdev

# Maks lengde på instansnavnet. Holdes godt under DNS-etikettgrensen (63) —
# nvt bygger både compose-prosjektnavn og Traefik-vertsnavn av det.
NVT_INSTANCE_NAME_MAX=40

# Sett til 1 for å kjøre uten nvt (fake-driver): logger hva som VILLE blitt
# kjørt, og skriver aldri en fabrikert suksess. Kun for tørrkjøring.
NVT_BRIDGE_DRY_RUN=0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Undeclared PIPELINE_ROOT breaks the compose mounts. docker-compose.yml loads .env and interpolates ${PIPELINE_ROOT} (and ${RESTART_POLICY}), but .env.example never declares them, so a copied template expands them to empty strings and the bridge quietly binds and polls the wrong triggers directory.

  • apps/nvt-bridge/.env.example#L35-L59: add documented PIPELINE_ROOT and RESTART_POLICY entries, and replace the personal NVT_ROOT=/home/ole/... value with a <bruker> placeholder.
  • apps/nvt-bridge/docker-compose.yml#L18-L29: guard both roots with ${VAR:?...} so an unset value fails startup instead of silently resolving to a relative path.
📍 Affects 2 files
  • apps/nvt-bridge/.env.example#L35-L59 (this comment)
  • apps/nvt-bridge/docker-compose.yml#L18-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nvt-bridge/.env.example` around lines 35 - 59, Declare documented
PIPELINE_ROOT and RESTART_POLICY entries in apps/nvt-bridge/.env.example,
replace the personal NVT_ROOT value with a <bruker> placeholder, and update
apps/nvt-bridge/docker-compose.yml lines 18-29 to require both root variables
with `${VAR:?...}` guards so startup fails when either is unset.

Comment on lines +7 to +13
"engines": {
"node": ">=22.6"
},
"scripts": {
"start": "node --disable-warning=ExperimentalWarning src/index.ts",
"test": "node --test --disable-warning=ExperimentalWarning",
"typecheck": "tsc --noEmit"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js version type stripping enabled by default without experimental-strip-types flag

💡 Result:

Yes, type stripping is enabled by default in modern versions of Node.js, and no experimental flag is required to use it [1][2]. As of the current date (July 29, 2026), type stripping has been a stable, default feature for some time [1][3]. The following timeline outlines its evolution: - Default Enablement: Type stripping was enabled by default starting in Node.js v23.6.0 and backported to v22.18.0 [1][2]. - Removal of Experimental Warning: The experimental warning associated with type stripping was removed in v24.3.0 and v22.18.0 [1]. - Stability: The feature was officially marked as stable in v25.2.0 and v24.12.0 [1][3]. When enabled, Node.js automatically executes TypeScript files by stripping erasable syntax (such as type annotations, interfaces, and import types) and running the remaining JavaScript [1][4]. This process is lightweight and does not perform type checking [1][2]. If you need to disable this behavior, you can use the --no-strip-types flag [1]. Note that more advanced TypeScript features requiring code generation (such as enums or parameter properties) still require the --experimental-transform-types flag, which remains experimental [2][3][5].

Citations:


🌐 Web query:

node --test default file discovery pattern TypeScript .test.ts files

💡 Result:

The Node.js test runner, invoked with the --test flag, automatically discovers test files using a predefined set of glob patterns [1][2]. When Node.js is running with its built-in TypeScript support (which performs type stripping), the test runner includes.ts,.cts, and.mts extensions in its default discovery pattern [1][3]. The default patterns for TypeScript files are: - /.test.{cts,mts,ts} - /-test.{cts,mts,ts} - /_test.{cts,mts,ts} - /test-.{cts,mts,ts} - /test.{cts,mts,ts} - /test//*.{cts,mts,ts} These patterns are active by default unless the --no-strip-types flag is used [1][3]. Node.js's built-in TypeScript support performs lightweight type stripping and does not perform type checking; it also ignores tsconfig.json configurations [4]. If you require full TypeScript support (such as type checking or specific tsconfig settings), you would typically use a third-party loader or tool (e.g., tsx) [4] rather than the default discovery mechanism.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package files =="
git ls-files '*package.json' | sed -n '1,120p'

echo
echo "== apps/nvt-bridge/package.json =="
if [ -f apps/nvt-bridge/package.json ]; then
  nl -ba apps/nvt-bridge/package.json | sed -n '1,120p'
fi

echo
echo "== relevant source/test files presence =="
git ls-files 'apps/nvt-bridge**' | sed -n '1,200p'

echo
echo "== Dockerfile / toolchain mentions =="
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' 'node|Dockerfile|engines|22\.18|23\.6|strip|typescript|test' . | sed -n '1,240p'

Repository: digdir/digdir-ai-agents

Length of output: 343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json =="
cat -A apps/nvt-bridge/package.json | sed -n '1,120p' || cat apps/nvt-bridge/package.json | sed -n '1,120p'

echo
echo "== nvt-bridge tracked files =="
git ls-files 'apps/nvt-bridge' | sed -n '1,240p'

echo
echo "== nvt-bridge source/test content summary =="
for f in $(git ls-files 'apps/nvt-bridge'); do
  if [ -f "$f" ]; then
    echo "--- $f"
    wc -l "$f"
    if [ "$f" = "apps/nvt-bridge/src/index.ts" ] || echo "$f" | grep -qE '\.test\.ts$|\.ts$'; then
      sed -n '1,220p' "$f"
    fi
  fi
done

Repository: digdir/digdir-ai-agents

Length of output: 50379


Make Node’s declared floor match the type-stripped runtime.

src/index.ts runs .ts imports without --experimental-strip-types, and that flag is required until Node strips erasable TS by default. The declared range allows >=22.6, but the current Node 22 behavior in the Dockerfile is 24; raise the floor to Node’s default-strip version (>=22.18.0) or document that these scripts must run on a later runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nvt-bridge/package.json` around lines 7 - 13, Raise the engines.node
minimum in package.json from >=22.6 to >=22.18.0 so the start, test, and
typecheck scripts run on a runtime with default type stripping, or explicitly
document a later required runtime if that is the project’s chosen policy.

Comment on lines +155 to +163
await this.opts.driver.sendPrompt(
ref,
renderPrompt(event, topic, this.opts.instanceTriggersPath ?? "/triggers"),
);
await this.opts.triggers.appendBridgeLog(event.id, "prompt injisert, venter på signal done");

const outcome = await this.opts.driver.waitForDone(ref, {
timeoutMs: this.opts.promptTimeoutMs,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

waitForDone gets no AbortSignal, so graceful shutdown can block for promptTimeoutMs (default 3600s).

run() awaits scheduler.idle() after abort (Line 101), but the in-flight waitForDone has no way to learn about the abort — the driver contract accepts signal (see apps/nvt-bridge/src/nvt/driver.ts lines 48-51) and it is never passed. With NVT_BRIDGE_PROMPT_TIMEOUT_SECONDS=3600, SIGTERM leaves the process hanging well past any container stop grace period, so it gets SIGKILLed — which is exactly the "event ends without a result line" failure the class invariant is built to prevent.

Plumb the run signal into handleEvent and pass it through, so an abort surfaces as a timeout-style outcome and the fallback line still gets written.

🛠️ Sketch of the plumbing
-      const outcome = await this.opts.driver.waitForDone(ref, {
-        timeoutMs: this.opts.promptTimeoutMs,
-      });
+      const outcome = await this.opts.driver.waitForDone(ref, {
+        timeoutMs: this.opts.promptTimeoutMs,
+        signal: this.shutdownSignal,
+      });

Store the signal passed to run() on the instance (or hand it to the scheduler handler) so handleEvent can forward it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nvt-bridge/src/bridge.ts` around lines 155 - 163, Propagate the abort
signal from run() into handleEvent and pass it as the signal option to
driver.waitForDone(). Ensure an abort produces the existing timeout-style
outcome so handleEvent still writes the fallback result line before shutdown.

Comment on lines +200 to +212
export function isDoneEvent(line: string): boolean {
const trimmed = line.trim();
if (trimmed === "") return false;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
return false;
}
const obj = parsed as { type?: unknown; event?: unknown };
const name = typeof obj.type === "string" ? obj.type : obj.event;
return name === "plugin.agent.signal.done";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

isDoneEvent crashes the whole bridge process on a bare null line.

JSON.parse("null") succeeds and returns null (no exception, so the catch at Line 206 never fires). The subsequent obj.type access at Line 210 then throws TypeError: Cannot read properties of null, and since this happens inside the child.stdout "data" handler in waitForDone (an EventEmitter callback, not inside any surrounding try/catch), it becomes an uncaught exception that can crash the entire node process — taking down every in-flight topic, not just this one. Given the subscribe output format is explicitly unverified per this file's own docstring, a stray null line is a realistic failure mode to guard against.

🐛 Proposed fix
   const obj = parsed as { type?: unknown; event?: unknown };
-  const name = typeof obj.type === "string" ? obj.type : obj.event;
+  if (typeof parsed !== "object" || parsed === null) return false;
+  const obj = parsed as { type?: unknown; event?: unknown };
+  const name = typeof obj.type === "string" ? obj.type : obj.event;
   return name === "plugin.agent.signal.done";

Add a test case for isDoneEvent("null") alongside the existing invalid-input tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function isDoneEvent(line: string): boolean {
const trimmed = line.trim();
if (trimmed === "") return false;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
return false;
}
const obj = parsed as { type?: unknown; event?: unknown };
const name = typeof obj.type === "string" ? obj.type : obj.event;
return name === "plugin.agent.signal.done";
}
export function isDoneEvent(line: string): boolean {
const trimmed = line.trim();
if (trimmed === "") return false;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
return false;
}
if (typeof parsed !== "object" || parsed === null) return false;
const obj = parsed as { type?: unknown; event?: unknown };
const name = typeof obj.type === "string" ? obj.type : obj.event;
return name === "plugin.agent.signal.done";
}
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/nvt-bridge/src/nvt/docker.ts` around lines 200 - 212, Update isDoneEvent
to verify the parsed JSON value is a non-null object before accessing obj.type
or obj.event, returning false for null and other non-object values; also add an
isDoneEvent("null") case alongside the existing invalid-input tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant